+67
@@ -69,6 +69,22 @@ class FailingSetRedisClient:
|
||||
return 1
|
||||
|
||||
|
||||
class RecordingCacheMetrics:
|
||||
def __init__(self) -> None:
|
||||
self.requests: list[tuple[str, str]] = []
|
||||
self.errors: list[tuple[str, str]] = []
|
||||
self.hits: list[bool] = []
|
||||
|
||||
def record_request(self, *, operation: str, outcome: str) -> None:
|
||||
self.requests.append((operation, outcome))
|
||||
|
||||
def record_error(self, *, operation: str, error_type: str) -> None:
|
||||
self.errors.append((operation, error_type))
|
||||
|
||||
def record_hit(self, *, hit: bool) -> None:
|
||||
self.hits.append(hit)
|
||||
|
||||
|
||||
def test_cache_miss_returns_none() -> None:
|
||||
cache = PriceCache(InMemoryRedisClient(), ttl_seconds=120)
|
||||
|
||||
@@ -263,3 +279,54 @@ def test_set_raises_deterministic_repository_error_on_redis_failure() -> None:
|
||||
|
||||
with pytest.raises(PriceCacheRepositoryError, match="Redis cache write failed."):
|
||||
asyncio.run(cache.set("key", {"v": 1}))
|
||||
|
||||
|
||||
def test_cache_metrics_record_hits_misses_and_successful_writes() -> None:
|
||||
redis_client = InMemoryRedisClient()
|
||||
metrics = RecordingCacheMetrics()
|
||||
cache = PriceCache(redis_client, ttl_seconds=10, metrics=metrics)
|
||||
|
||||
async def scenario() -> None:
|
||||
await cache.get("missing")
|
||||
await cache.set("key", {"v": 1})
|
||||
await cache.get("key")
|
||||
await cache.invalidate("key")
|
||||
|
||||
asyncio.run(scenario())
|
||||
|
||||
assert metrics.requests == [
|
||||
("get", "miss"),
|
||||
("set", "success"),
|
||||
("get", "hit"),
|
||||
("invalidate", "success"),
|
||||
]
|
||||
assert metrics.errors == []
|
||||
assert metrics.hits == [False, True]
|
||||
|
||||
|
||||
def test_cache_failure_records_metrics_and_structured_log(monkeypatch) -> None:
|
||||
events: list[tuple[str, dict[str, object]]] = []
|
||||
metrics = RecordingCacheMetrics()
|
||||
cache = PriceCache(FailingGetRedisClient(), ttl_seconds=10, metrics=metrics)
|
||||
|
||||
class StubLogger:
|
||||
def warning(self, event: str, **context: object) -> None:
|
||||
events.append((event, context))
|
||||
|
||||
monkeypatch.setattr(
|
||||
"app.repositories.cache.redis_cache.logger",
|
||||
StubLogger(),
|
||||
)
|
||||
|
||||
with pytest.raises(PriceCacheRepositoryError, match="Redis cache read failed."):
|
||||
asyncio.run(cache.get("key"))
|
||||
|
||||
assert metrics.requests == [("get", "error")]
|
||||
assert metrics.errors == [("get", "RuntimeError")]
|
||||
assert metrics.hits == []
|
||||
assert events == [
|
||||
(
|
||||
"cache.operation_failed",
|
||||
{"operation": "get", "error_type": "RuntimeError"},
|
||||
)
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user