from __future__ import annotations import json import logging from pathlib import Path from fastapi import FastAPI from fastapi.testclient import TestClient from core.config.settings import Settings from core.logging.config import configure_logging from core.logging.logger import get_logger from core.logging.middleware import ( RequestContextMiddleware, register_exception_handlers, ) def _read_json_lines(path: Path) -> list[dict[str, object]]: return [json.loads(line) for line in path.read_text().splitlines() if line.strip()] def _configure_test_logging(tmp_path: Path) -> None: settings = Settings() runtime = settings.runtime.model_copy( update={ "log_dir": str(tmp_path), "log_error_dir": str(tmp_path / "errors"), "log_rotation": "size", "log_rotation_max_bytes": 2048, } ) test_settings = settings.model_copy(update={"runtime": runtime}) configure_logging(test_settings) def test_middleware_binds_request_context(tmp_path: Path) -> None: _configure_test_logging(tmp_path) app = FastAPI() app.add_middleware(RequestContextMiddleware) # type: ignore[arg-type] @app.get("/ok") async def ok() -> dict[str, str]: logger = get_logger("tests.ok") logger.info("request accepted", context_key="context_value") return {"status": "ok"} client = TestClient(app) response = client.get("/ok", headers={"X-Request-ID": "req-1234"}) assert response.status_code == 200 assert response.headers["X-Request-ID"] == "req-1234" log_entries = _read_json_lines(Path(tmp_path) / "app.log") entry = next( item for item in log_entries if item.get("message") == "request accepted" ) assert entry["message"] == "request accepted" assert entry["request_id"] == "req-1234" assert entry["method"] == "GET" assert entry["path"] == "/ok" assert entry["context_key"] == "context_value" logging.shutdown() def test_exception_handler_logs_stack_and_sends_500(tmp_path: Path) -> None: _configure_test_logging(tmp_path) app = FastAPI() app.add_middleware(RequestContextMiddleware) register_exception_handlers(app) @app.get("/boom") async def boom() -> dict[str, str]: raise RuntimeError("boom") client = TestClient(app, raise_server_exceptions=False) response = client.get("/boom", headers={"X-Request-ID": "req-5000"}) assert response.status_code == 500 assert response.json()["detail"] == "Internal Server Error" error_entries = _read_json_lines(Path(tmp_path) / "errors" / "error.log") assert error_entries entry = error_entries[-1] assert entry["level"] == "error" assert entry["request_id"] == "req-5000" exception = str(entry["exception"]) assert "Traceback" in exception assert "test_fastapi_logging_integration" in exception logging.shutdown() def test_invalid_request_id_is_replaced_and_used_in_error_context( tmp_path: Path, ) -> None: _configure_test_logging(tmp_path) app = FastAPI() app.add_middleware(RequestContextMiddleware) register_exception_handlers(app) @app.get("/boom") async def boom() -> dict[str, str]: raise RuntimeError("boom") client = TestClient(app, raise_server_exceptions=False) response = client.get("/boom", headers={"X-Request-ID": "bad"}) assert response.status_code == 500 response_request_id = response.headers["X-Request-ID"] assert response_request_id != "bad" error_entries = _read_json_lines(Path(tmp_path) / "errors" / "error.log") assert error_entries entry = error_entries[-1] assert entry["request_id"] == response_request_id exception = str(entry["exception"]) assert "Traceback" in exception logging.shutdown()