"""Unit tests for the unhandled exception handler in api/app.py.""" from unittest import mock import pytest from fastapi import FastAPI, HTTPException from fastapi.responses import JSONResponse from starlette.requests import Request from starlette.testclient import TestClient def _build_app() -> FastAPI: """Build a minimal FastAPI app with the unhandled exception handler.""" app = FastAPI() @app.exception_handler(Exception) async def unhandled_exception_handler(request: Request, exc: Exception) -> JSONResponse: return JSONResponse( status_code=500, content={"detail": "Internal server error"}, ) @app.get("/ok") async def ok() -> dict[str, str]: return {"status": "ok"} @app.get("/crash") async def crash() -> None: raise RuntimeError("something unexpected") @app.get("/http-error") async def http_error() -> None: raise HTTPException(status_code=403, detail="forbidden") return app class TestUnhandledExceptionHandler: """Tests for the global exception handler.""" def test_normal_request_unaffected(self) -> None: client = TestClient(_build_app()) resp = client.get("/ok") assert resp.status_code == 200 assert resp.json() == {"status": "ok"} def test_unhandled_exception_returns_generic_500(self) -> None: client = TestClient(_build_app(), raise_server_exceptions=False) resp = client.get("/crash") assert resp.status_code == 500 assert resp.json() == {"detail": "Internal server error"} def test_http_exception_not_caught(self) -> None: """HTTPException should still be handled by FastAPI's built-in handler.""" client = TestClient(_build_app()) resp = client.get("/http-error") assert resp.status_code == 403 assert resp.json() == {"detail": "forbidden"} def test_error_body_does_not_leak_details(self) -> None: """The response must not contain the actual exception message.""" client = TestClient(_build_app(), raise_server_exceptions=False) resp = client.get("/crash") body = resp.text assert "something unexpected" not in body assert "RuntimeError" not in body