125 lines
5.5 KiB
TypeScript
125 lines
5.5 KiB
TypeScript
/**
|
|
* 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<string, string> = {
|
|
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 })
|
|
})
|