Files
Celes Renata 898f89926d feat: beta API integration test suite — 85 new tests across 6 modules
Extends integration test coverage from 108 to 193 tests for the beta gate.

New test modules:
- test_query_api_extended.py (33 tests): documents, evidence, macro/competitive, ops/admin, agents, analytics
- test_registry_write_paths.py (16 tests): write paths, validation, duplicates, competitor/exposure CRUD
- test_risk_approval_lifecycle.py (8 tests): evaluation edge cases, full approval lifecycle
- test_trading_extended.py (12 tests): config round-trips, decision filtering, override validation
- test_cross_service_roundtrip.py (4 tests): cross-service data consistency
- test_error_handling.py (12 tests): 404s, 422s, empty states, health checks

Seed script extended with watchlists, approvals, lockouts, notifications,
ingestion runs, saved queries, and daily risk snapshots.
2026-04-20 02:34:19 +00:00

171 lines
6.5 KiB
Python

"""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