| 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 |