"""Unit tests for caching utils.""" import datetime from unittest.mock import MagicMock, patch import pytest from playlist.connectors.redis import redis_client from playlist.utils.cache import cache_in_redis, cached @patch("playlist.utils.cache.is_disable_playlists_cache_enabled", return_value=False) def test_cache_in_redis(flag): """Test the cache_in_redis decorator.""" # Fakeredis stores state at the module level, so we use .flushall() # to ensure we have a clean slate every time we run the unit test. redis_client.flushall() assert "FakeStrictRedis" in str(redis_client) @cache_in_redis(ttl=1, key=None) def _date_time_func(): return test_result test_result = [ {"store_id": 1, "date": datetime.date(2018, 7, 5), "dl": 1}, {"store_id": 1, "date": datetime.date(2018, 7, 6), "dl": 2}, {"store_id": 496, "date": datetime.date(2018, 7, 5), "dl": 3}, {"store_id": 496, "date": datetime.date(2018, 7, 6), "dl": 4}, ] @cache_in_redis(ttl=1, key=None) def _none_func(): return None result1 = _date_time_func() assert result1 == _date_time_func() # one cache_key to exist for the _date_time_func() assert len(redis_client.keys()) == 1 result2 = _none_func() assert result2 == _none_func() # the result from _none_func() is None so we expect it not to be cached # therefore we expect no new cache_key for the result of this function assert len(redis_client.keys()) == 1 @patch("playlist.utils.cache.is_disable_playlists_cache_enabled", return_value=False) @pytest.mark.disable_mock_cache def test_cached(flag): redis_client.flushall() method = MagicMock() def fn(): method() return {"count": 2} with ( patch.object(redis_client, "set", wraps=redis_client.set) as redis_set, patch.object(redis_client, "get", wraps=redis_client.get) as redis_get, ): assert redis_get.call_count == 0 assert redis_set.call_count == 0 assert method.call_count == 0 result = cached( fn=fn, key="thekey", ttl=10, ) assert redis_get.call_count == 1 assert redis_set.call_count == 1 assert method.call_count == 1 assert result == {"count": 2} result = cached( fn=fn, key="thekey", ttl=10, ) assert redis_get.call_count == 2 assert redis_set.call_count == 1 assert method.call_count == 1 assert result == {"count": 2} @patch("playlist.utils.cache.is_disable_playlists_cache_enabled", return_value=False) @pytest.mark.disable_mock_cache def test_cached_with_args(flag): redis_client.flushall() method = MagicMock() def fn(x: int = 1): method() return {"count": 2 * x} with ( patch.object(redis_client, "set", wraps=redis_client.set) as redis_set, patch.object(redis_client, "get", wraps=redis_client.get) as redis_get, ): assert redis_get.call_count == 0 assert redis_set.call_count == 0 assert method.call_count == 0 result = cached(fn=fn, key="thekey", ttl=10, x=2) assert redis_get.call_count == 1 assert redis_set.call_count == 1 assert method.call_count == 1 assert result == {"count": 4} result = cached(fn=fn, key="thekey", ttl=10, x=123) assert redis_get.call_count == 2 assert redis_set.call_count == 1 assert method.call_count == 1 # still cached value although different x has been supplied assert result == {"count": 4} @patch("playlist.utils.cache.is_disable_playlists_cache_enabled", return_value=False) @pytest.mark.disable_mock_cache def test_cached_with_args_dynamic_key(flag): redis_client.flushall() method = MagicMock() def fn(x: int = 1): method() return {"count": 2 * x} def muh_cache_key(a): return f"cachekey:{a}" with ( patch.object(redis_client, "set", wraps=redis_client.set) as redis_set, patch.object(redis_client, "get", wraps=redis_client.get) as redis_get, ): assert redis_get.call_count == 0 assert redis_set.call_count == 0 assert method.call_count == 0 arg = 2 result = cached(fn=fn, key=muh_cache_key(arg), ttl=10, x=arg) assert redis_get.call_count == 1 assert redis_set.call_count == 1 assert method.call_count == 1 assert result == {"count": 4} arg = 123 result = cached(fn=fn, key=muh_cache_key(arg), ttl=10, x=arg) assert redis_get.call_count == 2 assert redis_set.call_count == 2 assert method.call_count == 2 # non cached value due to using a dynamic cache key assert result == {"count": 246} @patch("playlist.utils.cache.is_disable_playlists_cache_enabled", return_value=True) def test_cache_flag_True(flag): redis_client.flushall() with (patch.object(redis_client, "get", wraps=redis_client.get) as redis_get,): @cache_in_redis(ttl=1) def _date_time_func(): return "result" _date_time_func() assert redis_get.call_count == 0 @patch("playlist.utils.cache.is_disable_playlists_cache_enabled", return_value=False) def test_cache_flag_False(flag): redis_client.flushall() with (patch.object(redis_client, "get", wraps=redis_client.get) as redis_get,): @cache_in_redis(ttl=1) def _date_time_func(): return "result" _date_time_func() assert redis_get.call_count == 1 @patch("playlist.utils.cache.is_disable_playlists_cache_enabled", return_value=False) @pytest.mark.disable_mock_cache def test_force_refresh_bypasses_cache(flag): """Test that force_refresh=True bypasses Redis cache.""" redis_client.flushall() call_count = 0 @cache_in_redis(ttl=10) def _cached_func(): nonlocal call_count call_count += 1 return {"result": call_count} with ( patch.object(redis_client, "set", wraps=redis_client.set) as redis_set, patch.object(redis_client, "get", wraps=redis_client.get) as redis_get, ): # First call - should cache result1 = _cached_func() assert result1 == {"result": 1} assert call_count == 1 assert redis_get.call_count == 1 assert redis_set.call_count == 1 # Second call - should use cache result2 = _cached_func() assert result2 == {"result": 1} # Same cached result assert call_count == 1 # Function not called again assert redis_get.call_count == 2 assert redis_set.call_count == 1 # Third call with force_refresh=True - should bypass cache read but update cache result3 = _cached_func(force_refresh=True) assert result3 == {"result": 2} # New result assert call_count == 2 # Function called again assert redis_get.call_count == 2 # Cache not checked (skipped read) assert redis_set.call_count == 2 # Cache WAS updated with fresh data # Fourth call - should use the freshly updated cache result4 = _cached_func() assert result4 == {"result": 2} # Fresh cached result assert call_count == 2 # Function not called again assert redis_get.call_count == 3 # Cache checked assert redis_set.call_count == 2 # No new writes @patch("playlist.utils.cache.is_disable_playlists_cache_enabled", return_value=False) @pytest.mark.disable_mock_cache def test_cache_isolation_with_different_kwargs(flag): """Test that different kwargs create separate cache keys.""" redis_client.flushall() call_count = 0 @cache_in_redis(ttl=10) def _cached_func(include_hourly: bool = False): nonlocal call_count call_count += 1 return {"result": f"hourly={include_hourly}", "call": call_count} with ( patch.object(redis_client, "set", wraps=redis_client.set) as redis_set, patch.object(redis_client, "get", wraps=redis_client.get) as redis_get, ): # Call with include_hourly=False result1 = _cached_func(include_hourly=False) assert result1 == {"result": "hourly=False", "call": 1} assert call_count == 1 assert redis_get.call_count == 1 assert redis_set.call_count == 1 # Call with include_hourly=True (different kwarg value) result2 = _cached_func(include_hourly=True) assert result2 == {"result": "hourly=True", "call": 2} assert call_count == 2 # Function called again due to different kwarg assert redis_get.call_count == 2 assert redis_set.call_count == 2 # New cache entry created # Call with include_hourly=False again (should use first cache) call_count_before = call_count result3 = _cached_func(include_hourly=False) assert result3 == {"result": "hourly=False", "call": 1} # Same as result1 assert call_count == call_count_before # Function not called assert redis_get.call_count == 3 assert redis_set.call_count == 2 # Call with include_hourly=True again (should use second cache) call_count_before = call_count result4 = _cached_func(include_hourly=True) assert result4 == {"result": "hourly=True", "call": 2} # Same as result2 assert call_count == call_count_before # Function not called assert redis_get.call_count == 4 assert redis_set.call_count == 2 # Verify we have 2 separate cache keys assert len(redis_client.keys()) == 2