Files

375 lines
16 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Сквозной тест ядра: вход → группа → участники → партия → статистика → админ."""
from __future__ import annotations
from fastapi.testclient import TestClient
from tests.conftest import add_group_member, 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, engine):
# 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 в группе (прямое добавление — подготовка данных; UX-путь теперь приглашения).
b_id = add_group_member(engine, gid, "Олег")
# 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, 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, "Гость2")
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, engine):
"""Владелец группы может завершить партию, начатую другим участником (can_modify с бэкенда)."""
a = login(client, "Хозяин")
gid = client.post(
"/api/groups", json={"name": "Группа", "expansion_ids": []}, headers=csrf_headers(client)
).json()["id"]
b = add_group_member(engine, gid, "Согрупник")
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, engine):
"""Рядовой участник (не создатель, не владелец) может завершать партии и менять дополнения."""
a = login(client, "Овнер")
gid = client.post(
"/api/groups", json={"name": "Компания", "expansion_ids": []}, headers=csrf_headers(client)
).json()["id"]
b = add_group_member(engine, gid, "Бэ")
c = add_group_member(engine, gid, "Цэ")
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_csrf_required_for_session_mutations(client: TestClient):
"""После рефактора CSRF на ASGI защита сохраняется: мутация с сессией без X-CSRF-Token → 403."""
login(client, "Аня") # появились cookie сессии и csrf_token
no_header = client.post("/api/groups", json={"name": "Группа", "expansion_ids": []})
assert no_header.status_code == 403
assert no_header.json()["error"]["code"] == "CSRF_FAILED"
# С корректным заголовком — проходит.
ok = client.post(
"/api/groups", json={"name": "Группа", "expansion_ids": []}, headers=csrf_headers(client)
)
assert ok.status_code == 200, ok.text
def test_group_stats_includes_inactive_members(client: TestClient, engine):
"""Участники без завершённых партий попадают в отдельный блок inactive (не в provisional)."""
me = login(client, "Капитан")
gid = client.post(
"/api/groups", json={"name": "Эскадра", "expansion_ids": []}, headers=csrf_headers(client)
).json()["id"]
rookie = add_group_member(engine, gid, "Новобранец")
stats = client.get(f"/api/groups/{gid}/stats").json()
inactive_ids = {e["user_id"] for e in stats["inactive"]}
prov_ids = {e["user_id"] for e in stats["provisional"]}
lb_ids = {e["user_id"] for e in stats["leaderboard"]}
# Никто ещё не играл → и владелец, и новобранец в inactive, и нигде больше.
assert {me["id"], rookie}.issubset(inactive_ids)
assert not ({me["id"], rookie} & (prov_ids | lb_ids))
entry = next(e for e in stats["inactive"] if e["user_id"] == rookie)
assert entry["games"] == 0 and entry["rank"] is None
def test_tie_ranking_and_points(client: TestClient, engine):
"""Ничьи: места 1,2,2 валидны; очки делятся; некорректная расстановка отклоняется."""
me = login(client, "A")
r = client.post("/api/groups", json={"name": "Группа", "expansion_ids": []},
headers=csrf_headers(client))
gid = r.json()["id"]
b = add_group_member(engine, gid, "B")
c = add_group_member(engine, gid, "C")
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