mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-05 23:47:41 +08:00
fix(compat): preserve legacy plugin imports
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
import importlib
|
||||
|
||||
import pytest
|
||||
|
||||
from app.application.transfer import TransferTask as CanonicalTransferTask
|
||||
from app.schemas.file import FileItem
|
||||
|
||||
|
||||
def test_legacy_subscribe_add_delegates_to_application_service(monkeypatch):
|
||||
"""旧 SubscribeOper.add 应保留 mediainfo 写入签名。"""
|
||||
legacy = importlib.import_module("app.db.subscribe_oper")
|
||||
oper = object.__new__(legacy.SubscribeOper)
|
||||
mediainfo = object()
|
||||
captured = {}
|
||||
|
||||
def fake_add_subscribe(*, mediainfo, subscribe_oper, **kwargs):
|
||||
"""记录同步兼容门面转交的参数。"""
|
||||
captured.update({
|
||||
"mediainfo": mediainfo,
|
||||
"subscribe_oper": subscribe_oper,
|
||||
"kwargs": kwargs,
|
||||
})
|
||||
return 7, "新增订阅成功"
|
||||
|
||||
monkeypatch.setattr(legacy, "add_subscribe", fake_add_subscribe)
|
||||
|
||||
assert oper.add(mediainfo=mediainfo, season=1) == (7, "新增订阅成功")
|
||||
assert captured == {
|
||||
"mediainfo": mediainfo,
|
||||
"subscribe_oper": oper,
|
||||
"kwargs": {"season": 1},
|
||||
}
|
||||
|
||||
|
||||
def test_legacy_subscribe_facade_accepts_application_dictionary_callback(
|
||||
monkeypatch,
|
||||
):
|
||||
"""应用服务回调兼容 Oper 时应进入新字典签名,不能再次转回应用服务。"""
|
||||
legacy = importlib.import_module("app.db.subscribe_oper")
|
||||
canonical = importlib.import_module("app.db.oper.subscribe")
|
||||
oper = object.__new__(legacy.SubscribeOper)
|
||||
captured = {}
|
||||
|
||||
def fake_canonical_add(self, identity, payload, username=None):
|
||||
"""记录兼容类转交给 canonical Oper 的持久化参数。"""
|
||||
captured.update({
|
||||
"self": self,
|
||||
"identity": identity,
|
||||
"payload": payload,
|
||||
"username": username,
|
||||
})
|
||||
return 9, "新增订阅成功"
|
||||
|
||||
monkeypatch.setattr(canonical.SubscribeOper, "add", fake_canonical_add)
|
||||
|
||||
result = oper.add(
|
||||
identity={"media_source": "themoviedb", "media_id": "1"},
|
||||
payload={"name": "Test"},
|
||||
username="admin",
|
||||
)
|
||||
|
||||
assert result == (9, "新增订阅成功")
|
||||
assert captured == {
|
||||
"self": oper,
|
||||
"identity": {"media_source": "themoviedb", "media_id": "1"},
|
||||
"payload": {"name": "Test"},
|
||||
"username": "admin",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_legacy_subscribe_async_add_delegates_to_application_service(
|
||||
monkeypatch,
|
||||
):
|
||||
"""旧 SubscribeOper.async_add 应保留异步 mediainfo 写入签名。"""
|
||||
legacy = importlib.import_module("app.db.subscribe_oper")
|
||||
oper = object.__new__(legacy.SubscribeOper)
|
||||
mediainfo = object()
|
||||
captured = {}
|
||||
|
||||
async def fake_async_add_subscribe(*, mediainfo, subscribe_oper, **kwargs):
|
||||
"""记录异步兼容门面转交的参数。"""
|
||||
captured.update({
|
||||
"mediainfo": mediainfo,
|
||||
"subscribe_oper": subscribe_oper,
|
||||
"kwargs": kwargs,
|
||||
})
|
||||
return 8, "新增订阅成功"
|
||||
|
||||
monkeypatch.setattr(legacy, "async_add_subscribe", fake_async_add_subscribe)
|
||||
|
||||
result = await oper.async_add(mediainfo=mediainfo, season=2)
|
||||
|
||||
assert result == (8, "新增订阅成功")
|
||||
assert captured == {
|
||||
"mediainfo": mediainfo,
|
||||
"subscribe_oper": oper,
|
||||
"kwargs": {"season": 2},
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("method_name", "service_name"),
|
||||
[
|
||||
("add_success", "add_transfer_success"),
|
||||
("add_fail", "add_transfer_fail"),
|
||||
],
|
||||
)
|
||||
def test_legacy_transfer_history_writes_delegate_to_application_service(
|
||||
monkeypatch,
|
||||
method_name,
|
||||
service_name,
|
||||
):
|
||||
"""旧整理历史写入方法应只做代理,不把业务逻辑搬回 Oper。"""
|
||||
legacy = importlib.import_module("app.db.transferhistory_oper")
|
||||
oper = object.__new__(legacy.TransferHistoryOper)
|
||||
arguments = {
|
||||
"fileitem": object(),
|
||||
"mode": "copy",
|
||||
"meta": object(),
|
||||
"mediainfo": object(),
|
||||
"transferinfo": object(),
|
||||
"downloader": "qb",
|
||||
"download_hash": "hash",
|
||||
}
|
||||
captured = {}
|
||||
|
||||
def fake_service(**kwargs):
|
||||
"""记录整理历史兼容门面转交的参数。"""
|
||||
captured.update(kwargs)
|
||||
return "history"
|
||||
|
||||
monkeypatch.setattr(legacy, service_name, fake_service)
|
||||
|
||||
assert getattr(oper, method_name)(**arguments) == "history"
|
||||
assert captured == {**arguments, "transfer_history_oper": oper}
|
||||
|
||||
|
||||
class LegacyPydanticValue:
|
||||
"""模拟旧插件放进 TransferTask 的 Pydantic 风格对象。"""
|
||||
|
||||
def model_dump(self):
|
||||
"""返回测试用序列化结果。"""
|
||||
return {"kind": "pydantic"}
|
||||
|
||||
|
||||
class LegacyDomainValue:
|
||||
"""模拟新领域对象的 to_dict 序列化接口。"""
|
||||
|
||||
def to_dict(self):
|
||||
"""返回测试用序列化结果。"""
|
||||
return {"kind": "domain"}
|
||||
|
||||
|
||||
def test_legacy_transfer_task_keeps_wide_plugin_input_contract():
|
||||
"""旧 schemas.TransferTask 应接受自定义对象且仍可进入新整理链。"""
|
||||
schemas_package = importlib.import_module("app.schemas")
|
||||
task = schemas_package.TransferTask(
|
||||
fileitem=FileItem(path="/downloads/test.mkv", storage="local"),
|
||||
meta=LegacyPydanticValue(),
|
||||
mediainfo=LegacyDomainValue(),
|
||||
)
|
||||
|
||||
assert isinstance(task, CanonicalTransferTask)
|
||||
assert task.to_dict()["meta"] == {"kind": "pydantic"}
|
||||
assert task.to_dict()["mediainfo"] == {"kind": "domain"}
|
||||
@@ -17,6 +17,7 @@ from app.runtime.compat.manifest import (
|
||||
MODULE_ALIASES,
|
||||
PACKAGE_ALIASES,
|
||||
PACKAGE_EXPORTS,
|
||||
SYMBOL_ALIASES,
|
||||
VIRTUAL_PACKAGES,
|
||||
ModuleAlias,
|
||||
)
|
||||
@@ -203,3 +204,133 @@ def test_virtual_package_exports_resolve_exact_manifest_symbols():
|
||||
).MetaBase
|
||||
assert PACKAGE_ALIASES[legacy_package].replacement in messages[0]
|
||||
reset_legacy_import_diagnostics()
|
||||
|
||||
|
||||
def test_db_refactor_legacy_modules_are_all_registered():
|
||||
"""DB 分层迁移删除的旧模块必须全部有精确兼容入口。"""
|
||||
expected = {
|
||||
"app.db.agentchat_oper",
|
||||
"app.db.agenttask_oper",
|
||||
"app.db.downloadfailure_oper",
|
||||
"app.db.downloadhistory_oper",
|
||||
"app.db.init",
|
||||
"app.db.mediaserver_oper",
|
||||
"app.db.message_oper",
|
||||
"app.db.plugindata_oper",
|
||||
"app.db.site_oper",
|
||||
"app.db.subscribe_oper",
|
||||
"app.db.subscribehistory_oper",
|
||||
"app.db.systemconfig_oper",
|
||||
"app.db.transferhistory_oper",
|
||||
"app.db.transferpending_oper",
|
||||
"app.db.user_oper",
|
||||
"app.db.userconfig_oper",
|
||||
"app.db.workflow_oper",
|
||||
}
|
||||
|
||||
assert expected <= set(MODULE_ALIASES)
|
||||
|
||||
|
||||
def test_split_user_oper_facade_exports_data_and_auth_contracts():
|
||||
"""旧 user_oper 同时提供 UserOper 与八个认证依赖。"""
|
||||
legacy = importlib.import_module("app.db.user_oper")
|
||||
canonical_user = importlib.import_module("app.db.oper.user")
|
||||
canonical_deps = importlib.import_module("app.api.deps")
|
||||
|
||||
assert legacy.UserOper is canonical_user.UserOper
|
||||
for name in (
|
||||
"get_current_user",
|
||||
"get_current_user_async",
|
||||
"get_current_active_user",
|
||||
"get_current_active_user_async",
|
||||
"get_current_active_manage_user",
|
||||
"get_current_active_manage_user_async",
|
||||
"get_current_active_superuser",
|
||||
"get_current_active_superuser_async",
|
||||
):
|
||||
assert getattr(legacy, name) is getattr(canonical_deps, name)
|
||||
|
||||
|
||||
def test_legacy_utils_media_facade_combines_strategy_and_identity_symbols():
|
||||
"""旧 utils.media 同时保留领域策略和迁至 schemas 的身份原语。"""
|
||||
legacy = importlib.import_module("app.utils.media")
|
||||
domain_media = importlib.import_module("app.domain.media")
|
||||
schema_media = importlib.import_module("app.schemas.media")
|
||||
|
||||
assert legacy.is_music_media_source is domain_media.is_music_media_source
|
||||
assert legacy.resolve_media_identity is schema_media.resolve_media_identity
|
||||
assert legacy.build_media_key is schema_media.build_media_key
|
||||
assert legacy.MEDIA_SOURCE_ALIASES is schema_media.MEDIA_SOURCE_ALIASES
|
||||
|
||||
|
||||
def test_physical_modules_resolve_moved_symbols_without_reverse_imports():
|
||||
"""仍存在的旧物理模块应通过 Loader 叠加迁走的符号。"""
|
||||
domain_media = importlib.import_module("app.domain.media")
|
||||
schema_media = importlib.import_module("app.schemas.media")
|
||||
transfer_schema = importlib.import_module("app.schemas.transfer")
|
||||
legacy_transfer = importlib.import_module("app.sdk._legacy.transfer")
|
||||
schemas_package = importlib.import_module("app.schemas")
|
||||
|
||||
assert domain_media.build_media_key is schema_media.build_media_key
|
||||
assert domain_media.resolve_media_identity is schema_media.resolve_media_identity
|
||||
assert transfer_schema.TransferTask is legacy_transfer.TransferTask
|
||||
assert transfer_schema.TransferQueue is legacy_transfer.TransferQueue
|
||||
assert schemas_package.TransferTask is legacy_transfer.TransferTask
|
||||
assert schemas_package.TransferQueue is legacy_transfer.TransferQueue
|
||||
|
||||
|
||||
def test_debug_diagnostics_reports_moved_symbol_path():
|
||||
"""DEBUG 模式应对物理模块中的旧符号路径给出一次迁移提示。"""
|
||||
messages = []
|
||||
reset_legacy_import_diagnostics()
|
||||
configure_legacy_import_diagnostics(enabled=True, emitter=messages.append)
|
||||
|
||||
domain_media = importlib.import_module("app.domain.media")
|
||||
domain_media.build_media_key
|
||||
domain_media.build_media_key
|
||||
|
||||
assert len(messages) == 1
|
||||
assert "app.domain.media.build_media_key" in messages[0]
|
||||
assert "app.schemas.media.build_media_key" in messages[0]
|
||||
reset_legacy_import_diagnostics()
|
||||
|
||||
|
||||
def test_plugin_scan_reports_moved_symbol_import(tmp_path: Path):
|
||||
"""插件静态扫描应识别仍存在模块中的旧符号导入。"""
|
||||
messages = []
|
||||
reset_legacy_import_diagnostics()
|
||||
configure_legacy_import_diagnostics(enabled=True, emitter=messages.append)
|
||||
plugin_dir = tmp_path / "symbolplugin"
|
||||
plugin_dir.mkdir()
|
||||
(plugin_dir / "__init__.py").write_text(
|
||||
"from app.domain.media import build_media_key\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
scan_plugin_legacy_imports("SymbolPlugin", plugin_dir)
|
||||
|
||||
assert len(messages) == 1
|
||||
assert "app.domain.media.build_media_key" in messages[0]
|
||||
assert "__init__.py:1" in messages[0]
|
||||
reset_legacy_import_diagnostics()
|
||||
|
||||
|
||||
def test_symbol_alias_manifest_covers_all_moved_public_symbols():
|
||||
"""符号级映射清单应覆盖媒体身份与整理工作项的旧入口。"""
|
||||
assert set(SYMBOL_ALIASES["app.domain.media"]) == {
|
||||
"MEDIA_SOURCE_ALIASES",
|
||||
"MEDIA_SOURCE_PREFIXES",
|
||||
"normalize_media_source",
|
||||
"parse_media_key",
|
||||
"resolve_media_identity",
|
||||
"normalize_media_identity_payload",
|
||||
"build_media_key",
|
||||
}
|
||||
assert set(SYMBOL_ALIASES["app.schemas"]) == {
|
||||
"TransferTask",
|
||||
"TransferQueue",
|
||||
}
|
||||
assert set(SYMBOL_ALIASES["app.schemas.transfer"]) == {
|
||||
"TransferTask",
|
||||
"TransferQueue",
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user