from unittest.mock import Mock, patch from app.chain import ChainBase from app.core.context import MediaInfo from app.core.meta import MetaBase from app.schemas.types import MediaSource, MediaType from app.utils.media import resolve_media_identity def _chain_without_init() -> ChainBase: """构造不加载真实模块和外部服务的识别链实例。""" chain = object.__new__(ChainBase) chain.eventmanager = Mock(check=Mock(return_value=False)) return chain def test_generic_source_id_resolves_as_fixed_enum() -> None: """规范来源和原生 ID 应组成唯一媒体身份。""" assert resolve_media_identity( media_source="anilist", media_id="154587", ) == (MediaSource.AniList, "154587") def test_unknown_plugin_source_is_rejected() -> None: """固定枚举以外的来源不能进入通用识别链。""" assert resolve_media_identity( media_source="plugin_source", media_id="custom-1", ) == (None, None) def test_zero_media_id_is_rejected() -> None: """零值 ID 是旧表占位符,不得重新进入统一媒体身份链路。""" assert resolve_media_identity( media_source=MediaSource.TMDB, media_id="0", ) == (None, None) def test_explicit_source_recognition_reaches_modules_with_unified_identity() -> None: """显式身份应只以 media_source 和 media_id 进入模块调度。""" chain = _chain_without_init() media = MediaInfo( media_source=MediaSource.AniList, media_id="154587", type=MediaType.TV, title="Frieren", ) chain.run_module = Mock(return_value=media) with patch( "app.chain.MoviePilotServerHelper.report_recognize_share", return_value=False, ): result = chain.recognize_media( media_source=MediaSource.AniList, media_id="154587", mtype=MediaType.TV, ) assert result is media call = chain.run_module.call_args assert call.kwargs["media_source"] == MediaSource.AniList assert call.kwargs["media_id"] == "154587" assert not { "source", "mediaid", "tmdbid", "doubanid", "bangumiid", "anilistid" }.intersection(call.kwargs) def test_default_recognition_passes_empty_generic_identity() -> None: """默认识别也只向模块传递为空的统一媒体身份。""" chain = _chain_without_init() media = MediaInfo( media_source=MediaSource.TMDB, media_id="1", title="测试电影", type=MediaType.MOVIE, tmdb_id=1, ) chain.run_module = Mock(return_value=media) meta = MetaBase("测试电影") meta.cn_name = "测试电影" meta.type = MediaType.MOVIE with patch( "app.chain.MoviePilotServerHelper.report_recognize_share", return_value=False, ): result = chain.recognize_media(meta=meta) assert result is media call = chain.run_module.call_args assert call.kwargs["media_source"] is None assert call.kwargs["media_id"] is None def test_module_dispatch_always_reaches_plugins() -> None: """模块调度必须始终先执行插件模块。""" chain = _chain_without_init() chain._ChainBase__execute_plugin_modules = Mock(return_value="plugin") chain._ChainBase__execute_system_modules = Mock(return_value="system") result = chain.run_module("search_medias", meta=MetaBase("test")) assert result == "plugin" chain._ChainBase__execute_plugin_modules.assert_called_once() chain._ChainBase__execute_system_modules.assert_not_called() def test_explicit_search_source_reaches_plugins() -> None: """请求级搜索来源应以统一字段进入完整模块调度。""" chain = _chain_without_init() chain.run_module = Mock(return_value=[]) meta = MetaBase("Frieren") result = chain.search_medias(meta, media_source=MediaSource.AniList) assert result == [] chain.run_module.assert_called_once_with( "search_medias", meta=meta, media_source=MediaSource.AniList, )