import path from "node:path"; import { fileURLToPath } from "node:url"; import { request } from "@playwright/test"; import { OWNER_DISPLAY_NAME, OWNER_EMAIL, OWNER_PASSWORD, WORKSPACE_NAME } from "./specs/support/credentials"; const baseURL = process.env.KNOWLEDGE_HUB_E2E_BASE_URL ?? "http://localhost:1240"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); /** Where the pre-authenticated owner session's storage state is written, for specs * that need to start already signed in (see `authenticatedStatePath`). Not committed — * regenerated by every `globalSetup` run. */ export const authenticatedStatePath = path.join(__dirname, "../../artifacts/e2e-auth-state.json"); function extractCookie(response: Response): string | null { // Node's fetch exposes combined Set-Cookie via getSetCookie() (undici); fall back to // the raw header for older runtimes. const cookies = typeof response.headers.getSetCookie === "function" ? response.headers.getSetCookie() : [response.headers.get("set-cookie")].filter((value): value is string => value !== null); return cookies.map((cookie) => cookie.split(";")[0]).join("; ") || null; } async function completeFirstRunSetupIfNeeded(): Promise { const statusResponse = await fetch(`${baseURL}/api/v1/setup/status`); const status = (await statusResponse.json()) as { setupRequired: boolean }; if (!status.setupRequired) { // A previous run (or a manual setup) already completed this exact step — // idempotent no-op rather than a hard failure, so repeated local `pnpm test:e2e` // runs against the same stack keep working. return; } const csrfResponse = await fetch(`${baseURL}/api/v1/auth/csrf`); const csrfCookie = extractCookie(csrfResponse); const csrf = (await csrfResponse.json()) as { token: string; headerName: string }; const completeResponse = await fetch(`${baseURL}/api/v1/setup/complete`, { method: "POST", headers: { "Content-Type": "application/json", [csrf.headerName]: csrf.token, ...(csrfCookie ? { Cookie: csrfCookie } : {}) }, body: JSON.stringify({ workspaceName: WORKSPACE_NAME, displayName: OWNER_DISPLAY_NAME, email: OWNER_EMAIL, password: OWNER_PASSWORD }) }); if (!completeResponse.ok) { const body = await completeResponse.text(); throw new Error( `E2E global setup failed to complete first-run setup (${completeResponse.status}): ${body}` ); } } /** * Logs in exactly once for the whole suite and persists the resulting session as * Playwright storage state, so specs that only need to *already be signed in* * (everything except the login/logout journey itself) can start from that state * instead of each performing its own `POST /api/v1/auth/login`. The login endpoint is * deliberately rate-limited (docs/12-auth-security.md — 10/minute); with roughly a * dozen specs each logging in once, the previous "every spec logs in itself" design * reliably tripped that limiter on a full local run. One login per full suite run * keeps well under it while still exercising the real login endpoint (via * auth-journey.spec.ts, which intentionally does not use this shared state). */ async function saveAuthenticatedState(): Promise { const api = await request.newContext({ baseURL }); const csrfResponse = await api.get("/api/v1/auth/csrf"); const csrf = (await csrfResponse.json()) as { token: string; headerName: string }; const loginResponse = await api.post("/api/v1/auth/login", { headers: { [csrf.headerName]: csrf.token }, data: { email: OWNER_EMAIL, password: OWNER_PASSWORD } }); if (!loginResponse.ok()) { const body = await loginResponse.text(); throw new Error(`E2E global setup failed to log in the shared owner session (${loginResponse.status()}): ${body}`); } await api.storageState({ path: authenticatedStatePath }); await api.dispose(); } export default async function globalSetup(): Promise { await completeFirstRunSetupIfNeeded(); await saveAuthenticatedState(); }