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.
stock-oracle/tests/test_news_v2_ingest_transfo...

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]) == []