181 lines
8.0 KiB
Python
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"
|