Ревью: закрыть дыры в собственном покрытии

Прогон /code-review по тестам показал, что проверка обхода каталога ачивок
ничего не проверяла: httpx нормализует «..» в URL до отправки, запрос уходил
на /api/admin/ и до обработчика не доходил — тест был бы зелёным и без
защиты. Теперь percent-кодированная форма плюс проверка конверта ошибки,
чтобы промах роутинга не выдавал себя за отказ.

Добавлено недостающее: передача владения группой (обратная сторона защиты
последнего владельца), сдвиг версии партии при загрузке вложения, совпадение
кэш-бастера аватара между профилем и лидербордом. Проверка выживания группы
после отказа в удалении теперь смотрит на саму группу и её партии, а не
только на код ответа.

#8

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0186Fk74jkkszahEHSjBzTjD
This commit is contained in:
2026-09-09 18:32:47 +03:00
co-authored by Claude Opus 5
parent c4b72cca39
commit 1831a3e033
4 changed files with 101 additions and 2 deletions
+27 -1
View File
@@ -227,7 +227,9 @@ def test_admin_delete_group_with_matches_is_conflict(client: TestClient, make_ad
_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
# Важен не только код ответа: группа и её партии должны пережить отказ.
assert any(g["id"] == gid for g in client.get("/api/admin/groups").json())
assert any(m["group_id"] == gid for m in client.get("/api/admin/matches").json())
def test_last_owner_cannot_demote_self(client: TestClient, engine):
@@ -249,3 +251,27 @@ def test_last_owner_cannot_demote_self(client: TestClient, engine):
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)
def test_ownership_transfer_still_works(client: TestClient, engine):
"""Обратная сторона защиты последнего владельца: передать роль по-прежнему можно."""
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")
promote = client.patch(
f"/api/groups/{gid}/members/{p2}", json={"role": "owner"}, headers=csrf_headers(client)
)
assert promote.status_code == 200, promote.text
# Владельцев теперь двое — прежний может сложить полномочия.
demote = client.patch(
f"/api/groups/{gid}/members/{me['id']}",
json={"role": "member"},
headers=csrf_headers(client),
)
assert demote.status_code == 200, demote.text
members = client.get(f"/api/groups/{gid}/members").json()
assert [m["user_id"] for m in members if m["role"] == "owner"] == [p2]