Files
MoviePilot/tests/test_subscription_completion_command.py
T

240 lines
7.5 KiB
Python

"""订阅完成命令的原子写入、时序与 durable intent 测试。"""
from datetime import datetime
import pytest
from app.application.subscription.complete import CompleteSubscriptionCommand
class _Repository:
"""记录历史暂存和订阅删除顺序。"""
def __init__(self, calls: list[tuple]) -> None:
"""保存共享调用序列。"""
self.calls = calls
def add_history(self, **payload) -> None:
"""记录历史快照。"""
self.calls.append(("history", payload))
def delete(self, subscribe_id: int) -> None:
"""记录待删除订阅。"""
self.calls.append(("delete", subscribe_id))
class _UnitOfWork:
"""记录提交和回滚。"""
def __init__(self, calls: list[tuple], error: Exception | None = None) -> None:
"""保存调用序列与可选提交异常。"""
self.calls = calls
self.error = error
def commit(self) -> None:
"""记录提交并按需失败。"""
self.calls.append(("commit",))
if self.error:
raise self.error
def rollback(self) -> None:
"""记录回滚。"""
self.calls.append(("rollback",))
class _Outbox:
"""记录 intent 暂存与即时收口。"""
def __init__(self, calls: list[tuple], claim_result: bool = True) -> None:
"""保存共享调用序列。"""
self.calls = calls
self.claim_result = claim_result
def stage(self, intent, _now: datetime) -> None:
"""记录 durable intent。"""
self.calls.append(("stage", intent))
def claim_by_event_key(self, event_key: str, _now: datetime, _lease_until: datetime) -> bool:
"""记录同步投递认领结果。"""
self.calls.append(("claim", event_key))
return self.claim_result
def complete_by_event_key(self, event_key: str, _now: datetime) -> None:
"""记录成功副作用对应的 intent 收口。"""
self.calls.append(("complete", event_key))
def _command(
calls: list[tuple],
*,
publish_error=None,
report_result=True,
notify_error=None,
claim_result=True,
report_error=None,
):
"""构造可注入失败的完成命令。"""
def notify() -> None:
"""记录通知。"""
calls.append(("notify",))
if notify_error:
raise notify_error
def publish(payload) -> None:
"""记录完成事件。"""
calls.append(("event", payload))
if publish_error:
raise publish_error
def report(payload) -> bool:
"""记录完成统计。"""
calls.append(("report", payload))
if report_error:
raise report_error
return report_result
return CompleteSubscriptionCommand(
repository=_Repository(calls),
unit_of_work=_UnitOfWork(calls),
outbox=_Outbox(calls, claim_result),
publish=publish,
), notify, report
@pytest.mark.parametrize("failure", ["event", "notify"])
def test_completion_stages_business_and_independent_intents_before_commit(failure):
"""完成事务先提交业务和两个 intent,提交后按通知、事件、统计顺序执行。"""
calls = []
command, notify, report = _command(
calls,
publish_error=RuntimeError("event failed") if failure == "event" else None,
notify_error=RuntimeError("notify failed") if failure == "notify" else None,
)
with pytest.raises(RuntimeError):
command.execute(
7,
{"id": 7, "media_source": "tmdb", "media_id": "123", "season": 2},
{"title": "Test"},
notify=notify,
report=report,
)
assert [call[0] for call in calls[:5]] == [
"history", "delete", "stage", "stage", "commit",
]
assert calls[2][1].topic == "subscribe.complete"
assert calls[3][1].topic == "subscribe.complete.report"
if failure == "notify":
assert [call[0] for call in calls[5:]] == ["notify"]
elif failure == "event":
assert [call[0] for call in calls[5:]] == ["notify", "claim", "event"]
def test_completion_report_failure_returns_success_and_keeps_intent_pending():
"""统计未确认不得误报完成失败,且 report intent 必须留待重试。"""
calls = []
command, notify, report = _command(calls, report_result=False)
command.execute(
7,
{"id": 7, "media_source": "tmdb", "media_id": "123", "season": 2},
{"title": "Test"},
notify=notify,
report=report,
)
assert [call[0] for call in calls] == [
"history", "delete", "stage", "stage", "commit",
"notify", "claim", "event", "complete", "claim", "report",
]
def test_completion_report_error_returns_success_and_keeps_intent_pending():
"""统计上报抛出异常也不得覆盖已经成功提交的完成结果。"""
calls = []
command, notify, report = _command(
calls,
report_error=RuntimeError("remote failed"),
)
command.execute(
7,
{"id": 7, "media_source": "tmdb", "media_id": "123", "season": 2},
{"title": "Test"},
notify=notify,
report=report,
)
assert [call[0] for call in calls] == [
"history", "delete", "stage", "stage", "commit",
"notify", "claim", "event", "complete", "claim", "report",
]
def test_completion_success_closes_event_then_report_intent():
"""成功完成按兼容顺序通知、事件、统计,并分别收口两个 intent。"""
calls = []
command, notify, report = _command(calls)
command.execute(
7,
{"id": 7, "media_source": "tmdb", "media_id": "123", "season": 2},
{"title": "Test"},
notify=notify,
report=report,
)
assert [call[0] for call in calls] == [
"history", "delete", "stage", "stage", "commit",
"notify", "claim", "event", "complete",
"claim", "report", "complete",
]
assert calls[7][1]["idempotency_key"] == calls[2][1].event_key
assert calls[10][1]["idempotency_key"] == calls[3][1].event_key
def test_completion_stages_and_closes_notification_snapshot() -> None:
"""完成通知快照与业务事务同提交,成功即时投递后独立收口。"""
calls = []
command, notify, report = _command(calls)
command.execute(
7,
{"id": 7, "media_source": "tmdb", "media_id": "123", "season": 2},
{"title": "Test"},
notify=notify,
report=report,
notification={"title": "完成", "text": "Test"},
)
staged = [call[1] for call in calls if call[0] == "stage"]
assert [intent.topic for intent in staged] == [
"subscribe.complete",
"subscribe.complete.notification",
"subscribe.complete.report",
]
assert staged[1].payload["message"]["title"] == "完成"
completed = [call[1] for call in calls if call[0] == "complete"]
assert completed[0].endswith(":notification")
def test_completion_skips_sync_delivery_owned_by_outbox_dispatcher() -> None:
"""后台已认领 intent 时同步路径不得再次发送相同副作用。"""
calls = []
command, notify, report = _command(calls, claim_result=False)
command.execute(
7,
{"id": 7, "media_source": "tmdb", "media_id": "123", "season": 2},
{"title": "Test"},
notify=notify,
report=report,
notification={"title": "完成", "text": "Test"},
)
assert [call[0] for call in calls] == [
"history", "delete", "stage", "stage", "stage", "commit",
"claim", "claim", "claim",
]