mike/e2e/auth.setup.ts
Amal 94987e6bd7 test: Playwright e2e suite (auth, chat, projects, tabular reviews, workflows)
Port of the amal66/mike fork's Playwright end-to-end suite onto the
upstream backend/ + frontend/ layout:

- e2e/: auth.setup (bootstraps a confirmed e2e@mike.local user via the
  Supabase admin API and saves storageState), auth-flows, critical-path
  (create project -> upload PDF -> ask a question -> streamed response),
  chat-management, project-management, tabular-reviews,
  workflows-account; fixtures/test.pdf
- playwright.config.ts: single-worker (shared test user), setup project
  + chromium project with saved auth state; webServer adapted from the
  fork's monorepo command (npm run dev --workspace apps/web) to
  upstream's layouts: backend `npm run dev` (health-checked on :3001)
  and frontend `npm run dev` (:3000)
- root package.json (upstream has none): @playwright/test, typescript
  dev-deps and test:e2e scripts, trimmed from the fork's root manifest
- root tsconfig.json scoped to e2e/ + playwright.config.ts so
  `npx tsc --noEmit` typechecks the suite
- .gitignore: playwright artifacts + e2e/.auth (session tokens)

Specs select by ARIA role/name and placeholder text only - no
data-testid attributes, so no app-code changes are required.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CEguyEgXa9JjCciXCcVemC
2026-07-20 09:52:46 -07:00

110 lines
4.3 KiB
TypeScript

import { test as setup, expect } from "@playwright/test";
import path from "path";
import fs from "fs";
const authFile = path.join(__dirname, ".auth/user.json");
/**
* Read a key out of backend/.env so the setup can reach Supabase with the
* service-role key without requiring the operator to export it manually.
*/
function readApiEnv(key: string): string | undefined {
if (process.env[key]) return process.env[key];
const envPath = path.join(__dirname, "..", "backend", ".env");
try {
const contents = fs.readFileSync(envPath, "utf8");
// dotenv semantics: a later assignment wins over an earlier one. CI
// does `cp .env.example .env` (which ships a PLACEHOLDER SUPABASE_URL)
// and then APPENDS the real values, so returning the FIRST match would
// hand back the placeholder (getaddrinfo ENOTFOUND your-project...).
// Iterate every line and keep the LAST matching value, mirroring how
// the API's dotenv loader resolves the file.
let value: string | undefined;
for (const line of contents.split("\n")) {
const m = line.match(/^([A-Z0-9_]+)=(.*)$/);
if (m && m[1] === key) value = m[2].trim();
}
return value;
} catch {
/* .env not present — fall through to undefined */
}
return undefined;
}
/**
* Idempotently create a confirmed Supabase user via the admin API. If the user
* already exists the admin endpoint returns a 422 which we treat as success.
*/
async function ensureUser(email: string, password: string) {
const supabaseUrl =
readApiEnv("SUPABASE_URL") ?? "http://127.0.0.1:54321";
const serviceKey = readApiEnv("SUPABASE_SECRET_KEY");
if (!serviceKey) {
throw new Error(
"SUPABASE_SECRET_KEY not found (checked env and backend/.env); " +
"cannot bootstrap E2E users",
);
}
const res = await fetch(`${supabaseUrl}/auth/v1/admin/users`, {
method: "POST",
headers: {
"Content-Type": "application/json",
apikey: serviceKey,
Authorization: `Bearer ${serviceKey}`,
},
body: JSON.stringify({
email,
password,
email_confirm: true,
}),
});
if (!res.ok && res.status !== 422) {
const body = await res.text();
// 422 == user already registered, which is fine for an idempotent setup.
if (!body.includes("already been registered")) {
throw new Error(
`Failed to create user ${email}: ${res.status} ${body}`,
);
}
}
}
/**
* The main authenticated session shared by every non-destructive test.
* Stored to e2e/.auth/user.json and loaded via the chromium project config.
*/
setup("authenticate", async ({ page }) => {
// Default to the credentials the spec files use (the specs log in with
// e2e@mike.local / E2eTestPass1!), so the suite runs out-of-the-box against
// a local stack with no env juggling. The bootstrapped user MUST match the
// password the specs type, or the valid-login tests fail; keeping the
// default here is the single source of truth. Override via env in CI.
const email = process.env.E2E_EMAIL ?? "e2e@mike.local";
const password = process.env.E2E_PASSWORD ?? "E2eTestPass1!";
/* Bootstrap the shared user plus a dedicated user for destructive auth
tests (logout / account deletion). The logout test calls Supabase
signOut() which uses GLOBAL scope and revokes the user's session
server-side; running it against the shared user would 401 every other
parallel worker. Isolating it onto its own user keeps the suite stable. */
await ensureUser(email, password);
await ensureUser(
process.env.E2E_LOGOUT_EMAIL ?? "e2e-logout@mike.local",
process.env.E2E_LOGOUT_PASSWORD ?? "E2eLogoutPass1!",
);
await page.goto("/login");
await expect(page).toHaveURL(/\/login/);
await page.fill("#email", email);
await page.fill("#password", password);
await page.click('button[type="submit"]');
/* After login the app redirects to /assistant */
await page.waitForURL(/\/assistant/, { timeout: 15_000 });
/* Save the authenticated session for all subsequent tests */
await page.context().storageState({ path: authFile });
});