"""Статистика профиля: цифры сходятся с лидербордом, а главная не гоняет CTE лишний раз.""" from __future__ import annotations from fastapi.testclient import TestClient from sqlalchemy import event from tests.conftest import add_group_member, create_finished_match, csrf_headers, login def _group_with_matches(client: TestClient, engine, games: int = 3) -> tuple[dict, int, int]: me = login(client, "Статист") exps = [e["id"] for e in client.get("/api/expansions").json()] gid = client.post( "/api/groups", json={"name": "Группа", "expansion_ids": exps}, headers=csrf_headers(client) ).json()["id"] p2 = add_group_member(engine, gid, "Соперник") fids = [f["id"] for f in client.get(f"/api/groups/{gid}/factions").json()] for i in range(games): # Чередуем победителя и берём разные фракции: средние и разбивка по фракциям # должны получиться нетривиальными, а фракции в партии не повторяться. winner_first = i % 2 == 0 mine, theirs = fids[(2 * i) % len(fids)], fids[(2 * i + 1) % len(fids)] create_finished_match( client, gid, [ {"user_id": me["id"], "faction_id": mine, "place": 1 if winner_first else 2}, {"user_id": p2, "faction_id": theirs, "place": 2 if winner_first else 1}, ], ) return me, gid, p2 def test_profile_numbers_match_leaderboard(client: TestClient, engine): """Профиль считает в Python, лидерборд — в SQL: цифры обязаны совпадать. Формула сглаженного рейтинга живёт в двух видах (SMOOTHED_SCORE_SQL и scoring.smoothed_score); этот тест ловит их расхождение.""" me, gid, p2 = _group_with_matches(client, engine, games=4) profile = client.get("/api/users/me/stats").json()["overall"] board = client.get("/api/stats/leaderboard").json() entry = next( e for e in board["entries"] + board["provisional"] if e["user_id"] == me["id"] ) for field in ("games", "wins", "win_rate", "avg_place", "score"): assert profile[field] == entry[field], field def test_home_does_not_repeat_scored_cte(client: TestClient, engine): """Главная делает не больше двух проходов по SCORED_CTE. Было пять: лидерборд, три запроса профиля и итог по активной группе. Без этой проверки оптимизация тихо отъедет назад при следующей правке витрин.""" me, gid, p2 = _group_with_matches(client, engine, games=2) client.put( "/api/users/me/active-group", json={"group_id": gid}, headers=csrf_headers(client) ) seen: list[str] = [] def before_execute(conn, cursor, statement, params, context, executemany): if "WITH tie AS" in statement: seen.append(statement) event.listen(engine, "before_cursor_execute", before_execute) try: r = client.get("/api/home") assert r.status_code == 200, r.text finally: event.remove(engine, "before_cursor_execute", before_execute) # Сейчас ровно два: лидерборд и один проход по строкам игрока. Нижняя граница не # для красоты — без неё тест пройдёт и когда счётчик молча перестанет что-либо # ловить (сменился путь, переименован CTE). assert 1 <= len(seen) <= 2, f"ожидали 1–2 прохода, получили {len(seen)}" # Главная всё ещё показывает и профиль, и блок активной группы. body = r.json() assert body["profile"]["overall"]["games"] == 2 assert body["active_group"]["games"] == 2