mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/jxxghp/MoviePilot.git
synced 2026-09-20 08:03:34 +08:00
211 lines
8.3 KiB
Python
211 lines
8.3 KiB
Python
"""Chain 运行上下文注入和无参兼容 provider 测试。"""
|
||
|
||
import sys
|
||
from types import ModuleType
|
||
from unittest.mock import Mock, call
|
||
|
||
import pytest
|
||
|
||
from app.application.chain import context as chain_context
|
||
from app.application.chain.context import ChainRuntimeContext
|
||
from app.application.configuration import ChainRuntimeConfig
|
||
from app.chain.base import ChainBase
|
||
from app.runtime.extensions.module.dispatcher import ModuleInvocationDispatcher
|
||
from app.startup.composition.runtime import RuntimeDependencies
|
||
|
||
|
||
def _context() -> ChainRuntimeContext:
|
||
"""构造不连接数据库、不启动线程的最小 Chain 上下文。"""
|
||
message_queue = Mock()
|
||
message_queue.bind.return_value = Mock()
|
||
return ChainRuntimeContext(
|
||
module_manager=Mock(),
|
||
plugin_manager=Mock(),
|
||
event_manager=Mock(),
|
||
message_oper=Mock(),
|
||
message_helper=Mock(),
|
||
file_cache=Mock(),
|
||
async_file_cache=Mock(),
|
||
message_queue=message_queue,
|
||
module_dispatcher_factory=ModuleInvocationDispatcher,
|
||
site_repository=Mock(),
|
||
subscription_repository=Mock(),
|
||
subscription_mutation_scope=Mock(),
|
||
sync_subscription_mutation_scope=Mock(),
|
||
subscription_delete_scope=Mock(),
|
||
sync_subscription_delete_scope=Mock(),
|
||
subscription_completion_scope=Mock(),
|
||
rule_group_mutation_scope=Mock(),
|
||
site_reference_mutation_scope=Mock(),
|
||
download_history_repository=Mock(),
|
||
transfer_history_repository=Mock(),
|
||
transfer_admission_repository=Mock(),
|
||
transfer_execution_repository=Mock(),
|
||
media_server_repository=Mock(),
|
||
download_failure_repository=Mock(),
|
||
user_repository=Mock(),
|
||
configuration=ChainRuntimeConfig(media_extensions=(".mkv",)),
|
||
durable_event_writer=Mock(),
|
||
system_service=Mock(),
|
||
)
|
||
|
||
|
||
def test_chain_accepts_explicit_runtime_context() -> None:
|
||
"""新代码应能显式注入最小运行时依赖而不创建真实管理器。"""
|
||
context = _context()
|
||
|
||
chain = ChainBase(context)
|
||
|
||
assert chain.modulemanager is context.module_manager
|
||
assert chain.pluginmanager is context.plugin_manager
|
||
assert chain.eventmanager is context.event_manager
|
||
assert chain.messagehelper is context.message_helper
|
||
assert chain.durable_event_writer is context.durable_event_writer
|
||
assert chain.system_service is context.system_service
|
||
context.message_queue.bind.assert_called_once_with(chain.run_module)
|
||
|
||
|
||
def test_chains_bind_distinct_callbacks_without_starting_queue() -> None:
|
||
"""多个 Chain 只创建轻量客户端,不得启动或覆盖共享队列 owner。"""
|
||
context = _context()
|
||
|
||
first = ChainBase(context)
|
||
second = ChainBase(context)
|
||
|
||
assert context.message_queue.start.call_count == 0
|
||
assert context.message_queue.bind.call_args_list == [
|
||
call(first.run_module),
|
||
call(second.run_module),
|
||
]
|
||
|
||
|
||
def test_no_arg_chain_uses_compatibility_context_provider(monkeypatch) -> None:
|
||
"""V3 兼容期内无参 Chain() 应从组合根 provider 获取相同上下文。"""
|
||
context = _context()
|
||
provider = Mock(return_value=context)
|
||
monkeypatch.setattr(chain_context, "_context_provider", provider)
|
||
|
||
chain = ChainBase()
|
||
|
||
provider.assert_called_once_with()
|
||
assert chain.modulemanager is context.module_manager
|
||
assert chain.pluginmanager is context.plugin_manager
|
||
|
||
|
||
def test_chain_runtime_context_rejects_unconfigured_provider(monkeypatch) -> None:
|
||
"""未由组合根配置运行上下文时必须显式拒绝无参 Chain。"""
|
||
monkeypatch.setattr(
|
||
chain_context,
|
||
"_context_provider",
|
||
chain_context._unconfigured_chain_runtime_context,
|
||
)
|
||
|
||
with pytest.raises(RuntimeError, match="Chain 运行上下文尚未由启动组合根配置"):
|
||
chain_context.get_chain_runtime_context()
|
||
|
||
|
||
def test_chain_keeps_explicit_typed_repositories() -> None:
|
||
"""Chain 实例必须直接保存组合根注入的数据端口,不再查找全局注册表。"""
|
||
context = _context()
|
||
|
||
chain = ChainBase(context)
|
||
|
||
assert chain.site_repository is context.site_repository
|
||
assert chain.subscription_repository is context.subscription_repository
|
||
assert chain.subscription_mutation_scope is context.subscription_mutation_scope
|
||
assert chain.sync_subscription_mutation_scope is context.sync_subscription_mutation_scope
|
||
assert chain.subscription_delete_scope is context.subscription_delete_scope
|
||
assert chain.sync_subscription_delete_scope is context.sync_subscription_delete_scope
|
||
assert chain.subscription_completion_scope is context.subscription_completion_scope
|
||
assert chain.transfer_execution_repository is context.transfer_execution_repository
|
||
assert chain.user_repository is context.user_repository
|
||
|
||
|
||
def test_chain_composition_registers_lazy_compatibility_provider(monkeypatch) -> None:
|
||
"""组合 API 只登记 provider,首次无参 Chain 请求时才构造完整上下文。"""
|
||
from app.startup.composition import chain as chain_composition
|
||
|
||
context = _context()
|
||
builder = Mock(return_value=context)
|
||
register = Mock()
|
||
runtime_dependencies = RuntimeDependencies(
|
||
download_history=Mock(),
|
||
transfer_history=Mock(),
|
||
site=Mock(),
|
||
subscription=Mock(),
|
||
subscription_history=Mock(),
|
||
transfer_execution=Mock(),
|
||
message_helper=Mock(),
|
||
message_queue=Mock(),
|
||
)
|
||
inputs = {
|
||
"dependencies": runtime_dependencies,
|
||
"system_config": Mock(),
|
||
"configuration": Mock(),
|
||
"classification_service": Mock(),
|
||
"system_service": Mock(),
|
||
}
|
||
monkeypatch.setattr(chain_composition, "build_chain_runtime_context", builder)
|
||
monkeypatch.setattr(
|
||
chain_composition,
|
||
"configure_chain_runtime_context_provider",
|
||
register,
|
||
)
|
||
|
||
chain_composition.configure_chain_runtime_context(**inputs)
|
||
|
||
builder.assert_not_called()
|
||
provider = register.call_args.args[0]
|
||
assert provider() is context
|
||
builder.assert_called_once_with(**inputs)
|
||
|
||
|
||
def test_chain_composition_keeps_legacy_transfer_import_lazy(monkeypatch) -> None:
|
||
"""旧整理命令仅在真实调用时加载 TransferChain,并原样转交参数。"""
|
||
from app.startup.composition import chain as chain_composition
|
||
|
||
execute = Mock(return_value="transferred")
|
||
transfer_chain = Mock(return_value=Mock(execute_legacy_transfer_command=execute))
|
||
facade = ModuleType("app.chain.transfer.facade")
|
||
facade.TransferChain = transfer_chain
|
||
monkeypatch.setitem(sys.modules, "app.chain.transfer.facade", facade)
|
||
|
||
result = chain_composition.execute_legacy_transfer_command(
|
||
source="downloads",
|
||
target="library",
|
||
)
|
||
|
||
assert result == "transferred"
|
||
transfer_chain.assert_called_once_with()
|
||
execute.assert_called_once_with(source="downloads", target="library")
|
||
|
||
|
||
def test_chain_composition_registers_lazy_wallpaper_providers(monkeypatch) -> None:
|
||
"""壁纸装配只登记 callable,不在启动阶段提前物化业务 Chain。"""
|
||
from app.startup.composition import chain as chain_composition
|
||
|
||
register = Mock()
|
||
tmdb_instance = Mock()
|
||
tmdb_instance.get_random_wallpager.return_value = "tmdb-one"
|
||
tmdb_instance.get_trending_wallpapers.return_value = ["tmdb-many"]
|
||
media_instance = Mock()
|
||
media_instance.get_latest_wallpaper.return_value = "media-one"
|
||
media_instance.get_latest_wallpapers.return_value = ["media-many"]
|
||
tmdb_chain = Mock(return_value=tmdb_instance)
|
||
media_chain = Mock(return_value=media_instance)
|
||
monkeypatch.setattr(chain_composition, "configure_wallpaper_providers", register)
|
||
monkeypatch.setattr(chain_composition, "TmdbChain", tmdb_chain)
|
||
monkeypatch.setattr(chain_composition, "MediaServerChain", media_chain)
|
||
|
||
chain_composition.configure_wallpaper_services()
|
||
|
||
tmdb_chain.assert_not_called()
|
||
media_chain.assert_not_called()
|
||
providers = register.call_args.kwargs
|
||
assert providers["tmdb_wallpaper"]() == "tmdb-one"
|
||
assert providers["tmdb_wallpapers"](3) == ["tmdb-many"]
|
||
assert providers["mediaserver_wallpaper"]() == "media-one"
|
||
assert providers["mediaserver_wallpapers"](4) == ["media-many"]
|
||
tmdb_instance.get_trending_wallpapers.assert_called_once_with(3)
|
||
media_instance.get_latest_wallpapers.assert_called_once_with(count=4)
|