Portfolio optimization (3 endpoints): - POST /portfolio/optimize - HRP optimal weights via scipy clustering - POST /portfolio/correlation - pairwise correlation matrix - POST /portfolio/risk-parity - inverse-volatility risk parity weights Congress tracking (2 endpoints): - GET /regulators/congress/trades - congress member stock trades - GET /regulators/congress/bills?query= - search congress bills Implementation: - portfolio_service.py: HRP with scipy fallback to inverse-vol - congress_service.py: multi-provider fallback pattern - 51 new tests (14 portfolio unit, 20 portfolio route, 12 congress unit, 7 congress route) - All 312 tests passing
99 lines
3.4 KiB
Python
99 lines
3.4 KiB
Python
"""Tests for congress trading routes (TDD - RED phase first)."""
|
|
|
|
from unittest.mock import patch, AsyncMock
|
|
|
|
import pytest
|
|
from httpx import AsyncClient, ASGITransport
|
|
|
|
from main import app
|
|
|
|
|
|
@pytest.fixture
|
|
async def client():
|
|
transport = ASGITransport(app=app)
|
|
async with AsyncClient(transport=transport, base_url="http://test") as c:
|
|
yield c
|
|
|
|
|
|
# --- GET /api/v1/regulators/congress/trades ---
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("routes_regulators.congress_service.get_congress_trades", new_callable=AsyncMock)
|
|
async def test_congress_trades_happy_path(mock_fn, client):
|
|
mock_fn.return_value = [
|
|
{
|
|
"representative": "Nancy Pelosi",
|
|
"ticker": "NVDA",
|
|
"transaction_date": "2024-01-15",
|
|
"transaction_type": "Purchase",
|
|
"amount": "$1,000,001-$5,000,000",
|
|
}
|
|
]
|
|
resp = await client.get("/api/v1/regulators/congress/trades")
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert data["success"] is True
|
|
assert len(data["data"]) == 1
|
|
assert data["data"][0]["representative"] == "Nancy Pelosi"
|
|
mock_fn.assert_called_once()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("routes_regulators.congress_service.get_congress_trades", new_callable=AsyncMock)
|
|
async def test_congress_trades_empty(mock_fn, client):
|
|
mock_fn.return_value = []
|
|
resp = await client.get("/api/v1/regulators/congress/trades")
|
|
assert resp.status_code == 200
|
|
assert resp.json()["data"] == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("routes_regulators.congress_service.get_congress_trades", new_callable=AsyncMock)
|
|
async def test_congress_trades_service_error_returns_502(mock_fn, client):
|
|
mock_fn.side_effect = RuntimeError("Data provider unavailable")
|
|
resp = await client.get("/api/v1/regulators/congress/trades")
|
|
assert resp.status_code == 502
|
|
|
|
|
|
# --- GET /api/v1/regulators/congress/bills ---
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("routes_regulators.congress_service.search_congress_bills", new_callable=AsyncMock)
|
|
async def test_congress_bills_happy_path(mock_fn, client):
|
|
mock_fn.return_value = [
|
|
{"title": "Infrastructure Investment and Jobs Act", "bill_id": "HR3684"},
|
|
{"title": "Inflation Reduction Act", "bill_id": "HR5376"},
|
|
]
|
|
resp = await client.get("/api/v1/regulators/congress/bills?query=infrastructure")
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert data["success"] is True
|
|
assert len(data["data"]) == 2
|
|
assert data["data"][0]["bill_id"] == "HR3684"
|
|
mock_fn.assert_called_once_with("infrastructure")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_congress_bills_missing_query(client):
|
|
resp = await client.get("/api/v1/regulators/congress/bills")
|
|
assert resp.status_code == 422
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("routes_regulators.congress_service.search_congress_bills", new_callable=AsyncMock)
|
|
async def test_congress_bills_empty(mock_fn, client):
|
|
mock_fn.return_value = []
|
|
resp = await client.get("/api/v1/regulators/congress/bills?query=nonexistent")
|
|
assert resp.status_code == 200
|
|
assert resp.json()["data"] == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("routes_regulators.congress_service.search_congress_bills", new_callable=AsyncMock)
|
|
async def test_congress_bills_service_error_returns_502(mock_fn, client):
|
|
mock_fn.side_effect = RuntimeError("Congress API unavailable")
|
|
resp = await client.get("/api/v1/regulators/congress/bills?query=tax")
|
|
assert resp.status_code == 502
|