From cf13f2e114a8a8af1b6000583fc895e97bb9d5f3 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Sun, 27 Sep 2026 15:57:10 +0800 Subject: [PATCH] fix: align AniList and TheAudioDB network probes --- app/application/nettest/catalogue.py | 22 ++++++-- tests/test_nettest_probe_contract.py | 84 ++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 5 deletions(-) create mode 100644 tests/test_nettest_probe_contract.py diff --git a/app/application/nettest/catalogue.py b/app/application/nettest/catalogue.py index 7f9f81b47..4d9a8f1e7 100644 --- a/app/application/nettest/catalogue.py +++ b/app/application/nettest/catalogue.py @@ -8,6 +8,8 @@ from app.foundation.url import UrlUtils from .domain import NetworkTestRule, SettingsReader +_DEFAULT_THEAUDIODB_API_KEY = "123" + def _configured_service_rule( *, @@ -90,7 +92,7 @@ def build_network_rules( rules.extend(_build_metadata_module_rules(settings)) rules.extend(_build_notification_module_rules()) rules.extend(_build_external_media_rules(settings)) - rules.extend(_build_music_service_rules()) + rules.extend(_build_music_service_rules(settings)) rules.extend(_build_configured_module_rules(settings)) rules.extend(_build_fixed_service_rules(settings, github_proxy)) if enabled_module_ids is None: @@ -325,10 +327,16 @@ def _build_external_media_rules(settings: SettingsReader) -> list[NetworkTestRul id="anilist_api", name="AniList 番剧元数据", icon="anilist", - url=("https://graphql.anilist.co/?query=%7B%20Media%28id%3A1%29%20%7Bid%7D%20%7D"), + url="https://graphql.anilist.co", proxy=True, allowed_redirect_prefixes=("https://graphql.anilist.co/",), module_ids=("AniListModule",), + http_method="POST", + request_json=(("query", "{ Media(id: 1) { id } }"),), + headers=( + ("Accept", "application/json"), + ("Content-Type", "application/json"), + ), ), NetworkTestRule( id="anilist_trace", @@ -367,8 +375,8 @@ def _build_external_media_rules(settings: SettingsReader) -> list[NetworkTestRul ] -def _build_music_service_rules() -> list[NetworkTestRule]: - """构建 MusicBrainz、ListenBrainz 和 TheAudioDB 服务目标。""" +def _build_music_service_rules(settings: SettingsReader) -> list[NetworkTestRule]: + """构建音乐服务目标,并让 TheAudioDB 使用模块当前配置的 API Key。""" return [ NetworkTestRule( id="musicbrainz_api", @@ -392,7 +400,11 @@ def _build_music_service_rules() -> list[NetworkTestRule]: id="theaudiodb_api", name="TheAudioDB 音乐信息", icon="theaudiodb", - url=("https://www.theaudiodb.com/api/v1/json/123/search.php?s=coldplay"), + url=( + "https://www.theaudiodb.com/api/v1/json/" + f"{settings('THEAUDIODB_API_KEY', _DEFAULT_THEAUDIODB_API_KEY)}" + "/search.php?s=coldplay" + ), proxy=True, allowed_redirect_prefixes=("https://www.theaudiodb.com/",), module_ids=("TheAudioDbModule",), diff --git a/tests/test_nettest_probe_contract.py b/tests/test_nettest_probe_contract.py new file mode 100644 index 000000000..ef9bf08cf --- /dev/null +++ b/tests/test_nettest_probe_contract.py @@ -0,0 +1,84 @@ +"""验证内置网络探针与对应模块请求契约保持一致。""" + +import asyncio +from types import SimpleNamespace +from unittest.mock import AsyncMock, Mock + +from app.application.network import NetworkTestService + + +def _network_test_service(transport, **settings) -> NetworkTestService: + """创建读取独立设置快照的网络探测服务。""" + values = {"NORMAL_USER_AGENT": "MoviePilot-Test", **settings} + return NetworkTestService( + transport=transport, + settings=lambda key, default=None: values.get(key, default), + logger=Mock(), + ) + + +def test_anilist_probe_uses_post_graphql_request() -> None: + """AniList 官方 GraphQL 探针应使用模块相同的 POST JSON 协议。""" + captured = {} + response = SimpleNamespace( + status_code=200, + headers={}, + text='{"data":{"Media":{"id":1}}}', + aclose=AsyncMock(), + ) + + async def respond(method, url, **options): + """记录 AniList 探测请求并返回成功响应。""" + captured.update(method=method, url=url, **options) + return response + + transport = SimpleNamespace(request=AsyncMock(side_effect=respond)) + result = asyncio.run( + _network_test_service(transport).execute(target_id="anilist_api") + ) + + assert result.success + assert captured["method"] == "POST" + assert captured["url"] == "https://graphql.anilist.co" + assert captured["json_body"] == {"query": "{ Media(id: 1) { id } }"} + assert captured["headers"] == { + "Accept": "application/json", + "Content-Type": "application/json", + } + + +def test_theaudiodb_probe_uses_configured_key_and_module_health_endpoint() -> None: + """TheAudioDB 探针应使用模块配置的 Key 和相同的艺术家搜索接口。""" + configured_key = "custom-audio-key" + captured = {} + response = SimpleNamespace( + status_code=200, + headers={}, + text='{"artists":[]}', + aclose=AsyncMock(), + ) + + async def respond(method, url, **options): + """记录 TheAudioDB 探测请求并返回成功响应。""" + captured.update(method=method, url=url, **options) + return response + + transport = SimpleNamespace(request=AsyncMock(side_effect=respond)) + service = _network_test_service( + transport, + THEAUDIODB_API_KEY=configured_key, + ) + + public_target = next( + target for target in service.list_targets() if target.id == "theaudiodb_api" + ) + result = asyncio.run(service.execute(target_id="theaudiodb_api")) + + assert result.success + assert public_target.address == "https://www.theaudiodb.com" + assert configured_key not in public_target.address + assert captured["method"] == "GET" + assert captured["url"] == ( + "https://www.theaudiodb.com/api/v1/json/" + "custom-audio-key/search.php?s=coldplay" + )