Files
2026-04-03 15:53:00 +01:00

88 lines
3.1 KiB
Python

"""Shared test helpers for observability / log field assertions."""
from __future__ import annotations
import logging
from fastapi import FastAPI, HTTPException
from fastapi.exceptions import RequestValidationError
from fastapi.testclient import TestClient
from starlette.exceptions import HTTPException as StarletteHTTPException
from core.exception_handlers import (
circuit_open_error_handler,
client_disconnected_handler,
configuration_error_handler,
external_service_error_handler,
general_exception_handler,
http_exception_handler,
request_validation_error_handler,
resource_not_found_handler,
source_resolution_error_handler,
starlette_http_exception_handler,
validation_error_handler,
)
from core.exceptions import (
ClientDisconnectedError,
ConfigurationError,
ExternalServiceError,
ResourceNotFoundError,
SourceResolutionError,
ValidationError,
)
from infrastructure.resilience.retry import CircuitOpenError
def add_production_exception_handlers(app: FastAPI) -> FastAPI:
app.add_exception_handler(ClientDisconnectedError, client_disconnected_handler)
app.add_exception_handler(ResourceNotFoundError, resource_not_found_handler)
app.add_exception_handler(ExternalServiceError, external_service_error_handler)
app.add_exception_handler(CircuitOpenError, circuit_open_error_handler)
app.add_exception_handler(ValidationError, validation_error_handler)
app.add_exception_handler(ConfigurationError, configuration_error_handler)
app.add_exception_handler(SourceResolutionError, source_resolution_error_handler)
app.add_exception_handler(HTTPException, http_exception_handler)
app.add_exception_handler(StarletteHTTPException, starlette_http_exception_handler)
app.add_exception_handler(RequestValidationError, request_validation_error_handler)
app.add_exception_handler(Exception, general_exception_handler)
return app
def build_test_client(app: FastAPI) -> TestClient:
add_production_exception_handlers(app)
return TestClient(app, raise_server_exceptions=False)
def assert_log_fields(
records: list[logging.LogRecord],
prefix: str,
required_fields: list[str],
*,
min_count: int = 1,
) -> list[str]:
"""Assert that log records matching *prefix* contain all *required_fields*.
Returns the matching messages for further inspection.
Parameters
----------
records:
``caplog.records`` or equivalent list of ``LogRecord``.
prefix:
The log message prefix to filter on (e.g. ``"audiodb.cache"``).
required_fields:
Key names that must appear as ``key=`` in every matching message.
min_count:
Minimum number of matching records expected (default 1).
"""
matching = [r.message for r in records if r.message.startswith(prefix)]
assert len(matching) >= min_count, (
f"Expected >= {min_count} log(s) starting with '{prefix}', found {len(matching)}"
)
for msg in matching:
for field in required_fields:
assert f"{field}=" in msg, (
f"Field '{field}=' missing in log: {msg}"
)
return matching