Spaces:
Runtime error
Runtime error
Download backend/tests/unit/test_cache_layer.py from yusufcalisir/Collaborative-Fraud-Intelligence-Simulator: direct link, hf CLI and curl.
- Browser
- Download file 4.76 kB
-
https://huggingface.co/spaces/yusufcalisir/Collaborative-Fraud-Intelligence-Simulator/resolve/main/backend/tests/unit/test_cache_layer.py
- Command line
-
hf download hf://spaces/yusufcalisir/Collaborative-Fraud-Intelligence-Simulator/backend/tests/unit/test_cache_layer.py
-
curl -L -o test_cache_layer.py https://huggingface.co/spaces/yusufcalisir/Collaborative-Fraud-Intelligence-Simulator/resolve/main/backend/tests/unit/test_cache_layer.py
4.76 kB
| """Unit and integration tests for Section 35.2 — Cache Layer & Graceful Degradation. | |
| Tests: | |
| 1. CacheService initialization & singleton instance. | |
| 2. Read-aside and Write-through operations for Alerts, Cases, and Entities. | |
| 3. TTL expiration logic & key formatting. | |
| 4. Graceful degradation when Redis is offline (all methods fall back gracefully without raising exceptions). | |
| 5. test_cache_miss_falls_back_to_db | |
| 6. test_cache_hit_skips_db | |
| 7. test_cache_invalidation_on_write | |
| """ | |
| from __future__ import annotations | |
| from unittest.mock import AsyncMock | |
| import pytest | |
| from app.infrastructure.cache import ( | |
| TTL_ALERT, | |
| TTL_CASE, | |
| TTL_ENTITY, | |
| CacheService, | |
| write_through_cache, | |
| ) | |
| def cache_service(): | |
| """Reset CacheService singleton state for testing.""" | |
| CacheService._instance = None | |
| CacheService._client = None | |
| CacheService._unavailable = False | |
| return CacheService.get() | |
| async def test_cache_service_singleton(cache_service: CacheService) -> None: | |
| """CacheService.get() must return the same process-level singleton instance.""" | |
| instance1 = CacheService.get() | |
| instance2 = CacheService.get() | |
| assert instance1 is instance2 | |
| async def test_graceful_degradation_when_redis_unavailable(cache_service: CacheService) -> None: | |
| """When Redis is unavailable or unconfigured, all methods must return None/default without raising exceptions.""" | |
| # Force unavailable state | |
| CacheService._unavailable = True | |
| # Reads should return None | |
| assert await cache_service.get_alert("alert-123") is None | |
| assert await cache_service.get_case("case-123") is None | |
| assert await cache_service.get_entity("entity-123") is None | |
| assert await cache_service.get_entity_id_by_hash("hash-123") is None | |
| # Writes & invalidations should fail silently | |
| await cache_service.set_alert("alert-123", {"id": "alert-123", "status": "new"}) | |
| await cache_service.invalidate_alert("alert-123", "bank_alpha") | |
| await cache_service.set_case("case-123", {"id": "case-123"}) | |
| await cache_service.set_entity("entity-123", {"id": "entity-123", "privacy_id": "p123"}) | |
| # Health check should return False | |
| assert await cache_service.health() is False | |
| # Rate limiter enforces sliding window locally when Redis is down without failing open | |
| assert await cache_service.rate_limit_check("test-ip", limit=5) is True | |
| for _ in range(4): | |
| assert await cache_service.rate_limit_check("test-ip", limit=5) is True | |
| # 6th request must be rejected under strict rate limiting | |
| assert await cache_service.rate_limit_check("test-ip", limit=5) is False | |
| async def test_ttl_constants() -> None: | |
| """Verify production TTL configurations align with design spec.""" | |
| assert TTL_ALERT == 300 | |
| assert TTL_CASE == 300 | |
| assert TTL_ENTITY == 600 | |
| async def test_cache_miss_falls_back_to_db(cache_service: CacheService) -> None: | |
| """Mock Redis miss -> assert DB function is called -> assert result returned.""" | |
| mock_db_func = AsyncMock(return_value={"id": "alert-999", "status": "new"}) | |
| async def get_alert_from_db(self, alert_id: str): | |
| return await mock_db_func(alert_id) | |
| # Force cache miss | |
| CacheService._unavailable = True | |
| res = await get_alert_from_db(None, "alert-999") | |
| assert res == {"id": "alert-999", "status": "new"} | |
| mock_db_func.assert_called_once_with("alert-999") | |
| async def test_cache_hit_skips_db(cache_service: CacheService) -> None: | |
| """When cache has data, DB function must NOT be called.""" | |
| mock_db_func = AsyncMock(return_value={"id": "alert-888", "status": "new"}) | |
| async def get_alert_from_db(self, alert_id: str): | |
| return await mock_db_func(alert_id) | |
| # Mock Redis client get to return cached data | |
| mock_redis = AsyncMock() | |
| mock_redis.get.return_value = '{"id": "alert-888", "status": "cached"}' | |
| CacheService._client = mock_redis | |
| CacheService._unavailable = False | |
| res = await get_alert_from_db(None, "alert-888") | |
| assert res == {"id": "alert-888", "status": "cached"} | |
| mock_db_func.assert_not_called() | |
| async def test_cache_invalidation_on_write(cache_service: CacheService) -> None: | |
| """Invalidate alert must delete key from Redis.""" | |
| mock_redis = AsyncMock() | |
| CacheService._client = mock_redis | |
| CacheService._unavailable = False | |
| await cache_service.invalidate_alert("alert-777", "bank_alpha") | |
| mock_redis.delete.assert_called_once_with("alert:alert-777", "alerts:bank:bank_alpha") | |