""" Tests for volatile cache router and service. Tests: - Volatile record CRUD operations - Namespace listing and management - Scheduled record retrieval - TTL behavior """ import pytest from datetime import datetime from unittest.mock import AsyncMock, MagicMock, patch from src.models.volatile import ( VolatileRecord, VolatileRecordCreate, VolatileRecordResponse, VolatileListResponse, VolatileScheduledResponse, VolatileStatsResponse, VolatileDeleteResponse, VolatileBulkDeleteResponse, VolatileNamespace, NAMESPACE_DEFAULT_TTL, ) class TestVolatileModels: """Test volatile data models.""" def test_volatile_record_creation(self): """Test VolatileRecord model creation.""" record = VolatileRecord( key="rotterdam", namespace="weather", data={"temperature": 18, "conditions": "Cloudy"}, source="openweathermap", ttl=1800, user="jpmschweitzer", ) assert record.key == "rotterdam" assert record.namespace == "weather" assert record.data["temperature"] == 18 assert record.ttl == 1800 assert record.refresh_schedule is None def test_volatile_record_with_schedule(self): """Test VolatileRecord with refresh schedule.""" record = VolatileRecord( key="nos-headlines", namespace="news", data={"headlines": ["Test headline"]}, source="nos.nl", ttl=3600, refresh_schedule="0 * * * *", user="jpmschweitzer", ) assert record.refresh_schedule == "0 * * * *" def test_volatile_record_create(self): """Test VolatileRecordCreate model.""" create = VolatileRecordCreate( data={"price": 150.50, "change": 2.3}, source="alpha_vantage", ttl=300, ) assert create.data["price"] == 150.50 assert create.ttl == 300 def test_volatile_record_response(self): """Test VolatileRecordResponse model.""" response = VolatileRecordResponse( key="rotterdam", namespace="weather", data={"temperature": 18}, source="openweathermap", created_at=datetime.utcnow(), updated_at=datetime.utcnow(), ttl=1800, ttl_remaining=1500, user="jpmschweitzer", ) assert response.ttl_remaining == 1500 assert response.ttl == 1800 class TestVolatileNamespaces: """Test volatile namespaces and defaults.""" def test_all_namespaces_have_default_ttl(self): """Verify all namespaces have default TTLs defined.""" for ns in VolatileNamespace: assert ns in NAMESPACE_DEFAULT_TTL, f"Missing TTL for {ns}" assert NAMESPACE_DEFAULT_TTL[ns] > 0 def test_weather_default_ttl(self): """Test weather namespace default TTL.""" assert NAMESPACE_DEFAULT_TTL[VolatileNamespace.WEATHER] == 1800 # 30 min def test_financial_default_ttl(self): """Test financial namespace default TTL.""" assert NAMESPACE_DEFAULT_TTL[VolatileNamespace.FINANCIAL] == 300 # 5 min def test_sports_default_ttl(self): """Test sports namespace default TTL (fast updates).""" assert NAMESPACE_DEFAULT_TTL[VolatileNamespace.SPORTS] == 60 # 1 min class TestVolatileListResponse: """Test list response models.""" def test_list_response(self): """Test VolatileListResponse model.""" response = VolatileListResponse( namespace="weather", keys=["rotterdam", "amsterdam", "utrecht"], count=3, user="jpmschweitzer", ) assert response.count == 3 assert "rotterdam" in response.keys class TestVolatileScheduledResponse: """Test scheduled records response.""" def test_scheduled_response_empty(self): """Test empty scheduled response.""" response = VolatileScheduledResponse( records=[], count=0, user="jpmschweitzer", ) assert response.count == 0 assert response.records == [] def test_scheduled_response_with_records(self): """Test scheduled response with records.""" record = VolatileRecordResponse( key="nos-headlines", namespace="news", data={"headlines": []}, source="nos.nl", created_at=datetime.utcnow(), updated_at=datetime.utcnow(), ttl=3600, ttl_remaining=3000, refresh_schedule="0 */6 * * *", user="jpmschweitzer", ) response = VolatileScheduledResponse( records=[record], count=1, user="jpmschweitzer", ) assert response.count == 1 assert response.records[0].refresh_schedule == "0 */6 * * *" class TestVolatileStatsResponse: """Test stats response model.""" def test_stats_response(self): """Test VolatileStatsResponse model.""" response = VolatileStatsResponse( total_records=15, by_namespace={"weather": 3, "news": 5, "financial": 7}, scheduled_count=2, total_memory_bytes=None, user="jpmschweitzer", ) assert response.total_records == 15 assert response.by_namespace["weather"] == 3 assert response.scheduled_count == 2 class TestVolatileDeleteResponses: """Test delete response models.""" def test_delete_response(self): """Test VolatileDeleteResponse model.""" response = VolatileDeleteResponse( key="rotterdam", namespace="weather", deleted=True, user="jpmschweitzer", ) assert response.deleted is True def test_delete_not_found(self): """Test delete response when record not found.""" response = VolatileDeleteResponse( key="nonexistent", namespace="weather", deleted=False, user="jpmschweitzer", ) assert response.deleted is False def test_bulk_delete_response(self): """Test VolatileBulkDeleteResponse model.""" response = VolatileBulkDeleteResponse( namespace="weather", deleted_count=5, user="jpmschweitzer", ) assert response.deleted_count == 5 assert response.namespace == "weather" class TestVolatileService: """Test VolatileCacheService functionality.""" @pytest.fixture def mock_redis(self): """Create mock Redis client.""" redis = AsyncMock() redis.get = AsyncMock(return_value=None) redis.setex = AsyncMock() redis.delete = AsyncMock(return_value=1) redis.ttl = AsyncMock(return_value=1500) redis.scan_iter = MagicMock(return_value=iter([])) return redis @pytest.fixture def mock_settings(self): """Create mock settings.""" settings = MagicMock() settings.volatile_default_ttl = 3600 return settings @pytest.fixture def volatile_service(self, mock_redis, mock_settings): """Create VolatileCacheService with mocks.""" from src.services.volatile_service import VolatileCacheService return VolatileCacheService( redis_client=mock_redis, settings=mock_settings ) def test_build_key(self, volatile_service): """Test Redis key building.""" key = volatile_service._build_key("jpmschweitzer", "weather", "rotterdam") assert key.startswith("jpmschweitzer:volatile:weather:") assert len(key) > 30 # Has hash suffix def test_build_pattern(self, volatile_service): """Test pattern building.""" pattern = volatile_service._build_pattern("jpmschweitzer", "weather") assert pattern == "jpmschweitzer:volatile:weather:*" def test_build_pattern_all(self, volatile_service): """Test pattern building for all namespaces.""" pattern = volatile_service._build_pattern("jpmschweitzer") assert pattern == "jpmschweitzer:volatile:*" def test_get_default_ttl_known_namespace(self, volatile_service): """Test default TTL for known namespace.""" ttl = volatile_service._get_default_ttl("weather") assert ttl == 1800 # Weather namespace default def test_get_default_ttl_unknown_namespace(self, volatile_service): """Test default TTL for unknown namespace.""" ttl = volatile_service._get_default_ttl("unknown_namespace") assert ttl == 3600 # Falls back to settings default @pytest.mark.asyncio async def test_get_not_found(self, volatile_service, mock_redis): """Test get when record not found.""" mock_redis.get.return_value = None result = await volatile_service.get("jpmschweitzer", "weather", "rotterdam") assert result is None @pytest.mark.asyncio async def test_delete_success(self, volatile_service, mock_redis): """Test successful delete.""" mock_redis.delete.return_value = 1 result = await volatile_service.delete("jpmschweitzer", "weather", "rotterdam") assert result is True @pytest.mark.asyncio async def test_delete_not_found(self, volatile_service, mock_redis): """Test delete when record not found.""" mock_redis.delete.return_value = 0 result = await volatile_service.delete("jpmschweitzer", "weather", "nonexistent") assert result is False