"""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,"), _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