fix(api): restore recommendation response compatibility

This commit is contained in:
jxxghp
2026-08-13 05:03:38 +08:00
parent e5b52dc0a8
commit 00607bf5b3
8 changed files with 222 additions and 7 deletions

View File

@@ -0,0 +1,62 @@
"""插件事件异常处理的回归测试。"""
import pytest
from app.core.event import Event, EventManager
from app.core.plugin import PluginManager
from app.schemas.types import ChainEventType
class FailingDiscoverPlugin:
"""模拟只实现公开名称接口且事件处理失败的插件。"""
@staticmethod
def get_name() -> str:
"""返回插件显示名称。"""
return "测试发现插件"
@staticmethod
def handle(_event: Event) -> None:
"""模拟插件事件处理失败。"""
raise RuntimeError("discover failed")
@pytest.mark.asyncio
async def test_plugin_event_error_uses_public_display_name(monkeypatch):
"""同步和异步调度都应通过 get_name 获取插件名称并保留原始异常。"""
event_manager = EventManager()
plugin_manager = PluginManager()
plugin = FailingDiscoverPlugin()
errors: list[dict] = []
monkeypatch.setattr(
plugin_manager,
"_plugins",
{FailingDiscoverPlugin.__name__: FailingDiscoverPlugin},
)
monkeypatch.setattr(
plugin_manager,
"_running_plugins",
{FailingDiscoverPlugin.__name__: plugin},
)
monkeypatch.setattr(
event_manager,
"_EventManager__handle_event_error",
lambda **kwargs: errors.append(kwargs),
)
event = Event(ChainEventType.DiscoverSource)
event_manager._EventManager__invoke_handler_by_type_sync(
FailingDiscoverPlugin.handle, event
)
await event_manager._EventManager__invoke_plugin_method_async(
plugin_manager,
FailingDiscoverPlugin.__name__,
"handle",
event,
)
assert [error["module_name"] for error in errors] == [
"测试发现插件",
"测试发现插件",
]
assert all(str(error["e"]) == "discover failed" for error in errors)

View File

@@ -1,5 +1,6 @@
"""API 响应中稳定集合与动态 JSON 字段的模型契约测试。"""
from app.schemas.event import DiscoverMediaSource
from app.schemas.file import FileItem, StorageTransType
from app.schemas.mediaserver import MediaServerLibrary, MediaServerPlayItem, NotExistMediaInfo
from app.schemas.plugin import Plugin, PluginDashboard
@@ -7,6 +8,7 @@ from app.schemas.site import SiteStatistic, SiteUserData
from app.schemas.subscribe import Subscribe
from app.schemas.tmdb import TmdbEpisode
from app.schemas.token import Token
from app.schemas.types import MediaSource
from app.schemas.user import User
@@ -122,3 +124,27 @@ def test_collection_json_schemas_define_items_or_tuple_members():
for branch in messages_schema["items"]["anyOf"]
)
assert crew_schema["items"]["$ref"].endswith("/TmdbEpisodeCrew")
def test_discover_media_source_keeps_legacy_prefix_compatible():
"""发现源应兼容旧插件前缀,并同时输出规范媒体来源。"""
legacy = DiscoverMediaSource(
name="哔哩哔哩",
mediaid_prefix="bilibili",
api_path="plugin/BilibiliDiscover/discover",
)
current = DiscoverMediaSource(
name="腾讯视频",
media_source=MediaSource.TencentVideo,
api_path="plugin/TencentVideoDiscover/discover",
)
historical_alias = DiscoverMediaSource(
name="芒果 TV",
mediaid_prefix="mangguo",
api_path="plugin/MangoTVDiscover/discover",
)
assert legacy.media_source is MediaSource.Bilibili
assert legacy.model_dump(mode="json")["mediaid_prefix"] == "bilibili"
assert current.mediaid_prefix == MediaSource.TencentVideo.value
assert historical_alias.media_source is MediaSource.MangoTV

View File

@@ -6,7 +6,9 @@ from fastapi import HTTPException
from starlette.requests import Request
from starlette.responses import Response
from app import schemas
from app.api.endpoints import login as login_endpoint
from app.api.endpoints import mfa as mfa_endpoint
from app.chain.user import MfaRequired, UserChain
@@ -116,3 +118,57 @@ def test_wallpaper_returns_url_in_data(monkeypatch):
assert response.data == "https://images.example/wallpaper.jpg"
assert response.message == ""
assert not hasattr(response, "message_i18n")
def test_passkey_authentication_start_returns_object_options(monkeypatch):
"""Passkey 认证选项应作为对象返回,避免统一响应模型校验失败。"""
monkeypatch.setattr(
mfa_endpoint.PassKeyHelper,
"generate_authentication_options",
staticmethod(
lambda **_: ('{"challenge":"auth-challenge","timeout":60000}', "challenge")
),
)
monkeypatch.setattr(
mfa_endpoint.PasskeyChallengeStore,
"issue",
staticmethod(lambda **_: "authentication-transaction"),
)
response = mfa_endpoint.passkey_authenticate_start(
mfa_endpoint.PassKeyAuthenticationStart()
)
payload = schemas.PasskeyStartData.model_validate(response.data)
assert response.success is True
assert payload.options.root["challenge"] == "auth-challenge"
assert payload.transaction_token == "authentication-transaction"
def test_passkey_registration_start_returns_object_options(monkeypatch):
"""Passkey 注册选项应作为对象返回,避免统一响应模型校验失败。"""
monkeypatch.setattr(
mfa_endpoint.PassKey,
"get_by_user_id",
staticmethod(lambda **_: []),
)
monkeypatch.setattr(
mfa_endpoint.PassKeyHelper,
"generate_registration_options",
staticmethod(
lambda **_: ('{"challenge":"register-challenge"}', "challenge")
),
)
monkeypatch.setattr(
mfa_endpoint.PasskeyChallengeStore,
"issue",
staticmethod(lambda **_: "registration-transaction"),
)
user = SimpleNamespace(id=1, name="user", settings={})
response = mfa_endpoint.passkey_register_start(current_user=user)
payload = schemas.PasskeyStartData.model_validate(response.data)
assert response.success is True
assert payload.options.root["challenge"] == "register-challenge"
assert payload.transaction_token == "registration-transaction"

View File

@@ -48,3 +48,34 @@ def test_media_search_response_preserves_core_collection_fields() -> None:
assert "douban_info" in result
assert "bangumi_info" in result
assert "anilist_info" in result
def test_media_response_accepts_cross_source_credit_shapes() -> None:
"""媒体响应应同时保留豆瓣姓名字符串和 TMDB 演职员对象。"""
router = ResponseAPIRouter()
@router.get("/recommend", response_model=list[schemas.MediaInfo])
def recommend_media() -> list[dict]:
"""返回跨来源的演职员字段形态。"""
return [
{
"media_source": MediaSource.Douban,
"media_id": "1292052",
"title": "示例电影",
"actors": ["演员甲", {"id": 2, "name": "演员乙"}],
"directors": ["导演甲", {"id": 3, "name": "导演乙"}],
}
]
app = FastAPI()
app.include_router(router)
response = TestClient(app).get("/recommend")
assert response.status_code == 200
media = response.json()["data"][0]
assert media["actors"][0] == "演员甲"
assert media["actors"][1]["id"] == 2
assert media["actors"][1]["name"] == "演员乙"
assert media["directors"][0] == "导演甲"
assert media["directors"][1]["id"] == 3
assert media["directors"][1]["name"] == "导演乙"