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:
367
tests/test_finnhub_service_social.py
Normal file
367
tests/test_finnhub_service_social.py
Normal 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 == []
|
||||
433
tests/test_routes_economy.py
Normal file
433
tests/test_routes_economy.py
Normal 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®ion=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"] == []
|
||||
328
tests/test_routes_fixed_income.py
Normal file
328
tests/test_routes_fixed_income.py
Normal 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"] == []
|
||||
228
tests/test_routes_regulators.py
Normal file
228
tests/test_routes_regulators.py
Normal 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
|
||||
@@ -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
|
||||
|
||||
358
tests/test_routes_sentiment_social.py
Normal file
358
tests/test_routes_sentiment_social.py
Normal 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
176
tests/test_routes_shorts.py
Normal 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
|
||||
189
tests/test_routes_surveys.py
Normal file
189
tests/test_routes_surveys.py
Normal 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
|
||||
Reference in New Issue
Block a user