/** * Setup de autenticación para tests E2E de Playwright. * * El flujo de login de Anexo76 pasa por Workspace externo (workspace.aduanasoft.com) * y Keycloak SSO — el browser nunca renderiza un form propio. Por eso NO se puede * automatizar navegando a /login: ese route hace 303 inmediato al Workspace. * * Estrategia: obtener tokens directamente del token endpoint de Keycloak * (Resource Owner Password Credentials grant) e inyectarlos como cookies en el * contexto de Playwright, replicando exactamente lo que hace /auth/callback. * * Requiere en Keycloak: cliente con "Direct Access Grants" habilitado. * Requiere en Jenkins: credenciales a76-e2e-keycloak-url, a76-e2e-test-user, a76-e2e-test-pass. */ import { test as setup } from '@playwright/test' import { mkdirSync } from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' // Lógica de chunking idéntica a access-token-cookie.shared.ts const ACCESS_TOKEN_MAX_SINGLE = 2800 const ACCESS_TOKEN_CHUNK_SIZE = 2800 const ACCESS_TOKEN_CHUNK_COUNT = 'access_token_chunks' const chunkName = (i: number) => `access_token_${i}` const __dirname = path.dirname(fileURLToPath(import.meta.url)) const authFile = path.join(__dirname, '.auth/user.json') setup('autenticacion', async ({ page, request }) => { const baseUrl = (process.env.PLAYWRIGHT_TEST_BASE_URL ?? 'http://localhost:5173').replace(/\/$/, '') // E2E_KEYCLOAK_URL: URL base de Keycloak accesible desde el agente Jenkins. // Puede diferir de VITE_KEYCLOAK_URL si ese apunta al dominio del propio servidor // y el contenedor Docker no puede resolver ese dominio (hairpin NAT). const keycloakUrl = ( process.env.E2E_KEYCLOAK_URL || process.env.KEYCLOAK_URL || process.env.VITE_KEYCLOAK_URL || '' ).replace(/\/$/, '') const realm = process.env.KEYCLOAK_REALM || process.env.VITE_KEYCLOAK_REALM || 'master' const clientId = process.env.KEYCLOAK_CLIENT_ID || process.env.VITE_KEYCLOAK_CLIENT_ID || 'anexo76-frontend' const secret = process.env.KEYCLOAK_CLIENT_SECRET || '' const username = process.env.E2E_TEST_USER || '' const password = process.env.E2E_TEST_PASSWORD || '' if (!keycloakUrl) throw new Error('E2E_KEYCLOAK_URL / KEYCLOAK_URL no está configurado') if (!username) throw new Error('E2E_TEST_USER no está configurado') if (!password) throw new Error('E2E_TEST_PASSWORD no está configurado') // ── 1. Obtener tokens vía password grant ────────────────────────────────── const tokenEndpoint = `${keycloakUrl}/realms/${realm}/protocol/openid-connect/token` const form: Record = { grant_type: 'password', client_id: clientId, username, password, scope: 'openid' } if (secret) form.client_secret = secret const tokenRes = await request.post(tokenEndpoint, { form }) if (!tokenRes.ok()) { const body = await tokenRes.text() throw new Error( `Keycloak password grant falló (HTTP ${tokenRes.status()}).\n` + `Endpoint: ${tokenEndpoint}\n` + `Respuesta: ${body}\n` + `Verifica que el cliente "${clientId}" tenga "Direct Access Grants" habilitado en Keycloak.` ) } const tokens = await tokenRes.json() as { access_token: string refresh_token?: string id_token?: string } // ── 2. Navegar al app para establecer origen de cookies ─────────────────── await page.goto(baseUrl) const hostname = new URL(baseUrl).hostname const secure = baseUrl.startsWith('https') const base = { domain: hostname, path: '/', sameSite: 'Lax' as const, secure } // ── 3. Inyectar access_token respetando chunking (igual que setAccessTokenCookies) ── if (tokens.access_token.length <= ACCESS_TOKEN_MAX_SINGLE) { await page.context().addCookies([ { ...base, httpOnly: false, name: 'access_token', value: tokens.access_token } ]) } else { const parts: string[] = [] for (let i = 0; i < tokens.access_token.length; i += ACCESS_TOKEN_CHUNK_SIZE) { parts.push(tokens.access_token.slice(i, i + ACCESS_TOKEN_CHUNK_SIZE)) } await page.context().addCookies([ { ...base, httpOnly: false, name: ACCESS_TOKEN_CHUNK_COUNT, value: String(parts.length) }, ...parts.map((p, i) => ({ ...base, httpOnly: false, name: chunkName(i), value: p })) ]) } // ── 4. Cookies httpOnly (el servidor las gestiona; Playwright puede setearlas en test) ── if (tokens.refresh_token) { await page.context().addCookies([ { ...base, httpOnly: true, name: 'refresh_token', value: tokens.refresh_token } ]) } if (tokens.id_token) { await page.context().addCookies([ { ...base, httpOnly: true, name: 'id_token', value: tokens.id_token } ]) } // ── 5. Verificar que la sesión funciona navegando al dashboard ──────────── await page.goto(`${baseUrl}/dashboard`) await page.waitForURL(/dashboard/, { timeout: 30000 }) // ── 6. Guardar estado para los tests dependientes ───────────────────────── mkdirSync(path.dirname(authFile), { recursive: true }) await page.context().storageState({ path: authFile }) })