middleware test
This commit is contained in:
@@ -3,7 +3,7 @@ import { WebsiteRouteManager } from '../../shared/website/WebsiteRouteManager';
|
||||
import { WebsiteAuthManager } from '../../shared/website/WebsiteAuthManager';
|
||||
import { ConsoleErrorCapture } from '../../shared/website/ConsoleErrorCapture';
|
||||
|
||||
const API_BASE_URL = process.env.API_URL || 'http://localhost:3101';
|
||||
const WEBSITE_BASE_URL = process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:3000';
|
||||
|
||||
test.describe('Website Pages - TypeORM Integration', () => {
|
||||
let routeManager: WebsiteRouteManager;
|
||||
@@ -12,13 +12,13 @@ test.describe('Website Pages - TypeORM Integration', () => {
|
||||
routeManager = new WebsiteRouteManager();
|
||||
});
|
||||
|
||||
test('verify Docker and TypeORM are running', async ({ page }) => {
|
||||
const response = await page.goto(`${API_BASE_URL}/health`);
|
||||
test('website loads and connects to API', async ({ page }) => {
|
||||
// Test that the website loads
|
||||
const response = await page.goto(WEBSITE_BASE_URL);
|
||||
expect(response?.ok()).toBe(true);
|
||||
|
||||
const healthData = await response?.json().catch(() => null);
|
||||
expect(healthData).toBeTruthy();
|
||||
expect(healthData.database).toBe('connected');
|
||||
// Check that the page renders (body is visible)
|
||||
await expect(page.locator('body')).toBeVisible();
|
||||
});
|
||||
|
||||
test('all routes from RouteConfig are discoverable', async () => {
|
||||
@@ -31,8 +31,9 @@ test.describe('Website Pages - TypeORM Integration', () => {
|
||||
|
||||
for (const route of publicRoutes) {
|
||||
const path = routeManager.resolvePathTemplate(route.pathTemplate, route.params);
|
||||
const response = await page.goto(`${API_BASE_URL}${path}`);
|
||||
const response = await page.goto(`${WEBSITE_BASE_URL}${path}`);
|
||||
|
||||
// Should load successfully or show 404 page
|
||||
expect(response?.ok() || response?.status() === 404).toBeTruthy();
|
||||
}
|
||||
});
|
||||
@@ -43,7 +44,7 @@ test.describe('Website Pages - TypeORM Integration', () => {
|
||||
|
||||
for (const route of protectedRoutes) {
|
||||
const path = routeManager.resolvePathTemplate(route.pathTemplate, route.params);
|
||||
await page.goto(`${API_BASE_URL}${path}`);
|
||||
await page.goto(`${WEBSITE_BASE_URL}${path}`);
|
||||
|
||||
const currentUrl = new URL(page.url());
|
||||
expect(currentUrl.pathname).toBe('/auth/login');
|
||||
@@ -51,57 +52,71 @@ test.describe('Website Pages - TypeORM Integration', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('admin routes require admin role', async ({ page, browser }) => {
|
||||
test('admin routes require admin role', async ({ browser, request }) => {
|
||||
const routes = routeManager.getWebsiteRouteInventory();
|
||||
const adminRoutes = routes.filter(r => r.access === 'admin').slice(0, 2);
|
||||
|
||||
for (const route of adminRoutes) {
|
||||
const path = routeManager.resolvePathTemplate(route.pathTemplate, route.params);
|
||||
|
||||
|
||||
// Regular auth user should be blocked
|
||||
await WebsiteAuthManager.createAuthContext(browser, 'auth');
|
||||
await page.goto(`${API_BASE_URL}${path}`);
|
||||
expect(page.url().includes('login')).toBeTruthy();
|
||||
{
|
||||
const auth = await WebsiteAuthManager.createAuthContext(browser, request, 'auth');
|
||||
await auth.page.goto(`${WEBSITE_BASE_URL}${path}`);
|
||||
expect(auth.page.url().includes('login')).toBeTruthy();
|
||||
await auth.context.close();
|
||||
}
|
||||
|
||||
// Admin user should have access
|
||||
await WebsiteAuthManager.createAuthContext(browser, 'admin');
|
||||
await page.goto(`${API_BASE_URL}${path}`);
|
||||
expect(page.url().includes(path)).toBeTruthy();
|
||||
{
|
||||
const admin = await WebsiteAuthManager.createAuthContext(browser, request, 'admin');
|
||||
await admin.page.goto(`${WEBSITE_BASE_URL}${path}`);
|
||||
expect(admin.page.url().includes(path)).toBeTruthy();
|
||||
await admin.context.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('sponsor routes require sponsor role', async ({ page, browser }) => {
|
||||
test('sponsor routes require sponsor role', async ({ browser, request }) => {
|
||||
const routes = routeManager.getWebsiteRouteInventory();
|
||||
const sponsorRoutes = routes.filter(r => r.access === 'sponsor').slice(0, 2);
|
||||
|
||||
for (const route of sponsorRoutes) {
|
||||
const path = routeManager.resolvePathTemplate(route.pathTemplate, route.params);
|
||||
|
||||
|
||||
// Regular auth user should be blocked
|
||||
await WebsiteAuthManager.createAuthContext(browser, 'auth');
|
||||
await page.goto(`${API_BASE_URL}${path}`);
|
||||
expect(page.url().includes('login')).toBeTruthy();
|
||||
{
|
||||
const auth = await WebsiteAuthManager.createAuthContext(browser, request, 'auth');
|
||||
await auth.page.goto(`${WEBSITE_BASE_URL}${path}`);
|
||||
expect(auth.page.url().includes('login')).toBeTruthy();
|
||||
await auth.context.close();
|
||||
}
|
||||
|
||||
// Sponsor user should have access
|
||||
await WebsiteAuthManager.createAuthContext(browser, 'sponsor');
|
||||
await page.goto(`${API_BASE_URL}${path}`);
|
||||
expect(page.url().includes(path)).toBeTruthy();
|
||||
{
|
||||
const sponsor = await WebsiteAuthManager.createAuthContext(browser, request, 'sponsor');
|
||||
await sponsor.page.goto(`${WEBSITE_BASE_URL}${path}`);
|
||||
expect(sponsor.page.url().includes(path)).toBeTruthy();
|
||||
await sponsor.context.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('auth routes redirect authenticated users away', async ({ page, browser }) => {
|
||||
test('auth routes redirect authenticated users away', async ({ browser, request }) => {
|
||||
const routes = routeManager.getWebsiteRouteInventory();
|
||||
const authRoutes = routes.filter(r => r.access === 'auth').slice(0, 2);
|
||||
|
||||
for (const route of authRoutes) {
|
||||
const path = routeManager.resolvePathTemplate(route.pathTemplate, route.params);
|
||||
|
||||
await WebsiteAuthManager.createAuthContext(browser, 'auth');
|
||||
await page.goto(`${API_BASE_URL}${path}`);
|
||||
|
||||
|
||||
const auth = await WebsiteAuthManager.createAuthContext(browser, request, 'auth');
|
||||
await auth.page.goto(`${WEBSITE_BASE_URL}${path}`);
|
||||
|
||||
// Should redirect to dashboard or stay on the page
|
||||
const currentUrl = page.url();
|
||||
const currentUrl = auth.page.url();
|
||||
expect(currentUrl.includes('dashboard') || currentUrl.includes(path)).toBeTruthy();
|
||||
|
||||
await auth.context.close();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -110,7 +125,7 @@ test.describe('Website Pages - TypeORM Integration', () => {
|
||||
|
||||
for (const route of edgeCases) {
|
||||
const path = routeManager.resolvePathTemplate(route.pathTemplate, route.params);
|
||||
const response = await page.goto(`${API_BASE_URL}${path}`);
|
||||
const response = await page.goto(`${WEBSITE_BASE_URL}${path}`);
|
||||
|
||||
if (route.allowNotFound) {
|
||||
expect(response?.status() === 404 || response?.status() === 500).toBeTruthy();
|
||||
@@ -125,7 +140,7 @@ test.describe('Website Pages - TypeORM Integration', () => {
|
||||
const capture = new ConsoleErrorCapture(page);
|
||||
const path = routeManager.resolvePathTemplate(route.pathTemplate, route.params);
|
||||
|
||||
await page.goto(`${API_BASE_URL}${path}`);
|
||||
await page.goto(`${WEBSITE_BASE_URL}${path}`);
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const errors = capture.getErrors();
|
||||
@@ -139,7 +154,7 @@ test.describe('Website Pages - TypeORM Integration', () => {
|
||||
|
||||
for (const route of testRoutes) {
|
||||
const path = routeManager.resolvePathTemplate(route.pathTemplate, route.params);
|
||||
const response = await page.goto(`${API_BASE_URL}${path}`);
|
||||
const response = await page.goto(`${WEBSITE_BASE_URL}${path}`);
|
||||
|
||||
expect(response?.ok() || response?.status() === 404).toBeTruthy();
|
||||
}
|
||||
@@ -152,7 +167,7 @@ test.describe('Website Pages - TypeORM Integration', () => {
|
||||
const path = routeManager.resolvePathTemplate(route.pathTemplate, route.params);
|
||||
|
||||
// Try accessing protected route without auth
|
||||
await page.goto(`${API_BASE_URL}${path}`);
|
||||
await page.goto(`${WEBSITE_BASE_URL}${path}`);
|
||||
const currentUrl = page.url();
|
||||
|
||||
expect(currentUrl.includes('login') || currentUrl.includes('auth')).toBeTruthy();
|
||||
@@ -167,7 +182,7 @@ test.describe('Website Pages - TypeORM Integration', () => {
|
||||
];
|
||||
|
||||
for (const route of invalidRoutes) {
|
||||
const response = await page.goto(`${API_BASE_URL}${route}`);
|
||||
const response = await page.goto(`${WEBSITE_BASE_URL}${route}`);
|
||||
|
||||
const status = response?.status();
|
||||
const url = page.url();
|
||||
|
||||
@@ -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!' };
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user