返回 last30days-skill
test_reddit_shreddit.py
根目录 / tests / test_reddit_shreddit.py
1 """Tests for scripts/lib/reddit_shreddit.py — keyless shreddit comment scrape."""
2
3 from pathlib import Path
4 from unittest import mock
5
6 from lib import reddit_shreddit as rs
7
8 FIXTURE = Path(__file__).resolve().parent.parent / "fixtures" / "reddit_shreddit_comments_sample.html"
9
10
11 def _html():
12 return FIXTURE.read_text(encoding="utf-8")
13
14
15 class TestExtractPostRef:
16 def test_extracts_sub_and_id(self):
17 ref = rs.extract_post_ref("https://www.reddit.com/r/Rakuten/comments/1taeiw0/title/")
18 assert ref == ("Rakuten", "1taeiw0")
19
20 def test_non_thread_url_returns_none(self):
21 assert rs.extract_post_ref("https://www.reddit.com/r/Rakuten/") is None
22 assert rs.extract_post_ref("") is None
23
24 def test_svc_url_shape(self):
25 # sort=top guarantees the highest-scored comments land on page 1.
26 assert rs._svc_url("Rakuten", "1taeiw0") == (
27 "https://www.reddit.com/svc/shreddit/comments/r/Rakuten/t3_1taeiw0?sort=top"
28 )
29
30
31 class TestParseComments:
32 """parse_comments reads <shreddit-comment> elements into scored dicts."""
33
34 def test_happy_path(self):
35 comments = rs.parse_comments(_html())
36 assert len(comments) >= 1
37 for c in comments:
38 assert isinstance(c["score"], int)
39 assert c["author"] and c["author"] not in ("[deleted]", "[removed]")
40 assert c["body"]
41
42 def test_sorted_by_score_desc(self):
43 scores = [c["score"] for c in rs.parse_comments(_html())]
44 assert scores == sorted(scores, reverse=True)
45
46 def test_deleted_and_removed_filtered(self):
47 authors = [c["author"] for c in rs.parse_comments(_html())]
48 assert "[deleted]" not in authors and "[removed]" not in authors
49
50 def test_negative_score_retained(self):
51 scores = [c["score"] for c in rs.parse_comments(_html())]
52 assert -7 in scores # synthetic downvoted-but-real comment
53
54 def test_limit_honored(self):
55 assert len(rs.parse_comments(_html(), limit=2)) == 2
56
57 def test_body_text_extracted(self):
58 bodies = [c["body"] for c in rs.parse_comments(_html())]
59 assert any("$750" in b or "pending" in b for b in bodies)
60
61 def test_comment_url_built(self):
62 for c in rs.parse_comments(_html()):
63 if c["url"]:
64 assert c["url"].startswith("https://reddit.com/r/")
65
66 def test_empty_html_returns_empty(self):
67 assert rs.parse_comments("") == []
68 assert rs.parse_comments("<html>no comments here</html>") == []
69
70
71 class TestTotalComments:
72 def test_reads_total(self):
73 assert rs._total_comments(_html()) == 14
74
75 def test_missing_returns_none(self):
76 assert rs._total_comments("<html></html>") is None
77
78
79 class TestFetchComments:
80 """fetch_comments wires URL -> svc fetch -> parse, never raising."""
81
82 def test_happy_path(self):
83 url = "https://www.reddit.com/r/Rakuten/comments/1taeiw0/title/"
84 with mock.patch.object(rs.http, "get_text", return_value=_html()) as m:
85 out = rs.fetch_comments(url)
86 # svc endpoint, not .json
87 assert "/svc/shreddit/comments/" in m.call_args[0][0]
88 assert ".json" not in m.call_args[0][0]
89 assert out["num_comments"] == 14
90 assert len(out["top_comments"]) >= 1
91 first = out["top_comments"][0]
92 assert {"score", "date", "author", "excerpt", "url"} <= set(first.keys())
93 assert isinstance(out["comment_insights"], list)
94
95 def test_bad_url_returns_empty(self):
96 out = rs.fetch_comments("https://www.reddit.com/r/Rakuten/")
97 assert out["top_comments"] == [] and out["num_comments"] is None
98
99 def test_fetch_failure_returns_empty(self):
100 url = "https://www.reddit.com/r/Rakuten/comments/1taeiw0/title/"
101 with mock.patch.object(rs.http, "get_text", return_value=None):
102 out = rs.fetch_comments(url)
103 assert out["top_comments"] == [] and out["num_comments"] is None
104
104 lines PYTHON