import { createPrivateKey, randomBytes, sign } from 'node:crypto' import { expect, test } from '@playwright/test' import { createClient } from 'redis' test('authenticates and completes a deployed job lifecycle through ingress', async ({ page, }) => { const token = await mintZitadelToken() const claims = jwtClaims(token) const issuer = requiredEnv('REMORA_OIDC_ISSUER') const sessionId = randomBytes(32).toString('base64url') const csrfToken = randomBytes(24).toString('base64url') const coordination = createClient({ url: requiredEnv('REMORA_COORDINATION_URL'), }) await coordination.connect() await coordination.set( `${requiredEnv('REMORA_COORDINATION_NAMESPACE')}web-session:${sessionId}`, JSON.stringify({ kind: 'authenticated', identity: `${issuer}\u0000${stringValue(claims.sub, 'subject')}`, csrfToken, accessToken: token, refreshToken: null, idToken: 'qualification-session', accessExpiresAt: Number(claims.exp), }), { EX: 3600 }, ) await coordination.quit() await page.context().addCookies([ { name: 'remora-qualification-session', value: sessionId, url: requiredEnv('REMORA_SYSTEM_BASE_URL'), sameSite: 'Strict', }, ]) const assetResponse = page.waitForResponse((response) => { const path = new URL(response.url()).pathname return path.startsWith('/assets/') && path.endsWith('.js') }) await page.goto('/') expect(['br', 'gzip']).toContain( (await assetResponse).headers()['content-encoding'], ) await expect(page.getByRole('heading', { name: 'Remora' })).toBeVisible() await expect(page.getByRole('button', { name: 'Sign out' })).toBeVisible() await expect(page.getByLabel('Synchronization status')).toHaveText('live', { timeout: 30_000, }) await expect(page.getByText('deployment-coordination-rebuild')).toBeVisible() await page.getByRole('button', { name: 'Submit job' }).click() await page.getByLabel('Label').fill('Browser system qualification') await page.getByLabel('URL').fill('https://example.com/system.bin') await page.getByLabel('Result path').fill('system.bin') const submitted = page.waitForResponse( (response) => response.request().method() === 'POST' && new URL(response.url()).pathname === '/bff/api/jobs', ) await page.getByRole('button', { name: 'Submit job now' }).click() const response = await submitted if (!response.ok()) throw new Error( `job submission failed: ${response.status()} ${await response.text()}`, ) await expect(page.getByText('Job accepted')).toBeVisible() await expect( page.getByRole('heading', { name: 'Browser system qualification' }), ).toBeVisible() await page.getByRole('button', { name: 'Cancel job' }).click() await page.getByRole('button', { name: 'Confirm' }).click() await expect(page.getByText('Cancellation requested')).toBeVisible() }) async function mintZitadelToken(): Promise { const issuer = requiredEnv('REMORA_OIDC_ISSUER').replace(/\/$/, '') const tokenUrl = `${issuer}/oauth/v2/token` const key = jsonObject( Buffer.from( requiredEnv('REMORA_OIDC_USER_KEY_B64'), 'base64url', ).toString(), ) const subject = stringValue(key.userId ?? key.userID, 'user id') const keyId = stringValue(key.keyId ?? key.key_id, 'key id') const privateKey = stringValue(key.key, 'private key').replaceAll( String.raw`\n`, '\n', ) const projectId = requiredEnv('REMORA_OIDC_PROJECT_ID') const scope = [ 'openid', 'profile', `urn:zitadel:iam:org:project:id:${projectId}:aud`, 'urn:zitadel:iam:org:project:roles', 'urn:zitadel:iam:org:projects:roles', `urn:zitadel:iam:org:project:${projectId}:roles`, ].join(' ') let failure = '' for (const audience of [tokenUrl, issuer]) { const response = await fetch(tokenUrl, { method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', assertion: assertion(subject, keyId, audience, privateKey), scope, }), }) if (response.ok) { const body = jsonObject(await response.text()) return stringValue(body.access_token, 'access token') } failure = `${response.status} ${await response.text()}` } throw new Error(`token endpoint rejected assertion: ${failure}`) } function assertion( subject: string, keyId: string, audience: string, privateKey: string, ): string { const now = Math.floor(Date.now() / 1000) const header = encode({ alg: 'RS256', kid: keyId, typ: 'JWT' }) const payload = encode({ iss: subject, sub: subject, aud: audience, iat: now, exp: now + 3600, }) const unsigned = `${header}.${payload}` const signature = sign( 'RSA-SHA256', Buffer.from(unsigned), createPrivateKey(privateKey), ).toString('base64url') return `${unsigned}.${signature}` } function jwtClaims(token: string): Record { const payload = token.split('.')[1] if (payload === undefined) throw new Error('access token is not a JWT') return jsonObject(Buffer.from(payload, 'base64url').toString()) } function encode(value: unknown): string { return Buffer.from(JSON.stringify(value)).toString('base64url') } function stringValue(value: unknown, name: string): string { if (typeof value !== 'string' || value === '') throw new Error(`missing ${name}`) return value } function jsonObject(source: string): Record { const value: unknown = JSON.parse(source) if (!isJsonObject(value)) throw new Error('expected a JSON object') return value } function isJsonObject(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value) } function requiredEnv(name: string): string { const value = process.env[name] if (value === undefined || value === '') throw new Error(`${name} is required`) return value }