"""Integration tests for error handling and empty-state behavior across all services. Validates that all endpoints return structured JSON errors (not HTML or stack traces) and handle empty-state gracefully, so the frontend never receives unexpected response formats. """ import pytest pytestmark = pytest.mark.asyncio # --------------------------------------------------------------------------- # Empty list endpoints # --------------------------------------------------------------------------- class TestEmptyListEndpoints: """List endpoints with no matching data return 200 with [].""" async def test_documents_empty_filter(self, query_client): """GET /api/documents?ticker=ZZZZ — no matching docs returns 200 with list.""" resp = await query_client.get("/api/documents", params={"ticker": "ZZZZ"}) assert resp.status_code == 200 data = resp.json() assert isinstance(data, list) assert len(data) == 0 # --------------------------------------------------------------------------- # Not-found detail endpoints # --------------------------------------------------------------------------- class TestNotFoundEndpoints: """Detail endpoints with non-existent UUID return 404 with JSON body.""" async def test_query_api_document_not_found(self, query_client): """GET /api/documents/{id} — 404 with JSON body.""" fake_id = "00000000-0000-4000-ffff-000000000099" resp = await query_client.get(f"/api/documents/{fake_id}") assert resp.status_code == 404 data = resp.json() assert isinstance(data, dict) async def test_registry_company_not_found(self, registry_client): """GET /companies/{id} — 404 with JSON body.""" fake_id = "00000000-0000-4000-ffff-000000000099" resp = await registry_client.get(f"/companies/{fake_id}") assert resp.status_code == 404 data = resp.json() assert isinstance(data, dict) async def test_risk_approval_not_found(self, risk_client): """GET /approvals/{id} — 404 with JSON body.""" fake_id = "00000000-0000-4000-ffff-000000000099" resp = await risk_client.get(f"/approvals/{fake_id}") assert resp.status_code == 404 data = resp.json() assert isinstance(data, dict) # --------------------------------------------------------------------------- # Validation errors # --------------------------------------------------------------------------- class TestValidationErrors: """Invalid JSON body returns 422 with validation details.""" async def test_invalid_company_body(self, registry_client): """POST /companies — missing required fields returns 422.""" resp = await registry_client.post("/companies", json={}) assert resp.status_code == 422 data = resp.json() assert isinstance(data, dict) assert "detail" in data # --------------------------------------------------------------------------- # Trend projection edge case # --------------------------------------------------------------------------- class TestTrendProjectionNoProjection: """Trend projection with no projection returns appropriate response (not 500).""" async def test_trend_projection_nonexistent(self, query_client): """GET /api/trends/{id}/projection — non-existent trend returns 404, not 500.""" fake_id = "00000000-0000-4000-ffff-000000000099" resp = await query_client.get(f"/api/trends/{fake_id}/projection") assert resp.status_code != 500 assert resp.status_code in (200, 404) # --------------------------------------------------------------------------- # Macro impacts empty state # --------------------------------------------------------------------------- class TestMacroImpactsEmpty: """Macro impacts with no data returns 200 with empty impacts list.""" async def test_macro_impacts_no_data(self, query_client): """GET /api/macro/impacts/ZZZZ — no impacts returns 200 with empty list.""" resp = await query_client.get("/api/macro/impacts/ZZZZ") assert resp.status_code == 200 data = resp.json() assert "impacts" in data assert isinstance(data["impacts"], list) assert len(data["impacts"]) == 0 # --------------------------------------------------------------------------- # Health endpoints across all services # --------------------------------------------------------------------------- class TestAllServiceHealthEndpoints: """All four service health endpoints return {"status": "ok"}.""" async def test_query_api_health(self, query_client): """GET /health — query API.""" resp = await query_client.get("/health") assert resp.status_code == 200 assert resp.json()["status"] == "ok" async def test_registry_health(self, registry_client): """GET /health — symbol registry.""" resp = await registry_client.get("/health") assert resp.status_code == 200 assert resp.json()["status"] == "ok" async def test_risk_health(self, risk_client): """GET /health — risk engine.""" resp = await risk_client.get("/health") assert resp.status_code == 200 assert resp.json()["status"] == "ok" async def test_trading_health(self, trading_client): """GET /health — trading engine.""" resp = await trading_client.get("/health") assert resp.status_code == 200 assert resp.json()["status"] == "ok" # --------------------------------------------------------------------------- # Override order structured error # --------------------------------------------------------------------------- class TestOverrideStructuredError: """Override order returns structured JSON error when Redis unavailable.""" async def test_override_structured_error(self, trading_client): """POST /api/trading/override/order — structured error (not unhandled exception).""" payload = { "ticker": "AAPL", "side": "buy", "quantity": 1.0, "order_type": "market", } resp = await trading_client.post("/api/trading/override/order", json=payload) # Accept 202 (success) or structured error assert resp.status_code in (200, 202, 400, 422, 503) data = resp.json() assert isinstance(data, dict) # If 503, verify it's JSON not an unhandled exception if resp.status_code == 503: assert "detail" in data or "error" in data or "message" in data