P1 通用查询:viewsets / app / menu / mail / gitea / mc / stats / sync_apps 等视图与命令的裸查询改委托 SDK Manager(Role/Company/Department/Position/Menu/Tenant/App)。 P2 App 体系:app_manager.py 与 app_manifests.py 实现零裸 ORM,职务多值/虚拟职务/公司下拉全部走 SDK。 P3 工具下沉:money/json_encoder/celery_utils/exception_handler/redis_lock 改为纯转发展开;ip_filter/storage 转发并注入 app.ini 配置源;common 下 DateTime/String/Naming/UUID 转发 SDK。 删除死代码 commission/strategies/engine.py(SDK 已有同物且零引用)与 payment/models.py 的 FromTenantAndPlatform;单测 patch 目标同步为 gvsdsdk.*,test_attack_detect fixture 补齐限频去重状态。
167 lines
5.7 KiB
Python
167 lines
5.7 KiB
Python
"""模块:redis_lock.py 单元测试
|
||
|
||
无 Redis 环境下验证降级行为:acquire 永远返回 True,
|
||
release 为 no-op,上下文管理器与装饰器均正常工作。
|
||
有 Redis 时由集成测试覆盖。
|
||
"""
|
||
import unittest
|
||
from unittest import mock
|
||
|
||
from django.test import SimpleTestCase
|
||
|
||
from gvsdsdk import redis_lock
|
||
|
||
|
||
class HasRedisDetectionTestCase(SimpleTestCase):
|
||
"""_HasRedis 探测逻辑。"""
|
||
|
||
def testHasRedis_CachesResult(self):
|
||
# 重置缓存
|
||
redis_lock._REDIS_OK = None
|
||
result1 = redis_lock._HasRedis()
|
||
result2 = redis_lock._HasRedis()
|
||
self.assertEqual(result1, result2)
|
||
# 结果应被缓存(_REDIS_OK 非 None)
|
||
self.assertIsNotNone(redis_lock._REDIS_OK)
|
||
|
||
def testHasRedis_NoRedis_ReturnsFalse(self):
|
||
redis_lock._REDIS_OK = None
|
||
with mock.patch.object(redis_lock, '_is_redis_backend', return_value=False):
|
||
self.assertFalse(redis_lock._HasRedis())
|
||
|
||
def testHasRedis_WithRedis_ReturnsTrue(self):
|
||
redis_lock._REDIS_OK = None
|
||
with mock.patch.object(redis_lock, '_is_redis_backend', return_value=True):
|
||
self.assertTrue(redis_lock._HasRedis())
|
||
|
||
|
||
class RedisLockNoRedisTestCase(SimpleTestCase):
|
||
"""无 Redis 时降级为 no-op。"""
|
||
|
||
def setUp(self):
|
||
# 强制无 Redis 模式
|
||
self._orig = redis_lock._REDIS_OK
|
||
redis_lock._REDIS_OK = False
|
||
|
||
def tearDown(self):
|
||
redis_lock._REDIS_OK = self._orig
|
||
|
||
def testAcquire_NoRedis_ReturnsTrue(self):
|
||
lock = redis_lock.RedisLock('test-key')
|
||
self.assertTrue(lock.acquire())
|
||
self.assertTrue(lock.acquired)
|
||
|
||
def testRelease_NoRedis_NoOp(self):
|
||
lock = redis_lock.RedisLock('test-key')
|
||
lock.acquire()
|
||
# 无 Redis 时 release 为 no-op,不应抛异常
|
||
lock.release()
|
||
|
||
def testContextManager_NoRedis_AlwaysAcquired(self):
|
||
with redis_lock.RedisLock('ctx-key') as acquired:
|
||
self.assertTrue(acquired)
|
||
|
||
def testContextManager_NoRedis_NoExceptionOnExit(self):
|
||
# 即使块内抛异常,__exit__ 也不应传播
|
||
with self.assertRaises(ValueError):
|
||
with redis_lock.RedisLock('ctx-key') as acquired:
|
||
self.assertTrue(acquired)
|
||
raise ValueError('boom')
|
||
|
||
|
||
class RedisLockedDecoratorTestCase(SimpleTestCase):
|
||
"""redis_locked 装饰器。"""
|
||
|
||
def setUp(self):
|
||
self._orig = redis_lock._REDIS_OK
|
||
redis_lock._REDIS_OK = False
|
||
|
||
def tearDown(self):
|
||
redis_lock._REDIS_OK = self._orig
|
||
|
||
def testDecorator_StaticKey_ExecutesFunc(self):
|
||
@redis_lock.redis_locked('static-key', timeout=10)
|
||
def Task():
|
||
return 'done'
|
||
|
||
self.assertEqual(Task(), 'done')
|
||
|
||
def testDecorator_KeyArgByIndex(self):
|
||
@redis_lock.redis_locked('order', timeout=10, key_arg=0)
|
||
def PayOrder(order_uuid):
|
||
return f'paid:{order_uuid}'
|
||
|
||
self.assertEqual(PayOrder('abc-123'), 'paid:abc-123')
|
||
|
||
def testDecorator_KeyArgByName(self):
|
||
@redis_lock.redis_locked('order', timeout=10, key_arg='order_uuid')
|
||
def PayOrder(order_uuid=None):
|
||
return f'paid:{order_uuid}'
|
||
|
||
self.assertEqual(PayOrder(order_uuid='xyz'), 'paid:xyz')
|
||
|
||
def testDecorator_KeyArgMissing_FallsBackToStaticKey(self):
|
||
@redis_lock.redis_locked('order', timeout=10, key_arg='missing')
|
||
def PayOrder(order_uuid):
|
||
return f'paid:{order_uuid}'
|
||
|
||
# missing 参数不存在 → 使用静态 key 'order'
|
||
self.assertEqual(PayOrder('abc'), 'paid:abc')
|
||
|
||
def testDecorator_PreservesFuncMetadata(self):
|
||
@redis_lock.redis_locked('meta-key')
|
||
def ImportantTask():
|
||
"""Important docstring."""
|
||
|
||
self.assertEqual(ImportantTask.__name__, 'ImportantTask')
|
||
self.assertEqual(ImportantTask.__doc__, 'Important docstring.')
|
||
|
||
|
||
class RedisLockWithRedisTestCase(SimpleTestCase):
|
||
"""有 Redis 时通过 mock cache 验证核心逻辑。"""
|
||
|
||
def setUp(self):
|
||
self._orig = redis_lock._REDIS_OK
|
||
redis_lock._REDIS_OK = True
|
||
|
||
def tearDown(self):
|
||
redis_lock._REDIS_OK = self._orig
|
||
|
||
def testAcquire_CacheAddSuccess_ReturnsTrue(self):
|
||
lock = redis_lock.RedisLock('win-key', timeout=5)
|
||
with mock.patch('gvsdsdk.redis_lock.cache') as mock_cache:
|
||
mock_cache.add.return_value = True
|
||
self.assertTrue(lock.acquire())
|
||
mock_cache.add.assert_called_once()
|
||
args, kwargs = mock_cache.add.call_args
|
||
self.assertIn('win-key', args[0])
|
||
self.assertEqual(kwargs['timeout'], 5)
|
||
|
||
def testAcquire_CacheAddFail_NoWait_ReturnsFalse(self):
|
||
lock = redis_lock.RedisLock('lose-key', wait=False)
|
||
with mock.patch('gvsdsdk.redis_lock.cache') as mock_cache:
|
||
mock_cache.add.return_value = False
|
||
self.assertFalse(lock.acquire())
|
||
|
||
def testRelease_TokenMatch_Deletes(self):
|
||
lock = redis_lock.RedisLock('rel-key')
|
||
lock._acquired = True
|
||
with mock.patch('gvsdsdk.redis_lock.cache') as mock_cache:
|
||
mock_cache.get.return_value = lock.token
|
||
lock.release()
|
||
mock_cache.delete.assert_called_once()
|
||
|
||
def testRelease_TokenMismatch_DoesNotDelete(self):
|
||
lock = redis_lock.RedisLock('rel-key')
|
||
lock._acquired = True
|
||
with mock.patch('gvsdsdk.redis_lock.cache') as mock_cache:
|
||
mock_cache.get.return_value = 'other-token'
|
||
lock.release()
|
||
mock_cache.delete.assert_not_called()
|
||
|
||
def testRelease_NotAcquired_NoOp(self):
|
||
lock = redis_lock.RedisLock('rel-key')
|
||
with mock.patch('gvsdsdk.redis_lock.cache') as mock_cache:
|
||
lock.release()
|
||
mock_cache.get.assert_not_called()
|