Партии: оптимистичная блокировка (version) — устаревшие правки/отмена отклоняются (фикс гонки завершить-vs-отменить)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
"""Оптимистичная блокировка партии: устаревшие правки/удаление отклоняются (STALE_WRITE)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from tests.conftest import add_group_member, csrf_headers, finish_match, login, start_match
|
||||
|
||||
|
||||
def _start(client: TestClient, engine) -> tuple[dict, int, int]:
|
||||
me = login(client, "Хост")
|
||||
exps = [e["id"] for e in client.get("/api/expansions").json()]
|
||||
gid = client.post(
|
||||
"/api/groups", json={"name": "Группа", "expansion_ids": exps}, headers=csrf_headers(client)
|
||||
).json()["id"]
|
||||
p2 = add_group_member(engine, gid, "Игрок2")
|
||||
fids = [f["id"] for f in client.get(f"/api/groups/{gid}/factions").json()]
|
||||
started = start_match(
|
||||
client, gid,
|
||||
[{"user_id": me["id"], "faction_id": fids[0]}, {"user_id": p2, "faction_id": fids[1]}],
|
||||
)
|
||||
assert started.status_code == 200, started.text
|
||||
return me, p2, started.json()["id"]
|
||||
|
||||
|
||||
def test_stale_delete_rejected(client: TestClient, engine):
|
||||
"""Сценарий бага: ПК завершил, телефон со старой версией жмёт «Отменить»."""
|
||||
me, p2, mid = _start(client, engine)
|
||||
v1 = client.get(f"/api/matches/{mid}").json()["version"]
|
||||
|
||||
# «ПК» завершает партию — версия меняется.
|
||||
fin = finish_match(
|
||||
client, mid, [{"user_id": me["id"], "place": 1}, {"user_id": p2, "place": 2}],
|
||||
win_reason="objectives",
|
||||
)
|
||||
assert fin.status_code == 200, fin.text
|
||||
|
||||
# «Телефон» со старой версией пытается отменить → 409 STALE_WRITE, партия НЕ удаляется.
|
||||
stale = client.delete(
|
||||
f"/api/matches/{mid}", params={"expected_version": v1}, headers=csrf_headers(client)
|
||||
)
|
||||
assert stale.status_code == 409, stale.text
|
||||
assert stale.json()["error"]["code"] == "STALE_WRITE"
|
||||
assert client.get(f"/api/matches/{mid}").status_code == 200 # жива
|
||||
|
||||
# С актуальной версией удаление проходит.
|
||||
v2 = client.get(f"/api/matches/{mid}").json()["version"]
|
||||
ok = client.delete(
|
||||
f"/api/matches/{mid}", params={"expected_version": v2}, headers=csrf_headers(client)
|
||||
)
|
||||
assert ok.status_code == 200, ok.text
|
||||
assert client.get(f"/api/matches/{mid}").status_code == 404
|
||||
|
||||
|
||||
def test_stale_finish_rejected(client: TestClient, engine):
|
||||
me, p2, mid = _start(client, engine)
|
||||
v1 = client.get(f"/api/matches/{mid}").json()["version"]
|
||||
|
||||
# Партию изменили (правка комментария) — версия устарела.
|
||||
bump = client.patch(
|
||||
f"/api/matches/{mid}", json={"overall_comment": "правка"}, headers=csrf_headers(client)
|
||||
)
|
||||
assert bump.status_code == 200, bump.text
|
||||
|
||||
# Завершение со старой версией → 409 STALE_WRITE.
|
||||
r = client.post(
|
||||
f"/api/matches/{mid}/finish",
|
||||
json={
|
||||
"participants": [
|
||||
{"user_id": me["id"], "place": 1},
|
||||
{"user_id": p2, "place": 2},
|
||||
],
|
||||
"win_reason": "objectives",
|
||||
"expected_version": v1,
|
||||
},
|
||||
headers=csrf_headers(client),
|
||||
)
|
||||
assert r.status_code == 409 and r.json()["error"]["code"] == "STALE_WRITE", r.text
|
||||
Reference in New Issue
Block a user