DealDocumentScreening/tests/unit/test_auth.py
2026-08-12 21:29:36 +03:00

198 lines
6.3 KiB
Python

"""Unit tests for core.auth: JWT signing/verification and Telegram identity checks.
These tests do not touch the database; they exercise only the crypto helpers.
"""
from __future__ import annotations
import datetime as dt
import hashlib
import hmac
import uuid
from urllib.parse import urlencode
import pytest
from contract_check.core.auth import (
AuthError,
TokenExpiredError,
TokenInvalidError,
create_access_token,
verify_access_token,
verify_bot_identity,
verify_telegram_miniapp_init_data,
verify_telegram_web_payload,
)
from contract_check.core.config import get_settings
_BOT_TOKEN = "123456789:ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghi"
_SECRET_KEY = hmac.new(
_BOT_TOKEN.encode("utf-8"),
b"WebAppData",
hashlib.sha256,
).digest()
def _make_web_payload(telegram_id: int, auth_date: int | None = None) -> dict[str, object]:
if auth_date is None:
auth_date = int(dt.datetime.now(tz=dt.UTC).timestamp())
data = {
"id": telegram_id,
"first_name": "Test",
"auth_date": auth_date,
}
data_check_string = "\n".join(f"{k}={v}" for k, v in sorted(data.items()))
data["hash"] = hmac.new(
_SECRET_KEY,
data_check_string.encode("utf-8"),
hashlib.sha256,
).hexdigest()
return data
def _make_init_data(telegram_id: int, auth_date: int | None = None) -> str:
if auth_date is None:
auth_date = int(dt.datetime.now(tz=dt.UTC).timestamp())
user_json = f'{{"id":{telegram_id},"first_name":"Test"}}'
params = {"user": user_json, "auth_date": str(auth_date), "chat_type": "private"}
data_check_string = "\n".join(f"{k}={v}" for k, v in sorted(params.items()))
params["hash"] = hmac.new(
_SECRET_KEY,
data_check_string.encode("utf-8"),
hashlib.sha256,
).hexdigest()
return urlencode(params)
@pytest.fixture(autouse=True)
def _clear_settings_cache(monkeypatch: pytest.MonkeyPatch) -> None:
get_settings.cache_clear()
monkeypatch.setenv("JWT_SECRET", "unit-test-secret-do-not-use-in-prod")
monkeypatch.setenv("JWT_ALGORITHM", "HS256")
monkeypatch.setenv("JWT_ACCESS_TTL_MINUTES", "1440")
get_settings.cache_clear()
yield
get_settings.cache_clear()
def test_create_and_verify_access_token() -> None:
user_id = uuid.uuid4()
telegram_id = 42
token = create_access_token(user_id, telegram_id)
claims = verify_access_token(token)
assert claims.sub == user_id
assert claims.telegram_id == telegram_id
assert claims.type == "access"
def test_verify_token_rejects_tampered_signature() -> None:
token = create_access_token(uuid.uuid4(), 42)
# Flip a bit in the middle of the payload; this breaks the signature reliably.
tampered = token[:-10] + ("A" if token[-10] != "A" else "B") + token[-9:]
with pytest.raises(TokenInvalidError):
verify_access_token(tampered)
def test_verify_token_rejects_expired_token(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("JWT_ACCESS_TTL_MINUTES", "-1")
get_settings.cache_clear()
token = create_access_token(uuid.uuid4(), 42)
with pytest.raises(TokenExpiredError):
verify_access_token(token)
def test_verify_token_rejects_wrong_audience() -> None:
import jwt
user_id = uuid.uuid4()
token = jwt.encode(
{
"sub": str(user_id),
"telegram_id": 42,
"type": "access",
"exp": int((dt.datetime.now(tz=dt.UTC) + dt.timedelta(hours=1)).timestamp()),
"iat": int(dt.datetime.now(tz=dt.UTC).timestamp()),
"aud": "wrong-audience",
},
key=get_settings().jwt_secret,
algorithm="HS256",
)
with pytest.raises(TokenInvalidError):
verify_access_token(token)
def test_verify_token_rejects_missing_telegram_id() -> None:
import jwt
user_id = uuid.uuid4()
token = jwt.encode(
{
"sub": str(user_id),
"type": "access",
"exp": int((dt.datetime.now(tz=dt.UTC) + dt.timedelta(hours=1)).timestamp()),
"iat": int(dt.datetime.now(tz=dt.UTC).timestamp()),
"aud": "contract-check",
},
key=get_settings().jwt_secret,
algorithm="HS256",
)
with pytest.raises(TokenInvalidError):
verify_access_token(token)
def test_verify_telegram_web_payload_valid() -> None:
payload = _make_web_payload(12345)
identity = verify_telegram_web_payload(payload, _BOT_TOKEN)
assert identity.telegram_id == 12345
def test_verify_telegram_web_payload_bad_hash() -> None:
payload = _make_web_payload(12345)
payload["hash"] = "0" * 64
with pytest.raises(AuthError, match="signature mismatch"):
verify_telegram_web_payload(payload, _BOT_TOKEN)
def test_verify_telegram_web_payload_expired() -> None:
old_auth_date = int(dt.datetime.now(tz=dt.UTC).timestamp()) - 25 * 60 * 60
payload = _make_web_payload(12345, auth_date=old_auth_date)
with pytest.raises(AuthError, match="expired"):
verify_telegram_web_payload(payload, _BOT_TOKEN)
def test_verify_telegram_web_payload_missing_bot_token() -> None:
with pytest.raises(AuthError, match="not configured"):
verify_telegram_web_payload(_make_web_payload(1), "")
def test_verify_telegram_miniapp_init_data_valid() -> None:
init_data = _make_init_data(67890)
identity = verify_telegram_miniapp_init_data(init_data, _BOT_TOKEN)
assert identity.telegram_id == 67890
def test_verify_telegram_miniapp_init_data_bad_hash() -> None:
init_data = _make_init_data(67890)
init_data = init_data[:-64] + "0" * 64
with pytest.raises(AuthError, match="signature mismatch"):
verify_telegram_miniapp_init_data(init_data, _BOT_TOKEN)
def test_verify_telegram_miniapp_init_data_missing_user() -> None:
# Sign an empty data-check-string; hash is valid, but there is no user param.
empty_hash = hmac.new(_SECRET_KEY, b"", hashlib.sha256).hexdigest()
bad = f"hash={empty_hash}"
with pytest.raises(AuthError, match="missing user"):
verify_telegram_miniapp_init_data(bad, _BOT_TOKEN)
def test_verify_bot_identity_valid() -> None:
identity = verify_bot_identity(111222)
assert identity.telegram_id == 111222
def test_verify_bot_identity_invalid() -> None:
with pytest.raises(AuthError):
verify_bot_identity(-1)
with pytest.raises(AuthError):
verify_bot_identity(0)