mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-29 03:56:43 +08:00
186 lines
5.7 KiB
Python
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",
|
|
]
|