mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-04 15:09:46 +08:00
refactor: 推进后端分层架构治理
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, Mock
|
||||
|
||||
import pytest
|
||||
|
||||
from app.application.workflow import (
|
||||
WorkflowDefinitionCommand,
|
||||
WorkflowMutationCommand,
|
||||
)
|
||||
|
||||
|
||||
def _workflow(trigger_type="timer", timer="0 0 * * *", event_type="DownloadAdded"):
|
||||
"""构造工作流写用例使用的最小快照。"""
|
||||
return SimpleNamespace(
|
||||
id=7,
|
||||
trigger_type=trigger_type,
|
||||
timer=timer,
|
||||
event_type=event_type,
|
||||
)
|
||||
|
||||
|
||||
def _command(workflow=None, commit_error=None):
|
||||
"""构造可观察工作流事务与运行时副作用的命令。"""
|
||||
repository = Mock()
|
||||
repository.get = Mock(return_value=workflow)
|
||||
repository.stage_state = Mock(return_value=True)
|
||||
repository.stage_update = Mock(return_value=workflow)
|
||||
repository.stage_delete = Mock()
|
||||
unit_of_work = Mock()
|
||||
unit_of_work.commit = Mock(side_effect=commit_error)
|
||||
unit_of_work.rollback = Mock()
|
||||
dependencies = {
|
||||
"repository": repository,
|
||||
"unit_of_work": unit_of_work,
|
||||
"add_timer": Mock(),
|
||||
"remove_timer": Mock(),
|
||||
"load_event": Mock(),
|
||||
"remove_event": Mock(),
|
||||
"refresh_event": Mock(),
|
||||
"stop_running": Mock(),
|
||||
"delete_cache": Mock(),
|
||||
}
|
||||
return WorkflowMutationCommand(**dependencies), dependencies
|
||||
|
||||
|
||||
def test_start_timer_workflow_commits_before_registering_job():
|
||||
"""启用定时工作流必须先提交 W 状态,再登记定时任务。"""
|
||||
calls = []
|
||||
command, dependencies = _command(_workflow())
|
||||
dependencies["unit_of_work"].commit.side_effect = lambda: calls.append("commit")
|
||||
dependencies["add_timer"].side_effect = lambda _workflow: calls.append("timer")
|
||||
|
||||
result = command.start(7)
|
||||
|
||||
assert result.success is True
|
||||
assert calls == ["commit", "timer"]
|
||||
dependencies["repository"].stage_state.assert_called_once_with(7, "W")
|
||||
|
||||
|
||||
def test_start_rejects_invalid_trigger_without_transaction():
|
||||
"""未知触发类型不得更新数据库或注册运行时触发器。"""
|
||||
command, dependencies = _command(_workflow(trigger_type="unknown"))
|
||||
|
||||
result = command.start(7)
|
||||
|
||||
assert result.success is False
|
||||
assert result.message == "工作流触发类型不支持"
|
||||
dependencies["unit_of_work"].commit.assert_not_called()
|
||||
|
||||
|
||||
def test_pause_event_workflow_commits_before_runtime_cleanup():
|
||||
"""停用事件工作流必须提交 P 状态后再移除事件和停止执行。"""
|
||||
calls = []
|
||||
command, dependencies = _command(_workflow(trigger_type="event", timer=None))
|
||||
dependencies["unit_of_work"].commit.side_effect = lambda: calls.append("commit")
|
||||
dependencies["remove_event"].side_effect = lambda *_args: calls.append("event")
|
||||
dependencies["stop_running"].side_effect = lambda _id: calls.append("stop")
|
||||
|
||||
result = command.pause(7)
|
||||
|
||||
assert result.success is True
|
||||
assert calls == ["commit", "event", "stop"]
|
||||
|
||||
|
||||
def test_delete_commit_failure_rolls_back_without_runtime_side_effects():
|
||||
"""删除提交失败必须回滚,且不得删除缓存或运行时触发器。"""
|
||||
command, dependencies = _command(
|
||||
_workflow(),
|
||||
commit_error=RuntimeError("commit failed"),
|
||||
)
|
||||
|
||||
with pytest.raises(RuntimeError, match="commit failed"):
|
||||
command.delete(7)
|
||||
|
||||
dependencies["unit_of_work"].rollback.assert_called_once_with()
|
||||
dependencies["delete_cache"].assert_not_called()
|
||||
dependencies["remove_timer"].assert_not_called()
|
||||
|
||||
|
||||
def test_update_refreshes_timer_and_event_after_commit():
|
||||
"""更新工作流提交后重建定时器并刷新事件注册。"""
|
||||
workflow = _workflow()
|
||||
command, dependencies = _command(workflow)
|
||||
|
||||
result = command.update({"id": 7, "name": "updated"})
|
||||
|
||||
assert result.success is True
|
||||
dependencies["repository"].stage_update.assert_called_once()
|
||||
dependencies["unit_of_work"].commit.assert_called_once_with()
|
||||
dependencies["remove_timer"].assert_called_once_with(workflow)
|
||||
dependencies["add_timer"].assert_called_once_with(workflow)
|
||||
dependencies["refresh_event"].assert_called_once_with(workflow)
|
||||
|
||||
|
||||
def _definition_command(*, existing=None, commit_error=None, report_fork=None):
|
||||
"""构造可观察异步工作流定义事务的命令。"""
|
||||
repository = Mock()
|
||||
repository.async_get_by_name = AsyncMock(return_value=existing)
|
||||
repository.async_get = AsyncMock(return_value=existing)
|
||||
repository.stage_create = AsyncMock(return_value=SimpleNamespace(id=8))
|
||||
repository.stage_reset = AsyncMock(return_value=existing)
|
||||
unit_of_work = Mock()
|
||||
unit_of_work.commit = AsyncMock(side_effect=commit_error)
|
||||
unit_of_work.rollback = AsyncMock()
|
||||
dependencies = {
|
||||
"repository": repository,
|
||||
"unit_of_work": unit_of_work,
|
||||
"stop_running": Mock(),
|
||||
"delete_cache": Mock(),
|
||||
"report_fork": report_fork or AsyncMock(),
|
||||
}
|
||||
return WorkflowDefinitionCommand(**dependencies), dependencies
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_workflow_applies_defaults_and_commits_once():
|
||||
"""创建工作流由应用用例补齐默认状态并统一提交。"""
|
||||
command, dependencies = _definition_command()
|
||||
|
||||
result = await command.create({"name": "Demo", "state": None})
|
||||
|
||||
assert result.success is True
|
||||
payload = dependencies["repository"].stage_create.await_args.args[0]
|
||||
assert payload["trigger_type"] == "timer"
|
||||
assert payload["state"] == "P"
|
||||
assert payload["add_time"]
|
||||
dependencies["unit_of_work"].commit.assert_awaited_once_with()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_duplicate_name_has_no_transaction():
|
||||
"""名称重复时不得暂存或提交工作流。"""
|
||||
command, dependencies = _definition_command(existing=SimpleNamespace(id=1))
|
||||
|
||||
result = await command.create({"name": "Demo"})
|
||||
|
||||
assert result.success is False
|
||||
dependencies["repository"].stage_create.assert_not_awaited()
|
||||
dependencies["unit_of_work"].commit.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fork_commits_before_reporting_remote_count():
|
||||
"""共享工作流必须先本地提交,随后才更新远程复用次数。"""
|
||||
calls = []
|
||||
|
||||
async def commit():
|
||||
calls.append("commit")
|
||||
|
||||
async def report(_share_id):
|
||||
calls.append("report")
|
||||
|
||||
command, dependencies = _definition_command(report_fork=report)
|
||||
dependencies["unit_of_work"].commit.side_effect = commit
|
||||
|
||||
result = await command.fork(
|
||||
{
|
||||
"name": "Forked",
|
||||
"actions": "[]",
|
||||
"flows": "[]",
|
||||
"context": "{}",
|
||||
"event_conditions": "{}",
|
||||
},
|
||||
share_id=9,
|
||||
)
|
||||
|
||||
assert result.success is True
|
||||
assert calls == ["commit", "report"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fork_invalid_json_stops_before_database_write():
|
||||
"""共享内容 JSON 无效时不得创建半成品工作流。"""
|
||||
command, dependencies = _definition_command()
|
||||
|
||||
result = await command.fork({"name": "Forked", "actions": "{"})
|
||||
|
||||
assert result.success is False
|
||||
assert result.message == "actions字段JSON格式错误"
|
||||
dependencies["repository"].stage_create.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reset_commit_failure_does_not_stop_runtime_or_delete_cache():
|
||||
"""重置提交失败时只回滚数据库,不影响现有运行态。"""
|
||||
command, dependencies = _definition_command(
|
||||
existing=_workflow(),
|
||||
commit_error=RuntimeError("commit failed"),
|
||||
)
|
||||
|
||||
with pytest.raises(RuntimeError, match="commit failed"):
|
||||
await command.reset(7)
|
||||
|
||||
dependencies["unit_of_work"].rollback.assert_awaited_once_with()
|
||||
dependencies["stop_running"].assert_not_called()
|
||||
dependencies["delete_cache"].assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reset_commits_before_runtime_cleanup():
|
||||
"""工作流重置成功后再停止执行并删除缓存。"""
|
||||
calls = []
|
||||
command, dependencies = _definition_command(existing=_workflow())
|
||||
dependencies["unit_of_work"].commit.side_effect = lambda: calls.append("commit")
|
||||
dependencies["stop_running"].side_effect = lambda _id: calls.append("stop")
|
||||
dependencies["delete_cache"].side_effect = lambda _id: calls.append("cache")
|
||||
|
||||
result = await command.reset(7)
|
||||
|
||||
assert result.success is True
|
||||
assert calls == ["commit", "stop", "cache"]
|
||||
Reference in New Issue
Block a user