返回 last30days-skill
test_telegram_source.py
根目录 / tests / test_telegram_source.py
1 """Tests for the Telegram source adapter (lib/telegram.py).
2
3 Covers handle parsing, availability gating, pagination, date filtering,
4 and the channels-required constraint.
5 """
6
7 from __future__ import annotations
8
9 import pytest
10
11 from lib import pipeline, telegram
12
13
14 # ---- handle parsing ----
15
16 @pytest.mark.parametrize("raw,expected", [
17 ("aipost", "aipost"),
18 ("@aipost", "aipost"),
19 ("https://t.me/aipost", "aipost"),
20 ("https://t.me/s/aipost", "aipost"),
21 ("http://t.me/durov", "durov"),
22 ("t.me/channel", "channel"),
23 (" @spaced ", "spaced"),
24 ])
25 def test_parse_channel_handle_valid(raw, expected):
26 assert telegram.parse_channel_handle(raw) == expected
27
28
29 @pytest.mark.parametrize("raw", [
30 "",
31 " ",
32 "-1001234567890",
33 "-100999888777",
34 "https://t.me/joinchat/abcdef",
35 "joinchat/xyz",
36 ])
37 def test_parse_channel_handle_invalid(raw):
38 with pytest.raises(telegram.InvalidChannelHandle):
39 telegram.parse_channel_handle(raw)
40
41
42 def test_parse_channel_sources_filters_invalid():
43 raw = "aipost, @durov, https://t.me/joinchat/secret, -1001234, valid"
44 handles = telegram.parse_channel_sources(raw)
45 assert handles == ["aipost", "durov", "valid"]
46
47
48 def test_parse_channel_sources_dedupes():
49 raw = "aipost, @aipost, https://t.me/aipost, AIPOST"
50 handles = telegram.parse_channel_sources(raw)
51 assert handles == ["aipost"]
52
53
54 # ---- opt-in availability gating ----
55
56 def test_not_available_by_default():
57 config = {"SCRAPECREATORS_API_KEY": "test-key"}
58 avail = pipeline.available_sources(config)
59 assert "telegram" not in avail
60
61
62 def test_not_available_without_channels():
63 config = {
64 "SCRAPECREATORS_API_KEY": "test-key",
65 "INCLUDE_SOURCES": "telegram",
66 }
67 avail = pipeline.available_sources(config)
68 assert "telegram" not in avail
69
70
71 def test_available_when_fully_configured():
72 config = {
73 "SCRAPECREATORS_API_KEY": "test-key",
74 "INCLUDE_SOURCES": "telegram",
75 "TELEGRAM_SOURCES": "aipost,durov",
76 }
77 avail = pipeline.available_sources(config)
78 assert "telegram" in avail
79
80
81 def test_available_when_requested():
82 config = {
83 "SCRAPECREATORS_API_KEY": "test-key",
84 "TELEGRAM_SOURCES": "aipost",
85 }
86 avail = pipeline.available_sources(config, requested_sources=["telegram"])
87 assert "telegram" in avail
88
89
90 def test_excluded_when_in_exclude_sources():
91 config = {
92 "SCRAPECREATORS_API_KEY": "test-key",
93 "INCLUDE_SOURCES": "telegram",
94 "TELEGRAM_SOURCES": "aipost",
95 "EXCLUDE_SOURCES": "telegram",
96 }
97 avail = pipeline.available_sources(config)
98 assert "telegram" not in avail
99
100
101 # ---- is_telegram_configured ----
102
103 def test_is_telegram_configured_true():
104 config = {
105 "SCRAPECREATORS_API_KEY": "test-key",
106 "TELEGRAM_SOURCES": "aipost",
107 }
108 assert telegram.is_telegram_configured(config) is True
109
110
111 def test_is_telegram_configured_false_no_key():
112 config = {"TELEGRAM_SOURCES": "aipost"}
113 assert telegram.is_telegram_configured(config) is False
114
115
116 def test_is_telegram_configured_false_no_channels():
117 config = {"SCRAPECREATORS_API_KEY": "test-key"}
118 assert telegram.is_telegram_configured(config) is False
119
120
121 # ---- search without channels returns error ----
122
123 def test_search_returns_error_without_channels():
124 result = telegram.search_telegram(
125 "test topic",
126 "2026-08-01",
127 "2026-08-21",
128 token="test-key",
129 config={},
130 )
131 assert result["items"] == []
132 assert "channel list required" in result.get("error", "").lower()
133
134
135 def test_search_returns_error_without_token():
136 result = telegram.search_telegram(
137 "test topic",
138 "2026-08-01",
139 "2026-08-21",
140 token=None,
141 config={"TELEGRAM_SOURCES": "aipost"},
142 )
143 assert result["items"] == []
144 assert "SCRAPECREATORS_API_KEY" in result.get("error", "")
145
146
147 # ---- pagination and date filtering (mocked HTTP) ----
148
149 MOCK_CHANNEL = {
150 "handle": "testchannel",
151 "name": "Test Channel",
152 "subscriber_count": 10000,
153 }
154
155 MOCK_POSTS_PAGE1 = [
156 {
157 "id": "100",
158 "channel_handle": "testchannel",
159 "url": "https://t.me/testchannel/100",
160 "author_name": "Test Channel",
161 "text": "Recent post about AI agents",
162 "published_at": "2026-08-20T10:00:00+00:00",
163 "view_count": 5000,
164 "reaction_count": 100,
165 },
166 {
167 "id": "99",
168 "channel_handle": "testchannel",
169 "url": "https://t.me/testchannel/99",
170 "author_name": "Test Channel",
171 "text": "Another recent post",
172 "published_at": "2026-08-19T10:00:00+00:00",
173 "view_count": 3000,
174 "reaction_count": 50,
175 },
176 ]
177
178 MOCK_POSTS_PAGE2 = [
179 {
180 "id": "98",
181 "channel_handle": "testchannel",
182 "url": "https://t.me/testchannel/98",
183 "author_name": "Test Channel",
184 "text": "Old post",
185 "published_at": "2026-07-01T10:00:00+00:00",
186 "view_count": 1000,
187 "reaction_count": 10,
188 },
189 ]
190
191
192 def test_pagination_stops_on_old_page(monkeypatch):
193 pages_fetched = []
194
195 def mock_get(url, **kwargs):
196 params = kwargs.get("params", {})
197 cursor = params.get("cursor")
198 pages_fetched.append(cursor)
199
200 if cursor is None:
201 return {
202 "success": True,
203 "channel": MOCK_CHANNEL,
204 "posts": MOCK_POSTS_PAGE1,
205 "cursor": "98",
206 "has_more": True,
207 }
208 else:
209 return {
210 "success": True,
211 "channel": MOCK_CHANNEL,
212 "posts": MOCK_POSTS_PAGE2,
213 "cursor": "50",
214 "has_more": True,
215 }
216
217 monkeypatch.setattr(telegram.http, "get", mock_get)
218
219 result = telegram.search_telegram(
220 "AI agents",
221 "2026-08-01",
222 "2026-08-21",
223 depth="default",
224 token="test-key",
225 config={"TELEGRAM_SOURCES": "testchannel"},
226 )
227
228 assert len(pages_fetched) == 2
229 assert len(result["items"]) == 2
230
231
232 def test_date_filtering(monkeypatch):
233 def mock_get(url, **kwargs):
234 return {
235 "success": True,
236 "channel": MOCK_CHANNEL,
237 "posts": MOCK_POSTS_PAGE1 + MOCK_POSTS_PAGE2,
238 "has_more": False,
239 }
240
241 monkeypatch.setattr(telegram.http, "get", mock_get)
242
243 result = telegram.search_telegram(
244 "AI agents",
245 "2026-08-15",
246 "2026-08-21",
247 token="test-key",
248 config={"TELEGRAM_SOURCES": "testchannel"},
249 )
250
251 dates = [item["date"] for item in result["items"]]
252 assert all(d >= "2026-08-15" for d in dates)
253 assert "2026-07-01" not in dates
254
255
256 def test_topic_relevance_scoring(monkeypatch):
257 def mock_get(url, **kwargs):
258 return {
259 "success": True,
260 "channel": MOCK_CHANNEL,
261 "posts": [
262 {
263 "id": "1",
264 "text": "AI agents are transforming software development",
265 "published_at": "2026-08-20T10:00:00+00:00",
266 "view_count": 100,
267 "reaction_count": 10,
268 },
269 {
270 "id": "2",
271 "text": "Unrelated post about cooking recipes",
272 "published_at": "2026-08-20T10:00:00+00:00",
273 "view_count": 100,
274 "reaction_count": 10,
275 },
276 ],
277 "has_more": False,
278 }
279
280 monkeypatch.setattr(telegram.http, "get", mock_get)
281
282 result = telegram.search_telegram(
283 "AI agents",
284 "2026-08-01",
285 "2026-08-21",
286 token="test-key",
287 config={"TELEGRAM_SOURCES": "testchannel"},
288 )
289
290 items = result["items"]
291 assert len(items) == 2
292 ai_item = next(i for i in items if "AI agents" in i["text"])
293 cook_item = next(i for i in items if "cooking" in i["text"])
294 assert ai_item["relevance"] > cook_item["relevance"]
295
296
297 def test_depth_page_caps():
298 assert telegram.DEPTH_PAGE_CAPS["quick"] == 1
299 assert telegram.DEPTH_PAGE_CAPS["default"] == 3
300 assert telegram.DEPTH_PAGE_CAPS["deep"] == 6
301
302
303 def test_telegram_max_pages_override(monkeypatch):
304 pages_fetched = []
305
306 def mock_get(url, **kwargs):
307 params = kwargs.get("params", {})
308 pages_fetched.append(params.get("cursor"))
309 return {
310 "success": True,
311 "channel": MOCK_CHANNEL,
312 "posts": MOCK_POSTS_PAGE1,
313 "cursor": str(len(pages_fetched) * 10),
314 "has_more": True,
315 }
316
317 monkeypatch.setattr(telegram.http, "get", mock_get)
318
319 result = telegram.search_telegram(
320 "AI agents",
321 "2026-08-01",
322 "2026-08-21",
323 depth="quick",
324 token="test-key",
325 config={
326 "TELEGRAM_SOURCES": "testchannel",
327 "TELEGRAM_MAX_PAGES": "5",
328 },
329 )
330
331 assert len(pages_fetched) == 5
332
333
334 def test_http_error_gracefully_handled(monkeypatch):
335 def mock_get(url, **kwargs):
336 raise telegram.http.HTTPError("Connection failed", status_code=500)
337
338 monkeypatch.setattr(telegram.http, "get", mock_get)
339
340 result = telegram.search_telegram(
341 "AI agents",
342 "2026-08-01",
343 "2026-08-21",
344 token="test-key",
345 config={"TELEGRAM_SOURCES": "testchannel"},
346 )
347
348 assert result["items"] == []
349
350
351 def test_api_error_gracefully_handled(monkeypatch):
352 def mock_get(url, **kwargs):
353 return {"success": False, "error": "Channel not found"}
354
355 monkeypatch.setattr(telegram.http, "get", mock_get)
356
357 result = telegram.search_telegram(
358 "AI agents",
359 "2026-08-01",
360 "2026-08-21",
361 token="test-key",
362 config={"TELEGRAM_SOURCES": "testchannel"},
363 )
364
365 assert result["items"] == []
366
367
368 # ---- MAX_SOURCE_FETCHES cap ----
369
370 def test_telegram_capped_to_single_fetch():
371 assert pipeline.MAX_SOURCE_FETCHES.get("telegram") == 1
372
373
374 # ---- normalization ----
375
376 def test_normalize_telegram_posts():
377 from lib import normalize
378
379 items = [
380 {
381 "id": "123",
382 "handle": "testchannel",
383 "display_name": "Test Channel",
384 "text": "Test post about AI",
385 "url": "https://t.me/testchannel/123",
386 "date": "2026-08-20",
387 "engagement": {"views": 5000, "reactions": 100},
388 "relevance": 0.85,
389 "why_relevant": "Telegram @testchannel: Test post about AI",
390 },
391 ]
392
393 normalized = normalize.normalize_source_items("telegram", items, "2026-08-01", "2026-08-21")
394 assert len(normalized) == 1
395 item = normalized[0]
396 assert item.source == "telegram"
397 assert item.item_id == "123"
398 assert "Test post about AI" in item.title
399
399 lines PYTHON