multi-agent-system / tests /test_cache_invalidation.py
firepenguindisopanda
Add comprehensive tests for cache management, composite indexes, enum fields, and Pinecone integration
c62301e
Raw
History Blame Contribute Delete
8.54 kB
"""Tests for DS-11: Cache invalidation with key-pattern tracking."""
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
@pytest.fixture
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
@pytest.fixture
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."""
@patch("app.core.cache.UpstashRedisCache._get_async_client")
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
@patch("app.core.cache.UpstashRedisCache._get_async_client")
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
@patch("app.core.cache.UpstashRedisCache._get_async_client")
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."""
@patch("app.core.cache.UpstashRedisCache._get_sync_client")
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
@patch("app.core.cache.UpstashRedisCache._get_sync_client")
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()
@patch("app.core.cache.UpstashRedisCache._get_sync_client")
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
@patch("app.core.cache.UpstashRedisCache._get_sync_client")
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."""
@patch("app.core.cache.UpstashRedisCache._get_sync_client")
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
@patch("app.core.cache.UpstashRedisCache._get_sync_client")
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
@patch("app.core.cache.UpstashRedisCache._get_sync_client")
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