You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
135 lines
4.3 KiB
Python
135 lines
4.3 KiB
Python
"""
|
|
Unit tests for vendor → row dict transforms in headline_ingest_service.
|
|
|
|
These exercise the data-shape contract without hitting the database.
|
|
"""
|
|
from datetime import datetime, timezone
|
|
|
|
from app.services.news.alpaca_news_client import AlpacaNewsArticle
|
|
from app.services.news.headline_ingest_service import (
|
|
alpaca_articles_to_rows,
|
|
finnhub_articles_to_rows,
|
|
stocktwits_messages_to_rows,
|
|
)
|
|
|
|
|
|
def _alpaca_article(symbols, headline="Test", source="benzinga"):
|
|
return AlpacaNewsArticle(
|
|
id=12345,
|
|
headline=headline,
|
|
summary="summary",
|
|
url="https://example.com/n/12345",
|
|
author="Author",
|
|
created_at=datetime(2026, 4, 25, 13, 30, tzinfo=timezone.utc),
|
|
updated_at=datetime(2026, 4, 25, 13, 30, tzinfo=timezone.utc),
|
|
source=source,
|
|
symbols=symbols,
|
|
images=[],
|
|
content=None,
|
|
)
|
|
|
|
|
|
def test_alpaca_emits_one_row_per_ticker_with_primary_flag():
|
|
art = _alpaca_article(["AAPL", "MSFT", "NVDA"], headline="Tech rally continues")
|
|
rows = alpaca_articles_to_rows([art])
|
|
assert len(rows) == 3
|
|
tickers = [r["ticker"] for r in rows]
|
|
assert tickers == ["AAPL", "MSFT", "NVDA"]
|
|
primary_flags = [r["is_primary"] for r in rows]
|
|
assert primary_flags == [True, False, False]
|
|
# tickers_all preserves the full set on every row
|
|
for r in rows:
|
|
assert r["tickers_all"] == ["AAPL", "MSFT", "NVDA"]
|
|
assert r["source"] == "alpaca_benzinga"
|
|
assert r["source_id"] == "12345"
|
|
|
|
|
|
def test_alpaca_skips_articles_with_no_symbols():
|
|
art = _alpaca_article([])
|
|
assert alpaca_articles_to_rows([art]) == []
|
|
|
|
|
|
def test_alpaca_published_at_is_tz_aware_utc():
|
|
rows = alpaca_articles_to_rows([_alpaca_article(["AAPL"])])
|
|
assert rows[0]["published_at"].tzinfo is not None
|
|
assert rows[0]["ingested_at"].tzinfo is not None
|
|
|
|
|
|
def test_alpaca_normalizes_categories_with_headline_override():
|
|
art = _alpaca_article(["AAPL"], headline="Goldman downgrades AAPL to Sell")
|
|
rows = alpaca_articles_to_rows([art])
|
|
assert "analyst_rating_downgrade" in rows[0]["categories"]
|
|
|
|
|
|
def test_finnhub_payload_to_rows():
|
|
payload = [{
|
|
"id": 999,
|
|
"datetime": 1745596800, # 2025-04-25 16:00 UTC
|
|
"headline": "Apple posts strong earnings",
|
|
"summary": "Beat on EPS and revenue",
|
|
"url": "https://finn/news/999",
|
|
"category": "earnings",
|
|
}]
|
|
rows = finnhub_articles_to_rows("AAPL", payload)
|
|
assert len(rows) == 1
|
|
r = rows[0]
|
|
assert r["source"] == "finnhub"
|
|
assert r["source_id"] == "999"
|
|
assert r["ticker"] == "AAPL"
|
|
assert r["is_primary"] is True
|
|
assert "earnings_release" in r["categories"]
|
|
assert r["published_at"].tzinfo is not None
|
|
|
|
|
|
def test_finnhub_skips_rows_missing_id_or_timestamp():
|
|
payload = [
|
|
{"id": 1, "datetime": None, "headline": "x"}, # missing ts
|
|
{"datetime": 100, "headline": "x"}, # missing id
|
|
]
|
|
assert finnhub_articles_to_rows("AAPL", payload) == []
|
|
|
|
|
|
def test_stocktwits_bullish_score_and_dedup_key():
|
|
msg = {
|
|
"id": 555,
|
|
"created_at": "2026-04-25T13:00:00Z",
|
|
"body": "AAPL to the moon",
|
|
"entities": {"sentiment": {"basic": "Bullish"}},
|
|
"symbols": [{"symbol": "AAPL"}, {"symbol": "MSFT"}],
|
|
}
|
|
rows = stocktwits_messages_to_rows("AAPL", [msg])
|
|
assert len(rows) == 1
|
|
r = rows[0]
|
|
assert r["source"] == "stocktwits"
|
|
assert r["source_id"] == "555"
|
|
assert r["raw_sentiment"] == 1.0
|
|
assert "AAPL" in r["tickers_all"] and "MSFT" in r["tickers_all"]
|
|
assert r["published_at"].tzinfo is not None
|
|
|
|
|
|
def test_stocktwits_bearish_score_negative():
|
|
msg = {
|
|
"id": 1,
|
|
"created_at": "2026-04-25T13:00:00Z",
|
|
"body": "shorting this dog",
|
|
"entities": {"sentiment": {"basic": "Bearish"}},
|
|
}
|
|
rows = stocktwits_messages_to_rows("AAPL", [msg])
|
|
assert rows[0]["raw_sentiment"] == -1.0
|
|
|
|
|
|
def test_stocktwits_neutral_returns_none_sentiment():
|
|
msg = {
|
|
"id": 2,
|
|
"created_at": "2026-04-25T13:00:00Z",
|
|
"body": "watching this stock today",
|
|
"entities": {},
|
|
}
|
|
rows = stocktwits_messages_to_rows("AAPL", [msg])
|
|
assert rows[0]["raw_sentiment"] is None
|
|
|
|
|
|
def test_stocktwits_skips_empty_body():
|
|
msg = {"id": 3, "created_at": "2026-04-25T13:00:00Z", "body": " "}
|
|
assert stocktwits_messages_to_rows("AAPL", [msg]) == []
|