# -*- coding: utf-8 -*- """WBI 签名与搜索解析测试(不联网): uv run python tests/test_search.py""" import os import sys import urllib.parse sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) sys.stdout.reconfigure(encoding="utf-8") from reaction_board.bilibili.client import BilibiliError, BilibiliProtocolError # noqa: E402 from reaction_board.bilibili.search import ( # noqa: E402 SearchClient, key_from_url, mixin_key, parse_search_payload, sign_params, ) IMG_URL = "https://i0.hdslb.com/bfs/wbi/7cd084941338484aae1ad9425b84077c.png" SUB_URL = "https://i0.hdslb.com/bfs/wbi/4932caff0ff746eab6f01bf08b70ac45.png" NAV_PAYLOAD = { "code": 0, "message": "0", "ttl": 1, "data": { "isLogin": True, "wbi_img": {"img_url": IMG_URL, "sub_url": SUB_URL}, }, } SEARCH_PAYLOAD = { "code": 0, "message": "0", "ttl": 1, "data": { "result": [ { "type": "video", "id": 117530286889638, "bvid": "BV1xvxi6LEZD", "title": '咻咻满 reaction 初听', "author": "测试UP", "mid": 434442072, "pubdate": 1793370433, "duration": "105:5", "pic": "//i0.hdslb.com/bfs/archive/example.jpg", "play": 12345, "like": 678, "favorites": 90, "danmaku": 12, "review": 3, }, {"type": "article", "id": 999, "title": "非视频"}, ], "numPages": 1, "numResults": 1, }, } class FakeClient: def __init__(self, responses): self.responses = list(responses) self.urls = [] def get_json(self, url, *, auth=False): self.urls.append((url, auth)) item = self.responses.pop(0) if isinstance(item, BaseException): raise item return item def test_wbi_helpers(): assert key_from_url(IMG_URL) == "7cd084941338484aae1ad9425b84077c" assert key_from_url(None) == "" key = mixin_key( "7cd084941338484aae1ad9425b84077c", "4932caff0ff746eab6f01bf08b70ac45", ) assert key == "ea1db124af3c7062474693fa704f4ff8" signed = sign_params({"keyword": "咻咻满", "page": 1}, key, now=1_700_000_000) assert signed["wts"] == 1_700_000_000 assert len(signed["w_rid"]) == 32 changed = sign_params({"keyword": "咻咻满", "page": 2}, key, now=1_700_000_000) assert changed["w_rid"] != signed["w_rid"] def test_parse_search_payload(): items = parse_search_payload(SEARCH_PAYLOAD) assert len(items) == 1 item = items[0] assert item.aid == 117530286889638 assert item.title == "咻咻满 reaction 初听" assert item.duration_seconds == 105 * 60 + 5 assert item.cover.startswith("https://") assert item.play == 12345 and item.like == 678 assert item.favorite == 90 and item.danmaku == 12 and item.review == 3 def test_wbi_search_and_fallback(): fake = FakeClient([NAV_PAYLOAD, SEARCH_PAYLOAD]) client = SearchClient(lambda: fake, clock=lambda: 1_700_000_000) items = client.search("咻咻满 reaction", 1) assert len(items) == 1 assert "/x/web-interface/nav" in fake.urls[0][0] assert "/wbi/search/type" in fake.urls[1][0] query = urllib.parse.parse_qs(urllib.parse.urlparse(fake.urls[1][0]).query) assert query["w_rid"][0] and query["wts"][0] == "1700000000" # nav 失败时降级旧接口 fake = FakeClient([BilibiliError("wbi unavailable"), SEARCH_PAYLOAD]) client = SearchClient(lambda: fake, clock=lambda: 1_700_000_000) items = client.search("咻咻满 reaction", 2) assert len(items) == 1 assert "/x/web-interface/search/type" in fake.urls[1][0] def test_missing_result_is_protocol_error(): bad = {"code": 0, "message": "0", "data": {}} fake = FakeClient([NAV_PAYLOAD, bad, bad]) client = SearchClient(lambda: fake, clock=lambda: 1_700_000_000) try: client.search("x", 1) except BilibiliProtocolError as exc: assert "result" in str(exc) else: raise AssertionError("缺少 result 应报协议错误") def main(): test_wbi_helpers() test_parse_search_payload() test_wbi_search_and_fallback() test_missing_result_is_protocol_error() print("OK: WBI 签名、搜索解析与降级测试全部通过") if __name__ == "__main__": main()