mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-21 16:23:34 +08:00
fix(api): restore recommendation response compatibility
This commit is contained in:
62
tests/test_event_plugin_errors.py
Normal file
62
tests/test_event_plugin_errors.py
Normal 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)
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"] == "导演乙"
|
||||
|
||||
Reference in New Issue
Block a user