From c807838455b88cabe5b8a78d1032462d8c47d76b Mon Sep 17 00:00:00 2001 From: Galindo97 Date: Wed, 20 May 2026 13:31:55 -0500 Subject: [PATCH 1/4] =?UTF-8?q?fix(jenkins):=20mejorar=20diagn=C3=B3stico?= =?UTF-8?q?=20de=20error=20401=20en=20verificaci=C3=B3n=20de=20Keycloak?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit HTTP 401 indica credenciales inválidas o Direct Access Grants deshabilitado, no que Keycloak sea inaccesible. El mensaje ahora diferencia ambos casos y da instrucciones específicas para resolverlo. Co-Authored-By: Claude Sonnet 4.6 --- Jenkinsfile | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index d86812ee..26629fb1 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -229,8 +229,13 @@ pipeline { -d "grant_type=password&client_id=anexo76-frontend&username=${E2E_TEST_USER}&password=${E2E_TEST_PASSWORD}&scope=openid" \ 2>/dev/null || echo "000") echo "Keycloak token endpoint → HTTP ${KC_STATUS}" - if [ "$KC_STATUS" != "200" ]; then - echo "ERROR: Keycloak no accesible o password grant no habilitado (HTTP ${KC_STATUS})." + if [ "$KC_STATUS" = "401" ]; then + echo "ERROR 401 — Credenciales inválidas o Direct Access Grants deshabilitado." + echo " Verifica en Keycloak: Clients → anexo76-frontend → Settings → Direct access grants = ON" + echo " Verifica que el usuario '${E2E_TEST_USER}' exista en el realm y la contraseña sea correcta." + exit 1 + elif [ "$KC_STATUS" != "200" ]; then + echo "ERROR: Keycloak no accesible (HTTP ${KC_STATUS})." echo "URL: ${KC_TOKEN_EP}" exit 1 fi From 65832a92bc683ac27a32d27e1a987fd2b7a064a0 Mon Sep 17 00:00:00 2001 From: Galindo97 Date: Wed, 20 May 2026 13:50:39 -0500 Subject: [PATCH 2/4] =?UTF-8?q?feat(e2e):=20replicar=20patr=C3=B3n=20de=20?= =?UTF-8?q?autenticaci=C3=B3n=20de=20aduanasoft-hub?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fixtures/keycloak.ts: Admin REST API helpers (createTestUser, assignRoles, deleteTestUser) — copia exacta del hub - global-setup.ts: crea usuario e2e-anexo76 en KC antes de los tests - global-teardown.ts: elimina el usuario al finalizar - auth.setup.ts: navega a /login → Workspace → llena form → /dashboard (ya no usa password grant ni Direct Access Grants) - playwright.config.ts: agrega globalSetup y globalTeardown - Jenkinsfile: a76-e2e-credentials ahora son credenciales de KC admin; usuario de prueba es efímero (creado/borrado por global-setup/teardown) Co-Authored-By: Claude Sonnet 4.6 --- Jenkinsfile | 51 ++++-------- frontend/e2e/auth.setup.ts | 126 ++++++------------------------ frontend/e2e/fixtures/keycloak.ts | 108 +++++++++++++++++++++++++ frontend/e2e/global-setup.ts | 50 ++++++++++++ frontend/e2e/global-teardown.ts | 37 +++++++++ frontend/playwright.config.ts | 9 ++- 6 files changed, 241 insertions(+), 140 deletions(-) create mode 100644 frontend/e2e/fixtures/keycloak.ts create mode 100644 frontend/e2e/global-setup.ts create mode 100644 frontend/e2e/global-teardown.ts diff --git a/Jenkinsfile b/Jenkinsfile index 26629fb1..30e1287d 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -169,17 +169,21 @@ pipeline { stage('E2E (Playwright)') { steps { - // auth.setup.ts obtiene tokens directamente de Keycloak (password grant) - // sin pasar por el browser ni por Workspace — /login hace 303 inmediato. + // Patrón idéntico al de aduanasoft-hub: + // globalSetup crea usuario de prueba en KC vía Admin API + // auth.setup.ts navega a /login → Workspace → llena form → /dashboard + // globalTeardown elimina el usuario al finalizar + // // Credenciales requeridas en Jenkins: - // a76-public-url-dev : URL pública base — la URL de KC se deriva de ella ({url}/kcauth) - // a76-e2e-credentials : Username with password — cuenta de Workspace para pruebas E2E + // a76-public-url-dev : URL pública base de Anexo76 + // a76-e2e-credentials : Username with password — admin de Keycloak + // (para que globalSetup pueda crear/eliminar usuarios) withCredentials([ string(credentialsId: 'a76-public-url-dev', variable: 'A76_URL'), usernamePassword( credentialsId: 'a76-e2e-credentials', - usernameVariable: 'E2E_USER', - passwordVariable: 'E2E_PASS' + usernameVariable: 'E2E_KC_ADMIN_USER', + passwordVariable: 'E2E_KC_ADMIN_PASS' ) ]) { sh ''' @@ -200,17 +204,19 @@ pipeline { -e PLAYWRIGHT_TEST_BASE_URL=http://localhost:5173 \ -e "VITE_API_URL=${A76_URL}/api/" \ -e "INTERNAL_API_URL=${A76_URL}/api/" \ - -e "VITE_KEYCLOAK_URL=${A76_URL}/kcauth" \ - -e "KEYCLOAK_URL=${A76_URL}/kcauth" \ + -e "VITE_KEYCLOAK_URL=${KC_URL}" \ + -e "KEYCLOAK_URL=${KC_URL}" \ -e VITE_KEYCLOAK_REALM=master \ -e KEYCLOAK_REALM=master \ -e VITE_KEYCLOAK_CLIENT_ID=anexo76-frontend \ -e KEYCLOAK_CLIENT_ID=anexo76-frontend \ -e ORIGIN=http://localhost:5173 \ -e "KC_URL=${KC_URL}" \ - -e "E2E_KEYCLOAK_URL=${KC_URL}" \ - -e "E2E_TEST_USER=${E2E_USER}" \ - -e "E2E_TEST_PASSWORD=${E2E_PASS}" \ + -e "E2E_KC_ADMIN_USER=${E2E_KC_ADMIN_USER}" \ + -e "E2E_KC_ADMIN_PASSWORD=${E2E_KC_ADMIN_PASS}" \ + -e E2E_TEST_USER=e2e-anexo76 \ + -e E2E_TEST_PASSWORD=E2eAnexo76Test! \ + -e E2E_TEST_EMAIL=e2e-anexo76@test.local \ -w /workspace/frontend \ "$C" bash -lc ' set -euxo pipefail @@ -218,29 +224,6 @@ pipeline { pnpm install --frozen-lockfile pnpm run i18n:compile - # Verificar que Keycloak acepta el password grant - # (auth.setup.ts lo usa directamente — sin pasar por /login) - echo "--- Verificando Keycloak token endpoint ---" - KC_TOKEN_EP="${KC_URL}/realms/master/protocol/openid-connect/token" - # -s sin -f: curl no falla en HTTP 4xx/5xx, devuelve el código real - # || echo "000" solo se activa si curl no puede conectar (timeout, DNS, etc.) - KC_STATUS=$(curl -s --max-time 10 -o /dev/null -w "%{http_code}" \ - -X POST "$KC_TOKEN_EP" \ - -d "grant_type=password&client_id=anexo76-frontend&username=${E2E_TEST_USER}&password=${E2E_TEST_PASSWORD}&scope=openid" \ - 2>/dev/null || echo "000") - echo "Keycloak token endpoint → HTTP ${KC_STATUS}" - if [ "$KC_STATUS" = "401" ]; then - echo "ERROR 401 — Credenciales inválidas o Direct Access Grants deshabilitado." - echo " Verifica en Keycloak: Clients → anexo76-frontend → Settings → Direct access grants = ON" - echo " Verifica que el usuario '${E2E_TEST_USER}' exista en el realm y la contraseña sea correcta." - exit 1 - elif [ "$KC_STATUS" != "200" ]; then - echo "ERROR: Keycloak no accesible (HTTP ${KC_STATUS})." - echo "URL: ${KC_TOKEN_EP}" - exit 1 - fi - echo "Keycloak OK" - # Arrancar dev server pnpm run dev & DEV_PID=$! diff --git a/frontend/e2e/auth.setup.ts b/frontend/e2e/auth.setup.ts index a744d81f..57ee088e 100644 --- a/frontend/e2e/auth.setup.ts +++ b/frontend/e2e/auth.setup.ts @@ -1,124 +1,42 @@ /** * 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. + * El flujo de login de Anexo76 pasa por Workspace (workspace.aduanasoft.com): + * 1. Navegar a /login → 303 redirect al formulario de Workspace + * 2. Llenar el form de Workspace con el usuario de prueba (creado en globalSetup) + * 3. Workspace/Keycloak redirige de vuelta a /auth/callback → /dashboard + * 4. Guardar storageState para todos los tests dependientes * - * 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. + * El usuario de prueba es creado y eliminado por globalSetup / globalTeardown. + * Patrón idéntico al de aduanasoft-hub. */ -import { test as setup } from '@playwright/test' +import { test as setup, expect } 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(/\/$/, '') +setup('autenticacion', async ({ page }) => { + const username = process.env.E2E_TEST_USER ?? '' + const password = process.env.E2E_TEST_PASSWORD ?? '' - // 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(/\/$/, '') + if (!username) throw new Error('E2E_TEST_USER no está configurado') + if (!password) throw new Error('E2E_TEST_PASSWORD no está configurado') - 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 || '' + // ── 1. Navegar a /login — Workspace intercepta y muestra su formulario ── + await page.goto('/login') - 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') + // ── 2. Llenar el formulario de Workspace (mismos selectores que hub/login.spec.ts) ── + await page.getByRole('textbox', { name: /usuario o email/i }).fill(username) + await page.getByLabel(/contraseña/i).fill(password) + await page.getByRole('button', { name: /continuar/i }).click() - // ── 1. Obtener tokens vía password grant ────────────────────────────────── - const tokenEndpoint = `${keycloakUrl}/realms/${realm}/protocol/openid-connect/token` + // ── 3. Esperar redirect de vuelta a Anexo76 (/auth/callback → /dashboard) ── + await expect(page).toHaveURL(/\/dashboard/, { timeout: 30_000 }) - 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 ───────────────────────── + // ── 4. Guardar estado para los tests dependientes ───────────────────────── mkdirSync(path.dirname(authFile), { recursive: true }) await page.context().storageState({ path: authFile }) }) diff --git a/frontend/e2e/fixtures/keycloak.ts b/frontend/e2e/fixtures/keycloak.ts new file mode 100644 index 00000000..8f465f51 --- /dev/null +++ b/frontend/e2e/fixtures/keycloak.ts @@ -0,0 +1,108 @@ +/** + * Helpers para gestionar usuarios de prueba en Keycloak vía Admin REST API. + * Se usan en globalSetup / globalTeardown de tests E2E. + * Patrón idéntico al de aduanasoft-hub. + */ + +export interface KeycloakAdminConfig { + url: string; // ej. https://workspace.aduanasoft.com/kcauth + realm: string; // ej. master + adminUser: string; + adminPassword: string; +} + +async function getAdminToken(cfg: KeycloakAdminConfig): Promise { + const url = `${cfg.url}/realms/master/protocol/openid-connect/token`; + const res = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ + grant_type: 'password', + client_id: 'admin-cli', + username: cfg.adminUser, + password: cfg.adminPassword + }) + }); + if (!res.ok) throw new Error(`Keycloak admin token failed: ${res.status} ${await res.text()}`); + const data = await res.json(); + return data.access_token; +} + +export async function createTestUser( + cfg: KeycloakAdminConfig, + user: { username: string; password: string; email: string } +): Promise { + const token = await getAdminToken(cfg); + const res = await fetch(`${cfg.url}/admin/realms/${cfg.realm}/users`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + username: user.username, + email: user.email, + enabled: true, + emailVerified: true, + credentials: [{ type: 'password', value: user.password, temporary: false }] + }) + }); + // 409 = ya existe — idempotente + if (!res.ok && res.status !== 409) { + throw new Error(`createTestUser failed: ${res.status} ${await res.text()}`); + } + const searchRes = await fetch( + `${cfg.url}/admin/realms/${cfg.realm}/users?username=${encodeURIComponent(user.username)}&exact=true`, + { headers: { Authorization: `Bearer ${token}` } } + ); + const users = await searchRes.json(); + if (!users.length) throw new Error(`Usuario ${user.username} no encontrado tras creación`); + return users[0].id; +} + +export async function assignRoles( + cfg: KeycloakAdminConfig, + userId: string, + roleNames: string[] +): Promise { + const token = await getAdminToken(cfg); + + // Resolver los IDs de los roles por nombre + const roles: { id: string; name: string }[] = []; + for (const name of roleNames) { + const res = await fetch( + `${cfg.url}/admin/realms/${cfg.realm}/roles/${encodeURIComponent(name)}`, + { headers: { Authorization: `Bearer ${token}` } } + ); + if (!res.ok) throw new Error(`Rol '${name}' no encontrado: ${res.status}`); + const role = await res.json(); + roles.push({ id: role.id, name: role.name }); + } + + const assignRes = await fetch( + `${cfg.url}/admin/realms/${cfg.realm}/users/${userId}/role-mappings/realm`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify(roles) + } + ); + if (!assignRes.ok) { + throw new Error(`assignRoles failed: ${assignRes.status} ${await assignRes.text()}`); + } +} + +export async function deleteTestUser(cfg: KeycloakAdminConfig, userId: string): Promise { + const token = await getAdminToken(cfg); + const res = await fetch(`${cfg.url}/admin/realms/${cfg.realm}/users/${userId}`, { + method: 'DELETE', + headers: { Authorization: `Bearer ${token}` } + }); + // 404 = ya fue borrado — se ignora + if (!res.ok && res.status !== 404) { + throw new Error(`deleteTestUser failed: ${res.status} ${await res.text()}`); + } +} diff --git a/frontend/e2e/global-setup.ts b/frontend/e2e/global-setup.ts new file mode 100644 index 00000000..428d56e8 --- /dev/null +++ b/frontend/e2e/global-setup.ts @@ -0,0 +1,50 @@ +/** + * Playwright globalSetup — se ejecuta UNA vez antes de todos los tests E2E. + * + * Crea el usuario de prueba en Keycloak desde cero vía Admin REST API. + * No depende de que el usuario exista previamente — funciona con KC vacío + * siempre que el realm y los clientes estén configurados. + * + * Patrón idéntico al de aduanasoft-hub. + */ + +import { writeFileSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { createTestUser, assignRoles } from './fixtures/keycloak.js'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +const KC_CFG = { + url: process.env.KC_URL ?? 'https://workspace.aduanasoft.com/kcauth', + realm: process.env.KEYCLOAK_REALM ?? 'master', + adminUser: process.env.E2E_KC_ADMIN_USER ?? 'admin', + adminPassword: process.env.E2E_KC_ADMIN_PASSWORD ?? '' +}; + +const TEST_USER = { + username: process.env.E2E_TEST_USER ?? 'e2e-anexo76', + password: process.env.E2E_TEST_PASSWORD ?? '', + email: process.env.E2E_TEST_EMAIL ?? 'e2e-anexo76@test.local' +}; + +export default async function globalSetup() { + console.log('\n[E2E setup] Creando usuario de prueba en Keycloak...'); + + if (!KC_CFG.adminPassword) { + throw new Error('E2E_KC_ADMIN_PASSWORD no está configurado'); + } + if (!TEST_USER.password) { + throw new Error('E2E_TEST_PASSWORD no está configurado'); + } + + const userId = await createTestUser(KC_CFG, TEST_USER); + // Roles de Keycloak necesarios para acceder a Anexo76 + await assignRoles(KC_CFG, userId, ['admin']); + + // Guardar el ID para que globalTeardown pueda borrar el usuario + const statePath = join(__dirname, '..', '.e2e-state.json'); + writeFileSync(statePath, JSON.stringify({ userId, username: TEST_USER.username })); + + console.log(`[E2E setup] Usuario '${TEST_USER.username}' listo (id: ${userId})`); +} diff --git a/frontend/e2e/global-teardown.ts b/frontend/e2e/global-teardown.ts new file mode 100644 index 00000000..cf024db7 --- /dev/null +++ b/frontend/e2e/global-teardown.ts @@ -0,0 +1,37 @@ +/** + * Playwright globalTeardown — se ejecuta UNA vez después de todos los tests E2E. + * Elimina el usuario de prueba creado en globalSetup. + * + * Patrón idéntico al de aduanasoft-hub. + */ + +import { readFileSync, unlinkSync, existsSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { deleteTestUser } from './fixtures/keycloak.js'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +const KC_CFG = { + url: process.env.KC_URL ?? 'https://workspace.aduanasoft.com/kcauth', + realm: process.env.KEYCLOAK_REALM ?? 'master', + adminUser: process.env.E2E_KC_ADMIN_USER ?? 'admin', + adminPassword: process.env.E2E_KC_ADMIN_PASSWORD ?? '' +}; + +export default async function globalTeardown() { + const statePath = join(__dirname, '..', '.e2e-state.json'); + + if (!existsSync(statePath)) { + console.log('[E2E teardown] Sin estado guardado — nada que limpiar'); + return; + } + + const { userId, username } = JSON.parse(readFileSync(statePath, 'utf8')); + console.log(`\n[E2E teardown] Eliminando usuario '${username}' de Keycloak...`); + + await deleteTestUser(KC_CFG, userId); + unlinkSync(statePath); + + console.log(`[E2E teardown] Usuario '${username}' eliminado`); +} diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts index 065611dd..c0d841f5 100644 --- a/frontend/playwright.config.ts +++ b/frontend/playwright.config.ts @@ -9,9 +9,14 @@ const authStatePath = path.join(__dirname, 'e2e/.auth/user.json'); const inCI = process.env.CI === 'true' || Boolean(process.env.JENKINS_URL); export default defineConfig({ + // Crea usuario de prueba en KC antes de los tests; lo elimina al finalizar + // Patrón idéntico al de aduanasoft-hub + globalSetup: './e2e/global-setup.ts', + globalTeardown: './e2e/global-teardown.ts', + // En CI, falla si queda un .only; en local no forbidOnly: inCI, - timeout: 60000, + timeout: 60_000, use: { baseURL: process.env.PLAYWRIGHT_TEST_BASE_URL || 'http://localhost:5173', // Sin display en agentes de integración: obligatorio headless @@ -33,4 +38,4 @@ export default defineConfig({ use: { storageState: authStatePath } } ] -}); \ No newline at end of file +}); From 155ee0717354ff2b6d0492a832b0d2ed12aedba4 Mon Sep 17 00:00:00 2001 From: Galindo97 Date: Wed, 20 May 2026 14:01:57 -0500 Subject: [PATCH 3/4] =?UTF-8?q?fix(jenkins):=20corregir=20client=5Fid=20de?= =?UTF-8?q?=20Keycloak=20en=20E2E=20=E2=80=94=20hub-frontend=20no=20anexo7?= =?UTF-8?q?6-frontend?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit El cliente registrado en Keycloak es hub-frontend (Workspace). anexo76-frontend no existe en KC. Co-Authored-By: Claude Sonnet 4.6 --- Jenkinsfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 30e1287d..7dc41b47 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -208,8 +208,8 @@ pipeline { -e "KEYCLOAK_URL=${KC_URL}" \ -e VITE_KEYCLOAK_REALM=master \ -e KEYCLOAK_REALM=master \ - -e VITE_KEYCLOAK_CLIENT_ID=anexo76-frontend \ - -e KEYCLOAK_CLIENT_ID=anexo76-frontend \ + -e VITE_KEYCLOAK_CLIENT_ID=hub-frontend \ + -e KEYCLOAK_CLIENT_ID=hub-frontend \ -e ORIGIN=http://localhost:5173 \ -e "KC_URL=${KC_URL}" \ -e "E2E_KC_ADMIN_USER=${E2E_KC_ADMIN_USER}" \ From 4a9d4cfcd1f06fd3bf1e9f1ba4c867e6bb7c4e4e Mon Sep 17 00:00:00 2001 From: Galindo97 Date: Wed, 20 May 2026 14:10:59 -0500 Subject: [PATCH 4/4] fix(e2e): usar usuario de prueba existente en lugar de Admin API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit El usuario en a76-e2e-credentials no tiene permisos de KC admin (403). En lugar de crear usuario efímero vía Admin API: - Eliminar global-setup.ts, global-teardown.ts, fixtures/keycloak.ts - Quitar globalSetup/globalTeardown de playwright.config.ts - auth.setup.ts ya navega a /login → Workspace → llena form → /dashboard - a76-e2e-credentials = usuario de prueba existente en Workspace (E2E_USER/E2E_PASS) Co-Authored-By: Claude Sonnet 4.6 --- Jenkinsfile | 20 ++---- frontend/e2e/fixtures/keycloak.ts | 108 ------------------------------ frontend/e2e/global-setup.ts | 50 -------------- frontend/e2e/global-teardown.ts | 37 ---------- frontend/playwright.config.ts | 7 -- 5 files changed, 7 insertions(+), 215 deletions(-) delete mode 100644 frontend/e2e/fixtures/keycloak.ts delete mode 100644 frontend/e2e/global-setup.ts delete mode 100644 frontend/e2e/global-teardown.ts diff --git a/Jenkinsfile b/Jenkinsfile index 7dc41b47..65995822 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -169,21 +169,18 @@ pipeline { stage('E2E (Playwright)') { steps { - // Patrón idéntico al de aduanasoft-hub: - // globalSetup crea usuario de prueba en KC vía Admin API - // auth.setup.ts navega a /login → Workspace → llena form → /dashboard - // globalTeardown elimina el usuario al finalizar + // auth.setup.ts navega a /login → Workspace → llena form → /dashboard + // y guarda storageState para los tests dependientes. // // Credenciales requeridas en Jenkins: // a76-public-url-dev : URL pública base de Anexo76 - // a76-e2e-credentials : Username with password — admin de Keycloak - // (para que globalSetup pueda crear/eliminar usuarios) + // a76-e2e-credentials : Username with password — usuario de prueba en Workspace withCredentials([ string(credentialsId: 'a76-public-url-dev', variable: 'A76_URL'), usernamePassword( credentialsId: 'a76-e2e-credentials', - usernameVariable: 'E2E_KC_ADMIN_USER', - passwordVariable: 'E2E_KC_ADMIN_PASS' + usernameVariable: 'E2E_USER', + passwordVariable: 'E2E_PASS' ) ]) { sh ''' @@ -212,11 +209,8 @@ pipeline { -e KEYCLOAK_CLIENT_ID=hub-frontend \ -e ORIGIN=http://localhost:5173 \ -e "KC_URL=${KC_URL}" \ - -e "E2E_KC_ADMIN_USER=${E2E_KC_ADMIN_USER}" \ - -e "E2E_KC_ADMIN_PASSWORD=${E2E_KC_ADMIN_PASS}" \ - -e E2E_TEST_USER=e2e-anexo76 \ - -e E2E_TEST_PASSWORD=E2eAnexo76Test! \ - -e E2E_TEST_EMAIL=e2e-anexo76@test.local \ + -e "E2E_TEST_USER=${E2E_USER}" \ + -e "E2E_TEST_PASSWORD=${E2E_PASS}" \ -w /workspace/frontend \ "$C" bash -lc ' set -euxo pipefail diff --git a/frontend/e2e/fixtures/keycloak.ts b/frontend/e2e/fixtures/keycloak.ts deleted file mode 100644 index 8f465f51..00000000 --- a/frontend/e2e/fixtures/keycloak.ts +++ /dev/null @@ -1,108 +0,0 @@ -/** - * Helpers para gestionar usuarios de prueba en Keycloak vía Admin REST API. - * Se usan en globalSetup / globalTeardown de tests E2E. - * Patrón idéntico al de aduanasoft-hub. - */ - -export interface KeycloakAdminConfig { - url: string; // ej. https://workspace.aduanasoft.com/kcauth - realm: string; // ej. master - adminUser: string; - adminPassword: string; -} - -async function getAdminToken(cfg: KeycloakAdminConfig): Promise { - const url = `${cfg.url}/realms/master/protocol/openid-connect/token`; - const res = await fetch(url, { - method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: new URLSearchParams({ - grant_type: 'password', - client_id: 'admin-cli', - username: cfg.adminUser, - password: cfg.adminPassword - }) - }); - if (!res.ok) throw new Error(`Keycloak admin token failed: ${res.status} ${await res.text()}`); - const data = await res.json(); - return data.access_token; -} - -export async function createTestUser( - cfg: KeycloakAdminConfig, - user: { username: string; password: string; email: string } -): Promise { - const token = await getAdminToken(cfg); - const res = await fetch(`${cfg.url}/admin/realms/${cfg.realm}/users`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${token}` - }, - body: JSON.stringify({ - username: user.username, - email: user.email, - enabled: true, - emailVerified: true, - credentials: [{ type: 'password', value: user.password, temporary: false }] - }) - }); - // 409 = ya existe — idempotente - if (!res.ok && res.status !== 409) { - throw new Error(`createTestUser failed: ${res.status} ${await res.text()}`); - } - const searchRes = await fetch( - `${cfg.url}/admin/realms/${cfg.realm}/users?username=${encodeURIComponent(user.username)}&exact=true`, - { headers: { Authorization: `Bearer ${token}` } } - ); - const users = await searchRes.json(); - if (!users.length) throw new Error(`Usuario ${user.username} no encontrado tras creación`); - return users[0].id; -} - -export async function assignRoles( - cfg: KeycloakAdminConfig, - userId: string, - roleNames: string[] -): Promise { - const token = await getAdminToken(cfg); - - // Resolver los IDs de los roles por nombre - const roles: { id: string; name: string }[] = []; - for (const name of roleNames) { - const res = await fetch( - `${cfg.url}/admin/realms/${cfg.realm}/roles/${encodeURIComponent(name)}`, - { headers: { Authorization: `Bearer ${token}` } } - ); - if (!res.ok) throw new Error(`Rol '${name}' no encontrado: ${res.status}`); - const role = await res.json(); - roles.push({ id: role.id, name: role.name }); - } - - const assignRes = await fetch( - `${cfg.url}/admin/realms/${cfg.realm}/users/${userId}/role-mappings/realm`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${token}` - }, - body: JSON.stringify(roles) - } - ); - if (!assignRes.ok) { - throw new Error(`assignRoles failed: ${assignRes.status} ${await assignRes.text()}`); - } -} - -export async function deleteTestUser(cfg: KeycloakAdminConfig, userId: string): Promise { - const token = await getAdminToken(cfg); - const res = await fetch(`${cfg.url}/admin/realms/${cfg.realm}/users/${userId}`, { - method: 'DELETE', - headers: { Authorization: `Bearer ${token}` } - }); - // 404 = ya fue borrado — se ignora - if (!res.ok && res.status !== 404) { - throw new Error(`deleteTestUser failed: ${res.status} ${await res.text()}`); - } -} diff --git a/frontend/e2e/global-setup.ts b/frontend/e2e/global-setup.ts deleted file mode 100644 index 428d56e8..00000000 --- a/frontend/e2e/global-setup.ts +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Playwright globalSetup — se ejecuta UNA vez antes de todos los tests E2E. - * - * Crea el usuario de prueba en Keycloak desde cero vía Admin REST API. - * No depende de que el usuario exista previamente — funciona con KC vacío - * siempre que el realm y los clientes estén configurados. - * - * Patrón idéntico al de aduanasoft-hub. - */ - -import { writeFileSync } from 'fs'; -import { join, dirname } from 'path'; -import { fileURLToPath } from 'url'; -import { createTestUser, assignRoles } from './fixtures/keycloak.js'; - -const __dirname = dirname(fileURLToPath(import.meta.url)); - -const KC_CFG = { - url: process.env.KC_URL ?? 'https://workspace.aduanasoft.com/kcauth', - realm: process.env.KEYCLOAK_REALM ?? 'master', - adminUser: process.env.E2E_KC_ADMIN_USER ?? 'admin', - adminPassword: process.env.E2E_KC_ADMIN_PASSWORD ?? '' -}; - -const TEST_USER = { - username: process.env.E2E_TEST_USER ?? 'e2e-anexo76', - password: process.env.E2E_TEST_PASSWORD ?? '', - email: process.env.E2E_TEST_EMAIL ?? 'e2e-anexo76@test.local' -}; - -export default async function globalSetup() { - console.log('\n[E2E setup] Creando usuario de prueba en Keycloak...'); - - if (!KC_CFG.adminPassword) { - throw new Error('E2E_KC_ADMIN_PASSWORD no está configurado'); - } - if (!TEST_USER.password) { - throw new Error('E2E_TEST_PASSWORD no está configurado'); - } - - const userId = await createTestUser(KC_CFG, TEST_USER); - // Roles de Keycloak necesarios para acceder a Anexo76 - await assignRoles(KC_CFG, userId, ['admin']); - - // Guardar el ID para que globalTeardown pueda borrar el usuario - const statePath = join(__dirname, '..', '.e2e-state.json'); - writeFileSync(statePath, JSON.stringify({ userId, username: TEST_USER.username })); - - console.log(`[E2E setup] Usuario '${TEST_USER.username}' listo (id: ${userId})`); -} diff --git a/frontend/e2e/global-teardown.ts b/frontend/e2e/global-teardown.ts deleted file mode 100644 index cf024db7..00000000 --- a/frontend/e2e/global-teardown.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Playwright globalTeardown — se ejecuta UNA vez después de todos los tests E2E. - * Elimina el usuario de prueba creado en globalSetup. - * - * Patrón idéntico al de aduanasoft-hub. - */ - -import { readFileSync, unlinkSync, existsSync } from 'fs'; -import { join, dirname } from 'path'; -import { fileURLToPath } from 'url'; -import { deleteTestUser } from './fixtures/keycloak.js'; - -const __dirname = dirname(fileURLToPath(import.meta.url)); - -const KC_CFG = { - url: process.env.KC_URL ?? 'https://workspace.aduanasoft.com/kcauth', - realm: process.env.KEYCLOAK_REALM ?? 'master', - adminUser: process.env.E2E_KC_ADMIN_USER ?? 'admin', - adminPassword: process.env.E2E_KC_ADMIN_PASSWORD ?? '' -}; - -export default async function globalTeardown() { - const statePath = join(__dirname, '..', '.e2e-state.json'); - - if (!existsSync(statePath)) { - console.log('[E2E teardown] Sin estado guardado — nada que limpiar'); - return; - } - - const { userId, username } = JSON.parse(readFileSync(statePath, 'utf8')); - console.log(`\n[E2E teardown] Eliminando usuario '${username}' de Keycloak...`); - - await deleteTestUser(KC_CFG, userId); - unlinkSync(statePath); - - console.log(`[E2E teardown] Usuario '${username}' eliminado`); -} diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts index c0d841f5..22d6b4a5 100644 --- a/frontend/playwright.config.ts +++ b/frontend/playwright.config.ts @@ -9,17 +9,11 @@ const authStatePath = path.join(__dirname, 'e2e/.auth/user.json'); const inCI = process.env.CI === 'true' || Boolean(process.env.JENKINS_URL); export default defineConfig({ - // Crea usuario de prueba en KC antes de los tests; lo elimina al finalizar - // Patrón idéntico al de aduanasoft-hub - globalSetup: './e2e/global-setup.ts', - globalTeardown: './e2e/global-teardown.ts', - // En CI, falla si queda un .only; en local no forbidOnly: inCI, timeout: 60_000, use: { baseURL: process.env.PLAYWRIGHT_TEST_BASE_URL || 'http://localhost:5173', - // Sin display en agentes de integración: obligatorio headless headless: inCI ? true : false }, workers: 1, @@ -28,7 +22,6 @@ export default defineConfig({ { name: 'setup', testMatch: '**/auth.setup.ts', - // Navegador limpio: user.json se genera aquí y no debe existir al primer run / en CI use: { storageState: { cookies: [], origins: [] } } }, {