import { chromium, request } from '@playwright/test'; import { promises as fs } from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const API_BASE = process.env.E2E_API_BASE ?? 'http://127.0.0.1:18080'; const DASHBOARD_PORT = Number(process.env.PICLOUD_DASHBOARD_PORT ?? 5173); const DASHBOARD_ORIGIN = process.env.E2E_DASHBOARD_ORIGIN ?? `http://localhost:${DASHBOARD_PORT}`; const ADMIN_USERNAME = process.env.E2E_ADMIN_USERNAME ?? 'admin'; const ADMIN_PASSWORD = process.env.E2E_ADMIN_PASSWORD ?? 'admin'; const AUTH_DIR = path.join(__dirname, '.auth'); const ADMIN_STATE_PATH = path.join(AUTH_DIR, 'admin.json'); export default async function globalSetup(): Promise { await assertBackendUp(); await fs.mkdir(AUTH_DIR, { recursive: true }); const token = await loginAsAdmin(); await sweepOrphans(token); await persistAdminStorageState(token); } async function assertBackendUp(): Promise { const probe = await request.newContext(); try { const res = await probe.get(`${API_BASE}/healthz`, { timeout: 5_000 }); if (!res.ok()) { throw new Error( `backend /healthz returned ${res.status()} — is \`cargo run -p picloud\` listening on ${API_BASE}?` ); } } catch (err) { throw new Error( `Could not reach backend at ${API_BASE}/healthz. ` + `Bring it up before running E2E tests:\n\n` + ` docker compose up -d postgres\n` + ` PICLOUD_BIND=127.0.0.1:18080 \\\n` + ` PICLOUD_ADMIN_USERNAME=${ADMIN_USERNAME} \\\n` + ` PICLOUD_ADMIN_PASSWORD=${ADMIN_PASSWORD} \\\n` + ` DATABASE_URL=postgres://picloud:picloud@127.0.0.1:15432/picloud \\\n` + ` cargo run -p picloud\n\n` + `Underlying error: ${(err as Error).message}` ); } finally { await probe.dispose(); } } async function loginAsAdmin(): Promise { const ctx = await request.newContext(); try { const res = await ctx.post(`${API_BASE}/api/v1/admin/auth/login`, { data: { username: ADMIN_USERNAME, password: ADMIN_PASSWORD }, headers: { 'content-type': 'application/json' } }); if (!res.ok()) { const body = await res.text(); throw new Error( `Admin login failed (${res.status()}): ${body}. ` + `Verify PICLOUD_ADMIN_USERNAME / PICLOUD_ADMIN_PASSWORD match the seeded bootstrap admin.` ); } const payload = (await res.json()) as { token?: string }; if (!payload.token) { throw new Error('Admin login response missing token field'); } return payload.token; } finally { await ctx.dispose(); } } // Clean up apps + admin users left over from a previous crashed run. // The convention is that every e2e-created resource has a slug // starting with `e2e-` (apps) or a username starting with `e2e` // (admins) — see fixtures/ids.ts. Best-effort: a sweep failure must // not stop the suite from running. async function sweepOrphans(token: string): Promise { const ctx = await request.newContext({ baseURL: API_BASE, extraHTTPHeaders: { authorization: `Bearer ${token}` } }); try { try { const res = await ctx.get('/api/v1/admin/apps'); if (res.ok()) { const apps = (await res.json()) as Array<{ slug: string }>; for (const app of apps) { if (!app.slug.startsWith('e2e-')) continue; try { await ctx.delete( `/api/v1/admin/apps/${encodeURIComponent(app.slug)}?force=true` ); } catch { // Individual delete failure is non-fatal — the per-test // cleanup will catch it on the next run. } } } } catch { // Listing failed; nothing to do but proceed. } try { const res = await ctx.get('/api/v1/admin/admins'); if (res.ok()) { const admins = (await res.json()) as Array<{ id: string; username: string }>; for (const a of admins) { if (!/^e2e/.test(a.username)) continue; try { await ctx.delete(`/api/v1/admin/admins/${a.id}`); } catch { // Same per-row tolerance as above. } } } } catch { // Listing failed; same as above. } } finally { await ctx.dispose(); } } // The dashboard reads its session from localStorage under the key // `picloud.admin.token` (see src/lib/auth.ts). We can't write to // localStorage without a browser context, so launch a throwaway one, // seed the value, then save storageState for every test to reuse. async function persistAdminStorageState(token: string): Promise { const browser = await chromium.launch(); try { const context = await browser.newContext(); const page = await context.newPage(); await page.goto(`${DASHBOARD_ORIGIN}/admin/login`); await page.evaluate( ([key, value]) => { localStorage.setItem(key, value); }, ['picloud.admin.token', token] ); await context.storageState({ path: ADMIN_STATE_PATH }); } finally { await browser.close(); } }