Files

181 lines
8.0 KiB
Python

"""Аутентификация: методы по окружению, 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", "production")
assert enabled_methods() == ["password", "telegram"] # prod → без stub
def test_env_flags_and_db_path(monkeypatch):
"""dev → файл дева; prod → том /data."""
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", "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"