"""Error-handling regression tests. The previous global handler returned ``str(exc)`` in the response body, which for a SQLAlchemy failure includes the connection URL and its password. """ from __future__ import annotations import pytest from fastapi.testclient import TestClient from app.core.config import Settings from app.db.deps import get_db from app.main import create_app LEAKY_MESSAGE = ( "(psycopg2.OperationalError) connection to server at " "postgresql://admin:hunter2@db.example.com:5432/facevision failed" ) def _app_with_exploding_db(environment: str) -> TestClient: settings = Settings( environment=environment, database_url="sqlite://", FACEVISION_INGEST_KEY="ingest", log_level="CRITICAL", ) app = create_app(settings) class ExplodingSession: def execute(self, *_args, **_kwargs): raise RuntimeError(LEAKY_MESSAGE) def close(self) -> None: pass def exploding_db(): yield ExplodingSession() app.dependency_overrides[get_db] = exploding_db return TestClient(app, raise_server_exceptions=False) class TestInternalErrorsDoNotLeak: def test_production_body_omits_the_exception_text(self): client = _app_with_exploding_db("production") response = client.get("/api/logs", headers={"X-Session-Id": "error-test-session-01"}) assert response.status_code == 500 body = response.text assert "hunter2" not in body assert "postgresql://" not in body assert "Traceback" not in body def test_production_body_carries_a_request_id(self): client = _app_with_exploding_db("production") body = client.get("/api/logs", headers={"X-Session-Id": "error-test-session-01"}).json() assert body["message"] == "Internal Server Error" assert body["request_id"] def test_development_includes_detail_for_debugging(self): client = _app_with_exploding_db("development") body = client.get("/api/logs", headers={"X-Session-Id": "error-test-session-01"}).json() assert "detail" in body class TestValidationErrors: def test_validation_failure_reports_field_errors(self, client, ingest_headers): response = client.post( "/api/logs", json={"dominant_emotion": "Nope", "duration_seconds": "abc"}, headers=ingest_headers, ) assert response.status_code == 422 body = response.json() assert body["message"] == "Validation failed" assert body["errors"] class TestRequestId: def test_successful_responses_carry_a_request_id_header(self, client): response = client.get("/api/health") assert response.headers.get("X-Request-ID") class TestSecurityHeaders: @pytest.mark.parametrize( ("header", "value"), [ ("Cross-Origin-Opener-Policy", "same-origin"), ("Cross-Origin-Embedder-Policy", "require-corp"), ("X-Content-Type-Options", "nosniff"), ], ) def test_isolation_headers_are_present(self, client, header, value): """next.config.ts declares these but they are inert under output: 'export'.""" assert client.get("/api/health").headers.get(header) == value