168 lines
4.5 KiB
Python
168 lines
4.5 KiB
Python
"""Auth integration tests: Telegram identity sources issue a common JWT.
|
|
|
|
Run against the Docker Compose infrastructure (`docker compose up -d`).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import hmac
|
|
import time
|
|
from urllib.parse import urlencode
|
|
|
|
import httpx
|
|
import pytest
|
|
from sqlalchemy import text
|
|
|
|
pytestmark = pytest.mark.integration
|
|
|
|
_BOT_TOKEN = "it-test-bot-token:it-test-secret"
|
|
_SECRET_KEY = hmac.new(
|
|
_BOT_TOKEN.encode("utf-8"),
|
|
b"WebAppData",
|
|
hashlib.sha256,
|
|
).digest()
|
|
|
|
|
|
def _make_web_payload(telegram_id: int) -> dict[str, object]:
|
|
auth_date = int(time.time())
|
|
data = {"id": telegram_id, "first_name": "Integration", "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) -> str:
|
|
auth_date = int(time.time())
|
|
user_json = f'{{"id":{telegram_id},"first_name":"Integration"}}'
|
|
params = {"user": user_json, "auth_date": str(auth_date)}
|
|
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)
|
|
|
|
|
|
async def test_auth_telegram_bot_creates_user_and_issues_jwt(
|
|
client: httpx.AsyncClient,
|
|
infra: dict[str, str],
|
|
db_session,
|
|
) -> None:
|
|
telegram_id = 123_456_789
|
|
|
|
r = await client.post(
|
|
"/api/v1/auth/telegram/bot",
|
|
json={"telegram_id": telegram_id},
|
|
headers={"Authorization": f"Bearer {infra['token']}"},
|
|
)
|
|
assert r.status_code == 200
|
|
body = r.json()
|
|
assert body["token_type"] == "bearer"
|
|
assert body["telegram_id"] == telegram_id
|
|
assert body["access_token"]
|
|
|
|
# User row created.
|
|
result = await db_session.execute(
|
|
text("SELECT id, telegram_id FROM users WHERE telegram_id = :t"),
|
|
{"t": telegram_id},
|
|
)
|
|
row = result.first()
|
|
assert row is not None
|
|
assert str(row[0]) == body["user_id"]
|
|
|
|
# JWT works on /me.
|
|
me = await client.get(
|
|
"/api/v1/me",
|
|
headers={"Authorization": f"Bearer {body['access_token']}"},
|
|
)
|
|
assert me.status_code == 200
|
|
assert me.json()["telegram_id"] == telegram_id
|
|
|
|
|
|
async def test_auth_telegram_web_verifies_widget_payload(
|
|
client: httpx.AsyncClient,
|
|
infra: dict[str, str],
|
|
) -> None:
|
|
telegram_id = 222_333_444
|
|
|
|
r = await client.post(
|
|
"/api/v1/auth/telegram/web",
|
|
json=_make_web_payload(telegram_id),
|
|
)
|
|
assert r.status_code == 200
|
|
body = r.json()
|
|
assert body["telegram_id"] == telegram_id
|
|
assert body["access_token"]
|
|
|
|
|
|
async def test_auth_telegram_miniapp_verifies_init_data(
|
|
client: httpx.AsyncClient,
|
|
infra: dict[str, str],
|
|
) -> None:
|
|
telegram_id = 333_444_555
|
|
|
|
r = await client.post(
|
|
"/api/v1/auth/telegram/miniapp",
|
|
json={"init_data": _make_init_data(telegram_id)},
|
|
)
|
|
assert r.status_code == 200
|
|
body = r.json()
|
|
assert body["telegram_id"] == telegram_id
|
|
assert body["access_token"]
|
|
|
|
|
|
async def test_auth_telegram_web_rejects_bad_hash(
|
|
client: httpx.AsyncClient,
|
|
infra: dict[str, str],
|
|
) -> None:
|
|
payload = _make_web_payload(444_555_666)
|
|
payload["hash"] = "0" * 64
|
|
|
|
r = await client.post("/api/v1/auth/telegram/web", json=payload)
|
|
assert r.status_code == 401
|
|
|
|
|
|
async def test_protected_endpoint_rejects_missing_token(
|
|
client: httpx.AsyncClient,
|
|
) -> None:
|
|
r = await client.get("/api/v1/me")
|
|
assert r.status_code == 401
|
|
|
|
|
|
async def test_protected_endpoint_rejects_service_token(
|
|
client: httpx.AsyncClient,
|
|
infra: dict[str, str],
|
|
) -> None:
|
|
r = await client.get(
|
|
"/api/v1/me",
|
|
headers={"Authorization": f"Bearer {infra['token']}"},
|
|
)
|
|
assert r.status_code == 401
|
|
|
|
|
|
async def test_auth_me_introspects_jwt(
|
|
client: httpx.AsyncClient,
|
|
infra: dict[str, str],
|
|
) -> None:
|
|
telegram_id = 555_666_777
|
|
r = await client.post(
|
|
"/api/v1/auth/telegram/bot",
|
|
json={"telegram_id": telegram_id},
|
|
headers={"Authorization": f"Bearer {infra['token']}"},
|
|
)
|
|
token = r.json()["access_token"]
|
|
|
|
introspect = await client.get(
|
|
"/api/v1/auth/me",
|
|
headers={"Authorization": f"Bearer {token}"},
|
|
)
|
|
assert introspect.status_code == 200
|
|
body = introspect.json()
|
|
assert body["telegram_id"] == telegram_id
|
|
assert body["type"] == "access"
|