返回 last30days-skill
test_reddit_rss.py
根目录 / tests / test_reddit_rss.py
1 """Tests for scripts/lib/reddit_rss.py — keyless Reddit RSS discovery."""
2
3 from pathlib import Path
4 from unittest import mock
5
6 import pytest
7
8 from lib import reddit_rss
9
10
11 @pytest.fixture(autouse=True)
12 def _no_keyless_throttle():
13 with mock.patch.object(reddit_rss.http.REDDIT_KEYLESS_LIMITER, "acquire"):
14 yield
15
16 FIXTURE = Path(__file__).resolve().parent.parent / "fixtures" / "reddit_search_rss_sample.xml"
17
18
19 def _feed_text():
20 return FIXTURE.read_text(encoding="utf-8")
21
22
23 class TestParseFeed:
24 """_parse_feed turns Atom entries into normalized post dicts."""
25
26 def test_parses_entries(self):
27 posts = reddit_rss._parse_feed(_feed_text(), query="lifelock")
28 assert len(posts) == 5
29 for p in posts:
30 assert p["title"]
31 assert "/comments/" in p["url"]
32 assert p["url"].startswith("https://www.reddit.com/")
33
34 def test_normalized_shape_matches_scrapecreators(self):
35 post = reddit_rss._parse_feed(_feed_text(), query="x")[0]
36 required = {"id", "title", "url", "score", "num_comments", "subreddit",
37 "created_utc", "author", "selftext", "date",
38 "engagement", "relevance", "why_relevant", "metadata"}
39 assert required.issubset(set(post.keys()))
40 assert set(post["engagement"].keys()) == {"score", "num_comments", "upvote_ratio"}
41 assert post["why_relevant"] == "Reddit RSS"
42
43 def test_score_is_placeholder_zero(self):
44 # RSS carries no engagement score; it is backfilled during enrichment.
45 for p in reddit_rss._parse_feed(_feed_text(), query="x"):
46 assert p["score"] == 0
47 assert p["engagement"]["score"] == 0
48
49 def test_subreddit_derivation(self):
50 post = reddit_rss._parse_feed(_feed_text(), query="x")[0]
51 assert post["subreddit"] == "Rakuten"
52
53 def test_date_parsed_to_iso(self):
54 post = reddit_rss._parse_feed(_feed_text(), query="x")[0]
55 assert post["date"] and len(post["date"]) == 10 # YYYY-MM-DD
56 assert isinstance(post["created_utc"], float)
57
58 def test_author_strips_u_prefix(self):
59 authors = [p["author"] for p in reddit_rss._parse_feed(_feed_text(), query="x")]
60 assert all(not a.startswith("/u/") and not a.startswith("u/") for a in authors)
61
62 def test_empty_and_malformed_feed_never_raises(self):
63 assert reddit_rss._parse_feed("", query="x") == []
64 assert reddit_rss._parse_feed("<not xml", query="x") == []
65 assert reddit_rss._parse_feed("<feed></feed>", query="x") == []
66
67 def test_entry_without_comments_link_skipped(self):
68 feed = (
69 '<feed xmlns="http://www.w3.org/2005/Atom"><entry>'
70 '<title>Subreddit itself</title>'
71 '<link href="https://www.reddit.com/r/test/" />'
72 '<updated>2026-05-20T00:00:00+00:00</updated></entry></feed>'
73 )
74 assert reddit_rss._parse_feed(feed, query="x") == []
75
76
77 class TestSearchRss:
78 """search_rss fans out, dedupes, assigns IDs, and honors depth limits."""
79
80 def test_dedupe_and_ids(self):
81 # Same feed returned for every URL -> deduped to 5 unique posts.
82 with mock.patch.object(reddit_rss.http, "get_text", return_value=_feed_text()):
83 posts = reddit_rss.search_rss("lifelock", depth="default",
84 subreddits=["Rakuten", "ConsumerAdvice"])
85 urls = [p["url"] for p in posts]
86 assert len(urls) == len(set(urls)) # no duplicates
87 assert [p["id"] for p in posts] == [f"R{i+1}" for i in range(len(posts))]
88
89 def test_depth_limit_quick(self):
90 with mock.patch.object(reddit_rss.http, "get_text", return_value=_feed_text()):
91 posts = reddit_rss.search_rss("lifelock", depth="quick")
92 assert len(posts) <= reddit_rss.DEPTH_LIMITS["quick"]
93
94 def test_all_feeds_fail_returns_empty(self):
95 with mock.patch.object(reddit_rss.http, "get_text", return_value=None):
96 posts = reddit_rss.search_rss("lifelock", subreddits=["Rakuten"])
97 assert posts == []
98
99 def test_builds_keyless_rss_urls(self):
100 urls = reddit_rss._build_urls("life lock", "default", ["Rakuten"])
101 assert any("search.rss?q=life+lock" in u and "/r/" not in u.split("?")[0] for u in urls)
102 assert any("/r/Rakuten/search.rss" in u and "restrict_sr=on" in u for u in urls)
103 assert any("/r/Rakuten/top.rss" in u for u in urls)
104 assert all(".json" not in u for u in urls) # never the dead endpoint
105
105 lines PYTHON