wrongmove/tests/unit/test_unhandled_exception.py
Viktor Barzin 1ace45353a
Add API anti-abuse hardening: disable docs in prod, origin validator, exception handler
- Disable OpenAPI docs/redoc/openapi.json when APP_ENV=production
- Strip uvicorn Server header with --no-server-header in Dockerfile and docker-compose.yml
- Add OriginValidatorMiddleware to reject state-changing requests from disallowed origins
- Add global exception handler to prevent stack trace leakage on unhandled errors
- Add tests for all new security features (OpenAPI, origin validation, exception handler, server header)
2026-02-08 20:06:46 +00:00

65 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