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)
368 lines
13 KiB
Python
368 lines
13 KiB
Python
"""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 == []
|