"""Аутентификация: методы по окружению, dev-вход, Telegram (проверка подписи).""" from __future__ import annotations import hashlib import hmac import time from fastapi.testclient import TestClient from tests.conftest import csrf_headers def test_auth_config_dev_has_all_methods(client: TestClient): cfg = client.get("/api/auth/config").json() assert "password" in cfg["methods"] assert "telegram" in cfg["methods"] assert "stub" in cfg["methods"] # dev → доступен вход по нику def test_enabled_methods_by_env(monkeypatch): from app.auth.registry import enabled_methods from app.core.config import settings monkeypatch.setattr(settings, "app_env", "development") assert set(enabled_methods()) == {"password", "telegram", "stub"} monkeypatch.setattr(settings, "app_env", "test") assert enabled_methods() == ["password", "telegram"] # test (прод-клон) → без stub monkeypatch.setattr(settings, "app_env", "production") assert enabled_methods() == ["password", "telegram"] # prod → без stub def test_env_flags_and_db_path(monkeypatch): """dev → файл дева; test и prod → том /data (общая ветвь is_development).""" from app.core.config import settings monkeypatch.setattr(settings, "dev_database_url", "sqlite:///dev.db") monkeypatch.setattr(settings, "prod_database_url", "sqlite:////data/prod.db") monkeypatch.setattr(settings, "app_env", "development") assert settings.is_development and settings.database_url == "sqlite:///dev.db" monkeypatch.setattr(settings, "app_env", "test") assert settings.is_test and settings.database_url == "sqlite:////data/prod.db" monkeypatch.setattr(settings, "app_env", "production") assert settings.is_production and settings.database_url == "sqlite:////data/prod.db" def test_dev_login_works(client: TestClient): r = client.post("/api/auth/dev/login", json={"nickname": "Тестер"}) assert r.status_code == 200, r.text assert r.json()["nickname"] == "Тестер" def _telegram_payload(token: str, **fields) -> dict: data = {"id": 777, "first_name": "Иван", "username": "ivan_tg", "auth_date": int(time.time())} data.update(fields) secret = hashlib.sha256(token.encode()).digest() check = "\n".join(f"{k}={data[k]}" for k in sorted(data)) sig = hmac.new(secret, check.encode(), hashlib.sha256).hexdigest() return {**data, "hash": sig} def test_dev_login_into_telegram_user_no_duplicate(client: TestClient, engine, monkeypatch): """Dev-вход по нику Telegram-аккаунта входит в него, а не создаёт дубль «ник 2».""" from sqlmodel import Session, select from app.core.config import settings from app.models import User monkeypatch.setattr(settings, "telegram_bot_token", "TEST_BOT_TOKEN") r = client.post("/api/auth/telegram", json=_telegram_payload("TEST_BOT_TOKEN")) assert r.status_code == 200, r.text tg_id, nick = r.json()["id"], r.json()["nickname"] client.cookies.clear() # «другой вход» — без сессии r2 = client.post("/api/auth/dev/login", json={"nickname": nick}) assert r2.status_code == 200, r2.text assert r2.json()["id"] == tg_id # тот же аккаунт, не новый with Session(engine) as s: users = s.exec(select(User).where(User.nickname == nick)).all() assert len(users) == 1 # дубликат не создан def test_dev_login_after_rename_no_duplicate(client: TestClient, engine): """После смены ника dev-вход по новому нику входит в того же игрока (без дубля).""" from sqlmodel import Session, select from app.models import User uid = client.post("/api/auth/dev/login", json={"nickname": "Старый"}).json()["id"] pr = client.patch("/api/users/me", json={"nickname": "Новый"}, headers=csrf_headers(client)) assert pr.status_code == 200, pr.text client.cookies.clear() r2 = client.post("/api/auth/dev/login", json={"nickname": "Новый"}) assert r2.status_code == 200, r2.text assert r2.json()["id"] == uid with Session(engine) as s: assert len(s.exec(select(User).where(User.nickname == "Новый")).all()) == 1 def test_telegram_login_valid_signature(client: TestClient, monkeypatch): from app.core.config import settings monkeypatch.setattr(settings, "telegram_bot_token", "TEST_BOT_TOKEN") payload = _telegram_payload("TEST_BOT_TOKEN") r = client.post("/api/auth/telegram", json=payload) assert r.status_code == 200, r.text me = r.json() assert me["nickname"] # пользователь создан assert me["telegram_id"] == 777 def test_telegram_login_bad_signature_rejected(client: TestClient, monkeypatch): from app.core.config import settings monkeypatch.setattr(settings, "telegram_bot_token", "TEST_BOT_TOKEN") payload = _telegram_payload("TEST_BOT_TOKEN") payload["hash"] = "deadbeef" # подделка r = client.post("/api/auth/telegram", json=payload) assert r.status_code == 401 _ = csrf_headers # (для единообразия импорта) def test_telegram_same_id_no_duplicate_account(client: TestClient, monkeypatch): """Вход с двух устройств / после смены тега — один аккаунт (ключ = числовой id).""" from app.core.config import settings monkeypatch.setattr(settings, "telegram_bot_token", "TEST_BOT_TOKEN") r1 = client.post("/api/auth/telegram", json=_telegram_payload("TEST_BOT_TOKEN", username="firsttag")) assert r1.status_code == 200, r1.text client.cookies.clear() # «другое устройство» — без сессии # Тот же telegram id (777), но другой тег и свежий auth_date. r2 = client.post("/api/auth/telegram", json=_telegram_payload("TEST_BOT_TOKEN", username="changedtag")) assert r2.status_code == 200, r2.text assert r2.json()["id"] == r1.json()["id"] # тот же пользователь assert r2.json()["nickname"] == r1.json()["nickname"] # ник не пересоздаётся def test_telegram_register_nickname_collision_then_pick(client: TestClient, engine, monkeypatch): """Первый вход через ТГ, тег совпал с чужим ником → просим выбрать ник → регистрируем.""" from sqlmodel import Session from app.core.config import settings from app.models import User monkeypatch.setattr(settings, "telegram_bot_token", "TEST_BOT_TOKEN") with Session(engine) as s: # существующий игрок с ником = тегу из payload (ivan_tg) s.add(User(nickname="ivan_tg", role="player", auth_provider="stub")) s.commit() payload = _telegram_payload("TEST_BOT_TOKEN") # id=777, username=ivan_tg r = client.post("/api/auth/telegram", json=payload) assert r.status_code == 409, r.text err = r.json()["error"] assert err["code"] == "TELEGRAM_NICKNAME_REQUIRED" assert err["details"]["suggested"] == "ivan_tg" # Повтор с выбранным свободным ником (та же подпись виджета). r2 = client.post("/api/auth/telegram/register", json={**payload, "nickname": "Иван-новый"}) assert r2.status_code == 200, r2.text assert r2.json()["nickname"] == "Иван-новый" assert r2.json()["telegram_id"] == 777 def test_telegram_register_rejects_taken_nickname(client: TestClient, engine, monkeypatch): """На шаге выбора ника всё ещё занятый ник отклоняется (NICKNAME_TAKEN).""" from sqlmodel import Session from app.core.config import settings from app.models import User monkeypatch.setattr(settings, "telegram_bot_token", "TEST_BOT_TOKEN") with Session(engine) as s: s.add(User(nickname="ivan_tg", role="player", auth_provider="stub")) s.add(User(nickname="занятый", role="player", auth_provider="stub")) s.commit() payload = _telegram_payload("TEST_BOT_TOKEN") r = client.post("/api/auth/telegram/register", json={**payload, "nickname": "занятый"}) assert r.status_code == 409, r.text assert r.json()["error"]["code"] == "NICKNAME_TAKEN"