diff options
Diffstat (limited to 'backend/tests/test_news_service.py')
| -rw-r--r-- | backend/tests/test_news_service.py | 200 |
1 files changed, 200 insertions, 0 deletions
diff --git a/backend/tests/test_news_service.py b/backend/tests/test_news_service.py new file mode 100644 index 0000000..297c2f6 --- /dev/null +++ b/backend/tests/test_news_service.py @@ -0,0 +1,200 @@ +"""Tests for backend news service.""" +from datetime import datetime, timezone +from unittest.mock import MagicMock, patch + +import pytest + +from app.services import news_service + + +def _clear_news_cache() -> None: + news_service.NEWS_CACHE.clear() + + +def _finnhub_article(**kwargs) -> dict: + defaults = { + "datetime": int(datetime(2026, 6, 22, 18, 42, 0, tzinfo=timezone.utc).timestamp()), + "headline": "AMD surges on strong demand", + "summary": "Chipmaker gains as data-center revenue rises", + "source": "bloomberg.com", + "url": "https://www.bloomberg.com/news/article", + } + defaults.update(kwargs) + return defaults + + +def _fmp_article(**kwargs) -> dict: + defaults = { + "publishedDate": "2026-06-22 18:42:00", + "title": "AMD misses on guidance", + "text": "Semiconductor firm issues weak outlook", + "site": "barrons.com", + "newsURL": "https://www.barrons.com/articles/amd", + } + defaults.update(kwargs) + return defaults + + +def test_get_news_shape(monkeypatch) -> None: + _clear_news_cache() + monkeypatch.setattr(news_service, "_finnhub_company_news", lambda symbol: [_finnhub_article()]) + monkeypatch.setattr(news_service, "_finnhub_news_sentiment", lambda symbol: { + "buzz": {"articlesInLastWeek": 12}, + "sentiment": {"bullishPercent": 0.38, "bearishPercent": 0.40, "neutralPercent": 0.22}, + }) + monkeypatch.setattr(news_service, "_fmp_stock_news", lambda symbol: []) + + result = news_service.get_news("AMD") + + assert result["provider"] == "finnhub" + assert len(result["items"]) == 1 + item = result["items"][0] + assert item["id"] == "6aa272d2b9f4f7f4" + assert item["title"] == "AMD surges on strong demand" + assert item["summary"] == "Chipmaker gains as data-center revenue rises" + assert item["source"] == "bloomberg" + assert item["url"] == "https://www.bloomberg.com/news/article" + assert item["sentiment"] == "bullish" + assert item["published_at"] == "2026-06-22T18:42:00Z" + assert result["aggregate"] == { + "buzz_articles_last_week": 12, + "bullish_pct": 0.38, + "bearish_pct": 0.40, + "neutral_pct": 0.22, + } + assert result["fetched_at"] + + +def test_get_news_fmp_fallback_when_finnhub_empty(monkeypatch) -> None: + _clear_news_cache() + monkeypatch.setattr(news_service, "_finnhub_company_news", lambda symbol: []) + monkeypatch.setattr(news_service, "_finnhub_news_sentiment", lambda symbol: None) + monkeypatch.setattr(news_service, "_fmp_stock_news", lambda symbol: [_fmp_article()]) + + result = news_service.get_news("AMD") + + assert result["provider"] == "fmp" + assert len(result["items"]) == 1 + item = result["items"][0] + assert item["title"] == "AMD misses on guidance" + assert item["summary"] == "Semiconductor firm issues weak outlook" + assert item["source"] == "barrons" + assert item["url"] == "https://www.barrons.com/articles/amd" + assert item["sentiment"] == "bearish" + assert item["published_at"] == "2026-06-22T18:42:00Z" + assert result["aggregate"] is None + + +def test_get_news_drops_unsafe_urls(monkeypatch) -> None: + _clear_news_cache() + monkeypatch.setattr(news_service, "_finnhub_company_news", lambda symbol: [ + _finnhub_article(url="javascript:alert(1)"), + _finnhub_article(url="data:text/html,<script>alert(1)</script>"), + _finnhub_article(url="https://safe.example.com/news"), + ]) + monkeypatch.setattr(news_service, "_finnhub_news_sentiment", lambda symbol: None) + monkeypatch.setattr(news_service, "_fmp_stock_news", lambda symbol: []) + + result = news_service.get_news("AMD") + + assert len(result["items"]) == 1 + assert result["items"][0]["url"] == "https://safe.example.com/news" + + +def test_get_news_both_upstream_empty(monkeypatch) -> None: + _clear_news_cache() + monkeypatch.setattr(news_service, "_finnhub_company_news", lambda symbol: []) + monkeypatch.setattr(news_service, "_finnhub_news_sentiment", lambda symbol: None) + monkeypatch.setattr(news_service, "_fmp_stock_news", lambda symbol: []) + + result = news_service.get_news("AMD") + + assert result["items"] == [] + assert result["aggregate"] is None + assert result["provider"] is None + assert result["fetched_at"] + + +def test_get_news_aggregate_failure_still_returns_items(monkeypatch) -> None: + _clear_news_cache() + monkeypatch.setattr(news_service, "_finnhub_company_news", lambda symbol: [_finnhub_article()]) + monkeypatch.setattr(news_service, "_finnhub_news_sentiment", lambda symbol: None) + monkeypatch.setattr(news_service, "_fmp_stock_news", lambda symbol: []) + + result = news_service.get_news("AMD") + + assert len(result["items"]) == 1 + assert result["aggregate"] is None + + +def test_get_news_sentiment_classifications(monkeypatch) -> None: + _clear_news_cache() + monkeypatch.setattr(news_service, "_finnhub_company_news", lambda symbol: [ + _finnhub_article(headline="AMD surges on strong demand", summary="growth beats"), + _finnhub_article(headline="AMD misses and drops", summary="weak warning"), + _finnhub_article(headline="AMD reports quarterly results", summary="revenue flat"), + ]) + monkeypatch.setattr(news_service, "_finnhub_news_sentiment", lambda symbol: None) + monkeypatch.setattr(news_service, "_fmp_stock_news", lambda symbol: []) + + result = news_service.get_news("AMD") + sentiments = [i["sentiment"] for i in result["items"]] + + assert "bullish" in sentiments + assert "bearish" in sentiments + assert "neutral" in sentiments + + +def test_get_news_caches_result(monkeypatch) -> None: + _clear_news_cache() + call_count = {"finnhub": 0} + + def _finnhub(symbol: str) -> list[dict]: + call_count["finnhub"] += 1 + return [_finnhub_article()] + + monkeypatch.setattr(news_service, "_finnhub_company_news", _finnhub) + monkeypatch.setattr(news_service, "_finnhub_news_sentiment", lambda symbol: None) + monkeypatch.setattr(news_service, "_fmp_stock_news", lambda symbol: []) + + news_service.get_news("AMD") + news_service.get_news("AMD") + + assert call_count["finnhub"] == 1 + + +def test_get_news_date_parsing_variants(monkeypatch) -> None: + _clear_news_cache() + monkeypatch.setattr(news_service, "_finnhub_company_news", lambda symbol: []) + monkeypatch.setattr(news_service, "_finnhub_news_sentiment", lambda symbol: None) + monkeypatch.setattr(news_service, "_fmp_stock_news", lambda symbol: [ + _fmp_article(publishedDate="2026-06-21 09:15:30"), + ]) + + result = news_service.get_news("AMD") + + assert result["items"][0]["published_at"] == "2026-06-21T09:15:30Z" + + +def test_get_news_trims_and_cleans_source(monkeypatch) -> None: + _clear_news_cache() + monkeypatch.setattr(news_service, "_finnhub_company_news", lambda symbol: [ + _finnhub_article(source=" WWW.REUTERS.COM ", url="https://www.reuters.com/x"), + ]) + monkeypatch.setattr(news_service, "_finnhub_news_sentiment", lambda symbol: None) + monkeypatch.setattr(news_service, "_fmp_stock_news", lambda symbol: []) + + result = news_service.get_news("AMD") + + assert result["items"][0]["source"] == "reuters" + + +def test_get_news_capped_at_100(monkeypatch) -> None: + _clear_news_cache() + monkeypatch.setattr(news_service, "_finnhub_company_news", lambda symbol: [_finnhub_article(headline=f"Article {i}") for i in range(150)]) + monkeypatch.setattr(news_service, "_finnhub_news_sentiment", lambda symbol: None) + monkeypatch.setattr(news_service, "_fmp_stock_news", lambda symbol: []) + + result = news_service.get_news("AMD") + + assert len(result["items"]) == 100 |
