898f89926d
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.
171 lines
6.5 KiB
Python
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
|