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.
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user