| 1 | import unittest |
| 2 | |
| 3 | from lib.instagram import _parse_items |
| 4 | |
| 5 | |
| 6 | class TestInstagramOwnerTypeSafety(unittest.TestCase): |
| 7 | def _make_raw(self, **overrides): |
| 8 | base = { |
| 9 | "id": "1", |
| 10 | "code": "ABC123", |
| 11 | "caption": "test caption", |
| 12 | "owner": {"username": "testuser"}, |
| 13 | } |
| 14 | base.update(overrides) |
| 15 | return base |
| 16 | |
| 17 | def test_owner_as_dict(self): |
| 18 | items = _parse_items([self._make_raw()], "test") |
| 19 | self.assertEqual("testuser", items[0]["author_name"]) |
| 20 | |
| 21 | def test_owner_as_string(self): |
| 22 | items = _parse_items([self._make_raw(owner="stringuser")], "test") |
| 23 | self.assertEqual("stringuser", items[0]["author_name"]) |
| 24 | |
| 25 | def test_owner_missing(self): |
| 26 | raw = self._make_raw() |
| 27 | del raw["owner"] |
| 28 | items = _parse_items([raw], "test") |
| 29 | self.assertEqual("", items[0]["author_name"]) |
| 30 | |
| 31 | def test_owner_none(self): |
| 32 | items = _parse_items([self._make_raw(owner=None)], "test") |
| 33 | self.assertEqual("", items[0]["author_name"]) |
| 34 | |
| 35 | def test_user_field_fallback(self): |
| 36 | raw = self._make_raw() |
| 37 | del raw["owner"] |
| 38 | raw["user"] = {"username": "fallbackuser"} |
| 39 | items = _parse_items([raw], "test") |
| 40 | self.assertEqual("fallbackuser", items[0]["author_name"]) |
| 41 | |
| 42 | |
| 43 | class TestInstagramComments(unittest.TestCase): |
| 44 | """U1: Instagram comment fetching via ScrapeCreators.""" |
| 45 | |
| 46 | def test_fetch_post_comments_parses_and_sorts_by_likes(self): |
| 47 | from unittest.mock import patch |
| 48 | from lib import instagram |
| 49 | |
| 50 | fake = { |
| 51 | "success": True, |
| 52 | "comments": [ |
| 53 | {"text": "mid", "comment_like_count": 3, |
| 54 | "created_at": "2026-07-04T14:27:58.000Z", "user": {"username": "bob"}}, |
| 55 | {"text": "gold", "comment_like_count": 500, |
| 56 | "created_at": "2026-07-03T10:00:00.000Z", "user": {"username": "alice"}}, |
| 57 | {"text": "", "comment_like_count": 999, |
| 58 | "created_at": "2026-07-02T10:00:00.000Z", "user": {"username": "skip"}}, |
| 59 | ], |
| 60 | "cursor": None, |
| 61 | } |
| 62 | with patch.object(instagram.http, "get", return_value=fake): |
| 63 | out = instagram._fetch_post_comments( |
| 64 | "https://www.instagram.com/reel/ABC/", token="k", max_comments=5, |
| 65 | ) |
| 66 | # Empty-text dropped; sorted desc by comment_like_count. |
| 67 | self.assertEqual([c["text"] for c in out], ["gold", "mid"]) |
| 68 | self.assertEqual(out[0]["comment_like_count"], 500) |
| 69 | self.assertEqual(out[0]["author"], "alice") |
| 70 | self.assertEqual(out[0]["date"], "2026-07-03") |
| 71 | |
| 72 | def test_fetch_post_comments_error_returns_empty(self): |
| 73 | from unittest.mock import patch |
| 74 | from lib import instagram |
| 75 | |
| 76 | def _boom(*a, **k): |
| 77 | raise RuntimeError("network") |
| 78 | |
| 79 | with patch.object(instagram.http, "get", side_effect=_boom): |
| 80 | out = instagram._fetch_post_comments("https://x/", token="k") |
| 81 | self.assertEqual(out, []) |
| 82 | |
| 83 | def test_enrich_with_comments_no_token_or_items_noop(self): |
| 84 | from lib import instagram |
| 85 | self.assertEqual([], instagram.enrich_with_comments([], token="k")) |
| 86 | items = [{"url": "u", "engagement": {"likes": 5}}] |
| 87 | self.assertEqual(items, instagram.enrich_with_comments(items, token="")) |
| 88 | self.assertNotIn("top_comments", items[0]) |
| 89 | |
| 90 | def test_is_instagram_comments_available_gate(self): |
| 91 | from lib import env |
| 92 | self.assertFalse(env.is_instagram_comments_available({})) |
| 93 | self.assertFalse(env.is_instagram_comments_available( |
| 94 | {"SCRAPECREATORS_API_KEY": "k"})) # key but no INCLUDE_SOURCES |
| 95 | self.assertFalse(env.is_instagram_comments_available( |
| 96 | {"INCLUDE_SOURCES": "instagram_comments"})) # opt-in but no key |
| 97 | self.assertTrue(env.is_instagram_comments_available( |
| 98 | {"SCRAPECREATORS_API_KEY": "k", "INCLUDE_SOURCES": "tiktok,instagram_comments"})) |
| 99 | |
| 100 | |
| 101 | class TestExpandInstagramQueries(unittest.TestCase): |
| 102 | """Tests for expand_instagram_queries() multi-query generation.""" |
| 103 | |
| 104 | def test_default_depth_returns_two_plus_queries(self): |
| 105 | from lib.instagram import expand_instagram_queries |
| 106 | queries = expand_instagram_queries("Kanye West", "default") |
| 107 | self.assertGreaterEqual(len(queries), 2) |
| 108 | # Breaking_news intent should include reaction/edit variant |
| 109 | variant_found = any( |
| 110 | "reaction" in q.lower() or "edit" in q.lower() |
| 111 | for q in queries |
| 112 | ) |
| 113 | self.assertTrue(variant_found, f"Expected reaction/edit variant: {queries}") |
| 114 | |
| 115 | def test_quick_depth_returns_one_query(self): |
| 116 | from lib.instagram import expand_instagram_queries |
| 117 | queries = expand_instagram_queries("Kanye West", "quick") |
| 118 | self.assertEqual(len(queries), 1) |
| 119 | |
| 120 | if __name__ == "__main__": |
| 121 | unittest.main() |
| 122 |