Files
MoviePilot/tests/test_subscription_mutation_outbox.py
T

186 lines
5.7 KiB
Python

"""订阅修改 UoW 与 durable outbox 边界测试。"""
import pytest
from app.application.subscription.mutation import (
SubscriptionActor,
SubscriptionMutationService,
)
class _Subscribe:
"""提供稳定前后快照的订阅替身。"""
def __init__(self) -> None:
"""初始化可修改字段与 owner。"""
self.id = 7
self.username = "alice"
self.name = "旧标题"
def to_dict(self) -> dict:
"""返回当前订阅快照。"""
return {"id": self.id, "username": self.username, "name": self.name}
class _Repository:
"""记录订阅读取、兼容更新和事务内暂存顺序。"""
def __init__(self, subscribe: _Subscribe, calls: list) -> None:
"""保存订阅对象与共享调用序列。"""
self.subscribe = subscribe
self.calls = calls
async def async_get(self, subscribe_id: int):
"""返回指定订阅。"""
self.calls.append(("get", subscribe_id))
return self.subscribe
async def async_update(self, subscribe_id: int, payload: dict):
"""模拟旧兼容自动提交路径。"""
self.calls.append(("legacy_update", subscribe_id, payload))
for key, value in payload.items():
setattr(self.subscribe, key, value)
return self.subscribe
async def async_stage_update(self, subscribe_id: int, payload: dict):
"""模拟调用方事务内的更新暂存。"""
self.calls.append(("stage_update", subscribe_id, payload))
for key, value in payload.items():
setattr(self.subscribe, key, value)
return self.subscribe
def get(self, subscribe_id: int):
"""提供协议要求的同步读取。"""
return self.subscribe if subscribe_id == self.subscribe.id else None
class _UnitOfWork:
"""记录订阅修改事务提交和回滚。"""
def __init__(self, calls: list) -> None:
"""保存共享调用序列。"""
self.calls = calls
async def commit(self) -> None:
"""记录提交。"""
self.calls.append(("commit",))
async def rollback(self) -> None:
"""记录回滚。"""
self.calls.append(("rollback",))
class _Outbox:
"""记录修改事件 intent 暂存和完成。"""
def __init__(self, calls: list, stage_error: Exception | None = None) -> None:
"""保存共享调用序列与可选暂存异常。"""
self.calls = calls
self.stage_error = stage_error
async def stage(self, intent, _now) -> None:
"""记录 intent 并按需失败。"""
self.calls.append(("outbox_stage", intent))
if self.stage_error:
raise self.stage_error
async def complete_by_event_key(self, event_key: str, _completed_at) -> None:
"""记录即时事件成功后的完成键。"""
self.calls.append(("outbox_complete", event_key))
def _service(calls: list, *, event_error: Exception | None = None, outbox=None):
"""构造拥有请求级 UoW 和 outbox 的订阅修改服务。"""
subscribe = _Subscribe()
async def publish(payload: dict) -> None:
"""记录公开事件并按需失败。"""
calls.append(("event", payload))
if event_error:
raise event_error
return SubscriptionMutationService(
repository=_Repository(subscribe, calls),
unit_of_work=_UnitOfWork(calls),
outbox=outbox or _Outbox(calls),
publish_modified=publish,
)
@pytest.mark.asyncio
async def test_modified_event_is_staged_with_update_and_completed_after_publish():
"""订阅修改与 intent 同事务提交,事件成功后才标记完成。"""
calls = []
service = _service(calls)
change = await service.update(
7,
{"name": "新标题"},
SubscriptionActor(name="alice", is_superuser=False),
scene="update",
)
assert change is not None
assert change.event_published is True
assert change.old["name"] == "旧标题"
assert change.new["name"] == "新标题"
assert [call[0] for call in calls] == [
"get",
"stage_update",
"outbox_stage",
"commit",
"event",
"outbox_complete",
]
intent = calls[2][1]
assert intent.topic == "subscribe.modified"
assert intent.event_key.startswith("subscribe.modified:7:update:")
assert calls[4][1]["idempotency_key"] == intent.event_key
assert calls[5][1] == intent.event_key
@pytest.mark.asyncio
async def test_modified_outbox_stage_failure_rolls_back_update():
"""修改事件 intent 无法暂存时业务更新不得提交。"""
calls = []
service = _service(
calls,
outbox=_Outbox(calls, stage_error=RuntimeError("outbox failed")),
)
with pytest.raises(RuntimeError, match="outbox failed"):
await service.update(
7,
{"name": "新标题"},
SubscriptionActor(name="alice", is_superuser=False),
)
assert [call[0] for call in calls] == [
"get",
"stage_update",
"outbox_stage",
"rollback",
]
@pytest.mark.asyncio
async def test_modified_event_failure_keeps_committed_intent_pending():
"""提交后的事件失败向调用方传播,且不得错误收口待恢复 intent。"""
calls = []
service = _service(calls, event_error=RuntimeError("event failed"))
with pytest.raises(RuntimeError, match="event failed"):
await service.update(
7,
{"name": "新标题"},
SubscriptionActor(name="alice", is_superuser=False),
)
assert [call[0] for call in calls] == [
"get",
"stage_update",
"outbox_stage",
"commit",
"event",
]