"""Tests for core cache utilities""" from apps.core.cache import ( CacheManager, _build_cache_key, cache_method, cache_result, invalidate_cache, ) from django.core.cache import cache from django.test import TestCase from tests.utils.fixtures import fake class CacheResultDecoratorTest(TestCase): """Tests for @cache_result decorator""" def setUp(self): cache.clear() self.call_count = 0 def test_result_is_cached(self): """Test that function result is cached""" @cache_result(timeout=60, key_prefix="test") def expensive_function(x): self.call_count += 1 return x * 2 # First call - should execute result1 = expensive_function(5) self.assertEqual(result1, 10) self.assertEqual(self.call_count, 1) # Second call - should return cached result result2 = expensive_function(5) self.assertEqual(result2, 10) self.assertEqual(self.call_count, 1) # Still 1, not called again def test_key_builder_used(self): """Test custom key builder overrides default.""" called = {"count": 0} def key_builder(*_args, **_kwargs): return "custom-key" @cache_result(timeout=60, key_builder=key_builder) def expensive_function(x): called["count"] += 1 return x * 3 result1 = expensive_function(2) result2 = expensive_function(2) self.assertEqual(result1, 6) self.assertEqual(result2, 6) self.assertEqual(called["count"], 1) def test_invalidate_cache_fallback(self): """Test invalidate_cache fallback without delete_pattern.""" cache.set("cache-key", "value", timeout=60) invalidate_cache("cache-key") self.assertIsNone(cache.get("cache-key")) def test_invalidate_wrapper_with_key_builder(self): key_suffix = fake.pystr(min_chars=3, max_chars=8) def key_builder(x): return f"{key_suffix}:{x}" @cache_result(timeout=60, key_builder=key_builder) def expensive_function(x): self.call_count += 1 return x * 2 value = fake.random_int(min=1, max=9) result = expensive_function(value) self.assertEqual(result, value * 2) self.assertIsNotNone(cache.get(f"{key_suffix}:{value}")) expensive_function.invalidate(value) self.assertIsNone(cache.get(f"{key_suffix}:{value}")) def test_invalidate_wrapper_default_builder(self): value = fake.random_int(min=1, max=9) @cache_result(timeout=60, key_prefix="default") def expensive_function(x): self.call_count += 1 return x * 2 _ = expensive_function(value) key = _build_cache_key(expensive_function, "default", (value,), {}) self.assertIsNotNone(cache.get(key)) expensive_function.invalidate(value) self.assertIsNone(cache.get(key)) def test_different_args_not_cached(self): """Test that different arguments create different cache entries""" @cache_result(timeout=60, key_prefix="test") def expensive_function(x): self.call_count += 1 return x * 2 result1 = expensive_function(5) result2 = expensive_function(10) self.assertEqual(result1, 10) self.assertEqual(result2, 20) self.assertEqual(self.call_count, 2) def test_kwargs_included_in_cache_key(self): """Test that kwargs are included in cache key""" @cache_result(timeout=60, key_prefix="test") def expensive_function(x, multiplier=2): self.call_count += 1 return x * multiplier result1 = expensive_function(5, multiplier=2) result2 = expensive_function(5, multiplier=3) self.assertEqual(result1, 10) self.assertEqual(result2, 15) self.assertEqual(self.call_count, 2) class CacheMethodDecoratorTest(TestCase): """Tests for @cache_method decorator""" def setUp(self): cache.clear() def test_classmethod_caching(self): """Test caching works with classmethod""" call_count = {"value": 0} class MyService: @classmethod @cache_method(timeout=60, key_prefix="service") def get_data(cls, item_id): call_count["value"] += 1 return {"id": item_id, "data": "test"} # First call result1 = MyService.get_data(1) self.assertEqual(result1["id"], 1) self.assertEqual(call_count["value"], 1) # Second call - should be cached result2 = MyService.get_data(1) self.assertEqual(result2["id"], 1) self.assertEqual(call_count["value"], 1) # Different argument result3 = MyService.get_data(2) self.assertEqual(result3["id"], 2) self.assertEqual(call_count["value"], 2) class CacheManagerTest(TestCase): """Tests for CacheManager""" def setUp(self): cache.clear() self.manager = CacheManager("test_prefix") def test_set_and_get(self): """Test basic set and get operations""" self.manager.set("key1", "value1", timeout=60) result = self.manager.get("key1") self.assertEqual(result, "value1") def test_get_default(self): """Test get returns default for missing key""" result = self.manager.get("nonexistent", default="default_value") self.assertEqual(result, "default_value") def test_delete(self): """Test delete operation""" self.manager.set("key1", "value1") self.manager.delete("key1") result = self.manager.get("key1") self.assertIsNone(result) def test_get_or_set(self): """Test get_or_set operation""" call_count = {"value": 0} def compute_value(): call_count["value"] += 1 return "computed" # First call - should compute result1 = self.manager.get_or_set("key1", compute_value) self.assertEqual(result1, "computed") self.assertEqual(call_count["value"], 1) # Second call - should return cached result2 = self.manager.get_or_set("key1", compute_value) self.assertEqual(result2, "computed") self.assertEqual(call_count["value"], 1) def test_prefix_applied(self): """Test that prefix is applied to keys""" self.manager.set("mykey", "myvalue") # Direct cache access should use prefixed key direct_result = cache.get("test_prefix:mykey") self.assertEqual(direct_result, "myvalue") def test_clear_removes_prefixed_keys(self): self.manager.set("one", "1") self.manager.set("two", "2") self.manager.clear() # LocMemCache doesn't support delete_pattern, so keys may remain. self.assertIsNotNone(cache.get("test_prefix:one")) class BuildCacheKeyTest(TestCase): """Tests for _build_cache_key function""" def test_key_includes_function_name(self): """Test cache key includes function name""" def my_function(): pass key = _build_cache_key(my_function, "", (), {}) self.assertIn("my_function", key) def test_key_includes_prefix(self): """Test cache key includes prefix""" def my_function(): pass key = _build_cache_key(my_function, "myprefix", (), {}) self.assertTrue(key.startswith("myprefix:")) def test_different_args_different_keys(self): """Test different arguments produce different keys""" def my_function(): pass key1 = _build_cache_key(my_function, "", (1, 2), {}) key2 = _build_cache_key(my_function, "", (1, 3), {}) self.assertNotEqual(key1, key2) def test_different_kwargs_different_keys(self): """Test different kwargs produce different keys""" def my_function(): pass key1 = _build_cache_key(my_function, "", (), {"a": 1}) key2 = _build_cache_key(my_function, "", (), {"a": 2}) self.assertNotEqual(key1, key2) def test_build_cache_key_handles_circular(self): """Test circular references fallback to str.""" def my_function(): pass items: list[object] = [] items.append(items) key = _build_cache_key(my_function, "", (items,), {}) self.assertIn("my_function", key)