"""RSS/Atom parsing, including the RDF format the old system could not read."""
import httpx
import pytest
from news.sources import rss
RDF = """
Thttp://x
d
-
German economy frets about election result
http://x/1?utm_source=rss
Some <b>markup</b> here
"""
ATOM = """
F
Release v1.2.3
2026-09-08T12:00:00Z
"""
def _source():
return {"id": "s1", "url": "http://feed", "kind": "rss"}
async def _fetch(body: str, status: int = 200, headers: dict | None = None):
def handler(request):
return httpx.Response(status, content=body.encode(), headers=headers or {})
transport = httpx.MockTransport(handler)
async with httpx.AsyncClient(transport=transport) as client:
return await rss.fetch(client, _source())
@pytest.mark.asyncio
async def test_parses_rss_1_rdf():
"""DW ships RDF; the old hand-rolled parser yielded zero items silently."""
items, _ = await _fetch(RDF)
assert len(items) == 1
assert items[0]["title"].startswith("German economy")
# tracking parameter stripped, markup removed from the summary
assert items[0]["canonical_url"] == "http://x/1"
assert "" not in items[0]["summary"] and "markup" in items[0]["summary"]
@pytest.mark.asyncio
async def test_atom_prefers_alternate_link_over_self():
items, _ = await _fetch(ATOM)
assert len(items) == 1
assert items[0]["canonical_url"] == "http://x/rel"
@pytest.mark.asyncio
async def test_304_returns_no_items_and_is_not_an_error():
items, state = await _fetch("", status=304)
assert items == [] and state["status"] == "304"
@pytest.mark.asyncio
async def test_429_is_reported_as_rate_limited_not_raised():
"""Reddit 429s readily; that must back off, not raise every cycle."""
items, state = await _fetch("", status=429, headers={"retry-after": "120"})
assert items == []
assert state["rate_limited"] is True
assert state["retry_after"] == "120"
@pytest.mark.asyncio
async def test_server_error_still_raises():
with pytest.raises(httpx.HTTPStatusError):
await _fetch("", status=503)