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,136 @@
|
||||
"""Integration tests for Risk Engine — evaluation edge cases and approval lifecycle.
|
||||
|
||||
Validates evaluation with minimal/extreme orders, custom config overrides,
|
||||
and the full approval lifecycle (list → detail → review → expire) against
|
||||
the live sandbox with deterministic seed data.
|
||||
|
||||
Uses the ``risk_client`` and ``seed_ids`` fixtures from conftest.py.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
pytestmark = pytest.mark.asyncio
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1 Evaluation Edge Cases
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestRiskEvaluationEdgeCases:
|
||||
"""Edge-case scenarios for POST /evaluate."""
|
||||
|
||||
async def test_minimal_order_evaluation(self, risk_client):
|
||||
"""POST /evaluate — minimal order with only ticker."""
|
||||
payload = {"order": {"ticker": "AAPL"}}
|
||||
resp = await risk_client.post("/evaluate", json=payload)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert "evaluation_id" in data
|
||||
assert "eligible" in data
|
||||
assert "rejection_reasons" in data
|
||||
|
||||
async def test_order_exceeding_position_cap(self, risk_client):
|
||||
"""POST /evaluate — order exceeding position cap returns eligible: false."""
|
||||
payload = {
|
||||
"order": {
|
||||
"ticker": "AAPL",
|
||||
"action": "buy",
|
||||
"quantity": 100000,
|
||||
"estimated_value": 18550000.00,
|
||||
"confidence": 0.5,
|
||||
"sector": "Technology",
|
||||
},
|
||||
}
|
||||
resp = await risk_client.post("/evaluate", json=payload)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["eligible"] is False
|
||||
assert len(data["rejection_reasons"]) >= 1
|
||||
|
||||
async def test_evaluation_with_custom_config(self, risk_client):
|
||||
"""POST /evaluate — custom config override."""
|
||||
payload = {
|
||||
"order": {
|
||||
"ticker": "MSFT",
|
||||
"action": "buy",
|
||||
"quantity": 5,
|
||||
"estimated_value": 2050.00,
|
||||
"confidence": 0.8,
|
||||
},
|
||||
"config": {
|
||||
"max_portfolio_heat": 0.50,
|
||||
"max_single_position_pct": 0.10,
|
||||
"max_sector_concentration": 0.50,
|
||||
"daily_loss_limit_pct": 0.05,
|
||||
},
|
||||
}
|
||||
resp = await risk_client.post("/evaluate", json=payload)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert "evaluation_id" in data
|
||||
assert "eligible" in data
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 2 Approval Lifecycle
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestRiskApprovalLifecycle:
|
||||
"""Full approval lifecycle: list → detail → review → expire."""
|
||||
|
||||
async def test_pending_approvals_list(self, risk_client, seed_ids):
|
||||
"""GET /approvals/pending — list pending approvals from seed."""
|
||||
resp = await risk_client.get("/approvals/pending")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert isinstance(data, list)
|
||||
assert len(data) >= 1
|
||||
|
||||
async def test_approval_detail(self, risk_client, seed_ids):
|
||||
"""GET /approvals/{id} — seeded pending approval detail."""
|
||||
approval_id = seed_ids["approvals"]["PENDING"]
|
||||
resp = await risk_client.get(f"/approvals/{approval_id}")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["ticker"] == "AAPL"
|
||||
assert data["side"] == "buy"
|
||||
assert "status" in data
|
||||
assert "expires_at" in data
|
||||
|
||||
async def test_approval_review(self, risk_client, seed_ids):
|
||||
"""POST /approvals/{id}/review — approve the seeded pending approval."""
|
||||
approval_id = seed_ids["approvals"]["PENDING"]
|
||||
payload = {
|
||||
"approved": True,
|
||||
"reviewed_by": "test-operator",
|
||||
"review_note": "Integration test approval",
|
||||
}
|
||||
resp = await risk_client.post(
|
||||
f"/approvals/{approval_id}/review", json=payload,
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["status"] == "approved"
|
||||
|
||||
async def test_review_nonexistent_approval(self, risk_client):
|
||||
"""POST /approvals/{id}/review — 404 for non-existent approval."""
|
||||
fake_id = "00000000-0000-4000-ffff-000000000099"
|
||||
payload = {
|
||||
"approved": True,
|
||||
"reviewed_by": "test-operator",
|
||||
"review_note": "Should fail",
|
||||
}
|
||||
resp = await risk_client.post(
|
||||
f"/approvals/{fake_id}/review", json=payload,
|
||||
)
|
||||
assert resp.status_code == 404
|
||||
|
||||
async def test_approval_expiry(self, risk_client):
|
||||
"""POST /approvals/expire — expire stale approvals."""
|
||||
resp = await risk_client.post("/approvals/expire")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert "expired" in data
|
||||
assert isinstance(data["expired"], int)
|
||||
Reference in New Issue
Block a user