Spaces:
Sleeping
Sleeping
firepenguindisopanda
Add comprehensive tests for cache management, composite indexes, enum fields, and Pinecone integration
c62301e | """Tests for DS-11: Cache invalidation with key-pattern tracking.""" | |
| from unittest.mock import AsyncMock, MagicMock, patch | |
| import pytest | |
| def mock_redis(): | |
| """Patch upstash_redis.Redis so the cache client is usable.""" | |
| with patch("upstash_redis.Redis") as mock_cls: | |
| mock_instance = MagicMock() | |
| mock_cls.return_value = mock_instance | |
| # By default, all Redis operations return something truthy | |
| mock_instance.get.return_value = None | |
| mock_instance.set.return_value = "OK" | |
| mock_instance.sadd.return_value = 1 | |
| mock_instance.smembers.return_value = [] | |
| mock_instance.delete.return_value = 1 | |
| mock_instance.exists.return_value = 0 | |
| yield mock_instance | |
| def cache_with_mock_creds(mock_redis): | |
| """Return an UpstashRedisCache with mocked credentials and Redis client.""" | |
| from app.core.cache import UpstashRedisCache | |
| from app.core.config import settings | |
| with ( | |
| patch.object(settings, "upstash_redis_rest_url", "https://test.upstash.io"), | |
| patch.object(settings, "upstash_redis_rest_token", "test-token"), | |
| ): | |
| cache = UpstashRedisCache() | |
| # Force client creation so mock_redis is used | |
| cache._sync_client = mock_redis._mock_new_parent if hasattr(mock_redis, '_mock_new_parent') else mock_redis | |
| cache._async_client = mock_redis | |
| yield cache | |
| class TestCacheKeyTracking: | |
| """DS-11: Cache operations should track keys per user.""" | |
| async def test_cache_summary_tracks_key( | |
| self, mock_get_async, cache_with_mock_creds | |
| ) -> None: | |
| """cache_summary should track the cache key under the user's key set.""" | |
| mock_async = AsyncMock() | |
| mock_async.set.return_value = "OK" | |
| mock_async.sadd.return_value = 1 | |
| # Setting mock_get_async to return a mock client | |
| mock_get_async.return_value = mock_async | |
| from app.core.cache import UpstashRedisCache | |
| cache = cache_with_mock_creds | |
| result = await cache.cache_summary( | |
| user_id="user_abc", | |
| start_date="2024-01-01", | |
| end_date="2024-01-31", | |
| data={"total_runs": 42}, | |
| ) | |
| assert result is True | |
| # Should have called set AND sadd (for key tracking) | |
| assert mock_async.set.called | |
| assert mock_async.sadd.called | |
| async def test_cache_trends_tracks_key( | |
| self, mock_get_async, cache_with_mock_creds | |
| ) -> None: | |
| """cache_trends should track the cache key under the user's key set.""" | |
| mock_async = AsyncMock() | |
| mock_async.set.return_value = "OK" | |
| mock_async.sadd.return_value = 1 | |
| mock_get_async.return_value = mock_async | |
| cache = cache_with_mock_creds | |
| result = await cache.cache_trends( | |
| user_id="user_abc", | |
| period="7d", | |
| data={"trend": "up"}, | |
| ) | |
| assert result is True | |
| assert mock_async.set.called | |
| assert mock_async.sadd.called | |
| async def test_cache_projects_tracks_key( | |
| self, mock_get_async, cache_with_mock_creds | |
| ) -> None: | |
| """cache_projects should track the cache key under the user's key set.""" | |
| mock_async = AsyncMock() | |
| mock_async.set.return_value = "OK" | |
| mock_async.sadd.return_value = 1 | |
| mock_get_async.return_value = mock_async | |
| cache = cache_with_mock_creds | |
| result = await cache.cache_projects( | |
| user_id="user_abc", | |
| data={"projects": []}, | |
| ) | |
| assert result is True | |
| assert mock_async.set.called | |
| assert mock_async.sadd.called | |
| class TestCacheInvalidation: | |
| """DS-11: invalidate_user_cache should clear all keys for a user.""" | |
| async def test_invalidate_user_cache_clears_keys( | |
| self, mock_get_sync, cache_with_mock_creds | |
| ) -> None: | |
| """invalidate_user_cache should smembers, delete each key, then delete the set.""" | |
| mock_sync = MagicMock() | |
| # Return some tracked keys | |
| mock_sync.smembers.return_value = [ | |
| "cache_key_1", | |
| "cache_key_2", | |
| "cache_key_3", | |
| ] | |
| mock_sync.delete.return_value = 3 | |
| mock_get_sync.return_value = mock_sync | |
| cache = cache_with_mock_creds | |
| await cache.invalidate_user_cache("user_abc") | |
| # Should have called smembers to get keys | |
| mock_sync.smembers.assert_called_once() | |
| # Should have called delete for each key (pipeline or individual) | |
| assert mock_sync.delete.call_count >= 1 | |
| async def test_invalidate_user_cache_empty_set( | |
| self, mock_get_sync, cache_with_mock_creds | |
| ) -> None: | |
| """invalidate_user_cache handles empty key sets gracefully.""" | |
| mock_sync = MagicMock() | |
| mock_sync.smembers.return_value = [] # No tracked keys | |
| mock_sync.delete.return_value = 1 # Deleting the set itself | |
| mock_get_sync.return_value = mock_sync | |
| cache = cache_with_mock_creds | |
| # Should not raise | |
| await cache.invalidate_user_cache("user_xyz") | |
| mock_sync.smembers.assert_called_once() | |
| # Should delete the set itself | |
| mock_sync.delete.assert_called_once() | |
| async def test_invalidate_user_cache_no_crash_when_redis_unavailable( | |
| self, mock_get_sync, cache_with_mock_creds | |
| ) -> None: | |
| """invalidate_user_cache should not crash when Redis client returns None.""" | |
| mock_get_sync.return_value = None | |
| cache = cache_with_mock_creds | |
| # Should not raise | |
| result = await cache.invalidate_user_cache("user_no_redis") | |
| assert result == 0 # No keys cleared when Redis unavailable | |
| async def test_invalidate_user_cache_returns_key_count( | |
| self, mock_get_sync, cache_with_mock_creds | |
| ) -> None: | |
| """invalidate_user_cache should return the number of keys cleared.""" | |
| mock_sync = MagicMock() | |
| mock_sync.smembers.return_value = ["key1", "key2"] | |
| mock_sync.delete.return_value = 2 | |
| mock_get_sync.return_value = mock_sync | |
| cache = cache_with_mock_creds | |
| result = await cache.invalidate_user_cache("user_abc") | |
| assert result == 2 | |
| mock_sync.smembers.assert_called_once() | |
| assert mock_sync.delete.call_count >= 1 | |
| class TestCacheHealth: | |
| """DS-11: get_cache_health should report cache status.""" | |
| async def test_cache_health_ok( | |
| self, mock_get_sync, cache_with_mock_creds | |
| ) -> None: | |
| """get_cache_health returns ok status when Redis is connected.""" | |
| mock_sync = MagicMock() | |
| mock_sync.exists.return_value = 1 | |
| mock_sync.scard.return_value = 5 | |
| mock_get_sync.return_value = mock_sync | |
| cache = cache_with_mock_creds | |
| health = await cache.get_cache_health("user_abc") | |
| assert health["status"] == "ok" | |
| assert health["keys_tracked"] == 5 | |
| assert health["ttl_seconds"] > 0 | |
| async def test_cache_health_degraded( | |
| self, mock_get_sync, cache_with_mock_creds | |
| ) -> None: | |
| """get_cache_health returns degraded status when Redis is unavailable.""" | |
| mock_get_sync.return_value = None | |
| cache = cache_with_mock_creds | |
| health = await cache.get_cache_health("user_abc") | |
| assert health["status"] == "degraded" | |
| assert health["keys_tracked"] == 0 | |
| async def test_cache_health_no_keys( | |
| self, mock_get_sync, cache_with_mock_creds | |
| ) -> None: | |
| """get_cache_health returns 0 keys when no key set exists.""" | |
| mock_sync = MagicMock() | |
| mock_sync.exists.return_value = 0 # Key set doesn't exist | |
| mock_get_sync.return_value = mock_sync | |
| cache = cache_with_mock_creds | |
| health = await cache.get_cache_health("user_abc") | |
| assert health["status"] == "ok" | |
| assert health["keys_tracked"] == 0 | |