"""Tests for us2kr.collectors.news (no network: requests.get is mocked)."""
from __future__ import annotations
from datetime import UTC, datetime, timedelta
import pytest
import requests
from us2kr.collectors import news
from us2kr.collectors.news import fetch_news, normalize_title, strip_html
SINCE = datetime(2026, 8, 25, 12, 0, tzinfo=UTC)
def _rss(*items: str) -> bytes:
body = "".join(items)
return (
'
Rates unchanged & steady
", ), _item("Old story", "https://a.example/old", stale), _item("Nvidia jumps 5%", "https://a.example/2", fresher), ) feed_b = _rss( _item("Fed Holds Rates Steady", "https://b.example/9", fresh), # dup by normalised title _item("Different title, same link", "https://a.example/2/", fresh), # dup by link (trailing slash) _item("Oil slips", "https://b.example/3", fresh + timedelta(minutes=1)), ) mocker.patch.object( news.requests, "get", side_effect=lambda url, **kw: _Resp(feed_a if "a.example" in url else feed_b), ) items = fetch_news(SINCE, max_items=40) titles = [i.title for i in items] assert titles == ["Nvidia jumps 5%", "Oil slips", "Fed holds rates steady!"] assert all(i.published.tzinfo is not None and i.published.utcoffset() == timedelta(0) for i in items) assert items[0].published > items[1].published > items[2].published fed = items[-1] assert fed.summary == "Rates unchanged & steady" assert fed.source == "Feed A" # requests called with UA header + timeout for each feed for call in news.requests.get.call_args_list: assert call.kwargs["timeout"] == news.FEED_TIMEOUT_SECONDS assert "User-Agent" in call.kwargs["headers"] def test_max_items_cap(two_feeds, mocker): items_xml = [ _item(f"Story {i}", f"https://a.example/{i}", SINCE + timedelta(minutes=i)) for i in range(10) ] mocker.patch.object(news.requests, "get", return_value=_Resp(_rss(*items_xml))) items = fetch_news(SINCE, max_items=3) assert len(items) == 3 assert [i.title for i in items] == ["Story 9", "Story 8", "Story 7"] def test_single_feed_failure_is_isolated(two_feeds, mocker): good = _rss(_item("Survivor", "https://b.example/1", SINCE + timedelta(hours=1))) def fake_get(url, **kw): if "a.example" in url: raise requests.ConnectionError("boom") return _Resp(good) mocker.patch.object(news.requests, "get", side_effect=fake_get) items = fetch_news(SINCE) assert [i.title for i in items] == ["Survivor"] def test_http_error_and_garbage_are_isolated(two_feeds, mocker): good = _rss(_item("Survivor", "https://b.example/1", SINCE + timedelta(hours=1))) mocker.patch.object( news.requests, "get", side_effect=lambda url, **kw: _Resp(b"<<A & B