#!/usr/bin/env python3 """ tests/ui-sso.py — E2E браузерный тест SSO через Playwright. Проверяет полный поток: 1. Редирект на Keycloak 2. Вход с логином/паролем 3. Возврат в приложение 4. CRUD через UI 5. Logout Запуск: pip install playwright && playwright install chromium KC_LOGIN=tazet@narod.ru KC_PASS=*** python tests/ui-sso.py Если Keycloak запрашивает OTP/MFA код — тест запросит его через stdin. """ import os, sys, time from playwright.sync_api import sync_playwright APP_URL = os.environ.get("APP_URL", "https://whitelist.nodejsk8s.services.ngcloud.ru") KC_LOGIN = os.environ.get("KC_LOGIN") KC_PASS = os.environ.get("KC_PASS") if not KC_LOGIN or not KC_PASS: print("❌ Задай KC_LOGIN и KC_PASS") print(" KC_LOGIN=tazet@narod.ru KC_PASS=*** python tests/ui-sso.py") sys.exit(1) passed = 0 failed = 0 def ok(name, condition, details=""): global passed, failed if condition: passed += 1 print(f" ✅ {name}") else: failed += 1 print(f" ❌ {name} — {details}") def run(): with sync_playwright() as p: browser = p.chromium.launch(headless=False) page = browser.new_page() print(f"\n🧪 SSO E2E браузерный тест") print(f" App: {APP_URL}") print(f" User: {KC_LOGIN}\n") # ── 1. Заходим на / → редирект на Keycloak ── page.goto(f"{APP_URL}/") page.wait_for_timeout(2000) ok("1. Редирект на Keycloak", "keycloak.nubes.ru" in page.url) # ── 2. Логин в Keycloak ── if page.locator("input[name=username], #username").count() > 0: page.fill("input[name=username], #username", KC_LOGIN) page.fill("input[name=password], #password", KC_PASS) page.click("button[type=submit], input[type=submit], button[name=login]") page.wait_for_timeout(3000) # Проверяем OTP/MFA if page.locator("input[name=otp], input[name=totp]").count() > 0: otp = input("🔐 Введи код OTP/MFA: ").strip() page.fill("input[name=otp], input[name=totp]", otp) page.click("button[type=submit], input[type=submit]") page.wait_for_timeout(3000) ok("2. Вход в Keycloak", APP_URL in page.url or "ipwhitelist" in page.url.lower() or "WhiteList" in page.title()) else: ok("2. Вход в Keycloak", False, "no login form") # ── 3. Главная страница ── page.wait_for_timeout(2000) ok("3. Главная страница загружена", "Белые списки" in page.content() or "WhiteList" in page.title() or page.url.startswith(APP_URL)) # ── 4. Добавляем запись ── ts = int(time.time()) cidr = f"10.{ts % 200}.{ts % 100}.{ts % 50}" page.fill("input[name=value]", cidr) page.fill("textarea[name=comment], input[name=comment]", f"test-{ts}") page.click("button[type=submit]:has-text('Добавить'), button:has-text('Добавить')") page.wait_for_timeout(2000) ok("4. Запись добавлена", cidr in page.content()) # ── 5. Удаляем запись ── delete_btn = page.locator(f"tr:has-text('{cidr}') button:has-text('Удалить'), tr:has-text('{cidr}') a:has-text('Удалить')").first if delete_btn.count() > 0: delete_btn.click() page.wait_for_timeout(1000) page.click("button:has-text('OK'), button:has-text('Да'), button:has-text('Удалить')") page.wait_for_timeout(2000) ok("5. Запись удалена", cidr not in page.content()) else: ok("5. Запись удалена", False, "delete button not found") # ── 6. Export ── page.goto(f"{APP_URL}/export") page.wait_for_timeout(1000) ok("6. Export доступен", "text/plain" in page.content() or page.url.endswith("/export")) # ── 7. Logout ── page.goto(f"{APP_URL}/logout") page.wait_for_timeout(3000) ok("7. Logout → /login", "/login" in page.url or "keycloak" in page.url.lower()) # ── Итог ── print(f"\n═══ ИТОГ ═══") print(f" ✅ {passed} ❌ {failed} из {passed+failed}") browser.close() return failed == 0 if __name__ == "__main__": ok = run() sys.exit(0 if ok else 1)