mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
115 lines
5.2 KiB
TypeScript
115 lines
5.2 KiB
TypeScript
import { chromium, type FullConfig } from "@playwright/test";
|
|
import * as fs from "node:fs";
|
|
import * as path from "node:path";
|
|
|
|
import { PERSONAS, type Persona } from "./fixtures/personas";
|
|
|
|
/**
|
|
* Mints one storageState per persona before any spec runs.
|
|
*
|
|
* Both web apps keep their session in COOKIES (`auth-token`, `refresh-token`,
|
|
* `auth-user`) rather than localStorage — deliberately the same names
|
|
* freight-backoffice uses, so the apps share one IAM session. storageState is
|
|
* therefore cookie-shaped, and `auth-user` must be present: AuthContext seeds
|
|
* itself from that cookie so a reload does not flash the login screen, and
|
|
* without it the app renders logged-out for the first paint of every test.
|
|
*
|
|
* Most personas are minted programmatically (a direct POST to /auth/login) —
|
|
* eight real UI logins would add ~30s to every run and prove the same thing
|
|
* eight times. ONE persona per app goes through the real <LoginPage> form, so
|
|
* the login UI itself stays covered. That split follows the passenger suite.
|
|
*
|
|
* Fixture data (permissions, roles, personas) is seeded by run.sh before this
|
|
* runs; this file only mints sessions. If a login fails here it is almost
|
|
* always because the seeders were skipped — the error says so rather than
|
|
* letting 20 specs fail with a confusing redirect to /login.
|
|
*/
|
|
const STORAGE = path.join(__dirname, "fixtures", "storage");
|
|
const HR_API = process.env.HR_API_URL ?? "http://localhost:3105";
|
|
const FINANCE_API = process.env.FINANCE_API_URL ?? "http://localhost:3104";
|
|
const HR_WEB = process.env.HR_WEB_URL ?? "http://localhost:5285";
|
|
const FINANCE_WEB = process.env.FINANCE_WEB_URL ?? "http://localhost:5286";
|
|
|
|
const apiFor = (p: Persona) => (p.app === "hr" ? HR_API : FINANCE_API);
|
|
const webFor = (p: Persona) => (p.app === "hr" ? HR_WEB : FINANCE_WEB);
|
|
|
|
/** The apps read these three; `auth-user` is what AuthContext seeds from. */
|
|
const cookiesFor = (origin: string, token: string, refreshToken: string, user: unknown) => {
|
|
const { hostname } = new URL(origin);
|
|
const base = { domain: hostname, path: "/", expires: -1, httpOnly: false, secure: false, sameSite: "Lax" as const };
|
|
return [
|
|
{ ...base, name: "auth-token", value: token },
|
|
{ ...base, name: "refresh-token", value: refreshToken },
|
|
{ ...base, name: "auth-user", value: encodeURIComponent(JSON.stringify(user)) },
|
|
];
|
|
};
|
|
|
|
async function mintViaApi(persona: Persona): Promise<void> {
|
|
const api = apiFor(persona);
|
|
const res = await fetch(`${api}/api/v1/auth/login`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
// The login DTO validates `email` — it accepts a username in that field
|
|
// too, but the field name is `email` and an omitted one 400s with
|
|
// "email should not be empty".
|
|
body: JSON.stringify({ email: persona.email, password: persona.password }),
|
|
});
|
|
if (!res.ok) {
|
|
throw new Error(
|
|
`[global-setup] login failed for ${persona.key} (${persona.email}): HTTP ${res.status}. ` +
|
|
`Did run.sh seed the personas? ` +
|
|
`DB_NAME=smart_office_e2e node e2e-hr-finance/fixtures/seed-personas.cjs`,
|
|
);
|
|
}
|
|
const { token, refreshToken } = (await res.json()) as { token: string; refreshToken: string };
|
|
|
|
const meRes = await fetch(`${api}/api/v1/me`, { headers: { Authorization: `Bearer ${token}` } });
|
|
if (!meRes.ok) throw new Error(`[global-setup] /me failed for ${persona.key}: HTTP ${meRes.status}`);
|
|
const me = await meRes.json();
|
|
|
|
const state = { cookies: cookiesFor(webFor(persona), token, refreshToken, me), origins: [] };
|
|
fs.writeFileSync(path.join(STORAGE, `${persona.key}.json`), JSON.stringify(state, null, 2));
|
|
}
|
|
|
|
/** One per app, through the real form, so <LoginPage> itself is covered. */
|
|
async function mintViaUi(persona: Persona): Promise<void> {
|
|
const browser = await chromium.launch();
|
|
try {
|
|
const ctx = await browser.newContext();
|
|
const page = await ctx.newPage();
|
|
await page.goto(`${webFor(persona)}/login`, { waitUntil: "domcontentloaded" });
|
|
|
|
// Mantine renders the password field with a visibility-toggle BUTTON that
|
|
// also carries the accessible name "Password", so getByLabel matches two
|
|
// elements. Address the input by role instead.
|
|
await page.getByLabel("Email or username").fill(persona.email);
|
|
await page.getByRole("textbox", { name: "Password" }).fill(persona.password);
|
|
await Promise.all([
|
|
page.waitForURL((url) => !url.pathname.startsWith("/login"), { timeout: 30_000 }),
|
|
page.getByRole("button", { name: "Sign in" }).click(),
|
|
]);
|
|
|
|
await ctx.storageState({ path: path.join(STORAGE, `${persona.key}.json`) });
|
|
} finally {
|
|
await browser.close();
|
|
}
|
|
}
|
|
|
|
export default async function globalSetup(_config: FullConfig) {
|
|
fs.mkdirSync(STORAGE, { recursive: true });
|
|
|
|
const uiMinted = new Set(["hr-manager", "finance-manager"]);
|
|
|
|
for (const persona of PERSONAS) {
|
|
if (uiMinted.has(persona.key)) {
|
|
await mintViaUi(persona);
|
|
console.log(`[global-setup] ${persona.key.padEnd(24)} minted via real login UI`);
|
|
} else {
|
|
await mintViaApi(persona);
|
|
console.log(`[global-setup] ${persona.key.padEnd(24)} minted via API`);
|
|
}
|
|
}
|
|
|
|
console.log(`[global-setup] ${PERSONAS.length} storageStates written to fixtures/storage/`);
|
|
}
|