"""Integration tests for global error handling and envelope format consistency. Tests that all error responses from the FastAPI app conform to the standard envelope: {"success": false, "data": null, "error": "..."}. """ from __future__ import annotations from unittest.mock import MagicMock import pytest from httpx import ASGITransport, AsyncClient pytestmark = pytest.mark.integration def _build_app(): """Build the actual FastAPI app with exception handlers but mocked state.""" from fastapi import FastAPI, HTTPException from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse from app.analytics.api import router as analytics_router from app.api_utils import envelope from app.replay.api import router as replay_router test_app = FastAPI() test_app.include_router(analytics_router) test_app.include_router(replay_router) @test_app.exception_handler(HTTPException) async def _http_exc(request, exc): return JSONResponse( status_code=exc.status_code, content=envelope(None, success=False, error=exc.detail), ) @test_app.exception_handler(RequestValidationError) async def _validation_exc(request, exc): return JSONResponse( status_code=422, content=envelope(None, success=False, error=str(exc)), ) @test_app.exception_handler(Exception) async def _catch_all(request, exc): return JSONResponse( status_code=500, content=envelope(None, success=False, error="Internal server error"), ) @test_app.get("/api/v1/health") def health_check(): return {"status": "ok", "version": "0.6.0"} test_app.state.settings = MagicMock(admin_api_key="") test_app.state.pool = MagicMock() return test_app class TestEnvelopeFormat: """Tests that error responses consistently follow envelope format.""" async def test_http_400_produces_envelope(self) -> None: """A 400 error returns standard envelope with success=false.""" test_app = _build_app() async with AsyncClient( transport=ASGITransport(app=test_app), base_url="http://test" ) as client: resp = await client.get("/api/v1/analytics", params={"range": "invalid"}) assert resp.status_code == 400 body = resp.json() assert body["success"] is False assert body["data"] is None assert isinstance(body["error"], str) assert len(body["error"]) > 0 async def test_validation_error_produces_422_envelope(self) -> None: """Invalid query param type returns 422 with envelope format.""" test_app = _build_app() async with AsyncClient( transport=ASGITransport(app=test_app), base_url="http://test" ) as client: # page must be >= 1; passing 0 triggers validation error resp = await client.get("/api/v1/conversations", params={"page": 0}) assert resp.status_code == 422 body = resp.json() assert body["success"] is False assert body["data"] is None assert isinstance(body["error"], str) async def test_all_error_fields_present(self) -> None: """Error envelope contains exactly success, data, and error keys.""" test_app = _build_app() async with AsyncClient( transport=ASGITransport(app=test_app), base_url="http://test" ) as client: resp = await client.get("/api/v1/analytics", params={"range": "bad"}) body = resp.json() assert set(body.keys()) == {"success", "data", "error"} async def test_health_endpoint_returns_200(self) -> None: """Health check returns 200 with status ok.""" test_app = _build_app() async with AsyncClient( transport=ASGITransport(app=test_app), base_url="http://test" ) as client: resp = await client.get("/api/v1/health") assert resp.status_code == 200 body = resp.json() assert body["status"] == "ok" assert "version" in body async def test_unknown_endpoint_returns_404(self) -> None: """Requesting a non-existent path returns 404.""" test_app = _build_app() async with AsyncClient( transport=ASGITransport(app=test_app), base_url="http://test" ) as client: resp = await client.get("/api/v1/nonexistent-path") # FastAPI returns 404 for unknown routes; may or may not be wrapped assert resp.status_code == 404