Files
DevRunbook-Public/tests/e2e/milestone-zero.spec.ts
T
DevRunbook release export cfd2804e27
Managed validation / full (push) Successful in 3m18s
Publish DevRunbook source
2026-09-03 04:09:17 +02:00

173 lines
5.7 KiB
TypeScript

import { expect, test } from '@playwright/test'
test('catalog shell renders the verified built-in slice', async ({ page }) => {
await page.goto('/')
await expect(page).toHaveTitle(/DevRunbook/)
await expect(
page.getByRole('heading', {
level: 1,
name: /Turn intent into a verifiable change contract/i,
}),
).toBeVisible()
await expect(page.getByText('28 built-ins · no Gitea required')).toBeVisible()
await expect(page.getByRole('heading', { level: 3 })).toHaveCount(6)
await expect(page.locator('[data-nextjs-dialog]')).toHaveCount(0)
})
test('catalog API requires a local authenticated workspace', async ({
request,
}) => {
const response = await request.get('/api/v1/playbooks')
expect(response.status()).toBe(401)
await expect(response.json()).resolves.toMatchObject({
error: { code: 'authentication_required' },
})
})
test('legacy playbook links preserve the authentication boundary', async ({
page,
}) => {
await page.goto('/playbooks/root-cause-bugfix')
await expect(
page.getByRole('heading', {
level: 1,
name: /continue with your local account/i,
}),
).toBeVisible()
expect(new URL(page.url()).pathname).toBe('/login')
})
test('narrow layout has no horizontal overflow and keyboard focus is visible', async ({
page,
}) => {
await page.setViewportSize({ width: 390, height: 844 })
await page.goto('/')
expect(
await page.evaluate(
() =>
document.documentElement.scrollWidth <=
document.documentElement.clientWidth,
),
).toBe(true)
await page.keyboard.press('Tab')
await expect(page.getByRole('link', { name: /DevRunbook/i })).toBeFocused()
const outline = await page.evaluate(
() => getComputedStyle(document.activeElement as Element).outlineStyle,
)
expect(outline).not.toBe('none')
})
test('security headers are present on application pages', async ({
request,
}) => {
const response = await request.get('/')
expect(response.headers()['x-content-type-options']).toBe('nosniff')
expect(response.headers()['x-frame-options']).toBe('DENY')
expect(response.headers()['content-security-policy']).toContain(
"frame-ancestors 'none'",
)
})
test('tokenless bootstrap is denied through an untrusted proxy boundary', async ({
request,
}) => {
const response = await request.post('/api/v1/instance/setup', {
headers: { 'x-forwarded-for': '127.0.0.1' },
data: {
bootstrapToken: '',
instanceName: 'Test instance',
publicBaseUrl: 'http://127.0.0.1:3000',
owner: {
email: 'owner@example.test',
displayName: 'Owner',
password: 'not-a-real-password',
},
},
})
expect(response.status()).toBe(403)
expect(await response.text()).not.toContain('not-a-real-password')
})
test('setup UI exposes the database recovery state without enabling credentials', async ({
page,
}) => {
test.skip(
Boolean(process.env.DEVRUNBOOK_E2E_EMAIL),
'recovery state requires an uninitialized validation database',
)
const consoleErrors: string[] = []
const pageErrors: string[] = []
page.on('console', (message) => {
if (message.type() === 'error') consoleErrors.push(message.text())
})
page.on('pageerror', (error) => pageErrors.push(error.message))
await page.goto('/setup')
await expect(
page.getByRole('heading', { level: 1, name: 'Make this instance yours.' }),
).toBeVisible()
await expect(page.getByText('recovery required')).toBeVisible({
timeout: 20_000,
})
await expect(page.getByLabel('Owner email')).toBeDisabled()
await expect(page.getByText(/Gitea is optional/i)).toBeVisible()
expect(consoleErrors).toEqual([])
expect(pageErrors).toEqual([])
})
test('local sign-in fails generically and clears credentials when auth is unavailable', async ({
page,
}) => {
const pageErrors: string[] = []
page.on('pageerror', (error) => pageErrors.push(error.message))
await page.goto('/login?returnTo=https%3A%2F%2Fattacker.example%2Fsteal')
await expect(
page.getByRole('heading', {
level: 1,
name: /continue with your local account/i,
}),
).toBeVisible()
await page.getByLabel('Email address').fill('owner@example.test')
const password = page.getByLabel('Password', { exact: true })
await password.fill('not-a-real-password')
await page.getByRole('button', { name: 'Sign in' }).click()
await expect(page.locator('form').getByRole('alert')).toHaveText(
'Sign-in could not be completed. Check your credentials or try again later.',
)
await expect(password).toHaveValue('')
expect(new URL(page.url()).pathname).toBe('/login')
expect(pageErrors).toEqual([])
})
test('password recovery keeps its token out of HTTP and clears both credentials', async ({
page,
}) => {
const token = 'a'.repeat(43)
const requestedUrls: string[] = []
const pageErrors: string[] = []
page.on('request', (request) => requestedUrls.push(request.url()))
page.on('pageerror', (error) => pageErrors.push(error.message))
await page.goto(`/reset-password#token=${token}`)
await expect(
page.getByRole('heading', { level: 1, name: /restore access/i }),
).toBeVisible()
await expect(page).toHaveURL(/\/reset-password$/u)
expect(requestedUrls.every((url) => !url.includes(token))).toBe(true)
const password = page.getByLabel('New password', { exact: true })
const confirmation = page.getByLabel('Confirm new password')
await password.fill('a secure replacement password')
await confirmation.fill('a secure replacement password')
await page.getByRole('button', { name: 'Update password' }).click()
await expect(page.locator('form').getByRole('alert')).toContainText(
'invalid, expired, or already used',
)
await expect(password).toHaveValue('')
await expect(confirmation).toHaveValue('')
expect(pageErrors).toEqual([])
})