import { createPrivateKey, sign } from 'node:crypto' import { expect, test } from '@playwright/test' 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') await page.goto('/') await page.evaluate( ({ accessToken, authority, profile }) => { sessionStorage.setItem( `oidc.user:${authority}:remora-web`, JSON.stringify({ access_token: accessToken, token_type: 'Bearer', scope: 'openid profile', profile, expires_at: Number(profile.exp), }), ) }, { accessToken: token, authority: issuer, profile: claims }, ) await page.reload() 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') 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 === '/v1/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 }