Move browser authentication behind a generated-contract BFF
This commit is contained in:
parent
6f0f20f5cc
commit
fa7220ee33
2 changed files with 7259 additions and 2825 deletions
61
web/tests/e2e/accessibility.spec.ts
Normal file
61
web/tests/e2e/accessibility.spec.ts
Normal 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()
|
||||
})
|
||||
|
|
@ -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' &&
|
||||
|
|
|
|||
Loading…
Reference in a new issue