Ревью кода (#8) #34
@@ -111,3 +111,19 @@ def test_update_and_delete(client: TestClient, make_admin, monkeypatch, tmp_path
|
|||||||
f"/api/admin/achievements/{slug}", headers=csrf_headers(client)
|
f"/api/admin/achievements/{slug}", headers=csrf_headers(client)
|
||||||
).status_code == 200
|
).status_code == 200
|
||||||
assert all(a["slug"] != slug for a in client.get("/api/admin/achievements").json())
|
assert all(a["slug"] != slug for a in client.get("/api/admin/achievements").json())
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_rejects_traversal_slug(client: TestClient, make_admin, monkeypatch, tmp_path):
|
||||||
|
"""Slug из URL не должен уводить файловые операции за каталог ачивок.
|
||||||
|
|
||||||
|
Регрессия: `DELETE /api/admin/achievements/%2E%2E` снимал rmtree'ом родительскую
|
||||||
|
папку каталога (в проде это /data — БД, uploads и ачивки разом)."""
|
||||||
|
root = _use_tmp_achievements(monkeypatch, tmp_path / "achievements")
|
||||||
|
root.mkdir(parents=True, exist_ok=True)
|
||||||
|
sibling = tmp_path / "db.sqlite3"
|
||||||
|
sibling.write_bytes(b"data")
|
||||||
|
_admin(client, make_admin)
|
||||||
|
|
||||||
|
r = client.delete("/api/admin/achievements/..", headers=csrf_headers(client))
|
||||||
|
assert r.status_code == 404, r.text
|
||||||
|
assert sibling.exists() and root.is_dir()
|
||||||
|
|||||||
@@ -203,3 +203,49 @@ def test_admin_rename_faction_system_wide(client: TestClient, make_admin, engine
|
|||||||
detail = client.get(f"/api/admin/matches/{mid}").json()
|
detail = client.get(f"/api/admin/matches/{mid}").json()
|
||||||
ap = next(p for p in detail["participants"] if p["user_id"] == me["id"])
|
ap = next(p for p in detail["participants"] if p["user_id"] == me["id"])
|
||||||
assert ap["faction_name"] == "Орки WAAAGH"
|
assert ap["faction_name"] == "Орки WAAAGH"
|
||||||
|
|
||||||
|
|
||||||
|
def test_admin_delete_group_with_matches_is_conflict(client: TestClient, make_admin, engine):
|
||||||
|
"""Группу с партиями удалять нельзя — но ответ должен быть внятным 409.
|
||||||
|
|
||||||
|
Регрессия: matches.group_id — ON DELETE RESTRICT, и голый session.delete ронял
|
||||||
|
IntegrityError наружу пятисоткой без конверта ошибки."""
|
||||||
|
me = login(client, "Owner")
|
||||||
|
gid = client.post(
|
||||||
|
"/api/groups", json={"name": "Группа", "expansion_ids": []}, 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()]
|
||||||
|
create_finished_match(
|
||||||
|
client, gid,
|
||||||
|
[
|
||||||
|
{"user_id": me["id"], "faction_id": fids[0], "place": 1},
|
||||||
|
{"user_id": p2, "faction_id": fids[1], "place": 2},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
_admin_login(client, make_admin)
|
||||||
|
r = client.delete(f"/api/admin/groups/{gid}", headers=csrf_headers(client))
|
||||||
|
assert r.status_code == 409, r.text
|
||||||
|
assert client.get("/api/admin/groups").status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
def test_last_owner_cannot_demote_self(client: TestClient, engine):
|
||||||
|
"""Единственный владелец не может разжаловать сам себя.
|
||||||
|
|
||||||
|
Регрессия: change_role не проверял последнего владельца (в отличие от удаления),
|
||||||
|
и группа оставалась без владельца навсегда — назначить нового было некому."""
|
||||||
|
me = login(client, "Owner")
|
||||||
|
gid = client.post(
|
||||||
|
"/api/groups", json={"name": "Группа", "expansion_ids": []}, headers=csrf_headers(client)
|
||||||
|
).json()["id"]
|
||||||
|
add_group_member(engine, gid, "Игрок2")
|
||||||
|
|
||||||
|
r = client.patch(
|
||||||
|
f"/api/groups/{gid}/members/{me['id']}",
|
||||||
|
json={"role": "member"},
|
||||||
|
headers=csrf_headers(client),
|
||||||
|
)
|
||||||
|
assert r.status_code == 403, r.text
|
||||||
|
members = client.get(f"/api/groups/{gid}/members").json()
|
||||||
|
assert any(m["user_id"] == me["id"] and m["role"] == "owner" for m in members)
|
||||||
|
|||||||
@@ -75,3 +75,39 @@ def test_stale_finish_rejected(client: TestClient, engine):
|
|||||||
headers=csrf_headers(client),
|
headers=csrf_headers(client),
|
||||||
)
|
)
|
||||||
assert r.status_code == 409 and r.json()["error"]["code"] == "STALE_WRITE", r.text
|
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"
|
||||||
|
|||||||
Reference in New Issue
Block a user