From 8c1787df19fa3144a77a0b62d0f3e4a0f072b436 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Thu, 11 Jun 2026 10:49:54 +0400 Subject: [PATCH] =?UTF-8?q?bump=200.5.37=20=E2=80=94=20debug=20/debug-sess?= =?UTF-8?q?ion=20endpoint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- server.js | 12 +++++ tests/ui-sso.py | 118 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 tests/ui-sso.py diff --git a/package.json b/package.json index 17eaf47..bd70974 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ipwhitelist", - "version": "0.5.36", + "version": "0.5.37", "description": "IP WhiteList microservice for cloud provider", "main": "server.js", "scripts": { diff --git a/server.js b/server.js index a770446..0ad67d0 100644 --- a/server.js +++ b/server.js @@ -79,6 +79,18 @@ async function start() { // k8s liveness probe — простая проверка что сервер жив app.get('/healthz', (req, res) => res.send('OK')); + // 🔧 DEBUG: показать IAM-данные из сессии (убрать после теста!) + app.get('/debug-session', (req, res) => { + if (!req.session || !req.session.user) return res.json({ error: 'no session' }); + const { token, idToken, ...safe } = req.session; + res.json({ + user: req.session.user, + hasToken: !!token, + tokenPreview: token ? token.slice(0, 50) + '...' : null, + tokenPayload: token ? (() => { try { const p = JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString()); return { iss: p.iss, sub: p.sub, email: p.email, ClientID: p.ClientID, iat: p.iat }; } catch(e) { return 'decode error'; } })() : null, + }); + }); + // JWKS endpoint для валидации токенов внешними сервисами (только в mock-режиме) if (auth.jwksHandler) app.get('/.well-known/jwks.json', auth.jwksHandler); diff --git a/tests/ui-sso.py b/tests/ui-sso.py new file mode 100644 index 0000000..86fe794 --- /dev/null +++ b/tests/ui-sso.py @@ -0,0 +1,118 @@ +#!/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)