mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-05 07:27:15 +08:00
refactor: complete durable transfer execution settlement
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
"""验证 durable 整理人工复核 API 的鉴权和公开契约。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
from pydantic import ValidationError
|
||||
|
||||
from app.api.dependencies.auth import get_current_active_manage_user
|
||||
from app.api.endpoints import transfer as transfer_endpoint
|
||||
from app.application.transfer_execution import (
|
||||
TransferExecutionConflictError,
|
||||
TransferExecutionState,
|
||||
TransferManualReviewDecision,
|
||||
TransferManualReviewResult,
|
||||
TransferStepResult,
|
||||
)
|
||||
from app.schemas.transfer import TransferManualReviewRequest
|
||||
|
||||
|
||||
class _ManualReviewCommand:
|
||||
"""记录人工复核调用并返回可控结果。"""
|
||||
|
||||
calls: list[tuple[object, dict]] = []
|
||||
result: TransferManualReviewResult | None = None
|
||||
error: Exception | None = None
|
||||
|
||||
def __init__(self, repository: object) -> None:
|
||||
"""保存端点取得的仓储替身。"""
|
||||
self._repository = repository
|
||||
|
||||
def resolve_manual_review(self, **kwargs) -> TransferManualReviewResult:
|
||||
"""记录参数,并按测试配置返回或抛出结果。"""
|
||||
self.calls.append((self._repository, kwargs))
|
||||
if self.error:
|
||||
raise self.error
|
||||
assert self.result is not None
|
||||
return self.result
|
||||
|
||||
|
||||
def _install_command(monkeypatch) -> object:
|
||||
"""安装不接触数据库的人工复核命令替身。"""
|
||||
repository = object()
|
||||
_ManualReviewCommand.calls = []
|
||||
_ManualReviewCommand.error = None
|
||||
monkeypatch.setattr(
|
||||
transfer_endpoint,
|
||||
"get_chain_transfer_execution_port",
|
||||
lambda: repository,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
transfer_endpoint,
|
||||
"TransferExecutionCommand",
|
||||
_ManualReviewCommand,
|
||||
)
|
||||
return repository
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"endpoint",
|
||||
[
|
||||
transfer_endpoint.list_transfer_manual_reviews,
|
||||
transfer_endpoint.get_transfer_manual_review,
|
||||
transfer_endpoint.resolve_transfer_manual_review,
|
||||
],
|
||||
)
|
||||
def test_manual_review_endpoints_require_manage_permission(endpoint) -> None:
|
||||
"""人工复核发现、详情与判定必须复用全局 manage 权限依赖。"""
|
||||
dependency = inspect.signature(
|
||||
endpoint
|
||||
).parameters["current_user"].default.dependency
|
||||
|
||||
assert dependency is get_current_active_manage_user
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("payload", "message"),
|
||||
[
|
||||
(
|
||||
{"operation_id": "op-1", "decision": "failed", "reason": "确认失败"},
|
||||
"Input should be 'not_applied' or 'applied'",
|
||||
),
|
||||
(
|
||||
{"operation_id": "op-1", "decision": "applied", "reason": "已完成"},
|
||||
"必须提供 result_payload",
|
||||
),
|
||||
(
|
||||
{"operation_id": "op-1", "decision": "not_applied", "reason": " "},
|
||||
"String should have at least 1 character",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_manual_review_request_rejects_unsafe_decisions(
|
||||
payload: dict,
|
||||
message: str,
|
||||
) -> None:
|
||||
"""公开 schema 不允许 FAILED,且 APPLIED 必须携带结果证据。"""
|
||||
with pytest.raises(ValidationError, match=message):
|
||||
TransferManualReviewRequest.model_validate(payload)
|
||||
|
||||
|
||||
def test_manual_review_applied_wraps_result_and_hides_internal_state(
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
"""APPLIED 应包装版本化结果,响应不得泄漏 lease 或 attempt。"""
|
||||
repository = _install_command(monkeypatch)
|
||||
_ManualReviewCommand.result = SimpleNamespace(
|
||||
task_id="task-1",
|
||||
operation_id="op-1",
|
||||
decision=TransferManualReviewDecision.APPLIED,
|
||||
state=TransferExecutionState.RETRY_WAIT,
|
||||
review_revision=4,
|
||||
)
|
||||
|
||||
response = transfer_endpoint.resolve_transfer_manual_review(
|
||||
task_id="task-1",
|
||||
review=TransferManualReviewRequest(
|
||||
operation_id="op-1",
|
||||
decision="applied",
|
||||
reason="目标摘要匹配",
|
||||
result_payload={"dest_exists": True, "hash_match": True},
|
||||
),
|
||||
current_user=SimpleNamespace(name=" admin ", username="other", id=7),
|
||||
)
|
||||
|
||||
assert _ManualReviewCommand.calls == [
|
||||
(
|
||||
repository,
|
||||
{
|
||||
"task_id": "task-1",
|
||||
"operation_id": "op-1",
|
||||
"decision": TransferManualReviewDecision.APPLIED,
|
||||
"actor": "admin",
|
||||
"reason": "目标摘要匹配",
|
||||
"result": TransferStepResult(
|
||||
payload={"dest_exists": True, "hash_match": True}
|
||||
),
|
||||
},
|
||||
)
|
||||
]
|
||||
assert response.data is not None
|
||||
assert response.data.model_dump() == {
|
||||
"task_id": "task-1",
|
||||
"operation_id": "op-1",
|
||||
"decision": "applied",
|
||||
"state": "retry_wait",
|
||||
"review_revision": 4,
|
||||
}
|
||||
assert "lease" not in response.model_dump_json()
|
||||
assert "attempt" not in response.model_dump_json()
|
||||
|
||||
|
||||
def test_manual_review_not_applied_uses_username_fallback(monkeypatch) -> None:
|
||||
"""名称为空时应稳定回退到 username,并允许安全重新调度。"""
|
||||
_install_command(monkeypatch)
|
||||
_ManualReviewCommand.result = SimpleNamespace(
|
||||
task_id="task-2",
|
||||
operation_id="op-2",
|
||||
decision=TransferManualReviewDecision.NOT_APPLIED,
|
||||
state=TransferExecutionState.RETRY_WAIT,
|
||||
review_revision=2,
|
||||
)
|
||||
|
||||
response = transfer_endpoint.resolve_transfer_manual_review(
|
||||
task_id="task-2",
|
||||
review=TransferManualReviewRequest(
|
||||
operation_id="op-2",
|
||||
decision="not_applied",
|
||||
reason="确认源文件仍存在",
|
||||
),
|
||||
current_user=SimpleNamespace(name="", username="reviewer", id=8),
|
||||
)
|
||||
|
||||
assert _ManualReviewCommand.calls[0][1]["actor"] == "reviewer"
|
||||
assert _ManualReviewCommand.calls[0][1]["result"] is None
|
||||
assert response.data is not None
|
||||
assert response.data.state == "retry_wait"
|
||||
|
||||
|
||||
def test_manual_review_conflict_returns_http_409(monkeypatch) -> None:
|
||||
"""重复或过期人工判定必须返回资源冲突而非伪成功。"""
|
||||
_install_command(monkeypatch)
|
||||
_ManualReviewCommand.error = TransferExecutionConflictError("步骤已被判定")
|
||||
|
||||
with pytest.raises(HTTPException) as error:
|
||||
transfer_endpoint.resolve_transfer_manual_review(
|
||||
task_id="task-1",
|
||||
review=TransferManualReviewRequest(
|
||||
operation_id="op-1",
|
||||
decision="not_applied",
|
||||
reason="重复判定",
|
||||
),
|
||||
current_user=SimpleNamespace(name=None, username=None, id=11),
|
||||
)
|
||||
|
||||
assert error.value.status_code == 409
|
||||
assert error.value.detail == "步骤已被判定"
|
||||
assert _ManualReviewCommand.calls[0][1]["actor"] == "11"
|
||||
Reference in New Issue
Block a user