Move browser authentication behind a generated-contract BFF

This commit is contained in:
ms2k 2026-07-19 18:21:17 +09:00
commit fa7220ee33
2 changed files with 7259 additions and 2825 deletions

View file

@ -0,0 +1,61 @@
import AxeBuilder from '@axe-core/playwright'
import { expect, test } from '@playwright/test'
test.beforeEach(async ({ page }) => {
await page.route('**/auth/session', (route) =>
route.fulfill({
contentType: 'application/json',
body: JSON.stringify({
kind: 'authenticated',
identity: 'issuer\u0000subject',
csrfToken: 'csrf-value',
}),
}),
)
await page.route(/\/bff\/api\//, (route) => {
const path = new URL(route.request().url()).pathname
if (path.endsWith('/jobs'))
return route.fulfill({
contentType: 'application/json',
body: JSON.stringify({
items: [],
next_cursor: null,
event_cursor: 'cursor-snapshot-0001',
management_capabilities: { revoke_execution_grants: false },
}),
})
return route.fulfill({
status: 201,
contentType: 'application/json',
body: JSON.stringify({
ticket: 'ticket_00001',
expires_at: '2026-07-20T00:00:00Z',
}),
})
})
await page.routeWebSocket(/\/bff\/events/, () => {})
})
test('meets automated WCAG A and AA checks', async ({ page }) => {
await page.goto('/')
await expect(page.getByRole('heading', { name: 'Remora' })).toBeVisible()
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21aa', 'wcag22aa'])
.analyze()
expect(results.violations).toEqual([])
})
test('reflows at 320 CSS pixels and preserves keyboard focus', async ({
page,
}) => {
await page.setViewportSize({ width: 320, height: 720 })
await page.goto('/')
await expect(page.getByRole('heading', { name: 'Remora' })).toBeVisible()
expect(
await page.evaluate(() => document.documentElement.scrollWidth),
).toBeLessThanOrEqual(320)
await page.keyboard.press('Tab')
await expect(page.getByRole('button', { name: 'Sign out' })).toBeFocused()
await page.keyboard.press('Tab')
await expect(page.getByRole('button', { name: 'Jobs' })).toBeFocused()
})

View file

@ -17,23 +17,12 @@ const result: MockResult = {
}
test.beforeEach(async ({ page }) => {
await page.routeWebSocket(/\/v1\/events/, () => {})
await page.routeWebSocket(/\/bff\/events/, () => {})
})
test('completes credential, submission, result, history, and lifecycle workflows', async ({
page,
}) => {
test('creates, rotates, and revokes a credential', async ({ page }) => {
const state = await mockApi(page)
await authenticatedSession(page)
await expect
.poll(() =>
page.evaluate(() =>
sessionStorage.getItem(
`oidc.user:${window.location.origin}/oidc:remora-web`,
),
),
)
.not.toBeNull()
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')
@ -55,7 +44,19 @@ test('completes credential, submission, result, history, and lifecycle workflows
await page.getByLabel('Replacement token').fill('replacement-token')
await page.getByRole('button', { name: 'Rotate credential' }).click()
await expect(page.getByLabel('Replacement token')).toHaveValue('')
await page.getByRole('button', { name: 'Revoke credential' }).click()
await expect(page.getByRole('alertdialog')).toContainText('future execution')
await page.getByRole('button', { name: 'Confirm' }).click()
await expect(
page.getByText('Credential revoked for future grants'),
).toBeVisible()
expect(state.mutations).toEqual([])
expect(new Set(state.idempotencyKeys).size).toBe(state.idempotencyKeys.length)
})
test('submits a job and browses its history and result', async ({ page }) => {
const state = await mockApi(page)
await authenticatedSession(page)
await page.getByRole('button', { name: 'Submit job' }).click()
await page.getByLabel('Label').fill('Imported archive')
await page
@ -75,7 +76,15 @@ test('completes credential, submission, result, history, and lifecycle workflows
await expect(page.getByText('AttemptCommitted')).toBeVisible()
await page.getByRole('button', { name: 'Browse result' }).click()
await expect(page.getByText('archive.bin')).toBeVisible()
expect(new Set(state.idempotencyKeys).size).toBe(state.idempotencyKeys.length)
})
test('confirms reversible and destructive job lifecycle actions', async ({
page,
}) => {
const state = await mockApi(page)
await authenticatedSession(page)
await page.getByRole('button', { name: /Existing archive/ }).click()
const softDelete = page.getByRole('button', { name: 'Soft delete' })
await softDelete.click()
await expect(page.getByRole('alertdialog')).toContainText(
@ -89,7 +98,7 @@ test('completes credential, submission, result, history, and lifecycle workflows
await page.getByRole('button', { name: 'Confirm' }).click()
await page.reload()
await expect(page.getByLabel('Synchronization status')).toHaveText('live')
await page.getByRole('button', { name: /Imported archive/ }).click()
await page.getByRole('button', { name: /Existing archive/ }).click()
expect(state.includeDeletedRequests).toBeGreaterThan(1)
await expect(page.getByRole('button', { name: 'Browse result' })).toBeHidden()
await page.getByRole('button', { name: 'Restore' }).click()
@ -103,15 +112,6 @@ test('completes credential, submission, result, history, and lifecycle workflows
page.getByRole('list', { name: 'Deletion operations' }).getByText('queued'),
).toBeVisible()
await page.getByRole('button', { name: 'Credentials' }).click()
await page.getByRole('button', { name: /Archive token/ }).click()
await page.getByRole('button', { name: 'Revoke credential' }).click()
await expect(page.getByRole('alertdialog')).toContainText('future execution')
await page.getByRole('button', { name: 'Confirm' }).click()
await expect(
page.getByText('Credential revoked for future grants'),
).toBeVisible()
expect(state.mutations).toEqual([
'soft-delete',
'restore',
@ -122,22 +122,14 @@ test('completes credential, submission, result, history, and lifecycle workflows
})
async function authenticatedSession(page: Page): Promise<void> {
await page.route('**/auth/session', (route) =>
json(route, {
kind: 'authenticated',
identity: 'issuer\u0000subject-0001',
csrfToken: 'csrf-value',
}),
)
await page.goto('/')
await page.evaluate(() => {
const authority = `${window.location.origin}/oidc`
sessionStorage.setItem(
`oidc.user:${authority}:remora-web`,
JSON.stringify({
id_token: 'id-token',
access_token: 'access-token',
token_type: 'Bearer',
scope: 'openid profile',
profile: { iss: authority, sub: 'subject-0001' },
expires_at: Math.floor(Date.now() / 1000) + 3600,
}),
)
})
await page.reload()
}
async function mockApi(page: Page): Promise<{
@ -162,10 +154,10 @@ async function mockApi(page: Page): Promise<{
}
let credential = sourceCredential('active')
let currentJob = job('Existing archive')
await page.route(/\/v1\//, async (route) => {
await page.route(/\/bff\/api\//, async (route) => {
const request = route.request()
const url = new URL(request.url())
const path = url.pathname.replace('/v1/', '')
const path = url.pathname.replace('/bff/api/', '')
state.paths.push(path)
if (
path === 'jobs' &&