Files
gridpilot.gg/tests/integration/website/websiteAuth.ts
2026-01-03 11:38:51 +01:00

161 lines
5.5 KiB
TypeScript

import type { Page, BrowserContext } from '@playwright/test';
import type { RouteAccess } from './websiteRouteInventory';
export type WebsiteAuthContext = 'public' | 'auth' | 'admin' | 'sponsor';
export type WebsiteSessionDriftMode = 'invalid-cookie' | 'expired' | 'missing-sponsor-id';
export type WebsiteFaultMode = 'null-array' | 'missing-field' | 'invalid-date';
const demoSessionCookieCache = new Map<string, string>();
export function authContextForAccess(access: RouteAccess): WebsiteAuthContext {
if (access === 'public') return 'public';
if (access === 'auth') return 'auth';
if (access === 'admin') return 'admin';
return 'sponsor';
}
function getWebsiteBaseUrl(): string {
const configured = process.env.WEBSITE_BASE_URL ?? process.env.PLAYWRIGHT_BASE_URL;
if (configured && configured.trim()) {
return configured.trim().replace(/\/$/, '');
}
return 'http://localhost:3100';
}
// Note: All authenticated contexts use the same seeded demo driver user
// Role-based access control is tested separately in integration tests
function extractCookieValue(setCookieHeader: string, cookieName: string): string | null {
// set-cookie header value: "name=value; Path=/; HttpOnly; ..."
// Do not split on comma (Expires contains commas). Just regex out the first cookie value.
const match = setCookieHeader.match(new RegExp(`(?:^|\\s)${cookieName}=([^;]+)`));
return match?.[1] ?? null;
}
async function ensureNormalSessionCookie(): Promise<string> {
const cached = demoSessionCookieCache.get('driver');
if (cached) return cached;
const baseUrl = getWebsiteBaseUrl();
const url = `${baseUrl}/api/auth/login`;
const response = await fetch(url, {
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify({
email: 'demo.driver@example.com',
password: 'Demo1234!',
}),
});
if (!response.ok) {
const body = await response.text().catch(() => '');
throw new Error(`Normal login failed. ${response.status} ${response.statusText}. ${body}`);
}
// In Node (playwright runner) `headers.get('set-cookie')` returns a single comma-separated string.
// Parse cookies by splitting on `, ` and taking the first `name=value` segment.
const rawSetCookie = response.headers.get('set-cookie') ?? '';
const cookieHeaderPairs = rawSetCookie
? rawSetCookie
.split(', ')
.map((chunk) => chunk.split(';')[0]?.trim())
.filter(Boolean)
: [];
const gpSessionPair = cookieHeaderPairs.find((pair) => pair.startsWith('gp_session='));
if (!gpSessionPair) {
throw new Error(
`Normal login did not return gp_session cookie. set-cookie header: ${rawSetCookie}`,
);
}
const gpSessionValue = extractCookieValue(gpSessionPair, 'gp_session');
if (!gpSessionValue) {
throw new Error(
`Normal login returned a gp_session cookie, but it could not be parsed. Pair: ${gpSessionPair}`,
);
}
demoSessionCookieCache.set('driver', gpSessionValue);
return gpSessionValue;
}
export async function setWebsiteAuthContext(
context: BrowserContext,
auth: WebsiteAuthContext,
options: { sessionDrift?: WebsiteSessionDriftMode; faultMode?: WebsiteFaultMode } = {},
): Promise<void> {
const domain = 'localhost';
const base = { domain, path: '/' };
const driftCookie =
options.sessionDrift != null ? [{ ...base, name: 'gridpilot_session_drift', value: String(options.sessionDrift) }] : [];
const faultCookie =
options.faultMode != null ? [{ ...base, name: 'gridpilot_fault_mode', value: String(options.faultMode) }] : [];
await context.clearCookies();
if (auth === 'public') {
// Public access: no session cookie, only drift/fault cookies if specified
await context.addCookies([...driftCookie, ...faultCookie]);
return;
}
// For authenticated contexts, use normal login with seeded demo user
// Note: All auth contexts use the same seeded demo driver user for simplicity
// Role-based access control is tested separately in integration tests
const gpSessionValue = await ensureNormalSessionCookie();
// Only set gp_session cookie (no demo mode or sponsor cookies)
// For Docker/local testing, ensure cookies work with localhost
const sessionCookie = [{
...base,
name: 'gp_session',
value: gpSessionValue,
httpOnly: true,
secure: false, // Localhost doesn't need HTTPS
sameSite: 'Lax' as const // Ensure compatibility
}];
await context.addCookies([...sessionCookie, ...driftCookie, ...faultCookie]);
}
export type ConsoleCapture = {
consoleErrors: string[];
pageErrors: string[];
};
export function attachConsoleErrorCapture(page: Page): ConsoleCapture {
const consoleErrors: string[] = [];
const pageErrors: string[] = [];
page.on('pageerror', (err) => {
pageErrors.push(String(err));
});
page.on('console', (msg) => {
const type = msg.type();
if (type !== 'error') return;
const text = msg.text();
// Filter known benign warnings (keep small + generic).
if (text.includes('Download the React DevTools')) return;
// Next/Image accessibility warning (not a runtime failure for smoke coverage).
if (text.includes('Image is missing required "alt" property')) return;
// React controlled <select> warning (still renders fine; treat as non-fatal for route coverage).
if (text.includes('Use the `defaultValue` or `value` props on <select> instead of setting `selected` on <option>.')) return;
consoleErrors.push(`[${type}] ${text}`);
});
return { consoleErrors, pageErrors };
}