66 lines
2.2 KiB
Python
66 lines
2.2 KiB
Python
|
|
"""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
|