mirror of
https://github.com/jazzband/django-constance.git
synced 2026-03-16 22:40:24 +00:00
Some checks failed
Docs / docs (push) Has been cancelled
Test / ruff-format (push) Has been cancelled
Test / ruff-lint (push) Has been cancelled
Test / build (3.10) (push) Has been cancelled
Test / build (3.11) (push) Has been cancelled
Test / build (3.12) (push) Has been cancelled
Test / build (3.13) (push) Has been cancelled
Test / build (3.14) (push) Has been cancelled
Test / build (3.8) (push) Has been cancelled
Test / build (3.9) (push) Has been cancelled
* Added async logic * Added tests and fixed async deadlock on aset * Used abstract base class for backend to simplify code coverage * Reordered try except block * Added explicit thread safety * Fixed linting error * Worked on redis init block * Fixed async test setup * Added tests for redis instantiation * Fixed linting errors
263 lines
10 KiB
Python
263 lines
10 KiB
Python
from unittest import mock
|
|
|
|
from django.core.exceptions import ImproperlyConfigured
|
|
from django.test import TestCase
|
|
from django.test import TransactionTestCase
|
|
|
|
from constance import settings
|
|
from constance.backends.redisd import RedisBackend
|
|
from constance.base import Config
|
|
from tests.storage import StorageTestsMixin
|
|
|
|
|
|
class TestRedis(StorageTestsMixin, TestCase):
|
|
_BACKEND = "constance.backends.redisd.RedisBackend"
|
|
|
|
def setUp(self):
|
|
self.old_backend = settings.BACKEND
|
|
settings.BACKEND = self._BACKEND
|
|
super().setUp()
|
|
self.config._backend._rd.clear()
|
|
|
|
def tearDown(self):
|
|
self.config._backend._rd.clear()
|
|
settings.BACKEND = self.old_backend
|
|
|
|
def test_mget_empty_keys(self):
|
|
# Test that mget returns None for empty keys
|
|
result = list(self.config._backend.mget([]) or [])
|
|
self.assertEqual(result, [])
|
|
|
|
|
|
class TestCachingRedis(TestRedis):
|
|
_BACKEND = "constance.backends.redisd.CachingRedisBackend"
|
|
|
|
def test_mget_empty_keys(self):
|
|
# Test that mget returns None for empty keys
|
|
result = list(self.config._backend.mget([]) or [])
|
|
self.assertEqual(result, [])
|
|
|
|
|
|
class TestRedisAsync(TransactionTestCase):
|
|
_BACKEND = "constance.backends.redisd.RedisBackend"
|
|
|
|
def setUp(self):
|
|
self.old_backend = settings.BACKEND
|
|
settings.BACKEND = self._BACKEND
|
|
self.config = Config()
|
|
self.config._backend._rd.clear()
|
|
|
|
def tearDown(self):
|
|
self.config._backend._rd.clear()
|
|
settings.BACKEND = self.old_backend
|
|
|
|
async def test_aget_returns_none_for_missing_key(self):
|
|
result = await self.config._backend.aget("INT_VALUE")
|
|
self.assertIsNone(result)
|
|
|
|
async def test_aget_returns_value(self):
|
|
self.config._backend.set("INT_VALUE", 42)
|
|
result = await self.config._backend.aget("INT_VALUE")
|
|
self.assertEqual(result, 42)
|
|
|
|
async def test_aset_stores_value(self):
|
|
await self.config._backend.aset("INT_VALUE", 99)
|
|
result = self.config._backend.get("INT_VALUE")
|
|
self.assertEqual(result, 99)
|
|
|
|
async def test_amget_returns_empty_for_no_keys(self):
|
|
result = await self.config._backend.amget([])
|
|
self.assertEqual(result, {})
|
|
|
|
async def test_amget_returns_values(self):
|
|
self.config._backend.set("INT_VALUE", 10)
|
|
self.config._backend.set("BOOL_VALUE", value=True)
|
|
result = await self.config._backend.amget(["INT_VALUE", "BOOL_VALUE"])
|
|
self.assertEqual(result, {"INT_VALUE": 10, "BOOL_VALUE": True})
|
|
|
|
async def test_amget_skips_missing_keys(self):
|
|
self.config._backend.set("INT_VALUE", 10)
|
|
result = await self.config._backend.amget(["INT_VALUE", "MISSING_KEY"])
|
|
self.assertEqual(result, {"INT_VALUE": 10})
|
|
|
|
|
|
class TestCachingRedisAsync(TransactionTestCase):
|
|
_BACKEND = "constance.backends.redisd.CachingRedisBackend"
|
|
|
|
def setUp(self):
|
|
self.old_backend = settings.BACKEND
|
|
settings.BACKEND = self._BACKEND
|
|
self.config = Config()
|
|
self.config._backend._rd.clear()
|
|
self.config._backend._cache.clear()
|
|
|
|
def tearDown(self):
|
|
self.config._backend._rd.clear()
|
|
self.config._backend._cache.clear()
|
|
settings.BACKEND = self.old_backend
|
|
|
|
async def test_aget_caches_value(self):
|
|
# First set a value via sync
|
|
self.config._backend.set("INT_VALUE", 42)
|
|
# Clear the in-memory cache
|
|
self.config._backend._cache.clear()
|
|
|
|
# Async get should fetch and cache
|
|
result = await self.config._backend.aget("INT_VALUE")
|
|
self.assertEqual(result, 42)
|
|
|
|
# Verify it's cached
|
|
self.assertIn("INT_VALUE", self.config._backend._cache)
|
|
|
|
async def test_aget_returns_cached_value(self):
|
|
# Manually set cache
|
|
from time import monotonic
|
|
|
|
timeout = self.config._backend._timeout
|
|
self.config._backend._cache["INT_VALUE"] = (monotonic() + timeout, 100)
|
|
|
|
result = await self.config._backend.aget("INT_VALUE")
|
|
self.assertEqual(result, 100)
|
|
|
|
async def test_aget_refreshes_expired_cache(self):
|
|
from time import monotonic
|
|
|
|
# Set expired cache
|
|
self.config._backend._cache["INT_VALUE"] = (monotonic() - 10, 100)
|
|
# Set different value in redis using proper codec format
|
|
self.config._backend._rd.set(
|
|
self.config._backend.add_prefix("INT_VALUE"),
|
|
b'{"__type__": "default", "__value__": 200}',
|
|
)
|
|
|
|
result = await self.config._backend.aget("INT_VALUE")
|
|
self.assertEqual(result, 200)
|
|
|
|
async def test_aset_updates_cache(self):
|
|
await self.config._backend.aset("INT_VALUE", 55)
|
|
|
|
# Verify cache is updated
|
|
self.assertIn("INT_VALUE", self.config._backend._cache)
|
|
self.assertEqual(self.config._backend._cache["INT_VALUE"][1], 55)
|
|
|
|
async def test_amget_returns_empty_for_no_keys(self):
|
|
result = await self.config._backend.amget([])
|
|
self.assertEqual(result, {})
|
|
|
|
async def test_amget_returns_cached_values(self):
|
|
from time import monotonic
|
|
|
|
timeout = self.config._backend._timeout
|
|
self.config._backend._cache["INT_VALUE"] = (monotonic() + timeout, 10)
|
|
self.config._backend._cache["BOOL_VALUE"] = (monotonic() + timeout, True)
|
|
|
|
result = await self.config._backend.amget(["INT_VALUE", "BOOL_VALUE"])
|
|
self.assertEqual(result, {"INT_VALUE": 10, "BOOL_VALUE": True})
|
|
|
|
async def test_amget_fetches_missing_keys(self):
|
|
from time import monotonic
|
|
|
|
timeout = self.config._backend._timeout
|
|
# One key cached, one in Redis only
|
|
self.config._backend._cache["INT_VALUE"] = (monotonic() + timeout, 10)
|
|
self.config._backend._rd.set(
|
|
self.config._backend.add_prefix("BOOL_VALUE"),
|
|
b'{"__type__": "default", "__value__": true}',
|
|
)
|
|
|
|
result = await self.config._backend.amget(["INT_VALUE", "BOOL_VALUE"])
|
|
self.assertEqual(result["INT_VALUE"], 10)
|
|
self.assertEqual(result["BOOL_VALUE"], True)
|
|
|
|
async def test_amget_refreshes_expired_keys(self):
|
|
from time import monotonic
|
|
|
|
# Set expired cache
|
|
self.config._backend._cache["INT_VALUE"] = (monotonic() - 10, 100)
|
|
# Set different value in redis using proper codec format
|
|
self.config._backend._rd.set(
|
|
self.config._backend.add_prefix("INT_VALUE"),
|
|
b'{"__type__": "default", "__value__": 200}',
|
|
)
|
|
|
|
result = await self.config._backend.amget(["INT_VALUE"])
|
|
self.assertEqual(result["INT_VALUE"], 200)
|
|
|
|
|
|
class TestRedisBackendInit(TestCase):
|
|
"""Tests for RedisBackend.__init__ client initialization paths."""
|
|
|
|
def setUp(self):
|
|
self.old_conn_cls = settings.REDIS_CONNECTION_CLASS
|
|
self.old_async_conn_cls = settings.REDIS_ASYNC_CONNECTION_CLASS
|
|
self.old_conn = settings.REDIS_CONNECTION
|
|
|
|
def tearDown(self):
|
|
settings.REDIS_CONNECTION_CLASS = self.old_conn_cls
|
|
settings.REDIS_ASYNC_CONNECTION_CLASS = self.old_async_conn_cls
|
|
settings.REDIS_CONNECTION = self.old_conn
|
|
|
|
def test_no_redis_package_raises_improperly_configured(self):
|
|
settings.REDIS_CONNECTION_CLASS = None
|
|
settings.REDIS_ASYNC_CONNECTION_CLASS = "tests.redis_mockup.AsyncConnection"
|
|
with mock.patch.dict("sys.modules", {"redis": None}), self.assertRaises(ImproperlyConfigured):
|
|
RedisBackend()
|
|
|
|
def test_sync_redis_from_url_with_string_connection(self):
|
|
settings.REDIS_CONNECTION_CLASS = None
|
|
settings.REDIS_ASYNC_CONNECTION_CLASS = "tests.redis_mockup.AsyncConnection"
|
|
settings.REDIS_CONNECTION = "redis://localhost:6379/0"
|
|
mock_redis = mock.MagicMock()
|
|
with mock.patch.dict("sys.modules", {"redis": mock_redis, "redis.asyncio": mock_redis.asyncio}):
|
|
backend = RedisBackend()
|
|
mock_redis.from_url.assert_called_once_with("redis://localhost:6379/0")
|
|
self.assertEqual(backend._rd, mock_redis.from_url.return_value)
|
|
|
|
def test_sync_redis_with_dict_connection(self):
|
|
settings.REDIS_CONNECTION_CLASS = None
|
|
settings.REDIS_ASYNC_CONNECTION_CLASS = "tests.redis_mockup.AsyncConnection"
|
|
settings.REDIS_CONNECTION = {"host": "localhost", "port": 6379}
|
|
mock_redis = mock.MagicMock()
|
|
with mock.patch.dict("sys.modules", {"redis": mock_redis, "redis.asyncio": mock_redis.asyncio}):
|
|
backend = RedisBackend()
|
|
mock_redis.Redis.assert_called_once_with(host="localhost", port=6379)
|
|
self.assertEqual(backend._rd, mock_redis.Redis.return_value)
|
|
|
|
def test_async_redis_not_available_sets_ard_none(self):
|
|
settings.REDIS_CONNECTION_CLASS = "tests.redis_mockup.Connection"
|
|
settings.REDIS_ASYNC_CONNECTION_CLASS = None
|
|
mock_redis = mock.MagicMock()
|
|
# Simulate redis.asyncio not being available
|
|
with mock.patch.dict("sys.modules", {"redis": mock_redis, "redis.asyncio": None}):
|
|
backend = RedisBackend()
|
|
self.assertIsNone(backend._ard)
|
|
|
|
def test_async_redis_from_url_with_string_connection(self):
|
|
settings.REDIS_CONNECTION_CLASS = "tests.redis_mockup.Connection"
|
|
settings.REDIS_ASYNC_CONNECTION_CLASS = None
|
|
settings.REDIS_CONNECTION = "redis://localhost:6379/0"
|
|
mock_aredis = mock.MagicMock()
|
|
mock_redis = mock.MagicMock()
|
|
mock_redis.asyncio = mock_aredis
|
|
with mock.patch.dict("sys.modules", {"redis": mock_redis, "redis.asyncio": mock_aredis}):
|
|
backend = RedisBackend()
|
|
mock_aredis.from_url.assert_called_once_with("redis://localhost:6379/0")
|
|
self.assertEqual(backend._ard, mock_aredis.from_url.return_value)
|
|
|
|
def test_async_redis_with_dict_connection(self):
|
|
settings.REDIS_CONNECTION_CLASS = "tests.redis_mockup.Connection"
|
|
settings.REDIS_ASYNC_CONNECTION_CLASS = None
|
|
settings.REDIS_CONNECTION = {"host": "localhost", "port": 6379}
|
|
mock_aredis = mock.MagicMock()
|
|
mock_redis = mock.MagicMock()
|
|
mock_redis.asyncio = mock_aredis
|
|
with mock.patch.dict("sys.modules", {"redis": mock_redis, "redis.asyncio": mock_aredis}):
|
|
backend = RedisBackend()
|
|
mock_aredis.Redis.assert_called_once_with(host="localhost", port=6379)
|
|
self.assertEqual(backend._ard, mock_aredis.Redis.return_value)
|
|
|
|
def test_check_async_support_raises_when_ard_is_none(self):
|
|
backend = RedisBackend()
|
|
backend._ard = None
|
|
with self.assertRaises(ImproperlyConfigured):
|
|
backend._check_async_support()
|