| 1 | """Tests for reddit.py — ScrapeCreators Reddit search module.""" |
| 2 | |
| 3 | import unittest |
| 4 | |
| 5 | # Add lib to path |
| 6 | |
| 7 | from lib import reddit |
| 8 | |
| 9 | |
| 10 | class TestExtractCoreSubject(unittest.TestCase): |
| 11 | """Tests for _extract_core_subject().""" |
| 12 | |
| 13 | def test_strips_what_are_prefix(self): |
| 14 | self.assertEqual(reddit._extract_core_subject("what are the best AI tools"), "ai tools") |
| 15 | |
| 16 | def test_strips_how_to_prefix(self): |
| 17 | self.assertEqual(reddit._extract_core_subject("how to use cursor IDE"), "cursor ide") |
| 18 | |
| 19 | def test_strips_noise_words(self): |
| 20 | result = reddit._extract_core_subject("latest trending updates") |
| 21 | self.assertEqual(result, "latest trending updates") |
| 22 | |
| 23 | def test_preserves_product_name(self): |
| 24 | self.assertEqual(reddit._extract_core_subject("cursor IDE"), "cursor ide") |
| 25 | |
| 26 | def test_strips_trailing_punctuation(self): |
| 27 | result = reddit._extract_core_subject("what is Claude?") |
| 28 | self.assertFalse(result.endswith("?")) |
| 29 | |
| 30 | def test_empty_string(self): |
| 31 | result = reddit._extract_core_subject("") |
| 32 | self.assertEqual(result, "") |
| 33 | |
| 34 | def test_strips_what_do_people_think(self): |
| 35 | result = reddit._extract_core_subject("what do people think about React Server Components") |
| 36 | self.assertEqual(result, "react server components") |
| 37 | |
| 38 | |
| 39 | class TestExpandRedditQueries(unittest.TestCase): |
| 40 | """Tests for expand_reddit_queries().""" |
| 41 | |
| 42 | def test_quick_returns_one_query(self): |
| 43 | queries = reddit.expand_reddit_queries("cursor IDE", "quick") |
| 44 | self.assertGreaterEqual(len(queries), 1) |
| 45 | |
| 46 | def test_default_includes_review_variant(self): |
| 47 | queries = reddit.expand_reddit_queries("cursor IDE pricing", "default") |
| 48 | self.assertTrue(any("worth it" in q or "review" in q for q in queries)) |
| 49 | |
| 50 | def test_default_skips_review_variant_for_prediction(self): |
| 51 | queries = reddit.expand_reddit_queries("anthropic odds", "default") |
| 52 | self.assertFalse(any("worth it" in q or "review" in q for q in queries)) |
| 53 | |
| 54 | def test_default_skips_review_variant_for_breaking_news(self): |
| 55 | queries = reddit.expand_reddit_queries("kanye west", "default") |
| 56 | self.assertFalse(any("worth it" in q or "review" in q for q in queries)) |
| 57 | |
| 58 | def test_deep_includes_issues_variant(self): |
| 59 | queries = reddit.expand_reddit_queries("cursor IDE pricing", "deep") |
| 60 | self.assertTrue(any("issues" in q or "problems" in q for q in queries)) |
| 61 | |
| 62 | def test_deep_skips_issues_variant_for_prediction(self): |
| 63 | queries = reddit.expand_reddit_queries("anthropic odds", "deep") |
| 64 | self.assertFalse(any("issues" in q or "problems" in q for q in queries)) |
| 65 | |
| 66 | def test_deep_has_more_queries_than_quick(self): |
| 67 | quick = reddit.expand_reddit_queries("cursor IDE pricing", "quick") |
| 68 | deep = reddit.expand_reddit_queries("cursor IDE pricing", "deep") |
| 69 | self.assertGreater(len(deep), len(quick)) |
| 70 | |
| 71 | |
| 72 | class TestDiscoverSubreddits(unittest.TestCase): |
| 73 | """Tests for discover_subreddits().""" |
| 74 | |
| 75 | def test_ranks_by_frequency(self): |
| 76 | results = [ |
| 77 | {"subreddit": "programming", "score": 10}, |
| 78 | {"subreddit": "programming", "score": 20}, |
| 79 | {"subreddit": "python", "score": 5}, |
| 80 | ] |
| 81 | subs = reddit.discover_subreddits(results, max_subs=5) |
| 82 | self.assertEqual(subs[0], "programming") |
| 83 | |
| 84 | def test_utility_sub_penalty(self): |
| 85 | results = [ |
| 86 | {"subreddit": "tipofmytongue", "score": 100}, |
| 87 | {"subreddit": "tipofmytongue", "score": 100}, |
| 88 | {"subreddit": "python", "score": 10}, |
| 89 | ] |
| 90 | subs = reddit.discover_subreddits(results, topic="python", max_subs=5) |
| 91 | self.assertEqual(subs[0], "python") |
| 92 | |
| 93 | def test_topic_name_bonus(self): |
| 94 | results = [ |
| 95 | {"subreddit": "reactjs", "score": 10}, |
| 96 | {"subreddit": "webdev", "score": 10}, |
| 97 | ] |
| 98 | subs = reddit.discover_subreddits(results, topic="react hooks", max_subs=5) |
| 99 | self.assertEqual(subs[0], "reactjs") |
| 100 | |
| 101 | def test_engagement_bonus(self): |
| 102 | results = [ |
| 103 | {"subreddit": "AIsub", "ups": 500}, |
| 104 | {"subreddit": "OtherSub", "ups": 5}, |
| 105 | ] |
| 106 | subs = reddit.discover_subreddits(results, max_subs=5) |
| 107 | self.assertEqual(subs[0], "AIsub") |
| 108 | |
| 109 | def test_max_subs_limit(self): |
| 110 | results = [{"subreddit": f"sub{i}"} for i in range(20)] |
| 111 | subs = reddit.discover_subreddits(results, max_subs=3) |
| 112 | self.assertLessEqual(len(subs), 3) |
| 113 | |
| 114 | def test_empty_results(self): |
| 115 | self.assertEqual(reddit.discover_subreddits([]), []) |
| 116 | |
| 117 | def test_missing_subreddit_field(self): |
| 118 | results = [{"title": "no sub field"}] |
| 119 | self.assertEqual(reddit.discover_subreddits(results), []) |
| 120 | |
| 121 | |
| 122 | class TestParseDate(unittest.TestCase): |
| 123 | """Tests for _parse_date().""" |
| 124 | |
| 125 | def test_valid_timestamp(self): |
| 126 | self.assertEqual(reddit._parse_date(1705363200), "2024-01-16") |
| 127 | |
| 128 | def test_string_timestamp(self): |
| 129 | self.assertEqual(reddit._parse_date("1705363200"), "2024-01-16") |
| 130 | |
| 131 | def test_none_returns_none(self): |
| 132 | self.assertIsNone(reddit._parse_date(None)) |
| 133 | |
| 134 | def test_zero_returns_none(self): |
| 135 | self.assertIsNone(reddit._parse_date(0)) |
| 136 | |
| 137 | |
| 138 | class TestDepthConfig(unittest.TestCase): |
| 139 | """Tests for DEPTH_CONFIG structure.""" |
| 140 | |
| 141 | def test_all_depths_exist(self): |
| 142 | for depth in ("quick", "default", "deep"): |
| 143 | self.assertIn(depth, reddit.DEPTH_CONFIG) |
| 144 | |
| 145 | def test_required_keys(self): |
| 146 | required = {"global_searches", "subreddit_searches", "comment_enrichments", "timeframe"} |
| 147 | for depth, config in reddit.DEPTH_CONFIG.items(): |
| 148 | self.assertTrue(required.issubset(config.keys()), |
| 149 | f"Missing keys in {depth}: {required - config.keys()}") |
| 150 | |
| 151 | def test_deep_has_more_searches(self): |
| 152 | self.assertGreater( |
| 153 | reddit.DEPTH_CONFIG["deep"]["global_searches"], |
| 154 | reddit.DEPTH_CONFIG["quick"]["global_searches"], |
| 155 | ) |
| 156 | |
| 157 | |
| 158 | class TestPostRelevance(unittest.TestCase): |
| 159 | def test_body_cannot_rescue_weak_title_too_far(self): |
| 160 | score = reddit._compute_post_relevance( |
| 161 | "anthropic odds", |
| 162 | "President Trump orders agencies to stop using Anthropic technology", |
| 163 | "Long body text eventually mentions odds and other tangential details.", |
| 164 | ) |
| 165 | self.assertLess(score, 0.7) |
| 166 | self.assertGreaterEqual(score, 0.5) |
| 167 | |
| 168 | def test_exact_title_match_stays_high(self): |
| 169 | score = reddit._compute_post_relevance( |
| 170 | "claude code tips", |
| 171 | "Claude Code tips for faster workflows", |
| 172 | "", |
| 173 | ) |
| 174 | self.assertGreater(score, 0.7) |
| 175 | |
| 176 | if __name__ == "__main__": |
| 177 | unittest.main() |
| 178 |