Files
MoviePilot/tests/test_plugin_config_command.py

211 lines
7.2 KiB
Python

from contextlib import nullcontext
from app.application.plugin.config import PluginConfigCommand, PluginPurgeScope
from app.runtime.extensions.plugin.admission import PluginMutationAdmission
def _command(
calls: list[tuple],
*,
save_result: bool = True,
mutation=None,
directory_existed: bool = True,
is_clone: bool = False,
) -> PluginConfigCommand:
"""构造记录端口调用顺序的插件配置用例。"""
return PluginConfigCommand(
save_config=lambda plugin_id, config, force: (
calls.append(("save", plugin_id, config, force)) or save_result
),
initialize=lambda plugin_id, config: calls.append(
("initialize", plugin_id, config)
),
stop=lambda plugin_id: calls.append(("stop", plugin_id)),
delete_config=lambda plugin_id, force: (
calls.append(("delete_config", plugin_id, force)) or True
),
delete_data=lambda plugin_id, force: (
calls.append(("delete_data", plugin_id, force)) or True
),
reload_runtime=lambda plugin_id: calls.append(("reload", plugin_id)),
publish_reset=lambda plugin_id: calls.append(("publish", plugin_id)),
refresh_registrations=lambda plugin_id: calls.append(
("registrations", plugin_id)
),
mutation=mutation or (lambda _operation: nullcontext()),
delete_plugin_data_rows=lambda plugin_id: calls.append(("data_rows", plugin_id)),
destroy_own_database=lambda plugin_id: calls.append(("own_db", plugin_id)),
delete_data_directory=lambda plugin_id: (
calls.append(("data_dir", plugin_id)) or directory_existed
),
purge_instance=lambda plugin_id: (
calls.append(("purge_row", plugin_id)) or True
),
is_clone=lambda _plugin_id: is_clone,
)
def test_update_stops_before_runtime_side_effects_when_save_fails() -> None:
"""配置持久化失败时不得初始化插件或刷新宿主注册。"""
calls: list[tuple] = []
result = _command(calls, save_result=False).update("DemoPlugin", {"enabled": True})
assert result.success is False
assert result.message == "插件配置保存失败"
assert calls == [("save", "DemoPlugin", {"enabled": True}, False)]
def test_update_refreshes_runtime_only_after_config_is_saved() -> None:
"""配置保存成功后按初始化、注册刷新顺序生效。"""
calls: list[tuple] = []
result = _command(calls).update("DemoPlugin", {"enabled": True})
assert result.success is True
assert calls == [
("save", "DemoPlugin", {"enabled": True}, False),
("initialize", "DemoPlugin", {"enabled": True}),
("registrations", "DemoPlugin"),
]
def test_reset_preserves_compensation_cleanup_and_reload_order() -> None:
"""重置必须先让插件补偿,再停止、清理并重建运行态和注册。"""
calls: list[tuple] = []
result = _command(calls).reset("DemoPlugin")
assert result.success is True
assert calls == [
("publish", "DemoPlugin"),
("stop", "DemoPlugin"),
("delete_config", "DemoPlugin", True),
("delete_data", "DemoPlugin", True),
("reload", "DemoPlugin"),
("registrations", "DemoPlugin"),
]
def test_sealed_config_command_rejects_before_first_side_effect() -> None:
"""配置事务在 admission 封口后返回失败,且不得先发布 reset 事件。"""
calls: list[tuple] = []
admission = PluginMutationAdmission()
admission.seal()
update_result = _command(calls, mutation=admission.hold).update(
"DemoPlugin",
{"enabled": True},
)
reset_result = _command(calls, mutation=admission.hold).reset("DemoPlugin")
assert update_result.success is False
assert reset_result.success is False
assert "停机阶段" in update_result.message
assert "停机阶段" in reset_result.message
assert calls == []
def test_purge_refuses_an_empty_scope() -> None:
"""一项都没勾选时直接拒绝,不做「那就全清吧」这种猜测。"""
calls: list[tuple] = []
result = _command(calls).purge("DemoPluginWork", PluginPurgeScope())
assert result.success is False
assert "至少选择一项" in result.message
assert calls == []
def test_purge_only_touches_the_selected_scopes() -> None:
"""逐项执行且互不代劳:没勾的范围一个字节都不能动。"""
calls: list[tuple] = []
result = _command(calls).purge(
"DemoPluginWork",
PluginPurgeScope(config=True),
)
assert result.success is True
assert result.purged == ("config",)
assert calls == [
("stop", "DemoPluginWork"),
("delete_config", "DemoPluginWork", True),
]
def test_purge_destroys_the_database_before_removing_its_directory() -> None:
"""库文件就落在该目录下,不先销毁句柄直接删目录会留下被占用的文件。"""
calls: list[tuple] = []
result = _command(calls).purge(
"DemoPluginWork",
PluginPurgeScope(data_directory=True),
)
assert result.success is True
# 用户只勾了删目录,自有库仍必然先销毁,但不计入回报的清理范围
assert calls == [
("stop", "DemoPluginWork"),
("own_db", "DemoPluginWork"),
("data_dir", "DemoPluginWork"),
]
assert result.purged == ("data_directory",)
def test_purge_removes_the_row_for_a_clone_but_keeps_it_for_a_host() -> None:
"""分身的存在由那一行表达,清理即消失;本体的行要留着,它还说着「应当装载」。"""
clone_calls: list[tuple] = []
clone = _command(clone_calls, is_clone=True).purge(
"DemoPluginWork", PluginPurgeScope(config=True)
)
host_calls: list[tuple] = []
host = _command(host_calls, is_clone=False).purge(
"DemoPlugin", PluginPurgeScope(config=True)
)
assert clone.instance_removed is True
assert ("purge_row", "DemoPluginWork") in clone_calls
assert host.instance_removed is False
assert not any(call[0] == "purge_row" for call in host_calls)
def test_purge_reports_a_missing_directory_as_not_purged() -> None:
"""目录本就不存在时不谎报清理过它。"""
calls: list[tuple] = []
result = _command(calls, directory_existed=False).purge(
"DemoPluginWork",
PluginPurgeScope(config=True, data_directory=True),
)
assert result.purged == ("config",)
def test_purge_stops_the_instance_before_deleting_anything() -> None:
"""删除必须晚于停止:仍在跑的实例会一边被删一边把数据写回来。"""
calls: list[tuple] = []
_command(calls).purge(
"DemoPluginWork",
PluginPurgeScope(config=True, plugin_data=True, own_database=True),
)
assert calls[0] == ("stop", "DemoPluginWork")
def test_sealed_purge_is_rejected_before_any_deletion() -> None:
"""停机封口期间拒绝清理,且一步删除都不做。"""
calls: list[tuple] = []
admission = PluginMutationAdmission()
admission.seal()
result = _command(calls, mutation=admission.hold).purge(
"DemoPluginWork", PluginPurgeScope(config=True)
)
assert result.success is False
assert "停机阶段" in result.message
assert calls == []