"""Integration tests for Trading Engine — extended coverage. Validates configuration round-trips, pause/resume lifecycle, metrics consistency, notification config, decision filtering, and override order validation against the live sandbox. Uses the ``trading_client`` fixture from conftest.py. """ import pytest pytestmark = pytest.mark.asyncio # --------------------------------------------------------------------------- # 1 Config Round-Trip # --------------------------------------------------------------------------- class TestTradingConfigRoundTrip: """PUT config → GET status — verify risk_tier reflected.""" async def test_config_round_trip(self, trading_client): """PUT config → GET status — verify risk_tier reflected.""" # Set to aggressive resp = await trading_client.put( "/api/trading/config", json={"risk_tier": "aggressive"} ) assert resp.status_code == 200 # Verify in status status_resp = await trading_client.get("/api/trading/status") assert status_resp.status_code == 200 assert status_resp.json()["risk_tier"] == "aggressive" # Restore to moderate await trading_client.put( "/api/trading/config", json={"risk_tier": "moderate"} ) # --------------------------------------------------------------------------- # 2 Pause/Resume Round-Trip # --------------------------------------------------------------------------- class TestTradingPauseResumeRoundTrip: """POST pause → GET status → POST resume → GET status.""" async def test_pause_resume_round_trip(self, trading_client): """POST pause → GET status → POST resume → GET status.""" # Pause resp = await trading_client.post("/api/trading/pause") assert resp.status_code == 200 # Verify paused status_resp = await trading_client.get("/api/trading/status") assert status_resp.status_code == 200 assert status_resp.json()["paused"] is True # Resume resp2 = await trading_client.post("/api/trading/resume") assert resp2.status_code == 200 # Verify resumed status_resp2 = await trading_client.get("/api/trading/status") assert status_resp2.status_code == 200 assert status_resp2.json()["paused"] is False # --------------------------------------------------------------------------- # 3 Metrics Consistency # --------------------------------------------------------------------------- class TestTradingMetricsConsistency: """GET /api/trading/metrics — fields are present and non-negative.""" async def test_metrics_consistency(self, trading_client): """GET /api/trading/metrics — all fields present and non-negative.""" resp = await trading_client.get("/api/trading/metrics") assert resp.status_code == 200 data = resp.json() assert data["total_portfolio_value"] >= 0 assert data["active_pool"] >= 0 assert data["reserve_pool"] >= 0 # active_pool + reserve_pool should not exceed total assert data["active_pool"] + data["reserve_pool"] <= data["total_portfolio_value"] + 1.0 # --------------------------------------------------------------------------- # 4 Metrics History # --------------------------------------------------------------------------- class TestTradingMetricsHistoryExtended: """GET /api/trading/metrics/history — returns portfolio snapshots.""" async def test_metrics_history_snapshots(self, trading_client): """GET /api/trading/metrics/history — returns portfolio snapshots.""" resp = await trading_client.get("/api/trading/metrics/history") assert resp.status_code == 200 data = resp.json() assert isinstance(data, list) if len(data) >= 1: snap = data[0] assert "portfolio_value" in snap assert "snapshot_date" in snap # --------------------------------------------------------------------------- # 5 Notification Config Round-Trip # --------------------------------------------------------------------------- class TestTradingNotificationRoundTrip: """PUT → GET notification config round-trip.""" async def test_notification_config_round_trip(self, trading_client): """PUT → GET notification config round-trip.""" # Update phone number (which drives sms_enabled) resp = await trading_client.put( "/api/trading/notifications/config", json={"phone_number": "+15551234567"}, ) assert resp.status_code == 200 # Verify GET returns config structure get_resp = await trading_client.get("/api/trading/notifications/config") assert get_resp.status_code == 200 data = get_resp.json() assert "sms_enabled" in data assert "email_enabled" in data assert isinstance(data["sms_enabled"], bool) assert isinstance(data["email_enabled"], bool) # --------------------------------------------------------------------------- # 6 Notification History # --------------------------------------------------------------------------- class TestTradingNotificationHistory: """GET /api/trading/notifications/history — returns list.""" async def test_notification_history(self, trading_client): """GET /api/trading/notifications/history — returns list.""" resp = await trading_client.get("/api/trading/notifications/history") assert resp.status_code == 200 data = resp.json() assert isinstance(data, list) # --------------------------------------------------------------------------- # 7 Decision Filtering # --------------------------------------------------------------------------- class TestTradingDecisionFiltering: """GET /api/trading/decisions with various filters.""" async def test_decisions_filter_by_ticker(self, trading_client): """GET /api/trading/decisions?ticker=AAPL — only AAPL decisions.""" resp = await trading_client.get( "/api/trading/decisions", params={"ticker": "AAPL"} ) assert resp.status_code == 200 data = resp.json() assert isinstance(data, list) for d in data: assert d["ticker"] == "AAPL" async def test_decisions_filter_by_limit(self, trading_client): """GET /api/trading/decisions?limit=1 — at most 1 decision.""" resp = await trading_client.get( "/api/trading/decisions", params={"limit": "1"} ) assert resp.status_code == 200 data = resp.json() assert isinstance(data, list) assert len(data) <= 1 async def test_decisions_filter_by_decision_type(self, trading_client): """GET /api/trading/decisions?decision=execute — only execute decisions.""" resp = await trading_client.get( "/api/trading/decisions", params={"decision": "execute"} ) assert resp.status_code == 200 data = resp.json() assert isinstance(data, list) for d in data: assert d["decision"] == "execute" # --------------------------------------------------------------------------- # 8 Override Order Validation # --------------------------------------------------------------------------- class TestTradingOverrideValidation: """POST /api/trading/override/order — validation edge cases.""" async def test_override_invalid_ticker(self, trading_client): """POST /api/trading/override/order — invalid ticker returns 422.""" payload = { "ticker": "AAPL123", # contains digits — should be rejected "side": "buy", "quantity": 1.0, "order_type": "market", } resp = await trading_client.post( "/api/trading/override/order", json=payload ) assert resp.status_code == 422 async def test_override_zero_quantity(self, trading_client): """POST /api/trading/override/order — zero quantity returns 422.""" payload = { "ticker": "AAPL", "side": "buy", "quantity": 0, "order_type": "market", } resp = await trading_client.post( "/api/trading/override/order", json=payload ) assert resp.status_code == 422 async def test_override_valid_order(self, trading_client): """POST /api/trading/override/order — valid order returns 202 or structured error.""" payload = { "ticker": "AAPL", "side": "buy", "quantity": 1.0, "order_type": "market", } resp = await trading_client.post( "/api/trading/override/order", json=payload ) assert resp.status_code in (200, 202, 400, 422, 503) data = resp.json() if resp.status_code == 202: assert "job_id" in data assert data["status"] == "queued" assert data["ticker"] == "AAPL" assert data["side"] == "buy" assert data["quantity"] == 1.0 else: assert isinstance(data, dict)