import type { FullConfig } from '@playwright/test'; import { ADMIN_USER, REQUIRE_SEEDED_AUTH, TEST_USER } from './helpers/auth.js'; /** * Seed the E2E users through the gateway's real APIs (#1445, P6). * * On a fresh database (CI boots the gateway on the embedded PGlite path): * 1. POST /api/bootstrap/setup creates ADMIN_USER as the first admin. * 2. The admin signs in and creates TEST_USER via the better-auth admin API. * * Against an environment that already has users (needsSetup=false), seeding is * skipped entirely: the specs keep their own skip-when-login-fails guards, so * a live environment stays usable as a test target without mutation. Under * E2E_REQUIRE_SEEDED_AUTH=1 (CI) that state is instead a hard failure and the * guards are disabled — see helpers/auth.ts. * * On a fresh database, any seeding failure throws and fails the whole run: an * E2E gate whose authenticated suites silently skip would pass while proving * nothing. */ export default async function globalSetup(config: FullConfig): Promise { const baseURL = config.projects[0]?.use?.baseURL ?? 'http://localhost:14242'; const statusRes = await fetch(`${baseURL}/api/bootstrap/status`); if (!statusRes.ok) { throw new Error(`GET /api/bootstrap/status returned ${statusRes.status} — is the gateway up?`); } const status = (await statusRes.json()) as { needsSetup: boolean }; if (!status.needsSetup) { if (REQUIRE_SEEDED_AUTH) { // CI boots the gateway on a fresh HOME-isolated database, so an // already-populated one means the isolation regressed — refuse to run // against unknown data rather than skip-and-pass. throw new Error( 'E2E_REQUIRE_SEEDED_AUTH=1 but the database already has users — gateway HOME isolation regressed?', ); } console.info('[e2e setup] users already exist; skipping seed'); return; } const setupRes = await fetch(`${baseURL}/api/bootstrap/setup`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ name: ADMIN_USER.name, email: ADMIN_USER.email, password: ADMIN_USER.password, }), }); if (!setupRes.ok) { throw new Error( `POST /api/bootstrap/setup failed (${setupRes.status}): ${await setupRes.text()}`, ); } console.info(`[e2e setup] bootstrap admin created: ${ADMIN_USER.email}`); // better-auth's CSRF protection rejects requests without an Origin header // (403 MISSING_OR_NULL_ORIGIN), so the server-side fetches here send the // gateway's own origin — the same value a browser tab on the SPA would send. const authHeaders = { 'content-type': 'application/json', origin: baseURL }; const signInRes = await fetch(`${baseURL}/api/auth/sign-in/email`, { method: 'POST', headers: authHeaders, body: JSON.stringify({ email: ADMIN_USER.email, password: ADMIN_USER.password }), }); if (!signInRes.ok) { throw new Error(`admin sign-in failed (${signInRes.status}): ${await signInRes.text()}`); } const cookies = signInRes.headers .getSetCookie() .map((cookie) => cookie.split(';', 1)[0]) .join('; '); if (!cookies) { throw new Error('admin sign-in returned no session cookie'); } const createRes = await fetch(`${baseURL}/api/auth/admin/create-user`, { method: 'POST', headers: { ...authHeaders, cookie: cookies }, body: JSON.stringify({ name: TEST_USER.name, email: TEST_USER.email, password: TEST_USER.password, role: 'member', }), }); if (!createRes.ok) { throw new Error( `POST /api/auth/admin/create-user failed (${createRes.status}): ${await createRes.text()}`, ); } console.info(`[e2e setup] test user created: ${TEST_USER.email}`); }