test: add 159 tests for all new modules

New test files (171 tests):
- test_routes_shorts.py (16) - short volume, FTD, interest, darkpool
- test_routes_fixed_income.py (34) - treasury, yield curve, SOFR, etc.
- test_routes_economy.py (44) - CPI, GDP, FRED search, Fed holdings
- test_routes_surveys.py (17) - Michigan, SLOOS, NFP, Empire State
- test_routes_regulators.py (20) - COT, SEC litigation, institutions
- test_finnhub_service_social.py (20) - social/reddit sentiment unit tests
- test_routes_sentiment_social.py (20) - social endpoints + composite

Updated:
- test_routes_sentiment.py - match new composite sentiment response shape

Total: 261 tests passing (was 102)
This commit is contained in:
Yaojia Wang
2026-03-19 22:12:27 +01:00
parent ea72497587
commit 27b131492f
8 changed files with 2098 additions and 5 deletions

View File

@@ -0,0 +1,367 @@
"""Tests for the new social sentiment functions in finnhub_service."""
from unittest.mock import patch, AsyncMock, MagicMock
import pytest
import finnhub_service
# --- get_social_sentiment ---
@pytest.mark.asyncio
@patch("finnhub_service.settings")
async def test_social_sentiment_not_configured(mock_settings):
mock_settings.finnhub_api_key = ""
result = await finnhub_service.get_social_sentiment("AAPL")
assert result["configured"] is False
assert "INVEST_API_FINNHUB_API_KEY" in result["message"]
@pytest.mark.asyncio
@patch("finnhub_service.settings")
@patch("finnhub_service.httpx.AsyncClient")
async def test_social_sentiment_premium_required_403(mock_client_cls, mock_settings):
mock_settings.finnhub_api_key = "test_key"
mock_resp = MagicMock()
mock_resp.status_code = 403
mock_client = AsyncMock()
mock_client.get.return_value = mock_resp
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
mock_client.__aexit__ = AsyncMock(return_value=False)
mock_client_cls.return_value = mock_client
result = await finnhub_service.get_social_sentiment("AAPL")
assert result["configured"] is True
assert result["premium_required"] is True
assert result["reddit"] == []
assert result["twitter"] == []
@pytest.mark.asyncio
@patch("finnhub_service.settings")
@patch("finnhub_service.httpx.AsyncClient")
async def test_social_sentiment_premium_required_401(mock_client_cls, mock_settings):
mock_settings.finnhub_api_key = "test_key"
mock_resp = MagicMock()
mock_resp.status_code = 401
mock_client = AsyncMock()
mock_client.get.return_value = mock_resp
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
mock_client.__aexit__ = AsyncMock(return_value=False)
mock_client_cls.return_value = mock_client
result = await finnhub_service.get_social_sentiment("AAPL")
assert result["premium_required"] is True
@pytest.mark.asyncio
@patch("finnhub_service.settings")
@patch("finnhub_service.httpx.AsyncClient")
async def test_social_sentiment_success_with_data(mock_client_cls, mock_settings):
mock_settings.finnhub_api_key = "test_key"
mock_resp = MagicMock()
mock_resp.status_code = 200
mock_resp.raise_for_status = MagicMock()
mock_resp.json.return_value = {
"reddit": [
{"mention": 50, "positiveScore": 30, "negativeScore": 10, "score": 0.5},
{"mention": 30, "positiveScore": 20, "negativeScore": 5, "score": 0.6},
],
"twitter": [
{"mention": 100, "positiveScore": 60, "negativeScore": 20, "score": 0.4},
],
}
mock_client = AsyncMock()
mock_client.get.return_value = mock_resp
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
mock_client.__aexit__ = AsyncMock(return_value=False)
mock_client_cls.return_value = mock_client
result = await finnhub_service.get_social_sentiment("AAPL")
assert result["configured"] is True
assert result["symbol"] == "AAPL"
assert result["reddit_summary"]["total_mentions"] == 80
assert result["reddit_summary"]["data_points"] == 2
assert result["twitter_summary"]["total_mentions"] == 100
assert len(result["reddit"]) == 2
assert len(result["twitter"]) == 1
@pytest.mark.asyncio
@patch("finnhub_service.settings")
@patch("finnhub_service.httpx.AsyncClient")
async def test_social_sentiment_empty_lists(mock_client_cls, mock_settings):
mock_settings.finnhub_api_key = "test_key"
mock_resp = MagicMock()
mock_resp.status_code = 200
mock_resp.raise_for_status = MagicMock()
mock_resp.json.return_value = {"reddit": [], "twitter": []}
mock_client = AsyncMock()
mock_client.get.return_value = mock_resp
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
mock_client.__aexit__ = AsyncMock(return_value=False)
mock_client_cls.return_value = mock_client
result = await finnhub_service.get_social_sentiment("AAPL")
assert result["configured"] is True
assert result["reddit_summary"] is None
assert result["twitter_summary"] is None
@pytest.mark.asyncio
@patch("finnhub_service.settings")
@patch("finnhub_service.httpx.AsyncClient")
async def test_social_sentiment_non_dict_response(mock_client_cls, mock_settings):
mock_settings.finnhub_api_key = "test_key"
mock_resp = MagicMock()
mock_resp.status_code = 200
mock_resp.raise_for_status = MagicMock()
mock_resp.json.return_value = "unexpected string response"
mock_client = AsyncMock()
mock_client.get.return_value = mock_resp
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
mock_client.__aexit__ = AsyncMock(return_value=False)
mock_client_cls.return_value = mock_client
result = await finnhub_service.get_social_sentiment("AAPL")
assert result["reddit"] == []
assert result["twitter"] == []
# --- _summarize_social ---
def test_summarize_social_empty():
result = finnhub_service._summarize_social([])
assert result == {}
def test_summarize_social_single_entry():
entries = [{"mention": 10, "positiveScore": 7, "negativeScore": 2, "score": 0.5}]
result = finnhub_service._summarize_social(entries)
assert result["total_mentions"] == 10
assert result["total_positive"] == 7
assert result["total_negative"] == 2
assert result["avg_score"] == 0.5
assert result["data_points"] == 1
def test_summarize_social_multiple_entries():
entries = [
{"mention": 100, "positiveScore": 60, "negativeScore": 20, "score": 0.4},
{"mention": 50, "positiveScore": 30, "negativeScore": 10, "score": 0.6},
]
result = finnhub_service._summarize_social(entries)
assert result["total_mentions"] == 150
assert result["total_positive"] == 90
assert result["total_negative"] == 30
assert result["avg_score"] == 0.5
assert result["data_points"] == 2
def test_summarize_social_missing_fields():
entries = [{"mention": 5}]
result = finnhub_service._summarize_social(entries)
assert result["total_mentions"] == 5
assert result["total_positive"] == 0
assert result["total_negative"] == 0
# --- get_reddit_sentiment ---
@pytest.mark.asyncio
@patch("finnhub_service.httpx.AsyncClient")
async def test_reddit_sentiment_symbol_found(mock_client_cls):
mock_resp = MagicMock()
mock_resp.raise_for_status = MagicMock()
mock_resp.json.return_value = {
"results": [
{"ticker": "AAPL", "name": "Apple Inc", "rank": 3, "mentions": 150, "mentions_24h_ago": 100, "upvotes": 500, "rank_24h_ago": 5},
{"ticker": "TSLA", "name": "Tesla", "rank": 1, "mentions": 300, "mentions_24h_ago": 280, "upvotes": 900, "rank_24h_ago": 2},
]
}
mock_client = AsyncMock()
mock_client.get.return_value = mock_resp
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
mock_client.__aexit__ = AsyncMock(return_value=False)
mock_client_cls.return_value = mock_client
result = await finnhub_service.get_reddit_sentiment("AAPL")
assert result["found"] is True
assert result["symbol"] == "AAPL"
assert result["rank"] == 3
assert result["mentions_24h"] == 150
assert result["mentions_24h_ago"] == 100
assert result["mentions_change_pct"] == 50.0
@pytest.mark.asyncio
@patch("finnhub_service.httpx.AsyncClient")
async def test_reddit_sentiment_symbol_not_found(mock_client_cls):
mock_resp = MagicMock()
mock_resp.raise_for_status = MagicMock()
mock_resp.json.return_value = {
"results": [
{"ticker": "TSLA", "rank": 1, "mentions": 300, "mentions_24h_ago": 280}
]
}
mock_client = AsyncMock()
mock_client.get.return_value = mock_resp
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
mock_client.__aexit__ = AsyncMock(return_value=False)
mock_client_cls.return_value = mock_client
result = await finnhub_service.get_reddit_sentiment("AAPL")
assert result["found"] is False
assert result["symbol"] == "AAPL"
assert "not in Reddit" in result["message"]
@pytest.mark.asyncio
@patch("finnhub_service.httpx.AsyncClient")
async def test_reddit_sentiment_zero_mentions_prev(mock_client_cls):
mock_resp = MagicMock()
mock_resp.raise_for_status = MagicMock()
mock_resp.json.return_value = {
"results": [
{"ticker": "AAPL", "rank": 1, "mentions": 50, "mentions_24h_ago": 0, "upvotes": 200, "rank_24h_ago": None}
]
}
mock_client = AsyncMock()
mock_client.get.return_value = mock_resp
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
mock_client.__aexit__ = AsyncMock(return_value=False)
mock_client_cls.return_value = mock_client
result = await finnhub_service.get_reddit_sentiment("AAPL")
assert result["found"] is True
assert result["mentions_change_pct"] is None # division by zero handled
@pytest.mark.asyncio
@patch("finnhub_service.httpx.AsyncClient")
async def test_reddit_sentiment_api_failure(mock_client_cls):
mock_client = AsyncMock()
mock_client.get.side_effect = Exception("Connection error")
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
mock_client.__aexit__ = AsyncMock(return_value=False)
mock_client_cls.return_value = mock_client
result = await finnhub_service.get_reddit_sentiment("AAPL")
assert result["symbol"] == "AAPL"
assert "error" in result
@pytest.mark.asyncio
@patch("finnhub_service.httpx.AsyncClient")
async def test_reddit_sentiment_case_insensitive(mock_client_cls):
mock_resp = MagicMock()
mock_resp.raise_for_status = MagicMock()
mock_resp.json.return_value = {
"results": [
{"ticker": "aapl", "rank": 1, "mentions": 100, "mentions_24h_ago": 80, "upvotes": 400, "rank_24h_ago": 2}
]
}
mock_client = AsyncMock()
mock_client.get.return_value = mock_resp
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
mock_client.__aexit__ = AsyncMock(return_value=False)
mock_client_cls.return_value = mock_client
result = await finnhub_service.get_reddit_sentiment("AAPL")
assert result["found"] is True
# --- get_reddit_trending ---
@pytest.mark.asyncio
@patch("finnhub_service.httpx.AsyncClient")
async def test_reddit_trending_happy_path(mock_client_cls):
mock_resp = MagicMock()
mock_resp.raise_for_status = MagicMock()
mock_resp.json.return_value = {
"results": [
{"ticker": "TSLA", "name": "Tesla", "rank": 1, "mentions": 500, "upvotes": 1000, "rank_24h_ago": 2, "mentions_24h_ago": 400},
{"ticker": "AAPL", "name": "Apple", "rank": 2, "mentions": 300, "upvotes": 700, "rank_24h_ago": 1, "mentions_24h_ago": 350},
{"ticker": "GME", "name": "GameStop", "rank": 3, "mentions": 200, "upvotes": 500, "rank_24h_ago": 3, "mentions_24h_ago": 180},
]
}
mock_client = AsyncMock()
mock_client.get.return_value = mock_resp
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
mock_client.__aexit__ = AsyncMock(return_value=False)
mock_client_cls.return_value = mock_client
result = await finnhub_service.get_reddit_trending()
assert len(result) == 3
assert result[0]["symbol"] == "TSLA"
assert result[0]["rank"] == 1
assert result[1]["symbol"] == "AAPL"
assert "mentions_24h" in result[0]
assert "upvotes" in result[0]
@pytest.mark.asyncio
@patch("finnhub_service.httpx.AsyncClient")
async def test_reddit_trending_limits_to_25(mock_client_cls):
mock_resp = MagicMock()
mock_resp.raise_for_status = MagicMock()
mock_resp.json.return_value = {
"results": [
{"ticker": f"SYM{i}", "rank": i + 1, "mentions": 100 - i, "upvotes": 50, "rank_24h_ago": i, "mentions_24h_ago": 80}
for i in range(30)
]
}
mock_client = AsyncMock()
mock_client.get.return_value = mock_resp
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
mock_client.__aexit__ = AsyncMock(return_value=False)
mock_client_cls.return_value = mock_client
result = await finnhub_service.get_reddit_trending()
assert len(result) == 25
@pytest.mark.asyncio
@patch("finnhub_service.httpx.AsyncClient")
async def test_reddit_trending_empty_results(mock_client_cls):
mock_resp = MagicMock()
mock_resp.raise_for_status = MagicMock()
mock_resp.json.return_value = {"results": []}
mock_client = AsyncMock()
mock_client.get.return_value = mock_resp
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
mock_client.__aexit__ = AsyncMock(return_value=False)
mock_client_cls.return_value = mock_client
result = await finnhub_service.get_reddit_trending()
assert result == []
@pytest.mark.asyncio
@patch("finnhub_service.httpx.AsyncClient")
async def test_reddit_trending_api_failure(mock_client_cls):
mock_client = AsyncMock()
mock_client.get.side_effect = Exception("ApeWisdom down")
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
mock_client.__aexit__ = AsyncMock(return_value=False)
mock_client_cls.return_value = mock_client
result = await finnhub_service.get_reddit_trending()
assert result == []

View File

@@ -0,0 +1,433 @@
"""Tests for expanded economy 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
# --- CPI ---
@pytest.mark.asyncio
@patch("routes_economy.economy_service.get_cpi", new_callable=AsyncMock)
async def test_macro_cpi_happy_path(mock_fn, client):
mock_fn.return_value = [
{"date": "2026-02-01", "value": 312.5, "country": "united_states"}
]
resp = await client.get("/api/v1/macro/cpi")
assert resp.status_code == 200
data = resp.json()
assert data["success"] is True
assert data["data"][0]["value"] == 312.5
mock_fn.assert_called_once_with(country="united_states")
@pytest.mark.asyncio
@patch("routes_economy.economy_service.get_cpi", new_callable=AsyncMock)
async def test_macro_cpi_custom_country(mock_fn, client):
mock_fn.return_value = [{"date": "2026-02-01", "value": 120.0}]
resp = await client.get("/api/v1/macro/cpi?country=germany")
assert resp.status_code == 200
mock_fn.assert_called_once_with(country="germany")
@pytest.mark.asyncio
async def test_macro_cpi_invalid_country(client):
resp = await client.get("/api/v1/macro/cpi?country=INVALID!!!COUNTRY")
assert resp.status_code == 422
@pytest.mark.asyncio
@patch("routes_economy.economy_service.get_cpi", new_callable=AsyncMock)
async def test_macro_cpi_empty(mock_fn, client):
mock_fn.return_value = []
resp = await client.get("/api/v1/macro/cpi")
assert resp.status_code == 200
assert resp.json()["data"] == []
@pytest.mark.asyncio
@patch("routes_economy.economy_service.get_cpi", new_callable=AsyncMock)
async def test_macro_cpi_service_error_returns_502(mock_fn, client):
mock_fn.side_effect = RuntimeError("FRED down")
resp = await client.get("/api/v1/macro/cpi")
assert resp.status_code == 502
# --- GDP ---
@pytest.mark.asyncio
@patch("routes_economy.economy_service.get_gdp", new_callable=AsyncMock)
async def test_macro_gdp_default_real(mock_fn, client):
mock_fn.return_value = [{"date": "2026-01-01", "value": 22.5}]
resp = await client.get("/api/v1/macro/gdp")
assert resp.status_code == 200
data = resp.json()
assert data["success"] is True
mock_fn.assert_called_once_with(gdp_type="real")
@pytest.mark.asyncio
@patch("routes_economy.economy_service.get_gdp", new_callable=AsyncMock)
async def test_macro_gdp_nominal(mock_fn, client):
mock_fn.return_value = [{"date": "2026-01-01", "value": 28.3}]
resp = await client.get("/api/v1/macro/gdp?gdp_type=nominal")
assert resp.status_code == 200
mock_fn.assert_called_once_with(gdp_type="nominal")
@pytest.mark.asyncio
async def test_macro_gdp_invalid_type(client):
resp = await client.get("/api/v1/macro/gdp?gdp_type=invalid")
assert resp.status_code == 422
@pytest.mark.asyncio
@patch("routes_economy.economy_service.get_gdp", new_callable=AsyncMock)
async def test_macro_gdp_forecast(mock_fn, client):
mock_fn.return_value = [{"date": "2027-01-01", "value": 23.1}]
resp = await client.get("/api/v1/macro/gdp?gdp_type=forecast")
assert resp.status_code == 200
mock_fn.assert_called_once_with(gdp_type="forecast")
@pytest.mark.asyncio
@patch("routes_economy.economy_service.get_gdp", new_callable=AsyncMock)
async def test_macro_gdp_empty(mock_fn, client):
mock_fn.return_value = []
resp = await client.get("/api/v1/macro/gdp")
assert resp.status_code == 200
assert resp.json()["data"] == []
# --- Unemployment ---
@pytest.mark.asyncio
@patch("routes_economy.economy_service.get_unemployment", new_callable=AsyncMock)
async def test_macro_unemployment_happy_path(mock_fn, client):
mock_fn.return_value = [{"date": "2026-02-01", "value": 3.7, "country": "united_states"}]
resp = await client.get("/api/v1/macro/unemployment")
assert resp.status_code == 200
data = resp.json()
assert data["success"] is True
assert data["data"][0]["value"] == 3.7
mock_fn.assert_called_once_with(country="united_states")
@pytest.mark.asyncio
@patch("routes_economy.economy_service.get_unemployment", new_callable=AsyncMock)
async def test_macro_unemployment_custom_country(mock_fn, client):
mock_fn.return_value = [{"date": "2026-02-01", "value": 5.1}]
resp = await client.get("/api/v1/macro/unemployment?country=france")
assert resp.status_code == 200
mock_fn.assert_called_once_with(country="france")
@pytest.mark.asyncio
@patch("routes_economy.economy_service.get_unemployment", new_callable=AsyncMock)
async def test_macro_unemployment_empty(mock_fn, client):
mock_fn.return_value = []
resp = await client.get("/api/v1/macro/unemployment")
assert resp.status_code == 200
assert resp.json()["data"] == []
# --- PCE ---
@pytest.mark.asyncio
@patch("routes_economy.economy_service.get_pce", new_callable=AsyncMock)
async def test_macro_pce_happy_path(mock_fn, client):
mock_fn.return_value = [{"date": "2026-02-01", "value": 2.8}]
resp = await client.get("/api/v1/macro/pce")
assert resp.status_code == 200
data = resp.json()
assert data["success"] is True
assert data["data"][0]["value"] == 2.8
@pytest.mark.asyncio
@patch("routes_economy.economy_service.get_pce", new_callable=AsyncMock)
async def test_macro_pce_empty(mock_fn, client):
mock_fn.return_value = []
resp = await client.get("/api/v1/macro/pce")
assert resp.status_code == 200
assert resp.json()["data"] == []
@pytest.mark.asyncio
@patch("routes_economy.economy_service.get_pce", new_callable=AsyncMock)
async def test_macro_pce_service_error_returns_502(mock_fn, client):
mock_fn.side_effect = RuntimeError("FRED unavailable")
resp = await client.get("/api/v1/macro/pce")
assert resp.status_code == 502
# --- Money Measures ---
@pytest.mark.asyncio
@patch("routes_economy.economy_service.get_money_measures", new_callable=AsyncMock)
async def test_macro_money_measures_happy_path(mock_fn, client):
mock_fn.return_value = [{"date": "2026-02-01", "m1": 18200.0, "m2": 21000.0}]
resp = await client.get("/api/v1/macro/money-measures")
assert resp.status_code == 200
data = resp.json()
assert data["success"] is True
assert data["data"][0]["m2"] == 21000.0
@pytest.mark.asyncio
@patch("routes_economy.economy_service.get_money_measures", new_callable=AsyncMock)
async def test_macro_money_measures_empty(mock_fn, client):
mock_fn.return_value = []
resp = await client.get("/api/v1/macro/money-measures")
assert resp.status_code == 200
assert resp.json()["data"] == []
# --- CLI ---
@pytest.mark.asyncio
@patch("routes_economy.economy_service.get_composite_leading_indicator", new_callable=AsyncMock)
async def test_macro_cli_happy_path(mock_fn, client):
mock_fn.return_value = [{"date": "2026-01-01", "value": 99.2, "country": "united_states"}]
resp = await client.get("/api/v1/macro/cli")
assert resp.status_code == 200
data = resp.json()
assert data["success"] is True
assert data["data"][0]["value"] == 99.2
mock_fn.assert_called_once_with(country="united_states")
@pytest.mark.asyncio
@patch("routes_economy.economy_service.get_composite_leading_indicator", new_callable=AsyncMock)
async def test_macro_cli_empty(mock_fn, client):
mock_fn.return_value = []
resp = await client.get("/api/v1/macro/cli")
assert resp.status_code == 200
assert resp.json()["data"] == []
# --- House Price Index ---
@pytest.mark.asyncio
@patch("routes_economy.economy_service.get_house_price_index", new_callable=AsyncMock)
async def test_macro_hpi_happy_path(mock_fn, client):
mock_fn.return_value = [{"date": "2026-01-01", "value": 350.0, "country": "united_states"}]
resp = await client.get("/api/v1/macro/house-price-index")
assert resp.status_code == 200
data = resp.json()
assert data["success"] is True
assert data["data"][0]["value"] == 350.0
mock_fn.assert_called_once_with(country="united_states")
@pytest.mark.asyncio
@patch("routes_economy.economy_service.get_house_price_index", new_callable=AsyncMock)
async def test_macro_hpi_empty(mock_fn, client):
mock_fn.return_value = []
resp = await client.get("/api/v1/macro/house-price-index")
assert resp.status_code == 200
assert resp.json()["data"] == []
# --- FRED Regional ---
@pytest.mark.asyncio
@patch("routes_economy.economy_service.get_fred_regional", new_callable=AsyncMock)
async def test_economy_fred_regional_happy_path(mock_fn, client):
mock_fn.return_value = [{"region": "CA", "value": 5.2}]
resp = await client.get("/api/v1/economy/fred-regional?series_id=CAUR")
assert resp.status_code == 200
data = resp.json()
assert data["success"] is True
assert data["data"][0]["region"] == "CA"
mock_fn.assert_called_once_with(series_id="CAUR", region=None)
@pytest.mark.asyncio
@patch("routes_economy.economy_service.get_fred_regional", new_callable=AsyncMock)
async def test_economy_fred_regional_with_region(mock_fn, client):
mock_fn.return_value = [{"region": "state", "value": 4.1}]
resp = await client.get("/api/v1/economy/fred-regional?series_id=CAUR&region=state")
assert resp.status_code == 200
mock_fn.assert_called_once_with(series_id="CAUR", region="state")
@pytest.mark.asyncio
async def test_economy_fred_regional_missing_series_id(client):
resp = await client.get("/api/v1/economy/fred-regional")
assert resp.status_code == 422
@pytest.mark.asyncio
@patch("routes_economy.economy_service.get_fred_regional", new_callable=AsyncMock)
async def test_economy_fred_regional_empty(mock_fn, client):
mock_fn.return_value = []
resp = await client.get("/api/v1/economy/fred-regional?series_id=UNKNOWN")
assert resp.status_code == 200
assert resp.json()["data"] == []
# --- Primary Dealer Positioning ---
@pytest.mark.asyncio
@patch("routes_economy.economy_service.get_primary_dealer_positioning", new_callable=AsyncMock)
async def test_economy_primary_dealer_happy_path(mock_fn, client):
mock_fn.return_value = [{"date": "2026-03-12", "treasuries": 250000.0, "mbs": 80000.0}]
resp = await client.get("/api/v1/economy/primary-dealer-positioning")
assert resp.status_code == 200
data = resp.json()
assert data["success"] is True
assert data["data"][0]["treasuries"] == 250000.0
@pytest.mark.asyncio
@patch("routes_economy.economy_service.get_primary_dealer_positioning", new_callable=AsyncMock)
async def test_economy_primary_dealer_empty(mock_fn, client):
mock_fn.return_value = []
resp = await client.get("/api/v1/economy/primary-dealer-positioning")
assert resp.status_code == 200
assert resp.json()["data"] == []
# --- FRED Search ---
@pytest.mark.asyncio
@patch("routes_economy.economy_service.fred_search", new_callable=AsyncMock)
async def test_economy_fred_search_happy_path(mock_fn, client):
mock_fn.return_value = [
{"id": "FEDFUNDS", "title": "Effective Federal Funds Rate", "frequency": "Monthly"}
]
resp = await client.get("/api/v1/economy/fred-search?query=federal+funds")
assert resp.status_code == 200
data = resp.json()
assert data["success"] is True
assert data["data"][0]["id"] == "FEDFUNDS"
mock_fn.assert_called_once_with(query="federal funds")
@pytest.mark.asyncio
async def test_economy_fred_search_missing_query(client):
resp = await client.get("/api/v1/economy/fred-search")
assert resp.status_code == 422
@pytest.mark.asyncio
@patch("routes_economy.economy_service.fred_search", new_callable=AsyncMock)
async def test_economy_fred_search_empty(mock_fn, client):
mock_fn.return_value = []
resp = await client.get("/api/v1/economy/fred-search?query=nothingtofind")
assert resp.status_code == 200
assert resp.json()["data"] == []
# --- Balance of Payments ---
@pytest.mark.asyncio
@patch("routes_economy.economy_service.get_balance_of_payments", new_callable=AsyncMock)
async def test_economy_bop_happy_path(mock_fn, client):
mock_fn.return_value = [{"date": "2026-01-01", "current_account": -200.0, "capital_account": 5.0}]
resp = await client.get("/api/v1/economy/balance-of-payments")
assert resp.status_code == 200
data = resp.json()
assert data["success"] is True
assert data["data"][0]["current_account"] == -200.0
@pytest.mark.asyncio
@patch("routes_economy.economy_service.get_balance_of_payments", new_callable=AsyncMock)
async def test_economy_bop_empty(mock_fn, client):
mock_fn.return_value = []
resp = await client.get("/api/v1/economy/balance-of-payments")
assert resp.status_code == 200
assert resp.json()["data"] == []
# --- Central Bank Holdings ---
@pytest.mark.asyncio
@patch("routes_economy.economy_service.get_central_bank_holdings", new_callable=AsyncMock)
async def test_economy_central_bank_holdings_happy_path(mock_fn, client):
mock_fn.return_value = [{"date": "2026-03-13", "treasuries": 4500000.0, "mbs": 2300000.0}]
resp = await client.get("/api/v1/economy/central-bank-holdings")
assert resp.status_code == 200
data = resp.json()
assert data["success"] is True
assert data["data"][0]["treasuries"] == 4500000.0
@pytest.mark.asyncio
@patch("routes_economy.economy_service.get_central_bank_holdings", new_callable=AsyncMock)
async def test_economy_central_bank_holdings_empty(mock_fn, client):
mock_fn.return_value = []
resp = await client.get("/api/v1/economy/central-bank-holdings")
assert resp.status_code == 200
assert resp.json()["data"] == []
# --- FOMC Documents ---
@pytest.mark.asyncio
@patch("routes_economy.economy_service.get_fomc_documents", new_callable=AsyncMock)
async def test_economy_fomc_happy_path(mock_fn, client):
mock_fn.return_value = [
{"date": "2026-01-28", "type": "Minutes", "url": "https://federalreserve.gov/fomc"}
]
resp = await client.get("/api/v1/economy/fomc-documents")
assert resp.status_code == 200
data = resp.json()
assert data["success"] is True
assert data["data"][0]["type"] == "Minutes"
mock_fn.assert_called_once_with(year=None)
@pytest.mark.asyncio
@patch("routes_economy.economy_service.get_fomc_documents", new_callable=AsyncMock)
async def test_economy_fomc_with_year(mock_fn, client):
mock_fn.return_value = [{"date": "2024-01-30", "type": "Statement"}]
resp = await client.get("/api/v1/economy/fomc-documents?year=2024")
assert resp.status_code == 200
mock_fn.assert_called_once_with(year=2024)
@pytest.mark.asyncio
async def test_economy_fomc_invalid_year_too_low(client):
resp = await client.get("/api/v1/economy/fomc-documents?year=1999")
assert resp.status_code == 422
@pytest.mark.asyncio
async def test_economy_fomc_invalid_year_too_high(client):
resp = await client.get("/api/v1/economy/fomc-documents?year=2100")
assert resp.status_code == 422
@pytest.mark.asyncio
@patch("routes_economy.economy_service.get_fomc_documents", new_callable=AsyncMock)
async def test_economy_fomc_empty(mock_fn, client):
mock_fn.return_value = []
resp = await client.get("/api/v1/economy/fomc-documents")
assert resp.status_code == 200
assert resp.json()["data"] == []

View File

@@ -0,0 +1,328 @@
"""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"] == []

View File

@@ -0,0 +1,228 @@
"""Tests for regulatory data routes (CFTC, SEC)."""
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
# --- COT Report ---
@pytest.mark.asyncio
@patch("routes_regulators.regulators_service.get_cot", new_callable=AsyncMock)
async def test_cot_report_happy_path(mock_fn, client):
mock_fn.return_value = [
{
"date": "2026-03-11",
"symbol": "ES",
"commercial_long": 250000,
"commercial_short": 300000,
"noncommercial_long": 180000,
"noncommercial_short": 120000,
}
]
resp = await client.get("/api/v1/regulators/cot?symbol=ES")
assert resp.status_code == 200
data = resp.json()
assert data["success"] is True
assert data["data"][0]["commercial_long"] == 250000
mock_fn.assert_called_once_with("ES")
@pytest.mark.asyncio
@patch("routes_regulators.regulators_service.get_cot", new_callable=AsyncMock)
async def test_cot_report_empty(mock_fn, client):
mock_fn.return_value = []
resp = await client.get("/api/v1/regulators/cot?symbol=UNKNOWN")
assert resp.status_code == 200
assert resp.json()["data"] == []
@pytest.mark.asyncio
@patch("routes_regulators.regulators_service.get_cot", new_callable=AsyncMock)
async def test_cot_report_service_error_returns_502(mock_fn, client):
mock_fn.side_effect = RuntimeError("CFTC unavailable")
resp = await client.get("/api/v1/regulators/cot?symbol=ES")
assert resp.status_code == 502
@pytest.mark.asyncio
async def test_cot_report_missing_symbol(client):
resp = await client.get("/api/v1/regulators/cot")
assert resp.status_code == 422
@pytest.mark.asyncio
async def test_cot_report_invalid_symbol(client):
resp = await client.get("/api/v1/regulators/cot?symbol=DROP;TABLE")
assert resp.status_code == 400
# --- COT Search ---
@pytest.mark.asyncio
@patch("routes_regulators.regulators_service.cot_search", new_callable=AsyncMock)
async def test_cot_search_happy_path(mock_fn, client):
mock_fn.return_value = [
{"code": "13874P", "name": "E-MINI S&P 500"},
{"code": "13874A", "name": "S&P 500 CONSOLIDATED"},
]
resp = await client.get("/api/v1/regulators/cot/search?query=S%26P+500")
assert resp.status_code == 200
data = resp.json()
assert data["success"] is True
assert len(data["data"]) == 2
assert data["data"][0]["name"] == "E-MINI S&P 500"
@pytest.mark.asyncio
async def test_cot_search_missing_query(client):
resp = await client.get("/api/v1/regulators/cot/search")
assert resp.status_code == 422
@pytest.mark.asyncio
@patch("routes_regulators.regulators_service.cot_search", new_callable=AsyncMock)
async def test_cot_search_empty(mock_fn, client):
mock_fn.return_value = []
resp = await client.get("/api/v1/regulators/cot/search?query=nonexistentfutures")
assert resp.status_code == 200
assert resp.json()["data"] == []
@pytest.mark.asyncio
@patch("routes_regulators.regulators_service.cot_search", new_callable=AsyncMock)
async def test_cot_search_service_error_returns_502(mock_fn, client):
mock_fn.side_effect = RuntimeError("CFTC search failed")
resp = await client.get("/api/v1/regulators/cot/search?query=gold")
assert resp.status_code == 502
# --- SEC Litigation ---
@pytest.mark.asyncio
@patch("routes_regulators.regulators_service.get_sec_litigation", new_callable=AsyncMock)
async def test_sec_litigation_happy_path(mock_fn, client):
mock_fn.return_value = [
{
"date": "2026-03-15",
"title": "SEC Charges Former CEO with Fraud",
"url": "https://sec.gov/litigation/lr/2026/lr-99999.htm",
"summary": "The Commission charged...",
}
]
resp = await client.get("/api/v1/regulators/sec/litigation")
assert resp.status_code == 200
data = resp.json()
assert data["success"] is True
assert len(data["data"]) == 1
assert "CEO" in data["data"][0]["title"]
@pytest.mark.asyncio
@patch("routes_regulators.regulators_service.get_sec_litigation", new_callable=AsyncMock)
async def test_sec_litigation_empty(mock_fn, client):
mock_fn.return_value = []
resp = await client.get("/api/v1/regulators/sec/litigation")
assert resp.status_code == 200
assert resp.json()["data"] == []
@pytest.mark.asyncio
@patch("routes_regulators.regulators_service.get_sec_litigation", new_callable=AsyncMock)
async def test_sec_litigation_service_error_returns_502(mock_fn, client):
mock_fn.side_effect = RuntimeError("SEC RSS feed unavailable")
resp = await client.get("/api/v1/regulators/sec/litigation")
assert resp.status_code == 502
# --- SEC Institution Search ---
@pytest.mark.asyncio
@patch("routes_regulators.regulators_service.search_institutions", new_callable=AsyncMock)
async def test_sec_institutions_happy_path(mock_fn, client):
mock_fn.return_value = [
{"name": "Vanguard Group Inc", "cik": "0000102909"},
{"name": "BlackRock Inc", "cik": "0001364742"},
]
resp = await client.get("/api/v1/regulators/sec/institutions?query=vanguard")
assert resp.status_code == 200
data = resp.json()
assert data["success"] is True
assert len(data["data"]) == 2
assert data["data"][0]["name"] == "Vanguard Group Inc"
mock_fn.assert_called_once_with("vanguard")
@pytest.mark.asyncio
async def test_sec_institutions_missing_query(client):
resp = await client.get("/api/v1/regulators/sec/institutions")
assert resp.status_code == 422
@pytest.mark.asyncio
@patch("routes_regulators.regulators_service.search_institutions", new_callable=AsyncMock)
async def test_sec_institutions_empty(mock_fn, client):
mock_fn.return_value = []
resp = await client.get("/api/v1/regulators/sec/institutions?query=notarealfirm")
assert resp.status_code == 200
assert resp.json()["data"] == []
@pytest.mark.asyncio
@patch("routes_regulators.regulators_service.search_institutions", new_callable=AsyncMock)
async def test_sec_institutions_service_error_returns_502(mock_fn, client):
mock_fn.side_effect = RuntimeError("SEC API failed")
resp = await client.get("/api/v1/regulators/sec/institutions?query=blackrock")
assert resp.status_code == 502
# --- SEC CIK Map ---
@pytest.mark.asyncio
@patch("routes_regulators.regulators_service.get_cik_map", new_callable=AsyncMock)
async def test_sec_cik_map_happy_path(mock_fn, client):
mock_fn.return_value = [{"symbol": "AAPL", "cik": "0000320193"}]
resp = await client.get("/api/v1/regulators/sec/cik-map/AAPL")
assert resp.status_code == 200
data = resp.json()
assert data["success"] is True
assert data["data"][0]["cik"] == "0000320193"
mock_fn.assert_called_once_with("AAPL")
@pytest.mark.asyncio
@patch("routes_regulators.regulators_service.get_cik_map", new_callable=AsyncMock)
async def test_sec_cik_map_not_found(mock_fn, client):
mock_fn.return_value = []
resp = await client.get("/api/v1/regulators/sec/cik-map/XXXX")
assert resp.status_code == 200
assert resp.json()["data"] == []
@pytest.mark.asyncio
@patch("routes_regulators.regulators_service.get_cik_map", new_callable=AsyncMock)
async def test_sec_cik_map_service_error_returns_502(mock_fn, client):
mock_fn.side_effect = RuntimeError("SEC lookup failed")
resp = await client.get("/api/v1/regulators/sec/cik-map/AAPL")
assert resp.status_code == 502
@pytest.mark.asyncio
async def test_sec_cik_map_invalid_symbol(client):
resp = await client.get("/api/v1/regulators/sec/cik-map/INVALID!!!")
assert resp.status_code == 400

View File

@@ -14,13 +14,17 @@ async def client():
@pytest.mark.asyncio
@patch("routes_sentiment.alphavantage_service.get_news_sentiment", new_callable=AsyncMock)
@patch("routes_sentiment.finnhub_service.get_recommendation_trends", new_callable=AsyncMock)
@patch("routes_sentiment.openbb_service.get_upgrades_downgrades", new_callable=AsyncMock)
@patch("routes_sentiment.finnhub_service.get_reddit_sentiment", new_callable=AsyncMock)
@patch("routes_sentiment.finnhub_service.get_sentiment_summary", new_callable=AsyncMock)
async def test_stock_sentiment(mock_sentiment, mock_av, client):
@patch("routes_sentiment.alphavantage_service.get_news_sentiment", new_callable=AsyncMock)
async def test_stock_sentiment(mock_av, mock_sentiment, mock_reddit, mock_upgrades, mock_recs, client):
# Route was refactored to return composite_score/composite_label/details/source_scores
mock_sentiment.return_value = {
"symbol": "AAPL",
"news_sentiment": {"bullish_percent": 0.7, "bearish_percent": 0.3},
"recent_news": [],
"recent_news": [{"headline": "Apple strong"}],
"analyst_recommendations": [],
"recent_upgrades_downgrades": [],
}
@@ -31,12 +35,22 @@ async def test_stock_sentiment(mock_sentiment, mock_av, client):
"overall_sentiment": {"avg_score": 0.4, "label": "Bullish"},
"articles": [],
}
mock_reddit.return_value = {"found": False, "symbol": "AAPL"}
mock_upgrades.return_value = []
mock_recs.return_value = []
resp = await client.get("/api/v1/stock/AAPL/sentiment")
assert resp.status_code == 200
data = resp.json()["data"]
assert data["symbol"] == "AAPL"
assert data["news_sentiment"]["bullish_percent"] == 0.7
assert data["alpha_vantage_sentiment"]["overall_sentiment"]["label"] == "Bullish"
# New composite response shape
assert "composite_score" in data
assert "composite_label" in data
assert "source_scores" in data
assert "details" in data
# AV news data accessible via details
assert data["details"]["news_sentiment"]["overall_sentiment"]["label"] == "Bullish"
# Finnhub news accessible via details
assert len(data["details"]["finnhub_news"]) == 1
@pytest.mark.asyncio

View File

@@ -0,0 +1,358 @@
"""Tests for new sentiment routes: social sentiment, reddit, composite sentiment, trending."""
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
# --- Social Sentiment (Finnhub) ---
@pytest.mark.asyncio
@patch("routes_sentiment.finnhub_service.get_social_sentiment", new_callable=AsyncMock)
async def test_stock_social_sentiment_happy_path(mock_fn, client):
mock_fn.return_value = {
"configured": True,
"symbol": "AAPL",
"reddit_summary": {"total_mentions": 150, "avg_score": 0.55, "data_points": 5},
"twitter_summary": {"total_mentions": 300, "avg_score": 0.40, "data_points": 8},
"reddit": [{"mention": 30, "score": 0.5}],
"twitter": [{"mention": 40, "score": 0.4}],
}
resp = await client.get("/api/v1/stock/AAPL/social-sentiment")
assert resp.status_code == 200
data = resp.json()
assert data["success"] is True
assert data["data"]["symbol"] == "AAPL"
assert data["data"]["reddit_summary"]["total_mentions"] == 150
@pytest.mark.asyncio
@patch("routes_sentiment.finnhub_service.get_social_sentiment", new_callable=AsyncMock)
async def test_stock_social_sentiment_not_configured(mock_fn, client):
mock_fn.return_value = {"configured": False, "message": "Set INVEST_API_FINNHUB_API_KEY"}
resp = await client.get("/api/v1/stock/AAPL/social-sentiment")
assert resp.status_code == 200
data = resp.json()
assert data["data"]["configured"] is False
@pytest.mark.asyncio
@patch("routes_sentiment.finnhub_service.get_social_sentiment", new_callable=AsyncMock)
async def test_stock_social_sentiment_premium_required(mock_fn, client):
mock_fn.return_value = {
"configured": True,
"symbol": "AAPL",
"premium_required": True,
"reddit": [],
"twitter": [],
}
resp = await client.get("/api/v1/stock/AAPL/social-sentiment")
assert resp.status_code == 200
data = resp.json()
assert data["data"]["premium_required"] is True
@pytest.mark.asyncio
@patch("routes_sentiment.finnhub_service.get_social_sentiment", new_callable=AsyncMock)
async def test_stock_social_sentiment_service_error_returns_502(mock_fn, client):
mock_fn.side_effect = RuntimeError("Finnhub error")
resp = await client.get("/api/v1/stock/AAPL/social-sentiment")
assert resp.status_code == 502
@pytest.mark.asyncio
async def test_stock_social_sentiment_invalid_symbol(client):
resp = await client.get("/api/v1/stock/INVALID!!!/social-sentiment")
assert resp.status_code == 400
# --- Reddit Sentiment ---
@pytest.mark.asyncio
@patch("routes_sentiment.finnhub_service.get_reddit_sentiment", new_callable=AsyncMock)
async def test_stock_reddit_sentiment_found(mock_fn, client):
mock_fn.return_value = {
"symbol": "AAPL",
"found": True,
"rank": 3,
"mentions_24h": 150,
"mentions_24h_ago": 100,
"mentions_change_pct": 50.0,
"upvotes": 500,
"rank_24h_ago": 5,
}
resp = await client.get("/api/v1/stock/AAPL/reddit-sentiment")
assert resp.status_code == 200
data = resp.json()
assert data["success"] is True
assert data["data"]["found"] is True
assert data["data"]["rank"] == 3
assert data["data"]["mentions_24h"] == 150
assert data["data"]["mentions_change_pct"] == 50.0
@pytest.mark.asyncio
@patch("routes_sentiment.finnhub_service.get_reddit_sentiment", new_callable=AsyncMock)
async def test_stock_reddit_sentiment_not_found(mock_fn, client):
mock_fn.return_value = {
"symbol": "OBSCURE",
"found": False,
"message": "OBSCURE not in Reddit top trending (not enough mentions)",
}
resp = await client.get("/api/v1/stock/OBSCURE/reddit-sentiment")
assert resp.status_code == 200
data = resp.json()
assert data["data"]["found"] is False
assert "not in Reddit" in data["data"]["message"]
@pytest.mark.asyncio
@patch("routes_sentiment.finnhub_service.get_reddit_sentiment", new_callable=AsyncMock)
async def test_stock_reddit_sentiment_service_error_returns_502(mock_fn, client):
mock_fn.side_effect = RuntimeError("ApeWisdom down")
resp = await client.get("/api/v1/stock/AAPL/reddit-sentiment")
assert resp.status_code == 502
@pytest.mark.asyncio
async def test_stock_reddit_sentiment_invalid_symbol(client):
resp = await client.get("/api/v1/stock/BAD!!!/reddit-sentiment")
assert resp.status_code == 400
# --- Reddit Trending ---
@pytest.mark.asyncio
@patch("routes_sentiment.finnhub_service.get_reddit_trending", new_callable=AsyncMock)
async def test_reddit_trending_happy_path(mock_fn, client):
mock_fn.return_value = [
{"rank": 1, "symbol": "TSLA", "name": "Tesla", "mentions_24h": 500, "upvotes": 1200, "rank_24h_ago": 2, "mentions_24h_ago": 400},
{"rank": 2, "symbol": "AAPL", "name": "Apple", "mentions_24h": 300, "upvotes": 800, "rank_24h_ago": 1, "mentions_24h_ago": 350},
{"rank": 3, "symbol": "GME", "name": "GameStop", "mentions_24h": 200, "upvotes": 600, "rank_24h_ago": 3, "mentions_24h_ago": 180},
]
resp = await client.get("/api/v1/discover/reddit-trending")
assert resp.status_code == 200
data = resp.json()
assert data["success"] is True
assert len(data["data"]) == 3
assert data["data"][0]["symbol"] == "TSLA"
assert data["data"][0]["rank"] == 1
assert data["data"][1]["symbol"] == "AAPL"
@pytest.mark.asyncio
@patch("routes_sentiment.finnhub_service.get_reddit_trending", new_callable=AsyncMock)
async def test_reddit_trending_empty(mock_fn, client):
mock_fn.return_value = []
resp = await client.get("/api/v1/discover/reddit-trending")
assert resp.status_code == 200
assert resp.json()["data"] == []
@pytest.mark.asyncio
@patch("routes_sentiment.finnhub_service.get_reddit_trending", new_callable=AsyncMock)
async def test_reddit_trending_service_error_returns_502(mock_fn, client):
mock_fn.side_effect = RuntimeError("ApeWisdom unavailable")
resp = await client.get("/api/v1/discover/reddit-trending")
assert resp.status_code == 502
# --- Composite /stock/{symbol}/sentiment (aggregation logic) ---
@pytest.mark.asyncio
@patch("routes_sentiment.finnhub_service.get_recommendation_trends", new_callable=AsyncMock)
@patch("routes_sentiment.openbb_service.get_upgrades_downgrades", new_callable=AsyncMock)
@patch("routes_sentiment.finnhub_service.get_reddit_sentiment", new_callable=AsyncMock)
@patch("routes_sentiment.finnhub_service.get_sentiment_summary", new_callable=AsyncMock)
@patch("routes_sentiment.alphavantage_service.get_news_sentiment", new_callable=AsyncMock)
async def test_composite_sentiment_all_sources(mock_av, mock_fh, mock_reddit, mock_upgrades, mock_recs, client):
mock_av.return_value = {
"configured": True,
"symbol": "AAPL",
"overall_sentiment": {"avg_score": 0.2, "label": "Bullish"},
"articles": [],
}
mock_fh.return_value = {
"symbol": "AAPL",
"news_sentiment": {},
"recent_news": [{"headline": "Apple rises", "source": "Reuters"}],
"analyst_recommendations": [],
"recent_upgrades_downgrades": [],
}
mock_reddit.return_value = {
"symbol": "AAPL",
"found": True,
"rank": 2,
"mentions_24h": 200,
"mentions_24h_ago": 150,
"mentions_change_pct": 33.3,
"upvotes": 800,
}
mock_upgrades.return_value = [
{"action": "up", "company": "Goldman"},
{"action": "down", "company": "Morgan Stanley"},
{"action": "init", "company": "JPMorgan"},
]
mock_recs.return_value = [
{"strongBuy": 10, "buy": 15, "hold": 5, "sell": 2, "strongSell": 1}
]
resp = await client.get("/api/v1/stock/AAPL/sentiment")
assert resp.status_code == 200
data = resp.json()
assert data["success"] is True
result = data["data"]
assert result["symbol"] == "AAPL"
assert result["composite_score"] is not None
assert result["composite_label"] in ("Strong Bullish", "Bullish", "Neutral", "Bearish", "Strong Bearish")
assert "news" in result["source_scores"]
assert "analysts" in result["source_scores"]
assert "upgrades" in result["source_scores"]
assert "reddit" in result["source_scores"]
assert "details" in result
@pytest.mark.asyncio
@patch("routes_sentiment.finnhub_service.get_recommendation_trends", new_callable=AsyncMock)
@patch("routes_sentiment.openbb_service.get_upgrades_downgrades", new_callable=AsyncMock)
@patch("routes_sentiment.finnhub_service.get_reddit_sentiment", new_callable=AsyncMock)
@patch("routes_sentiment.finnhub_service.get_sentiment_summary", new_callable=AsyncMock)
@patch("routes_sentiment.alphavantage_service.get_news_sentiment", new_callable=AsyncMock)
async def test_composite_sentiment_no_data_returns_unknown(mock_av, mock_fh, mock_reddit, mock_upgrades, mock_recs, client):
mock_av.return_value = {}
mock_fh.return_value = {}
mock_reddit.return_value = {"found": False}
mock_upgrades.return_value = []
mock_recs.return_value = []
resp = await client.get("/api/v1/stock/AAPL/sentiment")
assert resp.status_code == 200
data = resp.json()["data"]
assert data["composite_score"] is None
assert data["composite_label"] == "Unknown"
assert data["source_scores"] == {}
@pytest.mark.asyncio
@patch("routes_sentiment.finnhub_service.get_recommendation_trends", new_callable=AsyncMock)
@patch("routes_sentiment.openbb_service.get_upgrades_downgrades", new_callable=AsyncMock)
@patch("routes_sentiment.finnhub_service.get_reddit_sentiment", new_callable=AsyncMock)
@patch("routes_sentiment.finnhub_service.get_sentiment_summary", new_callable=AsyncMock)
@patch("routes_sentiment.alphavantage_service.get_news_sentiment", new_callable=AsyncMock)
async def test_composite_sentiment_strong_bullish_label(mock_av, mock_fh, mock_reddit, mock_upgrades, mock_recs, client):
# All signals strongly bullish
mock_av.return_value = {"overall_sentiment": {"avg_score": 0.35}}
mock_fh.return_value = {}
mock_reddit.return_value = {"found": True, "mentions_24h": 500, "mentions_change_pct": 100.0}
mock_upgrades.return_value = [{"action": "up"}, {"action": "up"}, {"action": "up"}]
mock_recs.return_value = [{"strongBuy": 20, "buy": 10, "hold": 1, "sell": 0, "strongSell": 0}]
resp = await client.get("/api/v1/stock/AAPL/sentiment")
assert resp.status_code == 200
data = resp.json()["data"]
assert data["composite_score"] >= 0.5
assert data["composite_label"] == "Strong Bullish"
@pytest.mark.asyncio
@patch("routes_sentiment.finnhub_service.get_recommendation_trends", new_callable=AsyncMock)
@patch("routes_sentiment.openbb_service.get_upgrades_downgrades", new_callable=AsyncMock)
@patch("routes_sentiment.finnhub_service.get_reddit_sentiment", new_callable=AsyncMock)
@patch("routes_sentiment.finnhub_service.get_sentiment_summary", new_callable=AsyncMock)
@patch("routes_sentiment.alphavantage_service.get_news_sentiment", new_callable=AsyncMock)
async def test_composite_sentiment_bearish_label(mock_av, mock_fh, mock_reddit, mock_upgrades, mock_recs, client):
# All signals bearish
mock_av.return_value = {"overall_sentiment": {"avg_score": -0.3}}
mock_fh.return_value = {}
mock_reddit.return_value = {"found": True, "mentions_24h": 200, "mentions_change_pct": -70.0}
mock_upgrades.return_value = [{"action": "down"}, {"action": "down"}, {"action": "down"}]
mock_recs.return_value = [{"strongBuy": 0, "buy": 2, "hold": 5, "sell": 10, "strongSell": 5}]
resp = await client.get("/api/v1/stock/AAPL/sentiment")
assert resp.status_code == 200
data = resp.json()["data"]
assert data["composite_label"] in ("Bearish", "Strong Bearish")
@pytest.mark.asyncio
@patch("routes_sentiment.finnhub_service.get_recommendation_trends", new_callable=AsyncMock)
@patch("routes_sentiment.openbb_service.get_upgrades_downgrades", new_callable=AsyncMock)
@patch("routes_sentiment.finnhub_service.get_reddit_sentiment", new_callable=AsyncMock)
@patch("routes_sentiment.finnhub_service.get_sentiment_summary", new_callable=AsyncMock)
@patch("routes_sentiment.alphavantage_service.get_news_sentiment", new_callable=AsyncMock)
async def test_composite_sentiment_one_source_failing_is_graceful(mock_av, mock_fh, mock_reddit, mock_upgrades, mock_recs, client):
# Simulate an exception from one source — gather uses return_exceptions=True
mock_av.side_effect = RuntimeError("AV down")
mock_fh.return_value = {}
mock_reddit.return_value = {"found": False}
mock_upgrades.return_value = []
mock_recs.return_value = [{"strongBuy": 5, "buy": 5, "hold": 3, "sell": 1, "strongSell": 0}]
resp = await client.get("/api/v1/stock/AAPL/sentiment")
# Should still succeed, gracefully skipping the failed source
assert resp.status_code == 200
data = resp.json()["data"]
assert data["symbol"] == "AAPL"
@pytest.mark.asyncio
async def test_composite_sentiment_invalid_symbol(client):
resp = await client.get("/api/v1/stock/INVALID!!!/sentiment")
assert resp.status_code == 400
@pytest.mark.asyncio
@patch("routes_sentiment.finnhub_service.get_recommendation_trends", new_callable=AsyncMock)
@patch("routes_sentiment.openbb_service.get_upgrades_downgrades", new_callable=AsyncMock)
@patch("routes_sentiment.finnhub_service.get_reddit_sentiment", new_callable=AsyncMock)
@patch("routes_sentiment.finnhub_service.get_sentiment_summary", new_callable=AsyncMock)
@patch("routes_sentiment.alphavantage_service.get_news_sentiment", new_callable=AsyncMock)
async def test_composite_sentiment_reddit_low_mentions_excluded(mock_av, mock_fh, mock_reddit, mock_upgrades, mock_recs, client):
# Reddit mentions < 10 threshold should exclude reddit from scoring
mock_av.return_value = {}
mock_fh.return_value = {}
mock_reddit.return_value = {"found": True, "mentions_24h": 5, "mentions_change_pct": 50.0}
mock_upgrades.return_value = []
mock_recs.return_value = []
resp = await client.get("/api/v1/stock/AAPL/sentiment")
assert resp.status_code == 200
data = resp.json()["data"]
assert "reddit" not in data["source_scores"]
@pytest.mark.asyncio
@patch("routes_sentiment.finnhub_service.get_recommendation_trends", new_callable=AsyncMock)
@patch("routes_sentiment.openbb_service.get_upgrades_downgrades", new_callable=AsyncMock)
@patch("routes_sentiment.finnhub_service.get_reddit_sentiment", new_callable=AsyncMock)
@patch("routes_sentiment.finnhub_service.get_sentiment_summary", new_callable=AsyncMock)
@patch("routes_sentiment.alphavantage_service.get_news_sentiment", new_callable=AsyncMock)
async def test_composite_sentiment_details_structure(mock_av, mock_fh, mock_reddit, mock_upgrades, mock_recs, client):
mock_av.return_value = {"overall_sentiment": {"avg_score": 0.1}}
mock_fh.return_value = {"recent_news": [{"headline": "Test news"}]}
mock_reddit.return_value = {"found": False}
mock_upgrades.return_value = [{"action": "up"}, {"action": "up"}]
mock_recs.return_value = []
resp = await client.get("/api/v1/stock/MSFT/sentiment")
assert resp.status_code == 200
details = resp.json()["data"]["details"]
assert "news_sentiment" in details
assert "analyst_recommendations" in details
assert "recent_upgrades" in details
assert "reddit" in details
assert "finnhub_news" in details

176
tests/test_routes_shorts.py Normal file
View File

@@ -0,0 +1,176 @@
"""Tests for shorts and dark pool 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
# --- Short Volume ---
@pytest.mark.asyncio
@patch("routes_shorts.shorts_service.get_short_volume", new_callable=AsyncMock)
async def test_short_volume_happy_path(mock_fn, client):
mock_fn.return_value = [
{"date": "2026-03-18", "short_volume": 5000000, "short_exempt_volume": 10000, "total_volume": 20000000, "short_volume_percent": 0.25}
]
resp = await client.get("/api/v1/stock/AAPL/shorts/volume")
assert resp.status_code == 200
data = resp.json()
assert data["success"] is True
assert len(data["data"]) == 1
assert data["data"][0]["short_volume"] == 5000000
@pytest.mark.asyncio
@patch("routes_shorts.shorts_service.get_short_volume", new_callable=AsyncMock)
async def test_short_volume_empty(mock_fn, client):
mock_fn.return_value = []
resp = await client.get("/api/v1/stock/GME/shorts/volume")
assert resp.status_code == 200
data = resp.json()
assert data["success"] is True
assert data["data"] == []
@pytest.mark.asyncio
@patch("routes_shorts.shorts_service.get_short_volume", new_callable=AsyncMock)
async def test_short_volume_service_error_returns_502(mock_fn, client):
mock_fn.side_effect = RuntimeError("stockgrid unavailable")
resp = await client.get("/api/v1/stock/AAPL/shorts/volume")
assert resp.status_code == 502
@pytest.mark.asyncio
async def test_short_volume_invalid_symbol(client):
resp = await client.get("/api/v1/stock/AAPL;DROP/shorts/volume")
assert resp.status_code == 400
# --- Fails To Deliver ---
@pytest.mark.asyncio
@patch("routes_shorts.shorts_service.get_fails_to_deliver", new_callable=AsyncMock)
async def test_ftd_happy_path(mock_fn, client):
mock_fn.return_value = [
{"date": "2026-03-01", "cusip": "037833100", "failure_quantity": 50000, "symbol": "AAPL", "price": 175.0}
]
resp = await client.get("/api/v1/stock/AAPL/shorts/ftd")
assert resp.status_code == 200
data = resp.json()
assert data["success"] is True
assert data["data"][0]["symbol"] == "AAPL"
assert data["data"][0]["failure_quantity"] == 50000
@pytest.mark.asyncio
@patch("routes_shorts.shorts_service.get_fails_to_deliver", new_callable=AsyncMock)
async def test_ftd_empty(mock_fn, client):
mock_fn.return_value = []
resp = await client.get("/api/v1/stock/TSLA/shorts/ftd")
assert resp.status_code == 200
assert resp.json()["data"] == []
@pytest.mark.asyncio
@patch("routes_shorts.shorts_service.get_fails_to_deliver", new_callable=AsyncMock)
async def test_ftd_service_error_returns_502(mock_fn, client):
mock_fn.side_effect = RuntimeError("SEC connection failed")
resp = await client.get("/api/v1/stock/AAPL/shorts/ftd")
assert resp.status_code == 502
@pytest.mark.asyncio
async def test_ftd_invalid_symbol(client):
resp = await client.get("/api/v1/stock/BAD!!!/shorts/ftd")
assert resp.status_code == 400
# --- Short Interest ---
@pytest.mark.asyncio
@patch("routes_shorts.shorts_service.get_short_interest", new_callable=AsyncMock)
async def test_short_interest_happy_path(mock_fn, client):
mock_fn.return_value = [
{"settlement_date": "2026-02-28", "symbol": "GME", "short_interest": 20000000, "days_to_cover": 3.5}
]
resp = await client.get("/api/v1/stock/GME/shorts/interest")
assert resp.status_code == 200
data = resp.json()
assert data["success"] is True
assert data["data"][0]["short_interest"] == 20000000
assert data["data"][0]["days_to_cover"] == 3.5
@pytest.mark.asyncio
@patch("routes_shorts.shorts_service.get_short_interest", new_callable=AsyncMock)
async def test_short_interest_empty(mock_fn, client):
mock_fn.return_value = []
resp = await client.get("/api/v1/stock/NVDA/shorts/interest")
assert resp.status_code == 200
assert resp.json()["data"] == []
@pytest.mark.asyncio
@patch("routes_shorts.shorts_service.get_short_interest", new_callable=AsyncMock)
async def test_short_interest_service_error_returns_502(mock_fn, client):
mock_fn.side_effect = RuntimeError("FINRA unavailable")
resp = await client.get("/api/v1/stock/AAPL/shorts/interest")
assert resp.status_code == 502
@pytest.mark.asyncio
async def test_short_interest_invalid_symbol(client):
resp = await client.get("/api/v1/stock/INVALID!!!/shorts/interest")
assert resp.status_code == 400
# --- Dark Pool OTC ---
@pytest.mark.asyncio
@patch("routes_shorts.shorts_service.get_darkpool_otc", new_callable=AsyncMock)
async def test_darkpool_otc_happy_path(mock_fn, client):
mock_fn.return_value = [
{"date": "2026-03-18", "symbol": "AAPL", "shares": 3000000, "percentage": 12.5}
]
resp = await client.get("/api/v1/darkpool/AAPL/otc")
assert resp.status_code == 200
data = resp.json()
assert data["success"] is True
assert data["data"][0]["percentage"] == 12.5
@pytest.mark.asyncio
@patch("routes_shorts.shorts_service.get_darkpool_otc", new_callable=AsyncMock)
async def test_darkpool_otc_empty(mock_fn, client):
mock_fn.return_value = []
resp = await client.get("/api/v1/darkpool/TSLA/otc")
assert resp.status_code == 200
assert resp.json()["data"] == []
@pytest.mark.asyncio
@patch("routes_shorts.shorts_service.get_darkpool_otc", new_callable=AsyncMock)
async def test_darkpool_otc_service_error_returns_502(mock_fn, client):
mock_fn.side_effect = RuntimeError("FINRA connection timeout")
resp = await client.get("/api/v1/darkpool/AAPL/otc")
assert resp.status_code == 502
@pytest.mark.asyncio
async def test_darkpool_otc_invalid_symbol(client):
resp = await client.get("/api/v1/darkpool/BAD!!!/otc")
assert resp.status_code == 400

View File

@@ -0,0 +1,189 @@
"""Tests for economy survey 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
# --- Michigan Consumer Sentiment ---
@pytest.mark.asyncio
@patch("routes_surveys.surveys_service.get_michigan", new_callable=AsyncMock)
async def test_survey_michigan_happy_path(mock_fn, client):
mock_fn.return_value = [
{"date": "2026-03-01", "consumer_sentiment": 76.5, "inflation_expectation_1yr": 3.1}
]
resp = await client.get("/api/v1/economy/surveys/michigan")
assert resp.status_code == 200
data = resp.json()
assert data["success"] is True
assert data["data"][0]["consumer_sentiment"] == 76.5
@pytest.mark.asyncio
@patch("routes_surveys.surveys_service.get_michigan", new_callable=AsyncMock)
async def test_survey_michigan_empty(mock_fn, client):
mock_fn.return_value = []
resp = await client.get("/api/v1/economy/surveys/michigan")
assert resp.status_code == 200
assert resp.json()["data"] == []
@pytest.mark.asyncio
@patch("routes_surveys.surveys_service.get_michigan", new_callable=AsyncMock)
async def test_survey_michigan_service_error_returns_502(mock_fn, client):
mock_fn.side_effect = RuntimeError("FRED unavailable")
resp = await client.get("/api/v1/economy/surveys/michigan")
assert resp.status_code == 502
# --- SLOOS ---
@pytest.mark.asyncio
@patch("routes_surveys.surveys_service.get_sloos", new_callable=AsyncMock)
async def test_survey_sloos_happy_path(mock_fn, client):
mock_fn.return_value = [
{"date": "2026-01-01", "c_i_tightening_pct": 25.0, "consumer_tightening_pct": 10.0}
]
resp = await client.get("/api/v1/economy/surveys/sloos")
assert resp.status_code == 200
data = resp.json()
assert data["success"] is True
assert data["data"][0]["c_i_tightening_pct"] == 25.0
@pytest.mark.asyncio
@patch("routes_surveys.surveys_service.get_sloos", new_callable=AsyncMock)
async def test_survey_sloos_empty(mock_fn, client):
mock_fn.return_value = []
resp = await client.get("/api/v1/economy/surveys/sloos")
assert resp.status_code == 200
assert resp.json()["data"] == []
@pytest.mark.asyncio
@patch("routes_surveys.surveys_service.get_sloos", new_callable=AsyncMock)
async def test_survey_sloos_service_error_returns_502(mock_fn, client):
mock_fn.side_effect = RuntimeError("FRED down")
resp = await client.get("/api/v1/economy/surveys/sloos")
assert resp.status_code == 502
# --- Nonfarm Payrolls ---
@pytest.mark.asyncio
@patch("routes_surveys.surveys_service.get_nonfarm_payrolls", new_callable=AsyncMock)
async def test_survey_nfp_happy_path(mock_fn, client):
mock_fn.return_value = [
{"date": "2026-03-07", "value": 275000, "industry": "total_nonfarm"}
]
resp = await client.get("/api/v1/economy/surveys/nonfarm-payrolls")
assert resp.status_code == 200
data = resp.json()
assert data["success"] is True
assert data["data"][0]["value"] == 275000
@pytest.mark.asyncio
@patch("routes_surveys.surveys_service.get_nonfarm_payrolls", new_callable=AsyncMock)
async def test_survey_nfp_empty(mock_fn, client):
mock_fn.return_value = []
resp = await client.get("/api/v1/economy/surveys/nonfarm-payrolls")
assert resp.status_code == 200
assert resp.json()["data"] == []
@pytest.mark.asyncio
@patch("routes_surveys.surveys_service.get_nonfarm_payrolls", new_callable=AsyncMock)
async def test_survey_nfp_service_error_returns_502(mock_fn, client):
mock_fn.side_effect = RuntimeError("BLS unavailable")
resp = await client.get("/api/v1/economy/surveys/nonfarm-payrolls")
assert resp.status_code == 502
# --- Empire State Manufacturing ---
@pytest.mark.asyncio
@patch("routes_surveys.surveys_service.get_empire_state", new_callable=AsyncMock)
async def test_survey_empire_state_happy_path(mock_fn, client):
mock_fn.return_value = [
{"date": "2026-03-01", "general_business_conditions": -7.58}
]
resp = await client.get("/api/v1/economy/surveys/empire-state")
assert resp.status_code == 200
data = resp.json()
assert data["success"] is True
assert data["data"][0]["general_business_conditions"] == -7.58
@pytest.mark.asyncio
@patch("routes_surveys.surveys_service.get_empire_state", new_callable=AsyncMock)
async def test_survey_empire_state_empty(mock_fn, client):
mock_fn.return_value = []
resp = await client.get("/api/v1/economy/surveys/empire-state")
assert resp.status_code == 200
assert resp.json()["data"] == []
@pytest.mark.asyncio
@patch("routes_surveys.surveys_service.get_empire_state", new_callable=AsyncMock)
async def test_survey_empire_state_service_error_returns_502(mock_fn, client):
mock_fn.side_effect = RuntimeError("FRED connection error")
resp = await client.get("/api/v1/economy/surveys/empire-state")
assert resp.status_code == 502
# --- BLS Search ---
@pytest.mark.asyncio
@patch("routes_surveys.surveys_service.bls_search", new_callable=AsyncMock)
async def test_survey_bls_search_happy_path(mock_fn, client):
mock_fn.return_value = [
{"series_id": "CES0000000001", "series_title": "All employees, thousands, total nonfarm"},
{"series_id": "CES1000000001", "series_title": "All employees, thousands, mining and logging"},
]
resp = await client.get("/api/v1/economy/surveys/bls-search?query=nonfarm+payrolls")
assert resp.status_code == 200
data = resp.json()
assert data["success"] is True
assert len(data["data"]) == 2
assert data["data"][0]["series_id"] == "CES0000000001"
mock_fn.assert_called_once_with(query="nonfarm payrolls")
@pytest.mark.asyncio
async def test_survey_bls_search_missing_query(client):
resp = await client.get("/api/v1/economy/surveys/bls-search")
assert resp.status_code == 422
@pytest.mark.asyncio
@patch("routes_surveys.surveys_service.bls_search", new_callable=AsyncMock)
async def test_survey_bls_search_empty(mock_fn, client):
mock_fn.return_value = []
resp = await client.get("/api/v1/economy/surveys/bls-search?query=nothingtofind")
assert resp.status_code == 200
assert resp.json()["data"] == []
@pytest.mark.asyncio
@patch("routes_surveys.surveys_service.bls_search", new_callable=AsyncMock)
async def test_survey_bls_search_service_error_returns_502(mock_fn, client):
mock_fn.side_effect = RuntimeError("BLS API down")
resp = await client.get("/api/v1/economy/surveys/bls-search?query=wages")
assert resp.status_code == 502