Четыре теста закрывают то, что чинил предыдущий коммит: обход каталога ачивок через slug, неподвижная версия партии при правке участников, удаление группы с партиями (409 вместо 500) и разжалование последнего владельца. #8 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0186Fk74jkkszahEHSjBzTjD
114 lines
4.9 KiB
Python
114 lines
4.9 KiB
Python
"""Оптимистичная блокировка партии: устаревшие правки/удаление отклоняются (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
|
|
|
|
|
|
def test_participant_edit_moves_version(client: TestClient, engine):
|
|
"""Правка одних участников тоже двигает версию партии.
|
|
|
|
Регрессия: updated_at менялся только при UPDATE строки matches, поэтому после
|
|
правки участников версия оставалась прежней и вторая правка со старой версией
|
|
проходила вместо 409 — ровно то, от чего защищает блокировка."""
|
|
me, p2, mid = _start(client, engine)
|
|
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
|
|
v1 = client.get(f"/api/matches/{mid}").json()["version"]
|
|
|
|
parts = {p["user_id"]: p["faction_id"] for p in client.get(f"/api/matches/{mid}").json()["participants"]}
|
|
swap = [
|
|
{"user_id": me["id"], "faction_id": parts[me["id"]], "place": 2},
|
|
{"user_id": p2, "faction_id": parts[p2], "place": 1},
|
|
]
|
|
r = client.patch(
|
|
f"/api/matches/{mid}",
|
|
json={"participants": swap, "expected_version": v1},
|
|
headers=csrf_headers(client),
|
|
)
|
|
assert r.status_code == 200, r.text
|
|
assert client.get(f"/api/matches/{mid}").json()["version"] != v1
|
|
|
|
stale = client.patch(
|
|
f"/api/matches/{mid}",
|
|
json={"participants": swap, "expected_version": v1},
|
|
headers=csrf_headers(client),
|
|
)
|
|
assert stale.status_code == 409, stale.text
|
|
assert stale.json()["error"]["code"] == "STALE_WRITE"
|