| import sys |
| import threading |
| from pathlib import Path |
|
|
| import pytest |
|
|
| BACKEND_ROOT = Path(__file__).resolve().parents[1] |
| if str(BACKEND_ROOT) not in sys.path: |
| sys.path.insert(0, str(BACKEND_ROOT)) |
|
|
| from app.services import hot_videos |
| from app.services.hot_videos import ( |
| HotVideoItem, |
| PlatformHotVideoResult, |
| _format_bilibili_views, |
| _fetch_bilibili_hot, |
| _map_douyin_hot_items, |
| _map_bilibili_reader_markdown_items, |
| _map_bilibili_popular_items, |
| _map_youtube_trending_html, |
| fetch_hot_video_payload, |
| fetch_hot_videos, |
| ) |
|
|
|
|
| def test_format_bilibili_views_uses_chinese_units(): |
| assert _format_bilibili_views(9999) == "9999播放" |
| assert _format_bilibili_views(10000) == "1.0万播放" |
| assert _format_bilibili_views(1250000) == "125.0万播放" |
|
|
|
|
| def test_map_bilibili_popular_items_normalizes_expected_fields(): |
| payload = { |
| "data": { |
| "list": [ |
| { |
| "bvid": "BV1abc123", |
| "title": "测试热门视频", |
| "pic": "//i0.hdslb.com/bfs/archive/cover.jpg", |
| "owner": {"name": "测试 UP"}, |
| "stat": {"view": 123456}, |
| }, |
| { |
| "bvid": "", |
| "title": "没有 bvid 的条目会被跳过", |
| }, |
| ] |
| } |
| } |
|
|
| items = _map_bilibili_popular_items(payload, limit=5) |
|
|
| assert items == [ |
| HotVideoItem( |
| id="BV1abc123", |
| platform="bilibili", |
| title="测试热门视频", |
| url="https://www.bilibili.com/video/BV1abc123", |
| cover_url="https://i0.hdslb.com/bfs/archive/cover.jpg", |
| author="测试 UP", |
| rank=1, |
| hot_score="12.3万播放", |
| source="bilibili_popular", |
| ) |
| ] |
|
|
|
|
| def test_map_bilibili_reader_markdown_items_extracts_popular_page_links(): |
| markdown = """ |
| [](https://www.bilibili.com/video/BV1xkE46PE59) |
| |
| 参加了九次高考的考生采访 |
| |
| 自来卷三木 |
| |
| 275.2万 1847 |
| """ |
|
|
| items = _map_bilibili_reader_markdown_items(markdown, limit=3) |
|
|
| assert items == [ |
| HotVideoItem( |
| id="BV1xkE46PE59", |
| platform="bilibili", |
| title="参加了九次高考的考生采访", |
| url="https://www.bilibili.com/video/BV1xkE46PE59", |
| cover_url="https://i1.hdslb.com/bfs/archive/cover.jpg@412w_232h_1c_!web-popular.avif", |
| author="自来卷三木", |
| rank=1, |
| hot_score="275.2万播放", |
| source="bilibili_popular_reader", |
| ) |
| ] |
|
|
|
|
| def test_fetch_bilibili_hot_falls_back_to_reader_when_api_fails(monkeypatch): |
| markdown = """ |
| [](https://www.bilibili.com/video/BV1xkE46PE59) |
| |
| 参加了九次高考的考生采访 |
| |
| 自来卷三木 |
| |
| 275.2万 1847 |
| """ |
|
|
| class FakeResponse: |
| text = markdown |
|
|
| def raise_for_status(self): |
| return None |
|
|
| class FakeSession: |
| headers = {} |
|
|
| def __init__(self): |
| self.calls = [] |
|
|
| def get(self, url, **kwargs): |
| self.calls.append(url) |
| if "api.bilibili.com" in url: |
| raise RuntimeError("tls eof") |
| return FakeResponse() |
|
|
| session = FakeSession() |
| monkeypatch.setattr(hot_videos, "_session", lambda: session) |
|
|
| result = _fetch_bilibili_hot(limit=2) |
|
|
| assert result.status == "ok" |
| assert result.message == "官方热点接口暂不可用,已切换备用热点源" |
| assert result.items[0].source == "bilibili_popular_reader" |
| assert result.items[0].url == "https://www.bilibili.com/video/BV1xkE46PE59" |
| assert any("api.bilibili.com" in call for call in session.calls) |
| assert any("r.jina.ai" in call for call in session.calls) |
|
|
|
|
| def test_fetch_bilibili_hot_uses_snapshot_when_live_sources_fail(monkeypatch): |
| class FakeSession: |
| headers = {} |
|
|
| def get(self, url, **kwargs): |
| raise RuntimeError(f"blocked: {url}") |
|
|
| monkeypatch.setattr(hot_videos, "_session", lambda: FakeSession()) |
|
|
| result = _fetch_bilibili_hot(limit=2) |
|
|
| assert result.status == "ok" |
| assert result.message == "实时热点源暂不可用,已显示最近热门快照" |
| assert len(result.items) == 2 |
| assert {item.platform for item in result.items} == {"bilibili"} |
| assert {item.source for item in result.items} == {"bilibili_popular_snapshot"} |
| assert all(item.url.startswith("https://www.bilibili.com/video/BV") for item in result.items) |
|
|
|
|
| def test_fetch_all_keeps_successful_platform_when_another_platform_fails(monkeypatch): |
| def fake_bilibili(limit): |
| return PlatformHotVideoResult( |
| platform="bilibili", |
| status="ok", |
| message="", |
| items=[ |
| HotVideoItem( |
| id="BV1ok", |
| platform="bilibili", |
| title="可用热门", |
| url="https://www.bilibili.com/video/BV1ok", |
| rank=1, |
| source="bilibili_popular", |
| ) |
| ], |
| ) |
|
|
| def fake_youtube(limit): |
| raise RuntimeError("network blocked") |
|
|
| monkeypatch.setattr( |
| hot_videos, |
| "HOT_FETCHERS", |
| { |
| "bilibili": fake_bilibili, |
| "youtube": fake_youtube, |
| }, |
| ) |
| hot_videos.clear_hot_video_cache() |
|
|
| payload = fetch_hot_video_payload(platform="all", limit=3) |
|
|
| assert payload["platform"] == "all" |
| assert [p["platform"] for p in payload["platforms"]] == ["bilibili", "youtube"] |
| assert payload["platforms"][0]["status"] == "ok" |
| assert payload["platforms"][0]["items"][0]["title"] == "可用热门" |
| assert payload["platforms"][1]["status"] == "error" |
| assert "network blocked" in payload["platforms"][1]["message"] |
|
|
|
|
| def test_fetch_all_runs_platform_fetchers_concurrently(monkeypatch): |
| lock = threading.Lock() |
| started: list[str] = [] |
| both_started = threading.Event() |
|
|
| def make_fetcher(name: str): |
| def fake_fetcher(limit): |
| with lock: |
| started.append(name) |
| if len(started) == 2: |
| both_started.set() |
| if not both_started.wait(timeout=1): |
| raise AssertionError("platform fetchers did not overlap") |
| return PlatformHotVideoResult( |
| platform=name, |
| status="ok", |
| message="", |
| items=[ |
| HotVideoItem( |
| id=f"{name}-1", |
| platform=name, |
| title=f"{name} 热点", |
| url=f"https://example.com/{name}", |
| rank=1, |
| ) |
| ], |
| ) |
|
|
| return fake_fetcher |
|
|
| monkeypatch.setattr( |
| hot_videos, |
| "HOT_FETCHERS", |
| { |
| "bilibili": make_fetcher("bilibili"), |
| "youtube": make_fetcher("youtube"), |
| }, |
| ) |
| hot_videos.clear_hot_video_cache() |
|
|
| payload = fetch_hot_video_payload(platform="all", limit=1, force=True) |
|
|
| assert [item["status"] for item in payload["platforms"]] == ["ok", "ok"] |
| assert [item["platform"] for item in payload["platforms"]] == ["bilibili", "youtube"] |
|
|
|
|
| def test_fetch_hot_videos_rejects_unknown_platform(): |
| with pytest.raises(ValueError, match="不支持的热点平台"): |
| fetch_hot_videos(platform="instagram", limit=3) |
|
|
|
|
| def test_cache_returns_first_payload_inside_ttl(monkeypatch): |
| calls = [] |
|
|
| def fake_bilibili(limit): |
| calls.append(limit) |
| return PlatformHotVideoResult( |
| platform="bilibili", |
| status="ok", |
| message="", |
| items=[ |
| HotVideoItem( |
| id=f"BV{len(calls)}", |
| platform="bilibili", |
| title=f"第 {len(calls)} 次", |
| url=f"https://www.bilibili.com/video/BV{len(calls)}", |
| rank=1, |
| source="bilibili_popular", |
| ) |
| ], |
| ) |
|
|
| monkeypatch.setattr(hot_videos, "HOT_FETCHERS", {"bilibili": fake_bilibili}) |
| hot_videos.clear_hot_video_cache() |
|
|
| first = fetch_hot_video_payload(platform="bilibili", limit=1) |
| second = fetch_hot_video_payload(platform="bilibili", limit=1) |
|
|
| assert len(calls) == 1 |
| assert first == second |
| assert second["platforms"][0]["items"][0]["title"] == "第 1 次" |
|
|
|
|
| def test_map_youtube_trending_html_extracts_video_renderers(): |
| html = """ |
| <html><script> |
| var ytInitialData = {"contents":{"twoColumnBrowseResultsRenderer":{"tabs":[{"tabRenderer":{"content":{"richGridRenderer":{"contents":[{"richItemRenderer":{"content":{"videoRenderer":{"videoId":"abc123XYZ09","title":{"runs":[{"text":"YouTube 热门"}]},"ownerText":{"runs":[{"text":"频道名"}]},"thumbnail":{"thumbnails":[{"url":"https://i.ytimg.com/vi/abc123XYZ09/hqdefault.jpg"}]},"shortViewCountText":{"simpleText":"12万次观看"}}}}}]}}}}]}}}; |
| </script></html> |
| """ |
|
|
| items = _map_youtube_trending_html(html, limit=5) |
|
|
| assert items[0] == HotVideoItem( |
| id="abc123XYZ09", |
| platform="youtube", |
| title="YouTube 热门", |
| url="https://www.youtube.com/watch?v=abc123XYZ09", |
| cover_url="https://i.ytimg.com/vi/abc123XYZ09/hqdefault.jpg", |
| author="频道名", |
| rank=1, |
| hot_score="12万次观看", |
| source="youtube_trending", |
| ) |
|
|
|
|
| def test_map_douyin_hot_items_extracts_detail_aweme_entries(): |
| payload = { |
| "data": { |
| "word_list": [ |
| { |
| "word": "热点话题", |
| "hot_value": 123456, |
| "aweme_infos": [ |
| { |
| "aweme_id": "7123456789012345678", |
| "desc": "抖音热点视频", |
| "author": {"nickname": "创作者"}, |
| "video": {"cover": {"url_list": ["https://example.com/cover.jpg"]}}, |
| } |
| ], |
| } |
| ] |
| } |
| } |
|
|
| items = _map_douyin_hot_items(payload, limit=5) |
|
|
| assert items[0] == HotVideoItem( |
| id="7123456789012345678", |
| platform="douyin", |
| title="抖音热点视频", |
| url="https://www.douyin.com/video/7123456789012345678", |
| cover_url="https://example.com/cover.jpg", |
| author="创作者", |
| rank=1, |
| hot_score="123456热度", |
| source="douyin_hot_search", |
| ) |
|
|
|
|
| def test_fetch_newsnow_hot_maps_correctly(monkeypatch): |
| from app.services.hot_videos import _fetch_newsnow_hot |
|
|
| mock_payload = { |
| "status": "cache", |
| "id": "zhihu", |
| "items": [ |
| { |
| "id": "12345", |
| "title": "测试知乎标题", |
| "url": "https://zhihu.com/question/12345", |
| "author": "知乎网友", |
| "extra": {"info": "100万热度"}, |
| }, |
| { |
| "id": "67890", |
| "title": "", |
| "url": "https://zhihu.com/question/67890", |
| }, |
| ], |
| } |
|
|
| class FakeResponse: |
| status_code = 200 |
|
|
| def raise_for_status(self): |
| return None |
|
|
| def json(self): |
| return mock_payload |
|
|
| class FakeSession: |
| headers = {} |
|
|
| def get(self, url, params=None, **kwargs): |
| assert "newsnow" in url |
| assert params == {"id": "zhihu"} |
| return FakeResponse() |
|
|
| monkeypatch.setattr(hot_videos, "_session", lambda: FakeSession()) |
|
|
| result = _fetch_newsnow_hot("zhihu", limit=5) |
| assert result.status == "ok" |
| assert result.platform == "zhihu" |
| assert len(result.items) == 1 |
| assert result.items[0] == HotVideoItem( |
| id="12345", |
| platform="zhihu", |
| title="测试知乎标题", |
| url="https://zhihu.com/question/12345", |
| cover_url="", |
| author="知乎网友", |
| rank=1, |
| hot_score="100万热度", |
| source="newsnow", |
| ) |
|
|
|
|
| def test_fetch_newsnow_hot_handles_failure(monkeypatch): |
| from app.services.hot_videos import _fetch_newsnow_hot |
|
|
| class FakeSession: |
| headers = {} |
|
|
| def get(self, url, params=None, **kwargs): |
| raise RuntimeError("API timeout") |
|
|
| monkeypatch.setattr(hot_videos, "_session", lambda: FakeSession()) |
|
|
| result = _fetch_newsnow_hot("zhihu", limit=5) |
| assert result.status == "error" |
| assert "API timeout" in result.message |
| assert len(result.items) == 0 |
|
|
|
|