Files
GmRelayBot/tests/e2e/dashboard/test_dashboard_auth_and_sessions.py
T
Toutsu fcc8514847 feat(e2e): #145 Playwright dashboard tests with mock Telegram auth
- Add Playwright-based E2E tests in tests/e2e/dashboard/
- Authenticate via /auth/telegram-webapp using helpers/telegram_init_data.py
- Cover dashboard load and session edit flow
- Add requirements.txt and package dashboard folder
- Update README with setup and test descriptions

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 11:59:36 +03:00

252 lines
8.4 KiB
Python

"""
Playwright E2E tests for the GmRelay Blazor/Web dashboard.
These tests use a mocked Telegram Mini App initData payload so they can run
locally against a real GmRelay.Web instance without talking to Telegram.
Prerequisites:
pip install playwright
playwright install chromium
Environment:
GMRELAY_E2E_BASE_URL - Web dashboard URL (default: http://localhost:8080)
GMRELAY_E2E_BOT_TOKEN - Bot token matching the target Web instance
GMRELAY_E2E_TELEGRAM_ID - Telegram ID to use for the mocked user
"""
from __future__ import annotations
import os
import time
import urllib.parse
from typing import Optional
from playwright.sync_api import Page, expect, sync_playwright
import sys
import pathlib
sys.path.insert(0, str(pathlib.Path(__file__).parent.parent))
from helpers.telegram_init_data import build_mini_app_init_data
def _base_url() -> str:
return os.environ.get("GMRELAY_E2E_BASE_URL", "http://localhost:8080").rstrip("/")
def _bot_token() -> str:
token = os.environ.get("GMRELAY_E2E_BOT_TOKEN")
if not token:
raise RuntimeError(
"GMRELAY_E2E_BOT_TOKEN is required. It must match the bot token used by the target Web instance."
)
return token
def _telegram_id() -> int:
return int(os.environ.get("GMRELAY_E2E_TELEGRAM_ID", "9000000001"))
def _seed_group_in_database(
telegram_id: int,
group_id: str,
group_name: str,
session_id: str,
session_title: str,
) -> None:
"""
Insert minimal test data via the shared Npgsql connection so the dashboard
has something to render. This is intentionally thin: full state setup is
performed by the console runner in issue #151.
"""
import psycopg2
dsn = os.environ.get("GMRELAY_E2E_DATABASE_URL")
if not dsn:
return # rely on a pre-seeded dev database
platform = "Telegram"
external_user_id = str(telegram_id)
now = time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime())
scheduled_at = time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(time.time() + 86400))
with psycopg2.connect(dsn) as conn:
with conn.cursor() as cur:
cur.execute(
"""
INSERT INTO gmrelay.players (platform, external_user_id, display_name, created_at)
VALUES (%s, %s, %s, %s)
ON CONFLICT (platform, external_user_id) DO UPDATE SET display_name = EXCLUDED.display_name
RETURNING id
""",
(platform, external_user_id, "E2E Test GM", now),
)
player_id = cur.fetchone()[0]
cur.execute(
"""
INSERT INTO gmrelay.game_groups (id, platform, external_group_id, telegram_chat_id, name, created_at)
VALUES (%s, %s, %s, %s, %s, %s)
ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name
RETURNING id
""",
(group_id, platform, "-1001234567890", -1001234567890, group_name, now),
)
cur.execute(
"""
INSERT INTO gmrelay.group_managers (group_id, platform, external_user_id, role, created_at)
VALUES (%s, %s, %s, %s, %s)
ON CONFLICT (group_id, platform, external_user_id) DO UPDATE SET role = EXCLUDED.role
""",
(group_id, platform, external_user_id, "Owner", now),
)
cur.execute(
"""
INSERT INTO gmrelay.sessions (
id, group_id, title, scheduled_at, join_link, status, publication_mode,
notification_mode, max_players, created_at
)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
ON CONFLICT (id) DO UPDATE SET title = EXCLUDED.title
""",
(
session_id,
group_id,
session_title,
scheduled_at,
"https://example.com/join",
"Planned",
"None",
"GroupAndDirect",
5,
now,
),
)
cur.execute(
"""
INSERT INTO gmrelay.session_players (session_id, player_id, registration_status, rsvp_status, is_gm, created_at)
VALUES (%s, %s, %s, %s, %s, %s)
ON CONFLICT (session_id, player_id) DO NOTHING
""",
(session_id, player_id, "Active", "Pending", True, now),
)
conn.commit()
def _delete_test_data(group_id: str, session_id: str) -> None:
import psycopg2
dsn = os.environ.get("GMRELAY_E2E_DATABASE_URL")
if not dsn:
return
with psycopg2.connect(dsn) as conn:
with conn.cursor() as cur:
cur.execute("DELETE FROM gmrelay.session_players WHERE session_id = %s", (session_id,))
cur.execute("DELETE FROM gmrelay.sessions WHERE id = %s", (session_id,))
cur.execute("DELETE FROM gmrelay.group_managers WHERE group_id = %s", (group_id,))
cur.execute("DELETE FROM gmrelay.game_groups WHERE id = %s", (group_id,))
conn.commit()
def _authenticate_page(page: Page, base_url: str, bot_token: str, telegram_id: int) -> None:
"""
Authenticate by calling /auth/telegram-webapp with a valid initData payload.
The response sets the auth cookie; subsequent page navigations are authenticated.
"""
init_data = build_mini_app_init_data(
bot_token=bot_token,
telegram_id=telegram_id,
first_name="E2E",
last_name="Test",
username="e2e_test",
)
response = page.request.post(
f"{base_url}/auth/telegram-webapp",
data={"initData": init_data.init_data_raw},
)
expect(response).to_be_ok()
page.goto(base_url)
def _wait_for_blazor(page: Page) -> None:
"""Wait until Blazor has finished the initial render."""
page.wait_for_selector("text=Добро пожаловать", timeout=15000)
def test_dashboard_authenticates_and_shows_groups() -> None:
base_url = _base_url()
bot_token = _bot_token()
telegram_id = _telegram_id()
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
context = browser.new_context()
page = context.new_page()
_authenticate_page(page, base_url, bot_token, telegram_id)
_wait_for_blazor(page)
heading = page.locator("h2")
expect(heading).to_contain_text("Добро пожаловать")
browser.close()
def test_dashboard_session_edit_flow() -> None:
base_url = _base_url()
bot_token = _bot_token()
telegram_id = _telegram_id()
group_id = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa1"
session_id = "bbbbbbbb-bbbb-bbbb-bbbb-aaaaaaaaaaa1"
original_title = "E2E Original Title"
updated_title = "E2E Updated Title"
try:
_seed_group_in_database(
telegram_id=telegram_id,
group_id=group_id,
group_name="E2E Test Group",
session_id=session_id,
session_title=original_title,
)
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
context = browser.new_context()
page = context.new_page()
_authenticate_page(page, base_url, bot_token, telegram_id)
_wait_for_blazor(page)
page.goto(f"{base_url}/group/{group_id}")
page.wait_for_selector(f"text={original_title}", timeout=15000)
page.locator(f"text={original_title}").first.click()
page.wait_for_selector("text=Редактирование сессии", timeout=15000)
title_input = page.locator("input[placeholder*='Dragon\'s Hoard']").first
title_input.fill(updated_title)
save_button = page.locator("button:has-text('Сохранить изменения')").first
save_button.click()
page.wait_for_selector(f"text={updated_title}", timeout=15000)
expect(page.locator(f"text={updated_title}").first).to_be_visible()
browser.close()
finally:
_delete_test_data(group_id, session_id)
if __name__ == "__main__":
test_dashboard_authenticates_and_shows_groups()
print("PASS test_dashboard_authenticates_and_shows_groups")
test_dashboard_session_edit_flow()
print("PASS test_dashboard_session_edit_flow")