Files
thisORthat/app/tests/test_tg_auth.py
2026-08-31 14:11:09 +03:00

152 lines
4.6 KiB
Python

"""
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
import os
BOT_TOKEN = os.getenv("TG_BOT_TOKEN", "test:mock_token_for_testing_12345")
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