539d063c22
The first end-to-end smoke spec, run live against the Tier-1 stack
(§22 M3-0 Task 7). Drives the §6.2 email OTC sign-in: loads the app,
requests a code, reads it from Mailpit, verifies it, and asserts the
verify returns ok:true and sets the rfc_session cookie.
Supporting harness fixes:
- Makefile: add .PHONY so `make e2e` actually runs (the e2e/ directory
was shadowing the target, making it a no-op).
- mailpit.js: guard the per-message detail fetch (if !full.ok continue)
and scan the plain-text part before HTML so the \d{6} match can't
latch onto a stray number.
- spec uses a unique per-run email to avoid the per-email request
cooldown (OTC_REQUEST_COOLDOWN_SECONDS) on re-runs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
49 lines
2.0 KiB
JavaScript
49 lines
2.0 KiB
JavaScript
import { test, expect } from '@playwright/test'
|
|
import { waitForLatestOtc, clearMailpit } from './lib/mailpit.js'
|
|
|
|
// Unique per-run address. The §6.2 request path enforces a per-email
|
|
// cooldown (OTC_REQUEST_COOLDOWN_SECONDS, default 60s), so a fixed
|
|
// address would 429 on any re-run inside the window. A fresh address per
|
|
// run sidesteps that without touching backend config.
|
|
const EMAIL = `e2e-${Date.now()}-${Math.floor(Math.random() * 1e6)}@example.test`
|
|
|
|
// First end-to-end smoke spec (M3-0 Task 7). Validates the whole Tier-1
|
|
// harness: the web tier serves the app, the backend's OTC sign-in path
|
|
// (§6.2) issues a code, Mailpit captures the outbound mail, and a verify
|
|
// of that code succeeds and mints a session.
|
|
//
|
|
// No precondition/provisioning step is needed: the §6.2 OTC path admits
|
|
// any syntactically-valid email and provisions a fresh `pending` user on
|
|
// verify (see backend/app/otc.py). Turnstile is open in this stack
|
|
// (TURNSTILE_REQUIRED=false, no secret), so the request body carries only
|
|
// the email.
|
|
test('app loads and an OTC sign-in succeeds', async ({ page, request }) => {
|
|
await clearMailpit()
|
|
|
|
await page.goto('/')
|
|
await expect(page).toHaveTitle(/.+/)
|
|
|
|
const reqRes = await request.post('/auth/otc/request', {
|
|
data: { email: EMAIL },
|
|
})
|
|
expect(reqRes.ok()).toBeTruthy()
|
|
|
|
const code = await waitForLatestOtc(EMAIL)
|
|
expect(code).toMatch(/^\d{6}$/)
|
|
|
|
const verifyRes = await request.post('/auth/otc/verify', {
|
|
data: { email: EMAIL, code },
|
|
})
|
|
expect(verifyRes.ok()).toBeTruthy()
|
|
|
|
// Prove the sign-in actually took: the verify handler returns ok:true
|
|
// and sets the `rfc_session` session cookie (§ SessionMiddleware,
|
|
// backend/app/main.py). Asserting the cookie — not brittle UI text —
|
|
// is what distinguishes a real sign-in from a bare 2xx.
|
|
const verifyBody = await verifyRes.json()
|
|
expect(verifyBody.ok).toBe(true)
|
|
|
|
const setCookie = verifyRes.headers()['set-cookie'] || ''
|
|
expect(setCookie).toContain('rfc_session=')
|
|
})
|