seed_reference_data идёт при каждом старте (entrypoint.sh, lifespan) и перезаписывал name_ru существующих фракций значением из кода — переименование из админки жило до ближайшего рестарта. Теперь имя из кода получает только новая фракция; дополнение и порядок синхронизируются как раньше. #72 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LqSoRj99iwVEH5U5fnZgsd
313 lines
14 KiB
Python
313 lines
14 KiB
Python
"""Тесты: лимиты (группа/партия), правка партии админом, переименование фракции, фильтры."""
|
|
from __future__ import annotations
|
|
|
|
from fastapi.testclient import TestClient
|
|
from sqlalchemy.pool import StaticPool
|
|
from sqlmodel import Session, SQLModel, create_engine, select
|
|
|
|
from app.models import Faction
|
|
from app.seed.reference_data import FACTIONS, seed_reference_data
|
|
from tests.conftest import add_group_member, create_finished_match, csrf_headers, login, start_match
|
|
|
|
|
|
def _exp_id(client: TestClient, code: str) -> int:
|
|
return next(e["id"] for e in client.get("/api/expansions").json() if e["code"] == code)
|
|
|
|
|
|
def _admin_login(client: TestClient, make_admin) -> None:
|
|
make_admin("admin", "secret123")
|
|
r = client.post(
|
|
"/api/admin/auth/login",
|
|
json={"username": "admin", "password": "secret123"},
|
|
headers=csrf_headers(client),
|
|
)
|
|
assert r.status_code == 200, r.text
|
|
|
|
|
|
def test_group_invite_capacity_limit(client: TestClient):
|
|
login(client, "Owner")
|
|
gid = client.post(
|
|
"/api/groups", json={"name": "Большая", "expansion_ids": []}, headers=csrf_headers(client)
|
|
).json()["id"]
|
|
for i in range(1, 11):
|
|
client.post("/api/auth/dev/users", json={"nickname": f"U{i}"}, headers=csrf_headers(client))
|
|
statuses = []
|
|
for i in range(1, 11):
|
|
r = client.post(
|
|
f"/api/groups/{gid}/invitations", json={"nickname": f"U{i}"}, headers=csrf_headers(client)
|
|
)
|
|
statuses.append(r.status_code)
|
|
# owner(1) + 9 приглашений = 10 (лимит мест); 10-е приглашение сверх лимита → 409.
|
|
assert statuses[:9] == [200] * 9
|
|
assert statuses[9] == 409
|
|
|
|
|
|
def test_match_player_limit(client: TestClient, engine):
|
|
me = login(client, "Owner")
|
|
fw, fv = _exp_id(client, "forgotten_worlds"), _exp_id(client, "forsaken_voids")
|
|
gid = client.post(
|
|
"/api/groups", json={"name": "Группа", "expansion_ids": [fw, fv]}, headers=csrf_headers(client)
|
|
).json()["id"]
|
|
ids = [me["id"]]
|
|
for i in range(1, 7):
|
|
ids.append(add_group_member(engine, gid, f"P{i}"))
|
|
factions = client.get(f"/api/groups/{gid}/factions").json()
|
|
fids = [f["id"] for f in factions]
|
|
# 7 игроков на старте → 422 (максимум 6).
|
|
roster = [{"user_id": uid, "faction_id": fids[i]} for i, uid in enumerate(ids)]
|
|
r = start_match(client, gid, roster)
|
|
assert r.status_code == 422
|
|
assert "6" in r.json()["error"]["message"]
|
|
|
|
|
|
def test_admin_edit_match_and_filters(client: TestClient, make_admin, engine):
|
|
me = login(client, "Аня")
|
|
gid = client.post(
|
|
"/api/groups", json={"name": "Группа", "expansion_ids": []}, headers=csrf_headers(client)
|
|
).json()["id"]
|
|
b = add_group_member(engine, gid, "Боря")
|
|
factions = {f["code"]: f["id"] for f in client.get(f"/api/groups/{gid}/factions").json()}
|
|
mid = create_finished_match(
|
|
client,
|
|
gid,
|
|
[
|
|
{"user_id": me["id"], "faction_id": factions["orks"], "place": 1},
|
|
{"user_id": b, "faction_id": factions["eldar"], "place": 2},
|
|
],
|
|
)["id"]
|
|
|
|
_admin_login(client, make_admin)
|
|
|
|
# Фильтры: по игроку и по фракции находят партию.
|
|
assert any(m["id"] == mid for m in client.get(f"/api/admin/matches?user_id={b}").json())
|
|
assert any(
|
|
m["id"] == mid for m in client.get(f"/api/admin/matches?faction_id={factions['orks']}").json()
|
|
)
|
|
|
|
# Правка: меняем фракцию Бори eldar → chaos.
|
|
r = client.patch(
|
|
f"/api/admin/matches/{mid}",
|
|
json={
|
|
"participants": [
|
|
{"user_id": me["id"], "faction_id": factions["orks"], "place": 1},
|
|
{"user_id": b, "faction_id": factions["chaos"], "place": 2},
|
|
]
|
|
},
|
|
headers=csrf_headers(client),
|
|
)
|
|
assert r.status_code == 200, r.text
|
|
detail = client.get(f"/api/admin/matches/{mid}").json()
|
|
bp = next(p for p in detail["participants"] if p["user_id"] == b)
|
|
assert bp["faction_name"] == "Хаоситы"
|
|
|
|
|
|
def test_admin_rename_account(client: TestClient, make_admin):
|
|
"""Админ переименовывает аккаунт: валидируется формат и уникальность ника."""
|
|
a = login(client, "Старое")
|
|
client.post("/api/auth/dev/users", json={"nickname": "Занятое"}, headers=csrf_headers(client))
|
|
_admin_login(client, make_admin)
|
|
|
|
r = client.patch(
|
|
f"/api/admin/users/{a['id']}", json={"nickname": "Новое"}, headers=csrf_headers(client)
|
|
)
|
|
assert r.status_code == 200, r.text
|
|
assert r.json()["nickname"] == "Новое"
|
|
|
|
# Занятый ник → 409, некорректный формат → 422.
|
|
r = client.patch(
|
|
f"/api/admin/users/{a['id']}", json={"nickname": "Занятое"}, headers=csrf_headers(client)
|
|
)
|
|
assert r.status_code == 409 and r.json()["error"]["code"] == "NICKNAME_TAKEN", r.text
|
|
r = client.patch(
|
|
f"/api/admin/users/{a['id']}", json={"nickname": "x"}, headers=csrf_headers(client)
|
|
)
|
|
assert r.status_code == 422, r.text
|
|
|
|
|
|
def test_dev_delete_account_keeps_matches(client: TestClient, make_admin, engine):
|
|
"""DEV-удаление: аккаунт удаляется, партии живут (игрок вычёркивается),
|
|
его созданные партии и группы переназначаются на админа."""
|
|
me = login(client, "Аня") # владелец группы и создатель партии
|
|
gid = client.post(
|
|
"/api/groups", json={"name": "Группа", "expansion_ids": []}, headers=csrf_headers(client)
|
|
).json()["id"]
|
|
b = add_group_member(engine, gid, "Боря")
|
|
factions = {f["code"]: f["id"] for f in client.get(f"/api/groups/{gid}/factions").json()}
|
|
mid = create_finished_match(
|
|
client,
|
|
gid,
|
|
[
|
|
{"user_id": me["id"], "faction_id": factions["orks"], "place": 1},
|
|
{"user_id": b, "faction_id": factions["eldar"], "place": 2},
|
|
],
|
|
)["id"]
|
|
|
|
admin_id = make_admin("admin", "secret123")
|
|
r = client.post(
|
|
"/api/admin/auth/login",
|
|
json={"username": "admin", "password": "secret123"},
|
|
headers=csrf_headers(client),
|
|
)
|
|
assert r.status_code == 200, r.text
|
|
|
|
# Удаляем участника Борю → партия жива, его строка участия вычеркнута.
|
|
r = client.delete(f"/api/admin/dev/users/{b}", headers=csrf_headers(client))
|
|
assert r.status_code == 200, r.text
|
|
assert all(u["id"] != b for u in client.get("/api/admin/users").json())
|
|
detail = client.get(f"/api/admin/matches/{mid}").json()
|
|
assert [p["user_id"] for p in detail["participants"]] == [me["id"]] # Боря вычеркнут
|
|
assert detail["player_count"] == 2 # player_count НЕ пересчитывается (осознанно)
|
|
|
|
# Удаляем создателя/владельца Аню → партия и группа сохраняются, переходят к админу.
|
|
r = client.delete(f"/api/admin/dev/users/{me['id']}", headers=csrf_headers(client))
|
|
assert r.status_code == 200, r.text
|
|
assert client.get(f"/api/admin/matches/{mid}").json()["created_by"] == admin_id
|
|
grp = next(g for g in client.get("/api/admin/groups").json() if g["id"] == gid)
|
|
assert grp["owner_id"] == admin_id
|
|
|
|
|
|
def test_dev_delete_account_rejects_admin(client: TestClient, make_admin):
|
|
"""Админа удалить нельзя."""
|
|
admin_id = make_admin("admin", "secret123")
|
|
r = client.post(
|
|
"/api/admin/auth/login",
|
|
json={"username": "admin", "password": "secret123"},
|
|
headers=csrf_headers(client),
|
|
)
|
|
assert r.status_code == 200, r.text
|
|
r = client.delete(f"/api/admin/dev/users/{admin_id}", headers=csrf_headers(client))
|
|
assert r.status_code == 422, r.text
|
|
|
|
|
|
def test_admin_rename_faction_system_wide(client: TestClient, make_admin, engine):
|
|
me = login(client, "Кто-то")
|
|
gid = client.post(
|
|
"/api/groups", json={"name": "Группа", "expansion_ids": []}, headers=csrf_headers(client)
|
|
).json()["id"]
|
|
b = add_group_member(engine, gid, "Друг")
|
|
factions = {f["code"]: f["id"] for f in client.get(f"/api/groups/{gid}/factions").json()}
|
|
mid = create_finished_match(
|
|
client,
|
|
gid,
|
|
[
|
|
{"user_id": me["id"], "faction_id": factions["orks"], "place": 1},
|
|
{"user_id": b, "faction_id": factions["eldar"], "place": 2},
|
|
],
|
|
)["id"]
|
|
|
|
_admin_login(client, make_admin)
|
|
r = client.patch(
|
|
f"/api/admin/factions/{factions['orks']}",
|
|
json={"name_ru": "Орки WAAAGH"},
|
|
headers=csrf_headers(client),
|
|
)
|
|
assert r.status_code == 200, r.text
|
|
|
|
# Изменение отражается в существующей партии (имя хранится один раз).
|
|
detail = client.get(f"/api/admin/matches/{mid}").json()
|
|
ap = next(p for p in detail["participants"] if p["user_id"] == me["id"])
|
|
assert ap["faction_name"] == "Орки WAAAGH"
|
|
|
|
|
|
def test_faction_rename_survives_restart_seeding(client: TestClient, make_admin, engine):
|
|
"""Сидинг идёт при каждом старте (entrypoint.sh, lifespan) и не должен откатывать
|
|
имя, заданное админом (#72)."""
|
|
login(client, "Кто-то")
|
|
orks = next(f for f in client.get("/api/factions").json() if f["code"] == "orks")
|
|
_admin_login(client, make_admin)
|
|
r = client.patch(
|
|
f"/api/admin/factions/{orks['id']}",
|
|
json={"name_ru": "Орки WAAAGH"},
|
|
headers=csrf_headers(client),
|
|
)
|
|
assert r.status_code == 200, r.text
|
|
|
|
with Session(engine) as s:
|
|
seed_reference_data(s) # то же, что делает рестарт
|
|
assert s.get(Faction, orks["id"]).name_ru == "Орки WAAAGH"
|
|
|
|
|
|
def test_seeding_is_complete_and_idempotent():
|
|
"""Пустая БД: фракции из кода создаются со своими именами, повтор ничего не ломает."""
|
|
engine = create_engine(
|
|
"sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool
|
|
)
|
|
SQLModel.metadata.create_all(engine)
|
|
with Session(engine) as s:
|
|
seed_reference_data(s)
|
|
seed_reference_data(s)
|
|
names = {f.code: f.name_ru for f in s.exec(select(Faction)).all()}
|
|
assert names == {code: name for code, name, _exp, _order in FACTIONS}
|
|
|
|
|
|
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 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):
|
|
"""Единственный владелец не может разжаловать сам себя.
|
|
|
|
Регрессия: 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)
|
|
|
|
|
|
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]
|