v0.1 - макет интерфейса, аутентификация через логин, аккаунт админа, создание партии в 2 этапа, базовые настройки профиля и группы, переключение между группами, статистика

This commit is contained in:
2026-06-16 17:41:06 +03:00
parent 6ab74f01aa
commit 56b5d09a4d
123 changed files with 13572 additions and 21 deletions
+363
View File
@@ -0,0 +1,363 @@
"""Сквозной тест ядра: вход → группа → участники → партия → статистика → админ."""
from __future__ import annotations
from fastapi.testclient import TestClient
from tests.conftest import csrf_headers, finish_match, login, start_match
def _expansion_id(client: TestClient, code: str) -> int:
r = client.get("/api/expansions")
assert r.status_code == 200, r.text
return next(e["id"] for e in r.json() if e["code"] == code)
def test_full_flow(client: TestClient, make_admin):
# 1) Вход игрока A — групп ещё нет.
me = login(client, "Иван")
assert me["groups"] == []
assert me["active_group_id"] is None
# 2) Без группы создать партию нельзя (NO_GROUP).
r = client.post(
"/api/matches",
json={"group_id": 1, "played_at": "2026-06-15", "participants": []},
headers=csrf_headers(client),
)
assert r.status_code == 409
assert r.json()["error"]["code"] == "NO_GROUP"
# 3) Создать группу с дополнением Forgotten Worlds.
fw = _expansion_id(client, "forgotten_worlds")
r = client.post(
"/api/groups",
json={"name": "Вечер 40k", "expansion_ids": [fw]},
headers=csrf_headers(client),
)
assert r.status_code == 200, r.text
group = r.json()
gid = group["id"]
assert group["my_role"] == "owner"
assert fw in group["expansion_ids"]
# Активная группа выставилась.
me = client.get("/api/users/me").json()
assert me["active_group_id"] == gid
# 4) Создать игрока B и добавить в группу по нику.
rb = client.post(
"/api/auth/dev/users", json={"nickname": "Олег"}, headers=csrf_headers(client)
)
assert rb.status_code == 200, rb.text
r = client.post(
f"/api/groups/{gid}/members",
json={"nickname": "Олег"},
headers=csrf_headers(client),
)
assert r.status_code == 200, r.text
b_id = r.json()["user_id"]
# 5) Доступные фракции = база (4) + Forgotten Worlds (4) = 8.
factions = client.get(f"/api/groups/{gid}/factions").json()
assert len(factions) == 8
codes = {f["code"] for f in factions}
assert "orks" in codes and "tau" in codes
assert "inquisition" not in codes # Forsaken Voids не подключён
# 6) Рандом фракции — из доступного набора.
r = client.post(
"/api/matches/randomize-faction",
json={"group_id": gid, "exclude_faction_ids": []},
headers=csrf_headers(client),
)
assert r.status_code == 200, r.text
assert r.json()["faction"]["id"] in {f["id"] for f in factions}
# 7) Двухэтапная партия: старт (A=Орки рандом, B=Тау), затем завершение (места).
orks = next(f["id"] for f in factions if f["code"] == "orks")
tau = next(f["id"] for f in factions if f["code"] == "tau")
a_id = me["id"]
started = start_match(
client,
gid,
[
{"user_id": a_id, "faction_id": orks, "was_random": True},
{"user_id": b_id, "faction_id": tau},
],
)
assert started.status_code == 200, started.text
sm = started.json()
assert sm["status"] == "in_progress"
assert all(p["place"] is None for p in sm["participants"])
# Незавершённая партия в статистику пока не идёт.
assert client.get("/api/home").json()["profile"]["overall"]["games"] == 0
fin = finish_match(
client,
sm["id"],
[{"user_id": a_id, "place": 1}, {"user_id": b_id, "place": 2}],
win_reason="objectives",
overall_comment="Хорошая партия",
)
assert fin.status_code == 200, fin.text
match = fin.json()
assert match["status"] == "finished"
assert match["win_reason"] == "objectives"
assert match["duration_minutes"] is not None and match["duration_minutes"] >= 30
assert match["player_count"] == 2
# 8) Список партий группы и статистика.
lst = client.get(f"/api/groups/{gid}/matches").json()
assert lst["total"] == 1
gstats = client.get(f"/api/groups/{gid}/stats").json()
assert gstats["total_matches"] == 1
# Игр меньше MIN_GAMES → попадают в provisional.
prov_ids = {e["user_id"] for e in gstats["provisional"]}
assert a_id in prov_ids and b_id in prov_ids
# 9) Главная: профиль и топ.
home = client.get("/api/home").json()
assert home["profile"]["overall"]["games"] == 1
assert home["profile"]["overall"]["wins"] == 1
assert home["active_group"]["id"] == gid
# 10) Дубликат фракции запрещён уже на старте.
r = start_match(
client,
gid,
[
{"user_id": a_id, "faction_id": orks},
{"user_id": b_id, "faction_id": orks},
],
)
assert r.status_code == 422
assert r.json()["error"]["code"] == "DUPLICATE_PARTICIPANT"
# 11) Админ: вход и доступ; пользовательская сессия в админку не проходит.
make_admin("admin", "secret123")
# Пользователь не может в админку.
assert client.get("/api/admin/users").status_code in (401, 403)
# Вход админа.
ra = client.post(
"/api/admin/auth/login",
json={"username": "admin", "password": "secret123"},
headers=csrf_headers(client),
)
assert ra.status_code == 200, ra.text
users = client.get("/api/admin/users").json()
nicks = {u["nickname"] for u in users}
assert {"Иван", "Олег"}.issubset(nicks)
def test_in_progress_excluded_and_win_reason_required(client: TestClient):
me = login(client, "Хост")
gid = client.post(
"/api/groups", json={"name": "Группа", "expansion_ids": []}, headers=csrf_headers(client)
).json()["id"]
client.post("/api/auth/dev/users", json={"nickname": "Гость2"}, headers=csrf_headers(client))
b = client.post(
f"/api/groups/{gid}/members", json={"nickname": "Гость2"}, headers=csrf_headers(client)
).json()["user_id"]
factions = client.get(f"/api/groups/{gid}/factions").json()
f1, f2 = factions[0]["id"], factions[1]["id"]
started = start_match(client, gid, [
{"user_id": me["id"], "faction_id": f1},
{"user_id": b, "faction_id": f2},
])
assert started.status_code == 200, started.text
mid = started.json()["id"]
# Незавершённая видна в списке группы (можно дозавершить), но не в топе.
lst = client.get(f"/api/groups/{gid}/matches").json()
assert lst["items"][0]["status"] == "in_progress"
assert client.get("/api/stats/leaderboard").json()["entries"] == []
assert client.get("/api/stats/leaderboard").json()["provisional"] == []
# Незавершённая видна на главной (с именем группы) для быстрого перехода.
home = client.get("/api/home").json()
assert any(x["id"] == mid for x in home["in_progress"])
assert home["in_progress"][0]["group_name"] == "Группа"
# Завершение без причины победы → 422 (поле обязательно).
no_reason = client.post(
f"/api/matches/{mid}/finish",
json={"participants": [
{"user_id": me["id"], "place": 1},
{"user_id": b, "place": 2},
]},
headers=csrf_headers(client),
)
assert no_reason.status_code == 422
# С причиной победы → 200 и теперь учитывается в статистике.
ok = finish_match(client, mid, [
{"user_id": me["id"], "place": 1},
{"user_id": b, "place": 2},
], win_reason="worlds")
assert ok.status_code == 200, ok.text
assert client.get("/api/users/me/stats").json()["overall"]["games"] == 1
# После завершения партия пропадает из «незавершённых» на главной.
assert all(x["id"] != mid for x in client.get("/api/home").json()["in_progress"])
def test_owner_can_finish_member_started_match(client: TestClient):
"""Владелец группы может завершить партию, начатую другим участником (can_modify с бэкенда)."""
a = login(client, "Хозяин")
gid = client.post(
"/api/groups", json={"name": "Группа", "expansion_ids": []}, headers=csrf_headers(client)
).json()["id"]
client.post("/api/auth/dev/users", json={"nickname": "Согрупник"}, headers=csrf_headers(client))
b = client.post(
f"/api/groups/{gid}/members", json={"nickname": "Согрупник"}, headers=csrf_headers(client)
).json()["user_id"]
factions = client.get(f"/api/groups/{gid}/factions").json()
f1, f2 = factions[0]["id"], factions[1]["id"]
# Участник (не владелец) входит и стартует партию.
login(client, "Согрупник")
started = start_match(client, gid, [
{"user_id": a["id"], "faction_id": f1},
{"user_id": b, "faction_id": f2},
])
assert started.status_code == 200, started.text
mid = started.json()["id"]
# Создатель видит can_modify=True.
assert client.get(f"/api/matches/{mid}").json()["can_modify"] is True
# Владелец возвращается: тоже может завершить (форма должна показываться).
login(client, "Хозяин")
assert client.get(f"/api/matches/{mid}").json()["can_modify"] is True
fin = finish_match(client, mid, [
{"user_id": a["id"], "place": 1},
{"user_id": b, "place": 2},
], win_reason="objectives")
assert fin.status_code == 200, fin.text
def test_any_member_can_finish_and_manage(client: TestClient):
"""Рядовой участник (не создатель, не владелец) может завершать партии и менять дополнения."""
a = login(client, "Овнер")
gid = client.post(
"/api/groups", json={"name": "Компания", "expansion_ids": []}, headers=csrf_headers(client)
).json()["id"]
for nick in ("Бэ", "Цэ"):
client.post("/api/auth/dev/users", json={"nickname": nick}, headers=csrf_headers(client))
b = client.post(f"/api/groups/{gid}/members", json={"nickname": "Бэ"}, headers=csrf_headers(client)).json()["user_id"]
c = client.post(f"/api/groups/{gid}/members", json={"nickname": "Цэ"}, headers=csrf_headers(client)).json()["user_id"]
fids = [f["id"] for f in client.get(f"/api/groups/{gid}/factions").json()]
# Владелец стартует партию A+B+C.
started = start_match(client, gid, [
{"user_id": a["id"], "faction_id": fids[0]},
{"user_id": b, "faction_id": fids[1]},
{"user_id": c, "faction_id": fids[2]},
])
mid = started.json()["id"]
# Рядовой участник C завершает партию.
login(client, "Цэ")
assert client.get(f"/api/matches/{mid}").json()["can_modify"] is True
fin = finish_match(client, mid, [
{"user_id": a["id"], "place": 1},
{"user_id": b, "place": 2},
{"user_id": c, "place": 3},
], win_reason="plastic")
assert fin.status_code == 200, fin.text
# И может менять дополнения группы.
fw = _expansion_id(client, "forgotten_worlds")
r = client.put(
f"/api/groups/{gid}/expansions", json={"expansion_ids": [fw]}, headers=csrf_headers(client)
)
assert r.status_code == 200, r.text
def test_disabled_account_cannot_login(client: TestClient, make_admin):
# Создаём игрока и узнаём его id.
client.post("/api/auth/dev/users", json={"nickname": "Гость"})
# Вход админа.
make_admin("admin", "secret123")
ra = client.post(
"/api/admin/auth/login", json={"username": "admin", "password": "secret123"}
)
assert ra.status_code == 200, ra.text
users = client.get("/api/admin/users").json()
guest = next(u for u in users if u["nickname"] == "Гость")
# Админ отключает аккаунт.
rp = client.patch(
f"/api/admin/users/{guest['id']}",
json={"is_active": False},
headers=csrf_headers(client),
)
assert rp.status_code == 200, rp.text
# Вход под отключённым — запрещён с понятным кодом.
rl = client.post("/api/auth/dev/login", json={"nickname": "Гость"})
assert rl.status_code == 403
assert rl.json()["error"]["code"] == "ACCOUNT_DISABLED"
# В dev-списке он помечен неактивным (для подсветки серым).
dev_users = client.get("/api/auth/dev/users").json()
guest_dev = next(u for u in dev_users if u["nickname"] == "Гость")
assert guest_dev["is_active"] is False
def test_tie_ranking_and_points(client: TestClient):
"""Ничьи: места 1,2,2 валидны; очки делятся; некорректная расстановка отклоняется."""
me = login(client, "A")
r = client.post("/api/groups", json={"name": "Группа", "expansion_ids": []},
headers=csrf_headers(client))
gid = r.json()["id"]
client.post("/api/auth/dev/users", json={"nickname": "B"}, headers=csrf_headers(client))
client.post("/api/auth/dev/users", json={"nickname": "C"}, headers=csrf_headers(client))
b = client.post(f"/api/groups/{gid}/members", json={"nickname": "B"},
headers=csrf_headers(client)).json()["user_id"]
c = client.post(f"/api/groups/{gid}/members", json={"nickname": "C"},
headers=csrf_headers(client)).json()["user_id"]
factions = client.get(f"/api/groups/{gid}/factions").json()
f1, f2, f3 = (factions[0]["id"], factions[1]["id"], factions[2]["id"])
a = me["id"]
# Старт партии (мест ещё нет) — ранжирование проверяется на завершении.
started = start_match(
client,
gid,
[
{"user_id": a, "faction_id": f1},
{"user_id": b, "faction_id": f2},
{"user_id": c, "faction_id": f3},
],
)
assert started.status_code == 200, started.text
mid = started.json()["id"]
# Некорректная расстановка 1,1,2 → отклоняется на финише.
bad = finish_match(
client,
mid,
[
{"user_id": a, "place": 1},
{"user_id": b, "place": 1},
{"user_id": c, "place": 2},
],
)
assert bad.status_code == 422
assert bad.json()["error"]["code"] == "INVALID_RANKING"
# Корректная ничья за 2-е место: 1,2,2 → партия завершается.
ok = finish_match(
client,
mid,
[
{"user_id": a, "place": 1},
{"user_id": b, "place": 2},
{"user_id": c, "place": 2},
],
)
assert ok.status_code == 200, ok.text
# Повторное завершение запрещено.
again = finish_match(client, mid, [{"user_id": a, "place": 1}, {"user_id": b, "place": 2}, {"user_id": c, "place": 2}])
assert again.status_code == 409