Add Telegram Auth and Dev mode
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
"""
|
||||
Tests for Telegram initData HMAC-SHA256 validation (tg_auth module).
|
||||
|
||||
These tests directly exercise the ``validate_init_data`` function with
|
||||
synthetic initData, covering happy-path and all failure modes.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import time
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
|
||||
from tg_auth import validate_init_data
|
||||
|
||||
BOT_TOKEN = "7765587867:AAHYpUR_XHEZ1YjCKCAgjWaOiepeDY4XtPA"
|
||||
|
||||
|
||||
def _build_init_data(
|
||||
bot_token: str,
|
||||
user: dict,
|
||||
auth_date: int | None = None,
|
||||
tamper_hash: bool = False,
|
||||
omit_hash: bool = False,
|
||||
omit_user: bool = False,
|
||||
) -> str:
|
||||
"""Helper that constructs a valid (or intentionally broken) initData string."""
|
||||
if auth_date is None:
|
||||
auth_date = int(time.time())
|
||||
|
||||
params: dict[str, str] = {
|
||||
"auth_date": str(auth_date),
|
||||
}
|
||||
if not omit_user:
|
||||
params["user"] = json.dumps(user)
|
||||
|
||||
# Build data-check-string (sorted, \n-separated).
|
||||
data_check_string = "\n".join(f"{k}={v}" for k, v in sorted(params.items()))
|
||||
|
||||
# secret_key = HMAC-SHA256("WebAppData", bot_token)
|
||||
secret_key = hmac.new(
|
||||
key=b"WebAppData",
|
||||
msg=bot_token.encode(),
|
||||
digestmod=hashlib.sha256,
|
||||
).digest()
|
||||
|
||||
computed_hash = hmac.new(
|
||||
key=secret_key,
|
||||
msg=data_check_string.encode(),
|
||||
digestmod=hashlib.sha256,
|
||||
).hexdigest()
|
||||
|
||||
if tamper_hash:
|
||||
computed_hash = "a" * 64 # obviously wrong
|
||||
|
||||
if not omit_hash:
|
||||
params["hash"] = computed_hash
|
||||
|
||||
return urlencode(params)
|
||||
|
||||
|
||||
VALID_USER = {
|
||||
"id": 123456789,
|
||||
"first_name": "Igor",
|
||||
"last_name": "Volochay",
|
||||
"username": "IgorVolochay",
|
||||
"photo_url": "https://t.me/photo.jpg",
|
||||
}
|
||||
|
||||
|
||||
# ── Happy path ──────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_valid_init_data():
|
||||
raw = _build_init_data(BOT_TOKEN, VALID_USER)
|
||||
result = validate_init_data(raw, BOT_TOKEN)
|
||||
assert result["user_id"] == 123456789
|
||||
assert result["username"] == "IgorVolochay"
|
||||
assert result["first_name"] == "Igor"
|
||||
assert result["last_name"] == "Volochay"
|
||||
assert result["photo_url"] == "https://t.me/photo.jpg"
|
||||
|
||||
|
||||
# ── Failure modes ───────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_empty_init_data():
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
validate_init_data("", BOT_TOKEN)
|
||||
assert exc.value.status_code == 403
|
||||
|
||||
|
||||
def test_missing_hash():
|
||||
raw = _build_init_data(BOT_TOKEN, VALID_USER, omit_hash=True)
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
validate_init_data(raw, BOT_TOKEN)
|
||||
assert exc.value.status_code == 403
|
||||
assert "hash" in str(exc.value.detail).lower()
|
||||
|
||||
|
||||
def test_tampered_hash():
|
||||
raw = _build_init_data(BOT_TOKEN, VALID_USER, tamper_hash=True)
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
validate_init_data(raw, BOT_TOKEN)
|
||||
assert exc.value.status_code == 403
|
||||
assert "signature" in str(exc.value.detail).lower()
|
||||
|
||||
|
||||
def test_expired_auth_date():
|
||||
old_date = int(time.time()) - 7200 # 2 hours ago
|
||||
raw = _build_init_data(BOT_TOKEN, VALID_USER, auth_date=old_date)
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
validate_init_data(raw, BOT_TOKEN, max_age=3600)
|
||||
assert exc.value.status_code == 403
|
||||
assert "expired" in str(exc.value.detail).lower()
|
||||
|
||||
|
||||
def test_missing_user():
|
||||
raw = _build_init_data(BOT_TOKEN, VALID_USER, omit_user=True)
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
validate_init_data(raw, BOT_TOKEN)
|
||||
assert exc.value.status_code == 403
|
||||
assert "user" in str(exc.value.detail).lower()
|
||||
|
||||
|
||||
def test_missing_user_id():
|
||||
user_no_id = {"first_name": "Igor", "username": "test"}
|
||||
raw = _build_init_data(BOT_TOKEN, user_no_id)
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
validate_init_data(raw, BOT_TOKEN)
|
||||
assert exc.value.status_code == 403
|
||||
assert "user.id" in str(exc.value.detail).lower()
|
||||
|
||||
|
||||
def test_wrong_bot_token():
|
||||
raw = _build_init_data(BOT_TOKEN, VALID_USER)
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
validate_init_data(raw, "wrong:token")
|
||||
assert exc.value.status_code == 403
|
||||
|
||||
|
||||
def test_fresh_auth_date_passes():
|
||||
"""auth_date exactly 5 seconds ago should be fine with default max_age."""
|
||||
recent = int(time.time()) - 5
|
||||
raw = _build_init_data(BOT_TOKEN, VALID_USER, auth_date=recent)
|
||||
result = validate_init_data(raw, BOT_TOKEN)
|
||||
assert result["user_id"] == 123456789
|
||||
Reference in New Issue
Block a user