183 lines
5.7 KiB
TypeScript
183 lines
5.7 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';
|
|
|
|
export function authContextForAccess(access: RouteAccess): WebsiteAuthContext {
|
|
if (access === 'public') return 'public';
|
|
if (access === 'auth') return 'auth';
|
|
if (access === 'admin') return 'admin';
|
|
return 'sponsor';
|
|
}
|
|
|
|
export async function setWebsiteAuthContext(
|
|
context: BrowserContext,
|
|
auth: WebsiteAuthContext,
|
|
options: { sessionDrift?: WebsiteSessionDriftMode; faultMode?: WebsiteFaultMode } = {},
|
|
): Promise<void> {
|
|
const domain = 'localhost';
|
|
const base = { domain, path: '/' };
|
|
|
|
// The website uses `gp_session` cookie for authentication
|
|
// For smoke tests, we now use demo login API to get real session cookies
|
|
// instead of static cookie values
|
|
|
|
if (auth === 'public') {
|
|
// No authentication needed
|
|
await context.clearCookies();
|
|
return;
|
|
}
|
|
|
|
// For authenticated contexts, we need to perform a demo login
|
|
// This ensures we get real session cookies with proper structure
|
|
|
|
// Determine which demo role to use based on auth context
|
|
let demoRole: string;
|
|
switch (auth) {
|
|
case 'sponsor':
|
|
demoRole = 'sponsor';
|
|
break;
|
|
case 'admin':
|
|
demoRole = 'league-admin'; // Real admin role from AuthSessionDTO
|
|
break;
|
|
case 'auth':
|
|
default:
|
|
demoRole = 'driver';
|
|
break;
|
|
}
|
|
|
|
// Call the demo login API directly (not through Next.js rewrite)
|
|
// This bypasses any proxy/cookie issues
|
|
const response = await fetch('http://localhost:3101/auth/demo-login', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
role: demoRole,
|
|
rememberMe: true
|
|
}),
|
|
credentials: 'include',
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Demo login failed: ${response.status}`);
|
|
}
|
|
|
|
// Extract cookies from the response
|
|
const setCookieHeader = response.headers.get('set-cookie');
|
|
if (!setCookieHeader) {
|
|
throw new Error('No cookies set by demo login');
|
|
}
|
|
|
|
// Parse the Set-Cookie headers
|
|
const cookies = setCookieHeader.split(',').map(cookieStr => {
|
|
const parts = cookieStr.split(';').map(p => p.trim());
|
|
const [nameValue, ...attributes] = parts;
|
|
const [name, value] = nameValue.split('=');
|
|
|
|
const cookie: any = {
|
|
name,
|
|
value: decodeURIComponent(value),
|
|
domain: 'localhost',
|
|
path: '/',
|
|
expires: Math.floor(Date.now() / 1000) + 3600, // 1 hour
|
|
httpOnly: false,
|
|
secure: false,
|
|
sameSite: 'Lax' as const
|
|
};
|
|
|
|
for (const attr of attributes) {
|
|
const [attrName, attrValue] = attr.split('=');
|
|
const lowerName = attrName.toLowerCase();
|
|
|
|
if (lowerName === 'path') cookie.path = attrValue;
|
|
else if (lowerName === 'httponly') cookie.httpOnly = true;
|
|
else if (lowerName === 'secure') cookie.secure = true;
|
|
else if (lowerName === 'samesite') cookie.sameSite = attrValue as any;
|
|
else if (lowerName === 'domain') {
|
|
// Skip domain from API - we'll use localhost
|
|
}
|
|
else if (lowerName === 'max-age') cookie.expires = Math.floor(Date.now() / 1000) + parseInt(attrValue);
|
|
}
|
|
|
|
// For Docker/local testing, ensure cookies work with localhost
|
|
// Playwright's context.addCookies requires specific settings for localhost
|
|
if (cookie.domain === 'localhost') {
|
|
cookie.secure = false; // Localhost doesn't need HTTPS
|
|
// Keep sameSite as provided by API, but ensure it's compatible
|
|
if (cookie.sameSite === 'None') {
|
|
// For SameSite=None, we need Secure=true, but localhost doesn't support it
|
|
// So we fall back to Lax for local testing
|
|
cookie.sameSite = 'Lax';
|
|
}
|
|
}
|
|
|
|
return cookie;
|
|
});
|
|
|
|
// Apply session drift or fault modes if specified
|
|
if (options.sessionDrift || options.faultMode) {
|
|
const sessionCookie = cookies.find(c => c.name === 'gp_session');
|
|
|
|
if (sessionCookie) {
|
|
if (options.sessionDrift) {
|
|
sessionCookie.value = `drift-${options.sessionDrift}-${sessionCookie.value}`;
|
|
}
|
|
|
|
if (options.faultMode) {
|
|
cookies.push({
|
|
name: 'gridpilot_fault_mode',
|
|
value: options.faultMode,
|
|
domain,
|
|
path: '/',
|
|
expires: Math.floor(Date.now() / 1000) + 3600,
|
|
httpOnly: false,
|
|
secure: false,
|
|
sameSite: 'Lax' as const
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Clear existing cookies and add the new ones
|
|
await context.clearCookies();
|
|
await context.addCookies(cookies);
|
|
}
|
|
|
|
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 };
|
|
} |