clean routes
This commit is contained in:
362
tests/integration/website/auth-flow.test.ts
Normal file
362
tests/integration/website/auth-flow.test.ts
Normal file
@@ -0,0 +1,362 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import {
|
||||
setWebsiteAuthContext,
|
||||
} from './websiteAuth';
|
||||
import {
|
||||
getWebsiteRouteInventory,
|
||||
resolvePathTemplate,
|
||||
} from './websiteRouteInventory';
|
||||
|
||||
/**
|
||||
* Website Authentication Flow Integration Tests
|
||||
*
|
||||
* These tests verify the complete authentication flow including:
|
||||
* - Middleware route protection
|
||||
* - AuthGuard component functionality
|
||||
* - Session management and loading states
|
||||
* - Role-based access control
|
||||
* - Auth state transitions
|
||||
* - API integration
|
||||
*/
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
test.describe('Website Auth Flow - Middleware Protection', () => {
|
||||
const routes = getWebsiteRouteInventory();
|
||||
|
||||
// Test public routes are accessible without auth
|
||||
test('public routes are accessible without authentication', async ({ page, context }) => {
|
||||
const publicRoutes = routes.filter(r => r.access === 'public');
|
||||
expect(publicRoutes.length).toBeGreaterThan(0);
|
||||
|
||||
for (const route of publicRoutes.slice(0, 5)) { // Test first 5 to keep test fast
|
||||
const resolvedPath = resolvePathTemplate(route.pathTemplate, route.params);
|
||||
|
||||
await setWebsiteAuthContext(context, 'public');
|
||||
const response = await page.goto(`${getWebsiteBaseUrl()}${resolvedPath}`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
expect(response?.status()).toBe(200);
|
||||
await expect(page.locator('body')).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
// Test protected routes redirect unauthenticated users
|
||||
test('protected routes redirect unauthenticated users to login', async ({ page, context }) => {
|
||||
const protectedRoutes = routes.filter(r => r.access !== 'public');
|
||||
expect(protectedRoutes.length).toBeGreaterThan(0);
|
||||
|
||||
for (const route of protectedRoutes.slice(0, 3)) { // Test first 3
|
||||
const resolvedPath = resolvePathTemplate(route.pathTemplate, route.params);
|
||||
|
||||
await setWebsiteAuthContext(context, 'public');
|
||||
await page.goto(`${getWebsiteBaseUrl()}${resolvedPath}`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
const currentUrl = new URL(page.url());
|
||||
expect(currentUrl.pathname).toBe('/auth/login');
|
||||
expect(currentUrl.searchParams.get('returnTo')).toBe(resolvedPath);
|
||||
}
|
||||
});
|
||||
|
||||
// Test authenticated users can access protected routes
|
||||
test('authenticated users can access protected routes', async ({ page, context }) => {
|
||||
const authRoutes = routes.filter(r => r.access === 'auth');
|
||||
expect(authRoutes.length).toBeGreaterThan(0);
|
||||
|
||||
for (const route of authRoutes.slice(0, 3)) {
|
||||
const resolvedPath = resolvePathTemplate(route.pathTemplate, route.params);
|
||||
|
||||
await setWebsiteAuthContext(context, 'auth');
|
||||
const response = await page.goto(`${getWebsiteBaseUrl()}${resolvedPath}`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
expect(response?.status()).toBe(200);
|
||||
await expect(page.locator('body')).toBeVisible();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Website Auth Flow - AuthGuard Component', () => {
|
||||
test('dashboard route shows loading state then content', async ({ page, context }) => {
|
||||
await setWebsiteAuthContext(context, 'auth');
|
||||
|
||||
const navigationPromise = page.waitForNavigation({ waitUntil: 'domcontentloaded' });
|
||||
await page.goto(`${getWebsiteBaseUrl()}/dashboard`);
|
||||
await navigationPromise;
|
||||
|
||||
// Should show loading briefly then render dashboard
|
||||
await expect(page.locator('body')).toBeVisible();
|
||||
expect(page.url()).toContain('/dashboard');
|
||||
});
|
||||
|
||||
test('dashboard redirects unauthenticated users', async ({ page, context }) => {
|
||||
await setWebsiteAuthContext(context, 'public');
|
||||
|
||||
await page.goto(`${getWebsiteBaseUrl()}/dashboard`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Should redirect to login with returnTo parameter
|
||||
const currentUrl = new URL(page.url());
|
||||
expect(currentUrl.pathname).toBe('/auth/login');
|
||||
expect(currentUrl.searchParams.get('returnTo')).toBe('/dashboard');
|
||||
});
|
||||
|
||||
test('admin routes require admin role', async ({ page, context }) => {
|
||||
// Test as regular driver (should be denied)
|
||||
await setWebsiteAuthContext(context, 'auth');
|
||||
await page.goto(`${getWebsiteBaseUrl()}/admin`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Should redirect to login (no admin role)
|
||||
const currentUrl = new URL(page.url());
|
||||
expect(currentUrl.pathname).toBe('/auth/login');
|
||||
|
||||
// Test as admin (should be allowed)
|
||||
await setWebsiteAuthContext(context, 'admin');
|
||||
await page.goto(`${getWebsiteBaseUrl()}/admin`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
expect(page.url()).toContain('/admin');
|
||||
await expect(page.locator('body')).toBeVisible();
|
||||
});
|
||||
|
||||
test('sponsor routes require sponsor role', async ({ page, context }) => {
|
||||
// Test as driver (should be denied)
|
||||
await setWebsiteAuthContext(context, 'auth');
|
||||
await page.goto(`${getWebsiteBaseUrl()}/sponsor/dashboard`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
const currentUrl = new URL(page.url());
|
||||
expect(currentUrl.pathname).toBe('/auth/login');
|
||||
|
||||
// Test as sponsor (should be allowed)
|
||||
await setWebsiteAuthContext(context, 'sponsor');
|
||||
await page.goto(`${getWebsiteBaseUrl()}/sponsor/dashboard`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
expect(page.url()).toContain('/sponsor/dashboard');
|
||||
await expect(page.locator('body')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Website Auth Flow - Session Management', () => {
|
||||
test('session is properly loaded on page visit', async ({ page, context }) => {
|
||||
await setWebsiteAuthContext(context, 'auth');
|
||||
|
||||
// Visit dashboard
|
||||
await page.goto(`${getWebsiteBaseUrl()}/dashboard`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Verify session is available by checking for user-specific content
|
||||
// (This would depend on your actual UI, but we can verify no errors)
|
||||
await expect(page.locator('body')).toBeVisible();
|
||||
});
|
||||
|
||||
test('logout clears session and redirects', async ({ page, context }) => {
|
||||
await setWebsiteAuthContext(context, 'auth');
|
||||
|
||||
// Go to dashboard
|
||||
await page.goto(`${getWebsiteBaseUrl()}/dashboard`, { waitUntil: 'domcontentloaded' });
|
||||
await expect(page.locator('body')).toBeVisible();
|
||||
|
||||
// Find and click logout (assuming it exists)
|
||||
// This test would need to be adapted based on actual logout implementation
|
||||
// For now, we'll test that clearing cookies works
|
||||
await context.clearCookies();
|
||||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Should redirect to login
|
||||
const currentUrl = new URL(page.url());
|
||||
expect(currentUrl.pathname).toBe('/auth/login');
|
||||
});
|
||||
|
||||
test('auth state transitions work correctly', async ({ page, context }) => {
|
||||
// Start unauthenticated
|
||||
await setWebsiteAuthContext(context, 'public');
|
||||
await page.goto(`${getWebsiteBaseUrl()}/dashboard`, { waitUntil: 'domcontentloaded' });
|
||||
expect(new URL(page.url()).pathname).toBe('/auth/login');
|
||||
|
||||
// Simulate login by setting auth context
|
||||
await setWebsiteAuthContext(context, 'auth');
|
||||
await page.goto(`${getWebsiteBaseUrl()}/dashboard`, { waitUntil: 'domcontentloaded' });
|
||||
expect(new URL(page.url()).pathname).toBe('/dashboard');
|
||||
|
||||
// Simulate logout
|
||||
await setWebsiteAuthContext(context, 'public');
|
||||
await page.goto(`${getWebsiteBaseUrl()}/dashboard`, { waitUntil: 'domcontentloaded' });
|
||||
expect(new URL(page.url()).pathname).toBe('/auth/login');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Website Auth Flow - API Integration', () => {
|
||||
test('session endpoint returns correct data', async ({ page, context }) => {
|
||||
await setWebsiteAuthContext(context, 'auth');
|
||||
|
||||
// Direct API call to verify session endpoint
|
||||
const response = await page.request.get(`${getWebsiteBaseUrl()}/api/auth/session`);
|
||||
expect(response.ok()).toBe(true);
|
||||
|
||||
const session = await response.json();
|
||||
expect(session).toBeDefined();
|
||||
});
|
||||
|
||||
test('demo login flow works', async ({ page, context }) => {
|
||||
// Clear any existing cookies
|
||||
await context.clearCookies();
|
||||
|
||||
// Navigate to login page
|
||||
await page.goto(`${getWebsiteBaseUrl()}/auth/login`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Verify login page loads
|
||||
await expect(page.locator('body')).toBeVisible();
|
||||
|
||||
// Note: Actual demo login form interaction would go here
|
||||
// For now, we'll test the API endpoint directly
|
||||
const response = await page.request.post(`${getWebsiteBaseUrl()}/api/auth/demo-login`, {
|
||||
data: { role: 'driver' }
|
||||
});
|
||||
|
||||
expect(response.ok()).toBe(true);
|
||||
|
||||
// Verify cookies were set
|
||||
const cookies = await context.cookies();
|
||||
const gpSession = cookies.find(c => c.name === 'gp_session');
|
||||
expect(gpSession).toBeDefined();
|
||||
});
|
||||
|
||||
test('auth API handles different roles correctly', async ({ page }) => {
|
||||
const roles = ['driver', 'sponsor', 'admin'] as const;
|
||||
|
||||
for (const role of roles) {
|
||||
const response = await page.request.post(`${getWebsiteBaseUrl()}/api/auth/demo-login`, {
|
||||
data: { role }
|
||||
});
|
||||
|
||||
expect(response.ok()).toBe(true);
|
||||
|
||||
const session = await response.json();
|
||||
expect(session.user).toBeDefined();
|
||||
|
||||
// Verify role-specific data
|
||||
if (role === 'sponsor') {
|
||||
expect(session.user.sponsorId).toBeDefined();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Website Auth Flow - Edge Cases', () => {
|
||||
test('handles auth state drift gracefully', async ({ page, context }) => {
|
||||
// Set sponsor context but with missing sponsor ID
|
||||
await setWebsiteAuthContext(context, 'sponsor', { sessionDrift: 'missing-sponsor-id' });
|
||||
|
||||
await page.goto(`${getWebsiteBaseUrl()}/sponsor/dashboard`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Should redirect to login due to invalid session
|
||||
const currentUrl = new URL(page.url());
|
||||
expect(currentUrl.pathname).toBe('/auth/login');
|
||||
});
|
||||
|
||||
test('handles expired session', async ({ page, context }) => {
|
||||
await setWebsiteAuthContext(context, 'sponsor', { sessionDrift: 'expired' });
|
||||
|
||||
await page.goto(`${getWebsiteBaseUrl()}/sponsor/dashboard`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Should redirect to login
|
||||
const currentUrl = new URL(page.url());
|
||||
expect(currentUrl.pathname).toBe('/auth/login');
|
||||
});
|
||||
|
||||
test('handles invalid session cookie', async ({ page, context }) => {
|
||||
await setWebsiteAuthContext(context, 'sponsor', { sessionDrift: 'invalid-cookie' });
|
||||
|
||||
await page.goto(`${getWebsiteBaseUrl()}/sponsor/dashboard`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Should redirect to login
|
||||
const currentUrl = new URL(page.url());
|
||||
expect(currentUrl.pathname).toBe('/auth/login');
|
||||
});
|
||||
|
||||
test('public routes accessible even with invalid auth cookies', async ({ page, context }) => {
|
||||
await setWebsiteAuthContext(context, 'public', { sessionDrift: 'invalid-cookie' });
|
||||
|
||||
await page.goto(`${getWebsiteBaseUrl()}/leagues`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Should still work
|
||||
expect(page.url()).toContain('/leagues');
|
||||
await expect(page.locator('body')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Website Auth Flow - Redirect Scenarios', () => {
|
||||
test('auth routes redirect authenticated users away', async ({ page, context }) => {
|
||||
await setWebsiteAuthContext(context, 'auth');
|
||||
|
||||
// Try to access login page while authenticated
|
||||
await page.goto(`${getWebsiteBaseUrl()}/auth/login`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Should redirect to dashboard
|
||||
const currentUrl = new URL(page.url());
|
||||
expect(currentUrl.pathname).toBe('/dashboard');
|
||||
});
|
||||
|
||||
test('sponsor auth routes redirect to sponsor dashboard', async ({ page, context }) => {
|
||||
await setWebsiteAuthContext(context, 'sponsor');
|
||||
|
||||
await page.goto(`${getWebsiteBaseUrl()}/auth/login`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Should redirect to sponsor dashboard
|
||||
const currentUrl = new URL(page.url());
|
||||
expect(currentUrl.pathname).toBe('/sponsor/dashboard');
|
||||
});
|
||||
|
||||
test('returnTo parameter works correctly', async ({ page, context }) => {
|
||||
await setWebsiteAuthContext(context, 'public');
|
||||
|
||||
const targetRoute = '/leagues/league-1/settings';
|
||||
await page.goto(`${getWebsiteBaseUrl()}${targetRoute}`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Should redirect to login with returnTo
|
||||
const currentUrl = new URL(page.url());
|
||||
expect(currentUrl.pathname).toBe('/auth/login');
|
||||
expect(currentUrl.searchParams.get('returnTo')).toBe(targetRoute);
|
||||
|
||||
// After login, should return to target
|
||||
await setWebsiteAuthContext(context, 'admin');
|
||||
await page.goto(`${getWebsiteBaseUrl()}${targetRoute}`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
expect(page.url()).toContain(targetRoute);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Website Auth Flow - Performance', () => {
|
||||
test('auth verification completes quickly', async ({ page, context }) => {
|
||||
await setWebsiteAuthContext(context, 'auth');
|
||||
|
||||
const startTime = Date.now();
|
||||
await page.goto(`${getWebsiteBaseUrl()}/dashboard`, { waitUntil: 'domcontentloaded' });
|
||||
const endTime = Date.now();
|
||||
|
||||
// Should complete within reasonable time (under 5 seconds)
|
||||
expect(endTime - startTime).toBeLessThan(5000);
|
||||
|
||||
// Should show content
|
||||
await expect(page.locator('body')).toBeVisible();
|
||||
});
|
||||
|
||||
test('no infinite loading states', async ({ page, context }) => {
|
||||
await setWebsiteAuthContext(context, 'auth');
|
||||
|
||||
// Monitor for loading indicators
|
||||
let loadingCount = 0;
|
||||
page.on('request', (req) => {
|
||||
if (req.url().includes('/auth/session')) loadingCount++;
|
||||
});
|
||||
|
||||
await page.goto(`${getWebsiteBaseUrl()}/dashboard`, { waitUntil: 'networkidle' });
|
||||
|
||||
// Should not make excessive session calls
|
||||
expect(loadingCount).toBeLessThan(3);
|
||||
|
||||
// Should eventually show content
|
||||
await expect(page.locator('body')).toBeVisible();
|
||||
});
|
||||
});
|
||||
343
tests/integration/website/auth-guard.test.ts
Normal file
343
tests/integration/website/auth-guard.test.ts
Normal file
@@ -0,0 +1,343 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { setWebsiteAuthContext } from './websiteAuth';
|
||||
|
||||
/**
|
||||
* Website AuthGuard Component Tests
|
||||
*
|
||||
* These tests verify the AuthGuard component behavior:
|
||||
* - Loading states during session verification
|
||||
* - Redirect behavior for unauthorized access
|
||||
* - Role-based access control
|
||||
* - Component rendering with different auth states
|
||||
*/
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
test.describe('AuthGuard Component - Loading States', () => {
|
||||
test('shows loading state during session verification', async ({ page, context }) => {
|
||||
await setWebsiteAuthContext(context, 'auth');
|
||||
|
||||
// Monitor for loading indicators
|
||||
page.on('request', async (req) => {
|
||||
if (req.url().includes('/auth/session')) {
|
||||
// Check if loading indicator is visible during session fetch
|
||||
await page.locator('text=/Verifying authentication|Loading/').isVisible().catch(() => false);
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto(`${getWebsiteBaseUrl()}/dashboard`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Should eventually show dashboard content
|
||||
await expect(page.locator('body')).toBeVisible();
|
||||
expect(page.url()).toContain('/dashboard');
|
||||
});
|
||||
|
||||
test('handles rapid auth state changes', async ({ page, context }) => {
|
||||
// Start unauthenticated
|
||||
await setWebsiteAuthContext(context, 'public');
|
||||
await page.goto(`${getWebsiteBaseUrl()}/dashboard`, { waitUntil: 'domcontentloaded' });
|
||||
expect(new URL(page.url()).pathname).toBe('/auth/login');
|
||||
|
||||
// Quickly switch to authenticated
|
||||
await setWebsiteAuthContext(context, 'auth');
|
||||
await page.goto(`${getWebsiteBaseUrl()}/dashboard`, { waitUntil: 'domcontentloaded' });
|
||||
expect(new URL(page.url()).pathname).toBe('/dashboard');
|
||||
|
||||
// Quickly switch back
|
||||
await setWebsiteAuthContext(context, 'public');
|
||||
await page.goto(`${getWebsiteBaseUrl()}/dashboard`, { waitUntil: 'domcontentloaded' });
|
||||
expect(new URL(page.url()).pathname).toBe('/auth/login');
|
||||
});
|
||||
|
||||
test('handles session fetch failures gracefully', async ({ page, context }) => {
|
||||
// Clear cookies to simulate session fetch returning null
|
||||
await context.clearCookies();
|
||||
|
||||
await page.goto(`${getWebsiteBaseUrl()}/dashboard`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Should redirect to login
|
||||
const currentUrl = new URL(page.url());
|
||||
expect(currentUrl.pathname).toBe('/auth/login');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('AuthGuard Component - Redirect Behavior', () => {
|
||||
test('redirects to login with returnTo parameter', async ({ page, context }) => {
|
||||
await setWebsiteAuthContext(context, 'public');
|
||||
|
||||
const protectedRoutes = [
|
||||
'/dashboard',
|
||||
'/profile',
|
||||
'/leagues/league-1/settings',
|
||||
'/sponsor/dashboard',
|
||||
];
|
||||
|
||||
for (const route of protectedRoutes) {
|
||||
await page.goto(`${getWebsiteBaseUrl()}${route}`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
const currentUrl = new URL(page.url());
|
||||
expect(currentUrl.pathname).toBe('/auth/login');
|
||||
expect(currentUrl.searchParams.get('returnTo')).toBe(route);
|
||||
}
|
||||
});
|
||||
|
||||
test('redirects back to protected route after login', async ({ page, context }) => {
|
||||
const targetRoute = '/leagues/league-1/settings';
|
||||
|
||||
// Start unauthenticated, try to access protected route
|
||||
await setWebsiteAuthContext(context, 'public');
|
||||
await page.goto(`${getWebsiteBaseUrl()}${targetRoute}`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Verify redirect to login
|
||||
let currentUrl = new URL(page.url());
|
||||
expect(currentUrl.pathname).toBe('/auth/login');
|
||||
expect(currentUrl.searchParams.get('returnTo')).toBe(targetRoute);
|
||||
|
||||
// Simulate login by switching auth context
|
||||
await setWebsiteAuthContext(context, 'admin');
|
||||
await page.goto(`${getWebsiteBaseUrl()}${targetRoute}`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Should be on target route
|
||||
currentUrl = new URL(page.url());
|
||||
expect(currentUrl.pathname).toBe(targetRoute);
|
||||
});
|
||||
|
||||
test('handles auth routes when authenticated', async ({ page, context }) => {
|
||||
await setWebsiteAuthContext(context, 'auth');
|
||||
|
||||
// Try to access login page while authenticated
|
||||
await page.goto(`${getWebsiteBaseUrl()}/auth/login`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Should redirect to dashboard
|
||||
const currentUrl = new URL(page.url());
|
||||
expect(currentUrl.pathname).toBe('/dashboard');
|
||||
});
|
||||
|
||||
test('sponsor auth routes redirect to sponsor dashboard', async ({ page, context }) => {
|
||||
await setWebsiteAuthContext(context, 'sponsor');
|
||||
|
||||
await page.goto(`${getWebsiteBaseUrl()}/auth/login`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
const currentUrl = new URL(page.url());
|
||||
expect(currentUrl.pathname).toBe('/sponsor/dashboard');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('AuthGuard Component - Role-Based Access', () => {
|
||||
test('admin routes allow admin users', async ({ page, context }) => {
|
||||
await setWebsiteAuthContext(context, 'admin');
|
||||
|
||||
const adminRoutes = ['/admin', '/admin/users'];
|
||||
|
||||
for (const route of adminRoutes) {
|
||||
const response = await page.goto(`${getWebsiteBaseUrl()}${route}`, { waitUntil: 'domcontentloaded' });
|
||||
expect(response?.status()).toBe(200);
|
||||
expect(page.url()).toContain(route);
|
||||
}
|
||||
});
|
||||
|
||||
test('admin routes block non-admin users', async ({ page, context }) => {
|
||||
await setWebsiteAuthContext(context, 'auth');
|
||||
|
||||
const adminRoutes = ['/admin', '/admin/users'];
|
||||
|
||||
for (const route of adminRoutes) {
|
||||
await page.goto(`${getWebsiteBaseUrl()}${route}`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
const currentUrl = new URL(page.url());
|
||||
expect(currentUrl.pathname).toBe('/auth/login');
|
||||
}
|
||||
});
|
||||
|
||||
test('sponsor routes allow sponsor users', async ({ page, context }) => {
|
||||
await setWebsiteAuthContext(context, 'sponsor');
|
||||
|
||||
const sponsorRoutes = ['/sponsor', '/sponsor/dashboard', '/sponsor/settings'];
|
||||
|
||||
for (const route of sponsorRoutes) {
|
||||
const response = await page.goto(`${getWebsiteBaseUrl()}${route}`, { waitUntil: 'domcontentloaded' });
|
||||
expect(response?.status()).toBe(200);
|
||||
expect(page.url()).toContain(route);
|
||||
}
|
||||
});
|
||||
|
||||
test('sponsor routes block non-sponsor users', async ({ page, context }) => {
|
||||
await setWebsiteAuthContext(context, 'auth');
|
||||
|
||||
const sponsorRoutes = ['/sponsor', '/sponsor/dashboard', '/sponsor/settings'];
|
||||
|
||||
for (const route of sponsorRoutes) {
|
||||
await page.goto(`${getWebsiteBaseUrl()}${route}`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
const currentUrl = new URL(page.url());
|
||||
expect(currentUrl.pathname).toBe('/auth/login');
|
||||
}
|
||||
});
|
||||
|
||||
test('league admin routes require league admin role', async ({ page, context }) => {
|
||||
// Test as regular driver
|
||||
await setWebsiteAuthContext(context, 'auth');
|
||||
await page.goto(`${getWebsiteBaseUrl()}/leagues/league-1/settings`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
let currentUrl = new URL(page.url());
|
||||
expect(currentUrl.pathname).toBe('/auth/login');
|
||||
|
||||
// Test as admin (has access to league admin routes)
|
||||
await setWebsiteAuthContext(context, 'admin');
|
||||
await page.goto(`${getWebsiteBaseUrl()}/leagues/league-1/settings`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
currentUrl = new URL(page.url());
|
||||
expect(currentUrl.pathname).toBe('/leagues/league-1/settings');
|
||||
});
|
||||
|
||||
test('authenticated users can access auth-required routes', async ({ page, context }) => {
|
||||
await setWebsiteAuthContext(context, 'auth');
|
||||
|
||||
const authRoutes = ['/dashboard', '/profile', '/onboarding'];
|
||||
|
||||
for (const route of authRoutes) {
|
||||
const response = await page.goto(`${getWebsiteBaseUrl()}${route}`, { waitUntil: 'domcontentloaded' });
|
||||
expect(response?.status()).toBe(200);
|
||||
expect(page.url()).toContain(route);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('AuthGuard Component - Component Rendering', () => {
|
||||
test('renders protected content when access granted', async ({ page, context }) => {
|
||||
await setWebsiteAuthContext(context, 'auth');
|
||||
|
||||
await page.goto(`${getWebsiteBaseUrl()}/dashboard`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Should render the dashboard content
|
||||
await expect(page.locator('body')).toBeVisible();
|
||||
|
||||
// Should not show loading or redirect messages
|
||||
const loadingText = await page.locator('text=/Verifying authentication|Redirecting/').count();
|
||||
expect(loadingText).toBe(0);
|
||||
});
|
||||
|
||||
test('shows redirect message briefly before redirect', async ({ page, context }) => {
|
||||
await setWebsiteAuthContext(context, 'public');
|
||||
|
||||
// This is hard to catch, but we can verify the final state
|
||||
await page.goto(`${getWebsiteBaseUrl()}/dashboard`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Should end up at login
|
||||
const currentUrl = new URL(page.url());
|
||||
expect(currentUrl.pathname).toBe('/auth/login');
|
||||
});
|
||||
|
||||
test('handles multiple AuthGuard instances on same page', async ({ page, context }) => {
|
||||
await setWebsiteAuthContext(context, 'auth');
|
||||
|
||||
// Visit a page that might have nested AuthGuards
|
||||
await page.goto(`${getWebsiteBaseUrl()}/leagues/league-1`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Should render correctly
|
||||
await expect(page.locator('body')).toBeVisible();
|
||||
expect(page.url()).toContain('/leagues/league-1');
|
||||
});
|
||||
|
||||
test('preserves child component state during auth checks', async ({ page, context }) => {
|
||||
await setWebsiteAuthContext(context, 'auth');
|
||||
|
||||
// Visit dashboard
|
||||
await page.goto(`${getWebsiteBaseUrl()}/dashboard`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Should maintain component state (no full page reload)
|
||||
// This is verified by the fact that the page loads without errors
|
||||
await expect(page.locator('body')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('AuthGuard Component - Error Handling', () => {
|
||||
test('handles network errors during session check', async ({ page, context }) => {
|
||||
// Clear cookies to simulate failed session check
|
||||
await context.clearCookies();
|
||||
|
||||
await page.goto(`${getWebsiteBaseUrl()}/dashboard`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Should redirect to login
|
||||
const currentUrl = new URL(page.url());
|
||||
expect(currentUrl.pathname).toBe('/auth/login');
|
||||
});
|
||||
|
||||
test('handles invalid session data', async ({ page, context }) => {
|
||||
// Set invalid session cookie
|
||||
await setWebsiteAuthContext(context, 'sponsor', { sessionDrift: 'invalid-cookie' });
|
||||
|
||||
await page.goto(`${getWebsiteBaseUrl()}/sponsor/dashboard`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Should redirect to login
|
||||
const currentUrl = new URL(page.url());
|
||||
expect(currentUrl.pathname).toBe('/auth/login');
|
||||
});
|
||||
|
||||
test('handles expired session', async ({ page, context }) => {
|
||||
await setWebsiteAuthContext(context, 'sponsor', { sessionDrift: 'expired' });
|
||||
|
||||
await page.goto(`${getWebsiteBaseUrl()}/sponsor/dashboard`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Should redirect to login
|
||||
const currentUrl = new URL(page.url());
|
||||
expect(currentUrl.pathname).toBe('/auth/login');
|
||||
});
|
||||
|
||||
test('handles missing required role data', async ({ page, context }) => {
|
||||
await setWebsiteAuthContext(context, 'sponsor', { sessionDrift: 'missing-sponsor-id' });
|
||||
|
||||
await page.goto(`${getWebsiteBaseUrl()}/sponsor/dashboard`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Should redirect to login
|
||||
const currentUrl = new URL(page.url());
|
||||
expect(currentUrl.pathname).toBe('/auth/login');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('AuthGuard Component - Performance', () => {
|
||||
test('auth check completes within reasonable time', async ({ page, context }) => {
|
||||
await setWebsiteAuthContext(context, 'auth');
|
||||
|
||||
const startTime = Date.now();
|
||||
await page.goto(`${getWebsiteBaseUrl()}/dashboard`, { waitUntil: 'domcontentloaded' });
|
||||
const endTime = Date.now();
|
||||
|
||||
// Should complete within 5 seconds
|
||||
expect(endTime - startTime).toBeLessThan(5000);
|
||||
});
|
||||
|
||||
test('no excessive session checks', async ({ page, context }) => {
|
||||
await setWebsiteAuthContext(context, 'auth');
|
||||
|
||||
let sessionCheckCount = 0;
|
||||
page.on('request', (req) => {
|
||||
if (req.url().includes('/auth/session')) {
|
||||
sessionCheckCount++;
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto(`${getWebsiteBaseUrl()}/dashboard`, { waitUntil: 'networkidle' });
|
||||
|
||||
// Should check session once or twice (initial + maybe one refresh)
|
||||
expect(sessionCheckCount).toBeLessThan(3);
|
||||
});
|
||||
|
||||
test('handles concurrent route access', async ({ page, context }) => {
|
||||
await setWebsiteAuthContext(context, 'auth');
|
||||
|
||||
// Navigate to multiple routes rapidly
|
||||
const routes = ['/dashboard', '/profile', '/leagues', '/dashboard'];
|
||||
|
||||
for (const route of routes) {
|
||||
await page.goto(`${getWebsiteBaseUrl()}${route}`, { waitUntil: 'domcontentloaded' });
|
||||
expect(page.url()).toContain(route);
|
||||
}
|
||||
});
|
||||
});
|
||||
438
tests/integration/website/middleware.test.ts
Normal file
438
tests/integration/website/middleware.test.ts
Normal file
@@ -0,0 +1,438 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { setWebsiteAuthContext } from './websiteAuth';
|
||||
|
||||
/**
|
||||
* Website Middleware Route Protection Tests
|
||||
*
|
||||
* These tests specifically verify the Next.js middleware behavior:
|
||||
* - Public routes are always accessible
|
||||
* - Protected routes require authentication
|
||||
* - Auth routes redirect authenticated users
|
||||
* - Role-based access control
|
||||
*/
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
test.describe('Website Middleware - Public Route Protection', () => {
|
||||
test('root route is publicly accessible', async ({ page, context }) => {
|
||||
await setWebsiteAuthContext(context, 'public');
|
||||
|
||||
const response = await page.goto(`${getWebsiteBaseUrl()}/`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
expect(response?.status()).toBe(200);
|
||||
await expect(page.locator('body')).toBeVisible();
|
||||
});
|
||||
|
||||
test('league routes are publicly accessible', async ({ page, context }) => {
|
||||
await setWebsiteAuthContext(context, 'public');
|
||||
|
||||
const routes = ['/leagues', '/leagues/league-1', '/leagues/league-1/standings'];
|
||||
|
||||
for (const route of routes) {
|
||||
const response = await page.goto(`${getWebsiteBaseUrl()}${route}`, { waitUntil: 'domcontentloaded' });
|
||||
expect(response?.status()).toBe(200);
|
||||
await expect(page.locator('body')).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('driver routes are publicly accessible', async ({ page, context }) => {
|
||||
await setWebsiteAuthContext(context, 'public');
|
||||
|
||||
const response = await page.goto(`${getWebsiteBaseUrl()}/drivers/driver-1`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
expect(response?.status()).toBe(200);
|
||||
await expect(page.locator('body')).toBeVisible();
|
||||
});
|
||||
|
||||
test('team routes are publicly accessible', async ({ page, context }) => {
|
||||
await setWebsiteAuthContext(context, 'public');
|
||||
|
||||
const response = await page.goto(`${getWebsiteBaseUrl()}/teams/team-1`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
expect(response?.status()).toBe(200);
|
||||
await expect(page.locator('body')).toBeVisible();
|
||||
});
|
||||
|
||||
test('race routes are publicly accessible', async ({ page, context }) => {
|
||||
await setWebsiteAuthContext(context, 'public');
|
||||
|
||||
const response = await page.goto(`${getWebsiteBaseUrl()}/races/race-1`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
expect(response?.status()).toBe(200);
|
||||
await expect(page.locator('body')).toBeVisible();
|
||||
});
|
||||
|
||||
test('leaderboard routes are publicly accessible', async ({ page, context }) => {
|
||||
await setWebsiteAuthContext(context, 'public');
|
||||
|
||||
const response = await page.goto(`${getWebsiteBaseUrl()}/leaderboards`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
expect(response?.status()).toBe(200);
|
||||
await expect(page.locator('body')).toBeVisible();
|
||||
});
|
||||
|
||||
test('sponsor signup is publicly accessible', async ({ page, context }) => {
|
||||
await setWebsiteAuthContext(context, 'public');
|
||||
|
||||
const response = await page.goto(`${getWebsiteBaseUrl()}/sponsor/signup`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
expect(response?.status()).toBe(200);
|
||||
await expect(page.locator('body')).toBeVisible();
|
||||
});
|
||||
|
||||
test('auth routes are publicly accessible', async ({ page, context }) => {
|
||||
await setWebsiteAuthContext(context, 'public');
|
||||
|
||||
const authRoutes = [
|
||||
'/auth/login',
|
||||
'/auth/signup',
|
||||
'/auth/forgot-password',
|
||||
'/auth/reset-password',
|
||||
'/auth/iracing',
|
||||
];
|
||||
|
||||
for (const route of authRoutes) {
|
||||
const response = await page.goto(`${getWebsiteBaseUrl()}${route}`, { waitUntil: 'domcontentloaded' });
|
||||
expect(response?.status()).toBe(200);
|
||||
await expect(page.locator('body')).toBeVisible();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Website Middleware - Protected Route Protection', () => {
|
||||
test('dashboard redirects unauthenticated users to login', async ({ page, context }) => {
|
||||
await setWebsiteAuthContext(context, 'public');
|
||||
|
||||
await page.goto(`${getWebsiteBaseUrl()}/dashboard`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
const currentUrl = new URL(page.url());
|
||||
expect(currentUrl.pathname).toBe('/auth/login');
|
||||
expect(currentUrl.searchParams.get('returnTo')).toBe('/dashboard');
|
||||
});
|
||||
|
||||
test('profile routes redirect unauthenticated users to login', async ({ page, context }) => {
|
||||
await setWebsiteAuthContext(context, 'public');
|
||||
|
||||
const profileRoutes = ['/profile', '/profile/settings', '/profile/leagues'];
|
||||
|
||||
for (const route of profileRoutes) {
|
||||
await page.goto(`${getWebsiteBaseUrl()}${route}`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
const currentUrl = new URL(page.url());
|
||||
expect(currentUrl.pathname).toBe('/auth/login');
|
||||
expect(currentUrl.searchParams.get('returnTo')).toBe(route);
|
||||
}
|
||||
});
|
||||
|
||||
test('admin routes redirect unauthenticated users to login', async ({ page, context }) => {
|
||||
await setWebsiteAuthContext(context, 'public');
|
||||
|
||||
const adminRoutes = ['/admin', '/admin/users'];
|
||||
|
||||
for (const route of adminRoutes) {
|
||||
await page.goto(`${getWebsiteBaseUrl()}${route}`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
const currentUrl = new URL(page.url());
|
||||
expect(currentUrl.pathname).toBe('/auth/login');
|
||||
expect(currentUrl.searchParams.get('returnTo')).toBe(route);
|
||||
}
|
||||
});
|
||||
|
||||
test('sponsor routes redirect unauthenticated users to login', async ({ page, context }) => {
|
||||
await setWebsiteAuthContext(context, 'public');
|
||||
|
||||
const sponsorRoutes = ['/sponsor', '/sponsor/dashboard', '/sponsor/settings'];
|
||||
|
||||
for (const route of sponsorRoutes) {
|
||||
await page.goto(`${getWebsiteBaseUrl()}${route}`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
const currentUrl = new URL(page.url());
|
||||
expect(currentUrl.pathname).toBe('/auth/login');
|
||||
expect(currentUrl.searchParams.get('returnTo')).toBe(route);
|
||||
}
|
||||
});
|
||||
|
||||
test('league admin routes redirect unauthenticated users to login', async ({ page, context }) => {
|
||||
await setWebsiteAuthContext(context, 'public');
|
||||
|
||||
const leagueAdminRoutes = [
|
||||
'/leagues/league-1/roster/admin',
|
||||
'/leagues/league-1/schedule/admin',
|
||||
'/leagues/league-1/settings',
|
||||
'/leagues/league-1/stewarding',
|
||||
'/leagues/league-1/wallet',
|
||||
];
|
||||
|
||||
for (const route of leagueAdminRoutes) {
|
||||
await page.goto(`${getWebsiteBaseUrl()}${route}`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
const currentUrl = new URL(page.url());
|
||||
expect(currentUrl.pathname).toBe('/auth/login');
|
||||
expect(currentUrl.searchParams.get('returnTo')).toBe(route);
|
||||
}
|
||||
});
|
||||
|
||||
test('onboarding redirects unauthenticated users to login', async ({ page, context }) => {
|
||||
await setWebsiteAuthContext(context, 'public');
|
||||
|
||||
await page.goto(`${getWebsiteBaseUrl()}/onboarding`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
const currentUrl = new URL(page.url());
|
||||
expect(currentUrl.pathname).toBe('/auth/login');
|
||||
expect(currentUrl.searchParams.get('returnTo')).toBe('/onboarding');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Website Middleware - Authenticated Access', () => {
|
||||
test('dashboard is accessible when authenticated', async ({ page, context }) => {
|
||||
await setWebsiteAuthContext(context, 'auth');
|
||||
|
||||
const response = await page.goto(`${getWebsiteBaseUrl()}/dashboard`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
expect(response?.status()).toBe(200);
|
||||
expect(page.url()).toContain('/dashboard');
|
||||
await expect(page.locator('body')).toBeVisible();
|
||||
});
|
||||
|
||||
test('profile routes are accessible when authenticated', async ({ page, context }) => {
|
||||
await setWebsiteAuthContext(context, 'auth');
|
||||
|
||||
const profileRoutes = ['/profile', '/profile/settings', '/profile/leagues'];
|
||||
|
||||
for (const route of profileRoutes) {
|
||||
const response = await page.goto(`${getWebsiteBaseUrl()}${route}`, { waitUntil: 'domcontentloaded' });
|
||||
expect(response?.status()).toBe(200);
|
||||
expect(page.url()).toContain(route);
|
||||
}
|
||||
});
|
||||
|
||||
test('onboarding is accessible when authenticated', async ({ page, context }) => {
|
||||
await setWebsiteAuthContext(context, 'auth');
|
||||
|
||||
const response = await page.goto(`${getWebsiteBaseUrl()}/onboarding`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
expect(response?.status()).toBe(200);
|
||||
expect(page.url()).toContain('/onboarding');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Website Middleware - Role-Based Access', () => {
|
||||
test('admin routes require admin role', async ({ page, context }) => {
|
||||
// Test as regular driver (should be denied)
|
||||
await setWebsiteAuthContext(context, 'auth');
|
||||
await page.goto(`${getWebsiteBaseUrl()}/admin`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
let currentUrl = new URL(page.url());
|
||||
expect(currentUrl.pathname).toBe('/auth/login');
|
||||
|
||||
// Test as admin (should be allowed)
|
||||
await setWebsiteAuthContext(context, 'admin');
|
||||
await page.goto(`${getWebsiteBaseUrl()}/admin`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
currentUrl = new URL(page.url());
|
||||
expect(currentUrl.pathname).toBe('/admin');
|
||||
});
|
||||
|
||||
test('sponsor routes require sponsor role', async ({ page, context }) => {
|
||||
// Test as driver (should be denied)
|
||||
await setWebsiteAuthContext(context, 'auth');
|
||||
await page.goto(`${getWebsiteBaseUrl()}/sponsor/dashboard`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
let currentUrl = new URL(page.url());
|
||||
expect(currentUrl.pathname).toBe('/auth/login');
|
||||
|
||||
// Test as sponsor (should be allowed)
|
||||
await setWebsiteAuthContext(context, 'sponsor');
|
||||
await page.goto(`${getWebsiteBaseUrl()}/sponsor/dashboard`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
currentUrl = new URL(page.url());
|
||||
expect(currentUrl.pathname).toBe('/sponsor/dashboard');
|
||||
});
|
||||
|
||||
test('league admin routes require admin role', async ({ page, context }) => {
|
||||
// Test as regular driver (should be denied)
|
||||
await setWebsiteAuthContext(context, 'auth');
|
||||
await page.goto(`${getWebsiteBaseUrl()}/leagues/league-1/settings`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
let currentUrl = new URL(page.url());
|
||||
expect(currentUrl.pathname).toBe('/auth/login');
|
||||
|
||||
// Test as admin (should be allowed)
|
||||
await setWebsiteAuthContext(context, 'admin');
|
||||
await page.goto(`${getWebsiteBaseUrl()}/leagues/league-1/settings`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
currentUrl = new URL(page.url());
|
||||
expect(currentUrl.pathname).toBe('/leagues/league-1/settings');
|
||||
});
|
||||
|
||||
test('race stewarding routes require admin role', async ({ page, context }) => {
|
||||
// Test as regular driver (should be denied)
|
||||
await setWebsiteAuthContext(context, 'auth');
|
||||
await page.goto(`${getWebsiteBaseUrl()}/races/race-1/stewarding`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
let currentUrl = new URL(page.url());
|
||||
expect(currentUrl.pathname).toBe('/auth/login');
|
||||
|
||||
// Test as admin (should be allowed)
|
||||
await setWebsiteAuthContext(context, 'admin');
|
||||
await page.goto(`${getWebsiteBaseUrl()}/races/race-1/stewarding`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
currentUrl = new URL(page.url());
|
||||
expect(currentUrl.pathname).toBe('/races/race-1/stewarding');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Website Middleware - Auth Route Behavior', () => {
|
||||
test('auth routes redirect authenticated users away', async ({ page, context }) => {
|
||||
await setWebsiteAuthContext(context, 'auth');
|
||||
|
||||
const authRoutes = [
|
||||
'/auth/login',
|
||||
'/auth/signup',
|
||||
'/auth/iracing',
|
||||
];
|
||||
|
||||
for (const route of authRoutes) {
|
||||
await page.goto(`${getWebsiteBaseUrl()}${route}`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
const currentUrl = new URL(page.url());
|
||||
expect(currentUrl.pathname).toBe('/dashboard');
|
||||
}
|
||||
});
|
||||
|
||||
test('sponsor auth routes redirect to sponsor dashboard', async ({ page, context }) => {
|
||||
await setWebsiteAuthContext(context, 'sponsor');
|
||||
|
||||
const authRoutes = [
|
||||
'/auth/login',
|
||||
'/auth/signup',
|
||||
'/auth/iracing',
|
||||
];
|
||||
|
||||
for (const route of authRoutes) {
|
||||
await page.goto(`${getWebsiteBaseUrl()}${route}`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
const currentUrl = new URL(page.url());
|
||||
expect(currentUrl.pathname).toBe('/sponsor/dashboard');
|
||||
}
|
||||
});
|
||||
|
||||
test('admin auth routes redirect to admin dashboard', async ({ page, context }) => {
|
||||
await setWebsiteAuthContext(context, 'admin');
|
||||
|
||||
const authRoutes = [
|
||||
'/auth/login',
|
||||
'/auth/signup',
|
||||
'/auth/iracing',
|
||||
];
|
||||
|
||||
for (const route of authRoutes) {
|
||||
await page.goto(`${getWebsiteBaseUrl()}${route}`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
const currentUrl = new URL(page.url());
|
||||
expect(currentUrl.pathname).toBe('/admin');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Website Middleware - Edge Cases', () => {
|
||||
test('handles trailing slashes correctly', async ({ page, context }) => {
|
||||
await setWebsiteAuthContext(context, 'public');
|
||||
|
||||
const routes = [
|
||||
{ path: '/leagues', expected: '/leagues' },
|
||||
{ path: '/leagues/', expected: '/leagues' },
|
||||
{ path: '/drivers/driver-1', expected: '/drivers/driver-1' },
|
||||
{ path: '/drivers/driver-1/', expected: '/drivers/driver-1' },
|
||||
];
|
||||
|
||||
for (const { path, expected } of routes) {
|
||||
await page.goto(`${getWebsiteBaseUrl()}${path}`, { waitUntil: 'domcontentloaded' });
|
||||
expect(page.url()).toContain(expected);
|
||||
}
|
||||
});
|
||||
|
||||
test('handles invalid routes gracefully', async ({ page, context }) => {
|
||||
await setWebsiteAuthContext(context, 'public');
|
||||
|
||||
const invalidRoutes = [
|
||||
'/invalid-route',
|
||||
'/leagues/invalid-id',
|
||||
'/drivers/invalid-id',
|
||||
];
|
||||
|
||||
for (const route of invalidRoutes) {
|
||||
const response = await page.goto(`${getWebsiteBaseUrl()}${route}`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Should either show 404 or redirect to a valid page
|
||||
const status = response?.status();
|
||||
const url = page.url();
|
||||
|
||||
expect([200, 404].includes(status ?? 0) || url.includes('/auth/login')).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test('preserves query parameters during redirects', async ({ page, context }) => {
|
||||
await setWebsiteAuthContext(context, 'public');
|
||||
|
||||
const targetRoute = '/dashboard?tab=settings&filter=active';
|
||||
await page.goto(`${getWebsiteBaseUrl()}${targetRoute}`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
const currentUrl = new URL(page.url());
|
||||
expect(currentUrl.pathname).toBe('/auth/login');
|
||||
expect(currentUrl.searchParams.get('returnTo')).toBe('/dashboard?tab=settings&filter=active');
|
||||
});
|
||||
|
||||
test('handles deeply nested routes', async ({ page, context }) => {
|
||||
await setWebsiteAuthContext(context, 'public');
|
||||
|
||||
const deepRoute = '/leagues/league-1/stewarding/protests/protest-1';
|
||||
await page.goto(`${getWebsiteBaseUrl()}${deepRoute}`, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
const currentUrl = new URL(page.url());
|
||||
expect(currentUrl.pathname).toBe('/auth/login');
|
||||
expect(currentUrl.searchParams.get('returnTo')).toBe(deepRoute);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Website Middleware - Performance', () => {
|
||||
test('middleware adds minimal overhead', async ({ page, context }) => {
|
||||
await setWebsiteAuthContext(context, 'public');
|
||||
|
||||
const startTime = Date.now();
|
||||
await page.goto(`${getWebsiteBaseUrl()}/leagues`, { waitUntil: 'domcontentloaded' });
|
||||
const endTime = Date.now();
|
||||
|
||||
// Should complete quickly (under 3 seconds)
|
||||
expect(endTime - startTime).toBeLessThan(3000);
|
||||
});
|
||||
|
||||
test('no redirect loops', async ({ page, context }) => {
|
||||
await setWebsiteAuthContext(context, 'public');
|
||||
|
||||
// Try to access a protected route multiple times
|
||||
for (let i = 0; i < 3; i++) {
|
||||
await page.goto(`${getWebsiteBaseUrl()}/dashboard`, { waitUntil: 'domcontentloaded' });
|
||||
const currentUrl = new URL(page.url());
|
||||
expect(currentUrl.pathname).toBe('/auth/login');
|
||||
}
|
||||
});
|
||||
|
||||
test('handles rapid navigation', async ({ page, context }) => {
|
||||
await setWebsiteAuthContext(context, 'auth');
|
||||
|
||||
// Navigate between multiple protected routes rapidly
|
||||
const routes = ['/dashboard', '/profile', '/leagues', '/dashboard'];
|
||||
|
||||
for (const route of routes) {
|
||||
await page.goto(`${getWebsiteBaseUrl()}${route}`, { waitUntil: 'domcontentloaded' });
|
||||
expect(page.url()).toContain(route);
|
||||
}
|
||||
});
|
||||
});
|
||||
184
tests/integration/website/websiteAuth.ts
Normal file
184
tests/integration/website/websiteAuth.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
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';
|
||||
|
||||
type DemoLoginRole =
|
||||
| 'driver'
|
||||
| 'sponsor'
|
||||
| 'league-owner'
|
||||
| 'league-steward'
|
||||
| 'league-admin'
|
||||
| 'system-owner'
|
||||
| 'super-admin';
|
||||
|
||||
const demoSessionCookieCache = new Map<DemoLoginRole, 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';
|
||||
}
|
||||
|
||||
function demoLoginRoleForAuthContext(auth: WebsiteAuthContext): DemoLoginRole | null {
|
||||
switch (auth) {
|
||||
case 'public':
|
||||
return null;
|
||||
case 'auth':
|
||||
return 'driver';
|
||||
case 'sponsor':
|
||||
return 'sponsor';
|
||||
case 'admin':
|
||||
// Website "admin" pages need an elevated role; use the strongest demo role.
|
||||
return 'super-admin';
|
||||
default: {
|
||||
const exhaustive: never = auth;
|
||||
return exhaustive;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 ensureDemoSessionCookie(role: DemoLoginRole): Promise<string> {
|
||||
const cached = demoSessionCookieCache.get(role);
|
||||
if (cached) return cached;
|
||||
|
||||
const baseUrl = getWebsiteBaseUrl();
|
||||
const url = `${baseUrl}/api/auth/demo-login`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ role }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const body = await response.text().catch(() => '');
|
||||
throw new Error(`Smoke demo-login failed for role=${role}. ${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(
|
||||
`Smoke demo-login did not return gp_session cookie for role=${role}. set-cookie header: ${rawSetCookie}`,
|
||||
);
|
||||
}
|
||||
|
||||
const gpSessionValue = extractCookieValue(gpSessionPair, 'gp_session');
|
||||
if (!gpSessionValue) {
|
||||
throw new Error(
|
||||
`Smoke demo-login returned a gp_session cookie, but it could not be parsed for role=${role}. Pair: ${gpSessionPair}`,
|
||||
);
|
||||
}
|
||||
|
||||
demoSessionCookieCache.set(role, 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;
|
||||
}
|
||||
|
||||
const demoRole = demoLoginRoleForAuthContext(auth);
|
||||
if (!demoRole) {
|
||||
throw new Error(`Expected a demo role for auth context ${auth}`);
|
||||
}
|
||||
|
||||
const gpSessionValue = await ensureDemoSessionCookie(demoRole);
|
||||
|
||||
// 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 };
|
||||
}
|
||||
194
tests/integration/website/websiteRouteInventory.ts
Normal file
194
tests/integration/website/websiteRouteInventory.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
export type RouteAccess = 'public' | 'auth' | 'admin' | 'sponsor';
|
||||
|
||||
export type RouteParams = Record<string, string>;
|
||||
|
||||
export type WebsiteRouteDefinition = {
|
||||
pathTemplate: string;
|
||||
params?: RouteParams;
|
||||
access: RouteAccess;
|
||||
expectedPathTemplate?: string;
|
||||
allowNotFound?: boolean;
|
||||
};
|
||||
|
||||
function walkDir(rootDir: string): string[] {
|
||||
const entries = fs.readdirSync(rootDir, { withFileTypes: true });
|
||||
const results: string[] = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.name.startsWith('.')) continue;
|
||||
|
||||
const fullPath = path.join(rootDir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
results.push(...walkDir(fullPath));
|
||||
continue;
|
||||
}
|
||||
|
||||
results.push(fullPath);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
function toPathTemplate(appDir: string, pageFilePath: string): string {
|
||||
const rel = path.relative(appDir, pageFilePath);
|
||||
const segments = rel.split(path.sep);
|
||||
|
||||
// drop trailing "page.tsx"
|
||||
segments.pop();
|
||||
|
||||
// root page.tsx
|
||||
if (segments.length === 0) return '/';
|
||||
|
||||
return `/${segments.join('/')}`;
|
||||
}
|
||||
|
||||
export function listNextAppPageTemplates(appDir?: string): string[] {
|
||||
const resolvedAppDir = appDir ?? path.join(process.cwd(), 'apps', 'website', 'app');
|
||||
|
||||
const files = walkDir(resolvedAppDir);
|
||||
const pages = files.filter((f) => path.basename(f) === 'page.tsx');
|
||||
|
||||
return pages.map((pagePath) => toPathTemplate(resolvedAppDir, pagePath));
|
||||
}
|
||||
|
||||
export function resolvePathTemplate(pathTemplate: string, params: RouteParams = {}): string {
|
||||
return pathTemplate.replace(/\[([^\]]+)\]/g, (_match, key: string) => {
|
||||
const replacement = params[key];
|
||||
if (!replacement) {
|
||||
throw new Error(`Missing route param "${key}" for template "${pathTemplate}"`);
|
||||
}
|
||||
return replacement;
|
||||
});
|
||||
}
|
||||
|
||||
// Default IDs used to resolve dynamic routes in smoke tests.
|
||||
// These values must be supported by the docker mock API in docker-compose.test.yml.
|
||||
const LEAGUE_ID = 'league-1';
|
||||
const DRIVER_ID = 'driver-1';
|
||||
const TEAM_ID = 'team-1';
|
||||
const RACE_ID = 'race-1';
|
||||
const PROTEST_ID = 'protest-1';
|
||||
|
||||
const ROUTE_META: Record<string, Omit<WebsiteRouteDefinition, 'pathTemplate'>> = {
|
||||
'/': { access: 'public' },
|
||||
|
||||
'/404': { access: 'public' },
|
||||
'/500': { access: 'public' },
|
||||
|
||||
'/admin': { access: 'admin' },
|
||||
'/admin/users': { access: 'admin' },
|
||||
|
||||
'/auth/forgot-password': { access: 'public' },
|
||||
'/auth/iracing': { access: 'public' },
|
||||
'/auth/login': { access: 'public' },
|
||||
'/auth/reset-password': { access: 'public' },
|
||||
'/auth/signup': { access: 'public' },
|
||||
|
||||
'/dashboard': { access: 'auth' },
|
||||
|
||||
'/drivers': { access: 'public' },
|
||||
'/drivers/[id]': { access: 'public', params: { id: DRIVER_ID } },
|
||||
|
||||
'/leaderboards': { access: 'public' },
|
||||
'/leaderboards/drivers': { access: 'public' },
|
||||
|
||||
'/leagues': { access: 'public' },
|
||||
'/leagues/create': { access: 'auth' },
|
||||
'/leagues/[id]': { access: 'public', params: { id: LEAGUE_ID } },
|
||||
'/leagues/[id]/roster/admin': { access: 'admin', params: { id: LEAGUE_ID } },
|
||||
'/leagues/[id]/rulebook': { access: 'public', params: { id: LEAGUE_ID } },
|
||||
'/leagues/[id]/schedule': { access: 'public', params: { id: LEAGUE_ID } },
|
||||
'/leagues/[id]/schedule/admin': { access: 'admin', params: { id: LEAGUE_ID } },
|
||||
'/leagues/[id]/settings': { access: 'admin', params: { id: LEAGUE_ID } },
|
||||
'/leagues/[id]/sponsorships': { access: 'admin', params: { id: LEAGUE_ID } },
|
||||
'/leagues/[id]/standings': { access: 'public', params: { id: LEAGUE_ID } },
|
||||
'/leagues/[id]/stewarding': { access: 'admin', params: { id: LEAGUE_ID } },
|
||||
'/leagues/[id]/stewarding/protests/[protestId]': {
|
||||
access: 'admin',
|
||||
params: { id: LEAGUE_ID, protestId: PROTEST_ID },
|
||||
},
|
||||
'/leagues/[id]/wallet': { access: 'admin', params: { id: LEAGUE_ID } },
|
||||
|
||||
'/onboarding': { access: 'auth' },
|
||||
|
||||
'/profile': { access: 'auth' },
|
||||
'/profile/leagues': { access: 'auth' },
|
||||
'/profile/liveries': { access: 'auth' },
|
||||
'/profile/liveries/upload': { access: 'auth' },
|
||||
'/profile/settings': { access: 'auth' },
|
||||
'/profile/sponsorship-requests': { access: 'auth' },
|
||||
|
||||
'/races': { access: 'public' },
|
||||
'/races/all': { access: 'public' },
|
||||
'/races/[id]': { access: 'public', params: { id: RACE_ID } },
|
||||
'/races/[id]/results': { access: 'public', params: { id: RACE_ID } },
|
||||
'/races/[id]/stewarding': { access: 'admin', params: { id: RACE_ID } },
|
||||
|
||||
'/sponsor': { access: 'sponsor', expectedPathTemplate: '/sponsor/dashboard' },
|
||||
'/sponsor/billing': { access: 'sponsor' },
|
||||
'/sponsor/campaigns': { access: 'sponsor' },
|
||||
'/sponsor/dashboard': { access: 'sponsor' },
|
||||
'/sponsor/leagues': { access: 'sponsor' },
|
||||
'/sponsor/leagues/[id]': { access: 'sponsor', params: { id: LEAGUE_ID } },
|
||||
'/sponsor/settings': { access: 'sponsor' },
|
||||
'/sponsor/signup': { access: 'public' },
|
||||
|
||||
'/teams': { access: 'public' },
|
||||
'/teams/leaderboard': { access: 'public' },
|
||||
'/teams/[id]': { access: 'public', params: { id: TEAM_ID } },
|
||||
};
|
||||
|
||||
export function getWebsiteRouteInventory(): WebsiteRouteDefinition[] {
|
||||
const discovered = listNextAppPageTemplates();
|
||||
|
||||
const missingMeta = discovered.filter((template) => !ROUTE_META[template]);
|
||||
if (missingMeta.length > 0) {
|
||||
throw new Error(
|
||||
`Missing ROUTE_META entries for discovered pages:\n${missingMeta
|
||||
.slice()
|
||||
.sort()
|
||||
.map((t) => `- ${t}`)
|
||||
.join('\n')}`,
|
||||
);
|
||||
}
|
||||
|
||||
const extraMeta = Object.keys(ROUTE_META).filter((template) => !discovered.includes(template));
|
||||
if (extraMeta.length > 0) {
|
||||
throw new Error(
|
||||
`ROUTE_META contains templates that are not present as page.tsx routes:\n${extraMeta
|
||||
.slice()
|
||||
.sort()
|
||||
.map((t) => `- ${t}`)
|
||||
.join('\n')}`,
|
||||
);
|
||||
}
|
||||
|
||||
return discovered
|
||||
.slice()
|
||||
.sort()
|
||||
.map((pathTemplate) => ({ pathTemplate, ...ROUTE_META[pathTemplate] }));
|
||||
}
|
||||
|
||||
export function getWebsiteParamEdgeCases(): WebsiteRouteDefinition[] {
|
||||
return [
|
||||
{ pathTemplate: '/races/[id]', params: { id: 'does-not-exist' }, access: 'public', allowNotFound: true },
|
||||
{ pathTemplate: '/leagues/[id]', params: { id: 'does-not-exist' }, access: 'public', allowNotFound: true },
|
||||
];
|
||||
}
|
||||
|
||||
export function getWebsiteFaultInjectionRoutes(): WebsiteRouteDefinition[] {
|
||||
return [
|
||||
{ pathTemplate: '/leagues/[id]', params: { id: LEAGUE_ID }, access: 'public' },
|
||||
{ pathTemplate: '/leagues/[id]/schedule/admin', params: { id: LEAGUE_ID }, access: 'admin' },
|
||||
{ pathTemplate: '/sponsor/dashboard', access: 'sponsor' },
|
||||
{ pathTemplate: '/races/[id]', params: { id: RACE_ID }, access: 'public' },
|
||||
];
|
||||
}
|
||||
|
||||
export function getWebsiteAuthDriftRoutes(): WebsiteRouteDefinition[] {
|
||||
return [{ pathTemplate: '/sponsor/dashboard', access: 'sponsor' }];
|
||||
}
|
||||
@@ -136,6 +136,17 @@ async function runWebsiteSmokeScenario(args: {
|
||||
if (resourceType === 'fetch' && url.includes('_rsc=')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore fetch requests to the expected redirect target during page redirects
|
||||
// This handles cases like /sponsor -> /sponsor/dashboard where the redirect
|
||||
// causes an aborted fetch request to the target URL
|
||||
if (resourceType === 'fetch' && route.expectedPathTemplate) {
|
||||
const expectedPath = resolvePathTemplate(route.expectedPathTemplate, route.params);
|
||||
const urlObj = new URL(url);
|
||||
if (urlObj.pathname === expectedPath) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
requestFailures.push({
|
||||
|
||||
@@ -21,44 +21,131 @@ export async function setWebsiteAuthContext(
|
||||
const domain = 'localhost';
|
||||
const base = { domain, path: '/' };
|
||||
|
||||
// The website uses `gp_session` cookie for authentication and `gridpilot_demo_mode` for identity switching
|
||||
const cookies =
|
||||
auth === 'public'
|
||||
? [
|
||||
// No gp_session cookie for public access - this allows auth routes to render
|
||||
{ ...base, name: 'gridpilot_demo_mode', value: 'none' },
|
||||
{ ...base, name: 'gridpilot_sponsor_id', value: '' },
|
||||
{ ...base, name: 'gridpilot_sponsor_name', value: '' },
|
||||
]
|
||||
: auth === 'sponsor'
|
||||
? [
|
||||
{ ...base, name: 'gp_session', value: 'demo-sponsor-session' },
|
||||
{ ...base, name: 'gridpilot_demo_mode', value: 'sponsor' },
|
||||
{ ...base, name: 'gridpilot_sponsor_id', value: 'demo-sponsor-1' },
|
||||
{ ...base, name: 'gridpilot_sponsor_name', value: 'Demo Sponsor' },
|
||||
]
|
||||
: auth === 'admin'
|
||||
? [
|
||||
{ ...base, name: 'gp_session', value: 'demo-admin-session' },
|
||||
{ ...base, name: 'gridpilot_demo_mode', value: 'admin' },
|
||||
{ ...base, name: 'gridpilot_sponsor_id', value: '' },
|
||||
{ ...base, name: 'gridpilot_sponsor_name', value: '' },
|
||||
]
|
||||
: [
|
||||
{ ...base, name: 'gp_session', value: 'demo-driver-session' },
|
||||
{ ...base, name: 'gridpilot_demo_mode', value: 'driver' },
|
||||
{ ...base, name: 'gridpilot_sponsor_id', value: '' },
|
||||
{ ...base, name: 'gridpilot_sponsor_name', value: '' },
|
||||
];
|
||||
|
||||
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) }] : [];
|
||||
// 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, ...driftCookie, ...faultCookie]);
|
||||
await context.addCookies(cookies);
|
||||
}
|
||||
|
||||
export type ConsoleCapture = {
|
||||
|
||||
@@ -83,7 +83,6 @@ const ROUTE_META: Record<string, Omit<WebsiteRouteDefinition, 'pathTemplate'>> =
|
||||
'/admin/users': { access: 'admin' },
|
||||
|
||||
'/auth/forgot-password': { access: 'public' },
|
||||
'/auth/iracing': { access: 'public' },
|
||||
'/auth/login': { access: 'public' },
|
||||
'/auth/reset-password': { access: 'public' },
|
||||
'/auth/signup': { access: 'public' },
|
||||
|
||||
Reference in New Issue
Block a user