106 lines
4.0 KiB
TypeScript
106 lines
4.0 KiB
TypeScript
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
await completeFirstRunSetupIfNeeded();
|
|
await saveAuthenticatedState();
|
|
}
|