import json from typing import Optional from fastapi import FastAPI, HTTPException from fastapi.testclient import TestClient from pydantic import BaseModel, Field from app.middleware.logging import LoggingMiddleware from app.middleware.errors import register_exception_handlers class Item(BaseModel): name: str = Field(..., min_length=3) quantity: int = Field(..., ge=1) def build_test_app() -> FastAPI: app = FastAPI() app.add_middleware(LoggingMiddleware, log_requests=False, log_responses=False) register_exception_handlers(app) @app.get("/http-error") async def http_error(): raise HTTPException(status_code=403, detail="Forbidden action") @app.post("/validation") async def validation_endpoint(item: Item): # noqa: F841 return {"ok": True} @app.get("/crash") async def crash(): raise RuntimeError("Boom") return app def assert_envelope(resp, status: int, code: str, has_details: bool, expected_cid: Optional[str] = None): assert resp.status_code == status data = resp.json() assert data["success"] is False assert data["error"]["status"] == status assert data["error"]["code"] == code assert isinstance(data["error"]["message"], str) and data["error"]["message"] if has_details: assert "details" in data["error"] else: assert "details" not in data["error"] # Correlation id in body and header assert "correlation_id" in data and isinstance(data["correlation_id"], str) header_cid = resp.headers.get("X-Correlation-ID") assert header_cid == data["correlation_id"] if expected_cid is not None: assert header_cid == expected_cid def test_http_exception_envelope_and_correlation_id_echo(): app = build_test_app() client = TestClient(app) cid = "abc-12345-test" resp = client.get("/http-error", headers={"X-Correlation-ID": cid}) assert_envelope(resp, 403, "http_error", has_details=False, expected_cid=cid) def test_validation_exception_envelope_and_correlation_id_echo(): app = build_test_app() client = TestClient(app) cid = "cid-validation-67890" # Missing fields to trigger 422 resp = client.post("/validation", json={"name": "ab"}, headers={"X-Correlation-ID": cid}) assert_envelope(resp, 422, "validation_error", has_details=True, expected_cid=cid) def test_unhandled_exception_envelope_and_generated_correlation_id(): app = build_test_app() client = TestClient(app, raise_server_exceptions=False) resp = client.get("/crash") # Should have generated a correlation id and not echo None assert_envelope(resp, 500, "internal_error", has_details=False) assert resp.headers.get("X-Correlation-ID")