Files
PiCloud/dashboard/tests/e2e/global-setup.ts
MechaCat02 f694a6d504 test(e2e): orphan sweep at globalSetup
Wipes e2e-* apps and e2e* admin users before the suite starts so a
prior crashed run doesn't accumulate state across runs (45 rows
observed on 2026-05-28). Per-row try/catch keeps it best-effort; a
sweep failure never blocks the suite.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 19:37:57 +02:00

147 lines
4.8 KiB
TypeScript

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<void> {
await assertBackendUp();
await fs.mkdir(AUTH_DIR, { recursive: true });
const token = await loginAsAdmin();
await sweepOrphans(token);
await persistAdminStorageState(token);
}
async function assertBackendUp(): Promise<void> {
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<string> {
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<void> {
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<void> {
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();
}
}