"""Tests for fixed income routes.""" 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 # --- Treasury Rates --- @pytest.mark.asyncio @patch("routes_fixed_income.fixed_income_service.get_treasury_rates", new_callable=AsyncMock) async def test_treasury_rates_happy_path(mock_fn, client): mock_fn.return_value = [ {"date": "2026-03-18", "week_4": 5.27, "month_3": 5.30, "year_2": 4.85, "year_10": 4.32, "year_30": 4.55} ] resp = await client.get("/api/v1/fixed-income/treasury-rates") assert resp.status_code == 200 data = resp.json() assert data["success"] is True assert len(data["data"]) == 1 assert data["data"][0]["year_10"] == 4.32 @pytest.mark.asyncio @patch("routes_fixed_income.fixed_income_service.get_treasury_rates", new_callable=AsyncMock) async def test_treasury_rates_empty(mock_fn, client): mock_fn.return_value = [] resp = await client.get("/api/v1/fixed-income/treasury-rates") assert resp.status_code == 200 assert resp.json()["data"] == [] @pytest.mark.asyncio @patch("routes_fixed_income.fixed_income_service.get_treasury_rates", new_callable=AsyncMock) async def test_treasury_rates_service_error_returns_502(mock_fn, client): mock_fn.side_effect = RuntimeError("Federal Reserve API down") resp = await client.get("/api/v1/fixed-income/treasury-rates") assert resp.status_code == 502 # --- Yield Curve --- @pytest.mark.asyncio @patch("routes_fixed_income.fixed_income_service.get_yield_curve", new_callable=AsyncMock) async def test_yield_curve_happy_path(mock_fn, client): mock_fn.return_value = [ {"maturity": "3M", "rate": 5.30}, {"maturity": "2Y", "rate": 4.85}, {"maturity": "10Y", "rate": 4.32}, ] resp = await client.get("/api/v1/fixed-income/yield-curve") assert resp.status_code == 200 data = resp.json() assert data["success"] is True assert len(data["data"]) == 3 mock_fn.assert_called_once_with(date=None) @pytest.mark.asyncio @patch("routes_fixed_income.fixed_income_service.get_yield_curve", new_callable=AsyncMock) async def test_yield_curve_with_date(mock_fn, client): mock_fn.return_value = [{"maturity": "10Y", "rate": 3.80}] resp = await client.get("/api/v1/fixed-income/yield-curve?date=2024-01-15") assert resp.status_code == 200 mock_fn.assert_called_once_with(date="2024-01-15") @pytest.mark.asyncio async def test_yield_curve_invalid_date_format(client): resp = await client.get("/api/v1/fixed-income/yield-curve?date=not-a-date") assert resp.status_code == 422 @pytest.mark.asyncio @patch("routes_fixed_income.fixed_income_service.get_yield_curve", new_callable=AsyncMock) async def test_yield_curve_empty(mock_fn, client): mock_fn.return_value = [] resp = await client.get("/api/v1/fixed-income/yield-curve") assert resp.status_code == 200 assert resp.json()["data"] == [] # --- Treasury Auctions --- @pytest.mark.asyncio @patch("routes_fixed_income.fixed_income_service.get_treasury_auctions", new_callable=AsyncMock) async def test_treasury_auctions_happy_path(mock_fn, client): mock_fn.return_value = [ {"auction_date": "2026-03-10", "security_type": "Note", "security_term": "10-Year", "high_yield": 4.32, "bid_to_cover_ratio": 2.45} ] resp = await client.get("/api/v1/fixed-income/treasury-auctions") assert resp.status_code == 200 data = resp.json() assert data["success"] is True assert data["data"][0]["bid_to_cover_ratio"] == 2.45 @pytest.mark.asyncio @patch("routes_fixed_income.fixed_income_service.get_treasury_auctions", new_callable=AsyncMock) async def test_treasury_auctions_with_security_type(mock_fn, client): mock_fn.return_value = [{"security_type": "Bill"}] resp = await client.get("/api/v1/fixed-income/treasury-auctions?security_type=Bill") assert resp.status_code == 200 mock_fn.assert_called_once_with(security_type="Bill") @pytest.mark.asyncio @patch("routes_fixed_income.fixed_income_service.get_treasury_auctions", new_callable=AsyncMock) async def test_treasury_auctions_empty(mock_fn, client): mock_fn.return_value = [] resp = await client.get("/api/v1/fixed-income/treasury-auctions") assert resp.status_code == 200 assert resp.json()["data"] == [] @pytest.mark.asyncio async def test_treasury_auctions_invalid_security_type(client): resp = await client.get("/api/v1/fixed-income/treasury-auctions?security_type=DROP;TABLE") assert resp.status_code == 422 # --- TIPS Yields --- @pytest.mark.asyncio @patch("routes_fixed_income.fixed_income_service.get_tips_yields", new_callable=AsyncMock) async def test_tips_yields_happy_path(mock_fn, client): mock_fn.return_value = [ {"date": "2026-03-18", "year_5": 2.10, "year_10": 2.25, "year_30": 2.40} ] resp = await client.get("/api/v1/fixed-income/tips-yields") assert resp.status_code == 200 data = resp.json() assert data["success"] is True assert data["data"][0]["year_10"] == 2.25 @pytest.mark.asyncio @patch("routes_fixed_income.fixed_income_service.get_tips_yields", new_callable=AsyncMock) async def test_tips_yields_empty(mock_fn, client): mock_fn.return_value = [] resp = await client.get("/api/v1/fixed-income/tips-yields") assert resp.status_code == 200 assert resp.json()["data"] == [] @pytest.mark.asyncio @patch("routes_fixed_income.fixed_income_service.get_tips_yields", new_callable=AsyncMock) async def test_tips_yields_service_error_returns_502(mock_fn, client): mock_fn.side_effect = RuntimeError("FRED unavailable") resp = await client.get("/api/v1/fixed-income/tips-yields") assert resp.status_code == 502 # --- EFFR --- @pytest.mark.asyncio @patch("routes_fixed_income.fixed_income_service.get_effr", new_callable=AsyncMock) async def test_effr_happy_path(mock_fn, client): mock_fn.return_value = [ {"date": "2026-03-18", "rate": 5.33, "percentile_1": 5.31, "percentile_25": 5.32, "percentile_75": 5.33} ] resp = await client.get("/api/v1/fixed-income/effr") assert resp.status_code == 200 data = resp.json() assert data["success"] is True assert data["data"][0]["rate"] == 5.33 @pytest.mark.asyncio @patch("routes_fixed_income.fixed_income_service.get_effr", new_callable=AsyncMock) async def test_effr_empty(mock_fn, client): mock_fn.return_value = [] resp = await client.get("/api/v1/fixed-income/effr") assert resp.status_code == 200 assert resp.json()["data"] == [] # --- SOFR --- @pytest.mark.asyncio @patch("routes_fixed_income.fixed_income_service.get_sofr", new_callable=AsyncMock) async def test_sofr_happy_path(mock_fn, client): mock_fn.return_value = [ {"date": "2026-03-18", "rate": 5.31, "average_30d": 5.31, "average_90d": 5.30} ] resp = await client.get("/api/v1/fixed-income/sofr") assert resp.status_code == 200 data = resp.json() assert data["success"] is True assert data["data"][0]["rate"] == 5.31 @pytest.mark.asyncio @patch("routes_fixed_income.fixed_income_service.get_sofr", new_callable=AsyncMock) async def test_sofr_empty(mock_fn, client): mock_fn.return_value = [] resp = await client.get("/api/v1/fixed-income/sofr") assert resp.status_code == 200 assert resp.json()["data"] == [] # --- HQM --- @pytest.mark.asyncio @patch("routes_fixed_income.fixed_income_service.get_hqm", new_callable=AsyncMock) async def test_hqm_happy_path(mock_fn, client): mock_fn.return_value = [ {"date": "2026-02-01", "aaa": 5.10, "aa": 5.25, "a": 5.40} ] resp = await client.get("/api/v1/fixed-income/hqm") assert resp.status_code == 200 data = resp.json() assert data["success"] is True assert data["data"][0]["aaa"] == 5.10 @pytest.mark.asyncio @patch("routes_fixed_income.fixed_income_service.get_hqm", new_callable=AsyncMock) async def test_hqm_empty(mock_fn, client): mock_fn.return_value = [] resp = await client.get("/api/v1/fixed-income/hqm") assert resp.status_code == 200 assert resp.json()["data"] == [] # --- Commercial Paper --- @pytest.mark.asyncio @patch("routes_fixed_income.fixed_income_service.get_commercial_paper", new_callable=AsyncMock) async def test_commercial_paper_happy_path(mock_fn, client): mock_fn.return_value = [ {"date": "2026-03-18", "maturity": "overnight", "financial": 5.28, "nonfinancial": 5.30} ] resp = await client.get("/api/v1/fixed-income/commercial-paper") assert resp.status_code == 200 data = resp.json() assert data["success"] is True assert len(data["data"]) == 1 @pytest.mark.asyncio @patch("routes_fixed_income.fixed_income_service.get_commercial_paper", new_callable=AsyncMock) async def test_commercial_paper_empty(mock_fn, client): mock_fn.return_value = [] resp = await client.get("/api/v1/fixed-income/commercial-paper") assert resp.status_code == 200 assert resp.json()["data"] == [] # --- Spot Rates --- @pytest.mark.asyncio @patch("routes_fixed_income.fixed_income_service.get_spot_rates", new_callable=AsyncMock) async def test_spot_rates_happy_path(mock_fn, client): mock_fn.return_value = [ {"date": "2026-03-01", "year_1": 5.50, "year_5": 5.20, "year_10": 5.10} ] resp = await client.get("/api/v1/fixed-income/spot-rates") assert resp.status_code == 200 data = resp.json() assert data["success"] is True assert data["data"][0]["year_10"] == 5.10 @pytest.mark.asyncio @patch("routes_fixed_income.fixed_income_service.get_spot_rates", new_callable=AsyncMock) async def test_spot_rates_empty(mock_fn, client): mock_fn.return_value = [] resp = await client.get("/api/v1/fixed-income/spot-rates") assert resp.status_code == 200 assert resp.json()["data"] == [] # --- Spreads --- @pytest.mark.asyncio @patch("routes_fixed_income.fixed_income_service.get_spreads", new_callable=AsyncMock) async def test_spreads_default(mock_fn, client): mock_fn.return_value = [{"date": "2026-03-18", "spread": 1.10}] resp = await client.get("/api/v1/fixed-income/spreads") assert resp.status_code == 200 data = resp.json() assert data["success"] is True mock_fn.assert_called_once_with(series="tcm") @pytest.mark.asyncio @patch("routes_fixed_income.fixed_income_service.get_spreads", new_callable=AsyncMock) async def test_spreads_tcm_effr(mock_fn, client): mock_fn.return_value = [{"date": "2026-03-18", "spread": 0.02}] resp = await client.get("/api/v1/fixed-income/spreads?series=tcm_effr") assert resp.status_code == 200 mock_fn.assert_called_once_with(series="tcm_effr") @pytest.mark.asyncio async def test_spreads_invalid_series(client): resp = await client.get("/api/v1/fixed-income/spreads?series=invalid") assert resp.status_code == 422 @pytest.mark.asyncio @patch("routes_fixed_income.fixed_income_service.get_spreads", new_callable=AsyncMock) async def test_spreads_empty(mock_fn, client): mock_fn.return_value = [] resp = await client.get("/api/v1/fixed-income/spreads?series=treasury_effr") assert resp.status_code == 200 assert resp.json()["data"] == []