1import inspect
2
3import pytest
4
5from src import main
6from src.trends import GEOS, _traffic_to_int, parse
7
8SAMPLE = b"""<?xml version="1.0" encoding="UTF-8"?>
9<rss xmlns:ht="https://trends.google.com/trending/rss" version="2.0"><channel>
10<item>
11 <title>nottm forest vs blackburn rovers</title>
12 <ht:approx_traffic>1,000+</ht:approx_traffic>
13 <pubDate>Wed, 22 Jul 2026 02:40:00 -0700</pubDate>
14 <ht:picture>https://example.com/a.jpg</ht:picture>
15 <ht:picture_source>Lancashire Telegraph</ht:picture_source>
16 <ht:news_item>
17 <ht:news_item_title>Forest view on Blackburn friendly</ht:news_item_title>
18 <ht:news_item_url>https://example.com/story</ht:news_item_url>
19 <ht:news_item_source>Lancashire Telegraph</ht:news_item_source>
20 <ht:news_item_snippet> </ht:news_item_snippet>
21 </ht:news_item>
22</item>
23<item>
24 <title>mane</title>
25 <ht:approx_traffic>200+</ht:approx_traffic>
26</item>
27</channel></rss>"""
28
29
30def test_parses_rows_in_rank_order():
31 rows = parse(SAMPLE, "GB")
32 assert [r["rank"] for r in rows] == [1, 2]
33 assert rows[0]["query"] == "nottm forest vs blackburn rovers"
34 assert rows[0]["geo"] == "GB"
35 assert rows[0]["source_url"].endswith("geo=GB")
36
37
38def test_traffic_keeps_the_raw_estimate_and_a_sortable_number():
39 rows = parse(SAMPLE, "GB")
40 assert rows[0]["approx_traffic"] == "1,000+"
41 assert rows[0]["approx_traffic_min"] == 1000
42 assert _traffic_to_int(None) is None
43 assert _traffic_to_int("unknown") is None
44
45
46def test_news_items_are_nested_and_blank_snippets_become_null():
47 rows = parse(SAMPLE, "GB")
48 assert rows[0]["news_item_count"] == 1
49 story = rows[0]["news"][0]
50 assert story["source"] == "Lancashire Telegraph"
51 assert story["snippet"] is None, "whitespace-only snippet must not read as content"
52 assert rows[1]["news_item_count"] == 0
53
54
55def test_missing_optional_fields_are_null_not_absent():
56 rows = parse(SAMPLE, "GB")
57 for key in ("picture", "picture_source", "started_at"):
58 assert key in rows[1], f"{key} must be present so the dataset shape is stable"
59 assert rows[1][key] is None
60
61
62def test_entrypoint_runs_the_actor():
63 """The defect that made all three earlier actors in this repo inert."""
64 source = inspect.getsource(main)
65 assert "asyncio.run(main())" in source
66 assert "isatty" not in source, "no stdin branching: it is False in a container"
67
68
69def test_geo_list_is_upper_case_two_letter_codes():
70 assert all(len(g) == 2 and g.isupper() for g in GEOS)
71 assert "US" in GEOS and "AU" in GEOS
72
73
74@pytest.mark.parametrize("geo", ["US", "GB", "AU"])
75def test_live_feed_still_returns_ten_rows(geo):
76 """The product IS reliability, so the reliability claim gets a live check.
77
78 The `explore`/`widgetdata` endpoints the competing Actors use return 429
79 from datacenter IPs. If this feed ever starts doing the same, this test is
80 the thing that says so.
81 """
82 from src.trends import fetch
83
84 rows = parse(fetch(geo), geo)
85 assert len(rows) == 10, f"{geo} returned {len(rows)} rows, expected 10"
86 assert all(r["query"] for r in rows)