middleware test

This commit is contained in:
2026-01-03 22:05:00 +01:00
parent c589b3c3fe
commit bc7cb2e20a
7 changed files with 680 additions and 78 deletions

View File

@@ -1,20 +1,110 @@
import { BrowserContext, Browser } from '@playwright/test';
import { APIRequestContext, Browser, BrowserContext, Page } from '@playwright/test';
type AuthRole = 'auth' | 'admin' | 'sponsor';
type Credentials = {
email: string;
password: string;
};
export interface AuthContext {
context: BrowserContext;
role: 'auth' | 'admin' | 'sponsor';
page: Page;
role: AuthRole;
}
export class WebsiteAuthManager {
static async createAuthContext(browser: Browser, role: AuthRole): Promise<AuthContext>;
static async createAuthContext(browser: Browser, request: APIRequestContext, role: AuthRole): Promise<AuthContext>;
static async createAuthContext(
browser: Browser,
role: 'auth' | 'admin' | 'sponsor'
requestOrRole: APIRequestContext | AuthRole,
maybeRole?: AuthRole,
): Promise<AuthContext> {
const context = await browser.newContext();
const baseURL = process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:3000';
const apiBaseUrl = process.env.API_BASE_URL || 'http://localhost:3101';
const role = (typeof requestOrRole === 'string' ? requestOrRole : maybeRole) as AuthRole;
const request = typeof requestOrRole === 'string' ? null : requestOrRole;
const context = await browser.newContext({ baseURL });
if (request) {
const token = await WebsiteAuthManager.loginViaApi(request, apiBaseUrl, role);
// Critical: the website (localhost:3000) must receive `gp_session` so middleware can forward it.
await context.addCookies([
{
name: 'gp_session',
value: token,
url: baseURL,
path: '/',
httpOnly: true,
sameSite: 'Lax',
},
]);
}
const page = await context.newPage();
if (!request) {
await WebsiteAuthManager.loginViaUi(page, role);
}
return {
context,
page,
role,
};
}
private static async loginViaApi(
request: APIRequestContext,
apiBaseUrl: string,
role: AuthRole,
): Promise<string> {
const credentials = WebsiteAuthManager.getCredentials(role);
const res = await request.post(`${apiBaseUrl}/auth/login`, {
data: {
email: credentials.email,
password: credentials.password,
},
});
const setCookie = res.headers()['set-cookie'] ?? '';
const cookiePart = setCookie.split(';')[0] ?? '';
const token = cookiePart.startsWith('gp_session=') ? cookiePart.slice('gp_session='.length) : '';
if (!token) {
throw new Error(`Expected gp_session cookie from ${apiBaseUrl}/auth/login`);
}
return token;
}
private static async loginViaUi(page: Page, role: AuthRole): Promise<void> {
const credentials = WebsiteAuthManager.getCredentials(role);
await page.goto('/auth/login');
await page.getByLabel('Email Address').fill(credentials.email);
await page.getByLabel('Password').fill(credentials.password);
await Promise.all([
page.getByRole('button', { name: 'Sign In' }).click(),
page.waitForURL((url) => !url.pathname.startsWith('/auth/login'), { timeout: 15_000 }),
]);
}
private static getCredentials(role: AuthRole): Credentials {
if (role === 'admin') {
return { email: 'demo.admin@example.com', password: 'Demo1234!' };
}
if (role === 'sponsor') {
return { email: 'demo.sponsor@example.com', password: 'Demo1234!' };
}
return { email: 'demo.driver@example.com', password: 'Demo1234!' };
}
}

View File

@@ -1,5 +1,4 @@
import { routes, routeMatchers } from '../../../apps/website/lib/routing/RouteConfig';
import type { RouteGroup } from '../../../apps/website/lib/routing/RouteConfig';
export type RouteAccess = 'public' | 'auth' | 'admin' | 'sponsor';
export type RouteParams = Record<string, string>;
@@ -33,35 +32,37 @@ export class WebsiteRouteManager {
public getWebsiteRouteInventory(): WebsiteRouteDefinition[] {
const result: WebsiteRouteDefinition[] = [];
const processGroup = (group: keyof RouteGroup, groupRoutes: Record<string, string | ((id: string) => string)>) => {
Object.values(groupRoutes).forEach((value) => {
if (typeof value === 'function') {
const template = value(WebsiteRouteManager.IDs.LEAGUE);
result.push({
pathTemplate: template,
params: { id: WebsiteRouteManager.IDs.LEAGUE },
access: group as RouteAccess,
});
} else {
result.push({
pathTemplate: value,
access: group as RouteAccess,
});
}
const pushRoute = (pathTemplate: string, params?: RouteParams) => {
result.push({
pathTemplate,
...(params ? { params } : {}),
access: this.getAccessLevel(pathTemplate),
});
};
processGroup('auth', routes.auth);
processGroup('public', routes.public);
processGroup('protected', routes.protected);
processGroup('sponsor', routes.sponsor);
processGroup('admin', routes.admin);
processGroup('league', routes.league);
processGroup('race', routes.race);
processGroup('team', routes.team);
processGroup('driver', routes.driver);
processGroup('error', routes.error);
const processGroup = (groupRoutes: Record<string, string | ((id: string) => string)>) => {
Object.values(groupRoutes).forEach((value) => {
if (typeof value === 'function') {
const template = value(WebsiteRouteManager.IDs.LEAGUE);
pushRoute(template, { id: WebsiteRouteManager.IDs.LEAGUE });
return;
}
pushRoute(value);
});
};
processGroup(routes.auth);
processGroup(routes.public);
processGroup(routes.protected);
processGroup(routes.sponsor);
processGroup(routes.admin);
processGroup(routes.league);
processGroup(routes.race);
processGroup(routes.team);
processGroup(routes.driver);
processGroup(routes.error);
return result.sort((a, b) => a.pathTemplate.localeCompare(b.pathTemplate));
}
@@ -87,9 +88,11 @@ export class WebsiteRouteManager {
}
public getAccessLevel(pathTemplate: string): RouteAccess {
if (routeMatchers.isInGroup(pathTemplate, 'public')) return 'public';
// NOTE: `routeMatchers.isInGroup(path, 'public')` is prefix-based and will treat everything
// as public because the home route is `/`. Use `isPublic()` for correct classification.
if (routeMatchers.isInGroup(pathTemplate, 'admin')) return 'admin';
if (routeMatchers.isInGroup(pathTemplate, 'sponsor')) return 'sponsor';
if (routeMatchers.isPublic(pathTemplate)) return 'public';
if (routeMatchers.requiresAuth(pathTemplate)) return 'auth';
return 'public';
}