import dotenv from 'dotenv'; import fetch from 'node-fetch'; import { test, expect } from '@playwright/test'; import { chromium } from 'playwright'; dotenv.config(); const TEST_USER_EMAIL = process.env.TEST_USER_EMAIL || 'test-integration@example.com'; const TEST_USER_USERNAME = process.env.TEST_USER_USERNAME || 'test-integration'; const TEST_USER_PASSWORD = process.env.TEST_USER_PASSWORD || 'StrongP@ssw0rd!'; let BASE_URL = (process.env.BASE_URL || 'http://localhost:3000').trim(); // sometimes env can contain accidental concatenated vars; take first token BASE_URL = BASE_URL.split(/\s+/)[0]; if (!BASE_URL.startsWith('http://') && !BASE_URL.startsWith('https://')) { BASE_URL = 'http://' + BASE_URL; } // increase default timeout for slow integration flows test.setTimeout(60000); async function adminLogin() { const adminUser = process.env.ADMIN_USERNAME; const adminPass = process.env.ADMIN_PASSWORD; if (!adminUser || !adminPass) throw new Error('ADMIN_USERNAME/ADMIN_PASSWORD must be set in env for integration tests'); const browser = await chromium.launch({ headless: true }); const context = await browser.newContext(); const page = await context.newPage(); await page.goto(BASE_URL, { waitUntil: 'networkidle' }); try { await page.waitForSelector('input[type="text"], input[type="email"], input[name="username"]', { timeout: 10000 }); const usernameInput = await page.$('input[name="username"]') || await page.$('input[type="email"]') || await page.$('input[type="text"]'); if (usernameInput) await usernameInput.fill(adminUser); const passwordInput = await page.$('input[type="password"], input[name="password"]'); if (passwordInput) await passwordInput.fill(adminPass); const submitButton = await page.$('button[type="submit"], input[type="submit"]'); if (submitButton) { await Promise.all([page.waitForNavigation({ waitUntil: 'networkidle', timeout: 20000 }), submitButton.click()]); } } catch (e) { // ignore if login form not present } await page.waitForTimeout(1000); const cookies = await context.cookies(); const jwtCookie = cookies.find(c => c.name === 'raylab_jwt'); await context.close(); await browser.close(); if (!jwtCookie) throw new Error('Admin login failed: no session cookie'); return jwtCookie.value; } async function apiRequest(path, token, opts = {}) { const headers = Object.assign({ 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, opts.headers || {}); const method = opts.method || 'GET'; const body = opts.body ? JSON.stringify(opts.body) : undefined; let url; try { url = new URL(path, BASE_URL).toString(); } catch (e) { throw new Error(`Invalid BASE_URL for integration tests: ${BASE_URL}`); } const res = await fetch(url, { method, headers, body }); let json = null; try { json = await res.json(); } catch (e) { json = null; } return { status: res.status, body: json }; } test.describe('Integration - User lifecycle', () => { let adminToken; let createdUser = null; test.beforeAll(async () => { // Integration tests assume test users are created in Authentik prior to running. // RayLab will create local user record upon first successful login via Authentik. }); test('login via Authentik creates local user and issues internal JWT', async () => { // use browser flow to login as the test user const browser = await chromium.launch({ headless: true }); const context = await browser.newContext(); const page = await context.newPage(); await page.goto(BASE_URL, { waitUntil: 'networkidle' }); try { await page.waitForSelector('input[type="text"], input[type="email"], input[name="username"]', { timeout: 10000 }); const usernameInput = await page.$('input[name="username"]') || await page.$('input[type="email"]') || await page.$('input[type="text"]'); if (usernameInput) await usernameInput.fill(TEST_USER_USERNAME); const passwordInput = await page.$('input[type="password"], input[name="password"]'); if (passwordInput) await passwordInput.fill(TEST_USER_PASSWORD); const submitButton = await page.$('button[type="submit"], input[type="submit"]'); if (submitButton) { await Promise.all([page.waitForNavigation({ waitUntil: 'networkidle', timeout: 20000 }), submitButton.click()]); } } catch (e) { // ignore if login form not present } await page.waitForTimeout(1000); const cookies = await context.cookies(); const jwtCookie = cookies.find(c => c.name === 'raylab_jwt'); const refreshCookie = cookies.find(c => c.name === 'raylab_refresh'); if (!jwtCookie) { const fs = require('fs'); const dir = 'tests/test-artifacts'; if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); const ts = Date.now(); await page.screenshot({ path: `${dir}/failed-login-${ts}.png`, fullPage: true }); fs.writeFileSync(`${dir}/failed-login-${ts}-cookies.json`, JSON.stringify(cookies, null, 2), 'utf-8'); const content = await page.content(); fs.writeFileSync(`${dir}/failed-login-${ts}-page.html`, content, 'utf-8'); await context.close(); await browser.close(); throw new Error(`No jwt cookie set after login. Saved screenshot/cookies/page to ${dir}`); } // ensure we have an internal jwt cookie expect(jwtCookie).toBeDefined(); expect(jwtCookie.value).toBeTruthy(); // validate /auth/me using the internal jwt const token = jwtCookie.value; const meUrl = new URL('/auth/me', BASE_URL).toString(); const meResp = await fetch(meUrl, { headers: { Authorization: `Bearer ${token}` } }); const meJson = await meResp.json(); expect(meResp.status).toBeLessThan(300); expect(meJson.success).toBe(true); expect(meJson.data).toBeDefined(); expect(meJson.data.email).toBe(TEST_USER_EMAIL); await context.close(); await browser.close(); }); test('login via Authentik (created user) issues internal JWT', async () => { // use browser flow to login as the created user const browser = await chromium.launch({ headless: true }); const context = await browser.newContext(); const page = await context.newPage(); await page.goto(BASE_URL, { waitUntil: 'networkidle' }); try { await page.waitForSelector('input[type="text"], input[type="email"], input[name="username"]', { timeout: 10000 }); const usernameInput = await page.$('input[name="username"]') || await page.$('input[type="email"]') || await page.$('input[type="text"]'); if (usernameInput) await usernameInput.fill(TEST_USER_USERNAME); const passwordInput = await page.$('input[type="password"], input[name="password"]'); if (passwordInput) await passwordInput.fill(TEST_USER_PASSWORD); const submitButton = await page.$('button[type="submit"], input[type="submit"]'); if (submitButton) { await Promise.all([page.waitForNavigation({ waitUntil: 'networkidle', timeout: 20000 }), submitButton.click()]); } } catch (e) { // ignore if login form not present } await page.waitForTimeout(1000); const cookies = await context.cookies(); const jwtCookie = cookies.find(c => c.name === 'raylab_jwt'); if (!jwtCookie) { const fs = require('fs'); const dir = 'tests/test-artifacts'; if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); const ts = Date.now(); await page.screenshot({ path: `${dir}/failed-login-2-${ts}.png`, fullPage: true }); fs.writeFileSync(`${dir}/failed-login-2-${ts}-cookies.json`, JSON.stringify(cookies, null, 2), 'utf-8'); const content = await page.content(); fs.writeFileSync(`${dir}/failed-login-2-${ts}-page.html`, content, 'utf-8'); await context.close(); await browser.close(); throw new Error(`No jwt cookie set after login (second test). Saved screenshot/cookies/page to ${dir}`); } await context.close(); await browser.close(); expect(jwtCookie).toBeDefined(); expect(jwtCookie.value).toBeTruthy(); }); // Cleanup via API provisioning has been removed. If test environment requires cleanup, perform manually in Authentik. });