Some checks failed
CD / Promote to Int (push) Blocked by required conditions
CD / Build and push images (push) Successful in 1m41s
CI / Lint, typecheck, test (push) Failing after 56s
CI / Auth e2e pack (push) Failing after 43s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 8s
CD / Smoke tests against Test (push) Has been cancelled
The seed script now provisions the documented fixture matrix (fixture-admin / fixture-user / fixture-pending, idempotent upserts, rate-limit reset for disposable databases). A six-test Playwright pack drives the real UI against a full local stack with Mailpit: complete signup→mail→verify→first-login journey, wrong-password error, guarded route redirect honoring ?next (race between the login page and the anonymous guard fixed by teaching the guard about ?next), menu logout, site-admin gating, and a profile rename reflected in the top bar. The pack self-skips without E2E_MAILPIT_URL, so the CD smoke stage (now pinned to smoke.spec.ts) stays untouched; a new CI job boots api + web dev server against postgres/mailpit service containers and runs the pack on every PR and push. Also fixed: the web api client choked on empty 201 bodies. e2e/README.md documents targets and fixtures. Closes #20 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
58 lines
2.1 KiB
TypeScript
58 lines
2.1 KiB
TypeScript
import { type Browser, type BrowserContext, request } from '@playwright/test';
|
|
|
|
export const FIXTURE_PASSWORD = 'fixture passwort 123';
|
|
|
|
/**
|
|
* Signs a fixture user in through the api and returns a browser context
|
|
* that already carries the session cookie — UI tests skip the login form
|
|
* unless the form itself is under test.
|
|
*/
|
|
export async function contextForUser(
|
|
browser: Browser,
|
|
baseURL: string,
|
|
username: string,
|
|
password: string = FIXTURE_PASSWORD,
|
|
): Promise<BrowserContext> {
|
|
const api = await request.newContext({ baseURL });
|
|
const response = await api.post('/api/v1/auth/login', {
|
|
data: { usernameOrEmail: username, password },
|
|
});
|
|
if (!response.ok()) {
|
|
throw new Error(`fixture login for ${username} failed: ${response.status()}`);
|
|
}
|
|
const storageState = await api.storageState();
|
|
await api.dispose();
|
|
return browser.newContext({ baseURL, storageState });
|
|
}
|
|
|
|
/** Latest mail for an address from the Mailpit REST api. */
|
|
export async function latestMailFor(
|
|
mailpitUrl: string,
|
|
address: string,
|
|
): Promise<{ subject: string; text: string }> {
|
|
const api = await request.newContext({ baseURL: mailpitUrl });
|
|
for (let attempt = 0; attempt < 30; attempt += 1) {
|
|
const list = await api.get('/api/v1/search', {
|
|
params: { query: `to:${address}`, limit: 1 },
|
|
});
|
|
const body = (await list.json()) as { messages?: { ID: string; Subject: string }[] };
|
|
const found = body.messages?.[0];
|
|
if (found) {
|
|
const message = await api.get(`/api/v1/message/${found.ID}`);
|
|
const details = (await message.json()) as { Text: string };
|
|
await api.dispose();
|
|
return { subject: found.Subject, text: details.Text };
|
|
}
|
|
// The outbox worker delivers every 15s — poll until it does.
|
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
}
|
|
await api.dispose();
|
|
throw new Error(`no mail arrived for ${address}`);
|
|
}
|
|
|
|
export function tokenFromMail(text: string): string {
|
|
const match = text.match(/token=([A-Za-z0-9_-]+)/);
|
|
if (!match) throw new Error('mail contains no token link');
|
|
return match[1]!;
|
|
}
|