Files
gridpilot.gg/apps/website/tests/flows/auth.test.ts
Marc Mintel fb1221701d
Some checks failed
Contract Testing / contract-tests (push) Failing after 6m7s
Contract Testing / contract-snapshot (push) Failing after 4m46s
add tests
2026-01-22 11:52:42 +01:00

1148 lines
44 KiB
TypeScript

/**
* Auth Feature Flow Tests
*
* These tests verify routing, guards, navigation, cross-screen state, and user flows
* for the auth module. They run with real frontend and mocked contracts.
*
* Contracts are defined in apps/website/lib/types/generated
*
* @file apps/website/tests/flows/auth.test.ts
*/
import { test, expect } from '@playwright/test';
import { WebsiteAuthManager } from '../../../tests/shared/website/WebsiteAuthManager';
import { WebsiteRouteManager } from '../../../tests/shared/website/WebsiteRouteManager';
import { ConsoleErrorCapture } from '../../../tests/shared/website/ConsoleErrorCapture';
import { HttpDiagnostics } from '../../../tests/shared/website/HttpDiagnostics';
import { RouteContractSpec } from '../../../tests/shared/website/RouteContractSpec';
test.describe('Auth Feature Flow', () => {
describe('Login Flow', () => {
test('should navigate to login page', async ({ page }) => {
const routeManager = new WebsiteRouteManager(page);
// Navigate to /auth/login
await page.goto(routeManager.getRoute('/auth/login'));
// Verify login form is displayed
await expect(page.locator('form')).toBeVisible();
// Check for email and password inputs
await expect(page.locator('[data-testid="email-input"]')).toBeVisible();
await expect(page.locator('[data-testid="password-input"]')).toBeVisible();
});
test('should display validation errors for empty fields', async ({ page }) => {
const routeManager = new WebsiteRouteManager(page);
// Navigate to /auth/login
await page.goto(routeManager.getRoute('/auth/login'));
// Click submit without entering credentials
await page.locator('[data-testid="submit-button"]').click();
// Verify validation errors are shown
await expect(page.locator('[data-testid="email-error"]')).toBeVisible();
await expect(page.locator('[data-testid="password-error"]')).toBeVisible();
});
test('should display validation errors for invalid email format', async ({ page }) => {
const routeManager = new WebsiteRouteManager(page);
// Navigate to /auth/login
await page.goto(routeManager.getRoute('/auth/login'));
// Enter invalid email format
await page.locator('[data-testid="email-input"]').fill('invalid-email');
// Verify validation error is shown
await expect(page.locator('[data-testid="email-error"]')).toBeVisible();
await expect(page.locator('[data-testid="email-error"]')).toContainText(/invalid email/i);
});
test('should successfully login with valid credentials', async ({ page }) => {
const routeManager = new WebsiteRouteManager(page);
const routeContractSpec = new RouteContractSpec(page);
// Mock LoginParamsDTO and AuthSessionDTO response
const mockAuthSession = {
token: 'test-token-123',
user: {
userId: 'user-123',
email: 'test@example.com',
displayName: 'Test User',
role: 'user',
},
};
await routeContractSpec.mockApiCall('Login', mockAuthSession);
// Navigate to /auth/login
await page.goto(routeManager.getRoute('/auth/login'));
// Enter valid email and password
await page.locator('[data-testid="email-input"]').fill('test@example.com');
await page.locator('[data-testid="password-input"]').fill('ValidPass123!');
// Click submit
await page.locator('[data-testid="submit-button"]').click();
// Verify authentication is successful
await expect(page).toHaveURL(/.*\/dashboard/);
// Verify redirect to dashboard
await expect(page.locator('[data-testid="dashboard"]')).toBeVisible();
});
test('should handle login with remember me option', async ({ page }) => {
const routeManager = new WebsiteRouteManager(page);
const routeContractSpec = new RouteContractSpec(page);
// Mock AuthSessionDTO response
const mockAuthSession = {
token: 'test-token-123',
user: {
userId: 'user-123',
email: 'test@example.com',
displayName: 'Test User',
role: 'user',
},
};
await routeContractSpec.mockApiCall('Login', mockAuthSession);
// Navigate to /auth/login
await page.goto(routeManager.getRoute('/auth/login'));
// Check remember me checkbox
await page.locator('[data-testid="remember-me-checkbox"]').check();
// Enter valid credentials
await page.locator('[data-testid="email-input"]').fill('test@example.com');
await page.locator('[data-testid="password-input"]').fill('ValidPass123!');
// Click submit
await page.locator('[data-testid="submit-button"]').click();
// Verify authentication is successful
await expect(page).toHaveURL(/.*\/dashboard/);
// Verify AuthSessionDTO is stored with longer expiration
// This would be verified by checking the cookie expiration
const cookies = await page.context().cookies();
const sessionCookie = cookies.find(c => c.name === 'gp_session');
expect(sessionCookie).toBeDefined();
// Remember me should set a longer expiration (e.g., 30 days)
// The exact expiration depends on the implementation
});
test('should handle login errors (invalid credentials)', async ({ page }) => {
const routeManager = new WebsiteRouteManager(page);
const routeContractSpec = new RouteContractSpec(page);
// Mock API to return authentication error
await routeContractSpec.mockApiCall('Login', {
error: 'Invalid credentials',
status: 401,
});
// Navigate to /auth/login
await page.goto(routeManager.getRoute('/auth/login'));
// Enter credentials
await page.locator('[data-testid="email-input"]').fill('wrong@example.com');
await page.locator('[data-testid="password-input"]').fill('WrongPass123!');
// Click submit
await page.locator('[data-testid="submit-button"]').click();
// Verify error message is displayed
await expect(page.locator('[data-testid="error-message"]')).toBeVisible();
await expect(page.locator('[data-testid="error-message"]')).toContainText(/invalid credentials/i);
// Verify form remains in error state
await expect(page.locator('[data-testid="email-input"]')).toHaveValue('wrong@example.com');
});
test('should handle login errors (server/network error)', async ({ page }) => {
const routeManager = new WebsiteRouteManager(page);
const routeContractSpec = new RouteContractSpec(page);
const consoleErrorCapture = new ConsoleErrorCapture(page);
// Mock API to return 500 error
await routeContractSpec.mockApiCall('Login', {
error: 'Internal Server Error',
status: 500,
});
// Navigate to /auth/login
await page.goto(routeManager.getRoute('/auth/login'));
// Enter credentials
await page.locator('[data-testid="email-input"]').fill('test@example.com');
await page.locator('[data-testid="password-input"]').fill('ValidPass123!');
// Click submit
await page.locator('[data-testid="submit-button"]').click();
// Verify generic error message is shown
await expect(page.locator('[data-testid="error-message"]')).toBeVisible();
await expect(page.locator('[data-testid="error-message"]')).toContainText(/error/i);
// Verify console error was captured
const errors = consoleErrorCapture.getErrors();
expect(errors.length).toBeGreaterThan(0);
});
test('should redirect to dashboard if already authenticated', async ({ page }) => {
const routeManager = new WebsiteRouteManager(page);
const authManager = new WebsiteAuthManager(page);
// Mock existing AuthSessionDTO by logging in
await authManager.loginAsUser();
// Navigate to /auth/login
await page.goto(routeManager.getRoute('/auth/login'));
// Verify redirect to dashboard
await expect(page).toHaveURL(/.*\/dashboard/);
});
test('should navigate to forgot password from login', async ({ page }) => {
const routeManager = new WebsiteRouteManager(page);
// Navigate to /auth/login
await page.goto(routeManager.getRoute('/auth/login'));
// Click forgot password link
await page.locator('[data-testid="forgot-password-link"]').click();
// Verify navigation to /auth/forgot-password
await expect(page).toHaveURL(/.*\/auth\/forgot-password/);
});
test('should navigate to signup from login', async ({ page }) => {
const routeManager = new WebsiteRouteManager(page);
// Navigate to /auth/login
await page.goto(routeManager.getRoute('/auth/login'));
// Click signup link
await page.locator('[data-testid="signup-link"]').click();
// Verify navigation to /auth/signup
await expect(page).toHaveURL(/.*\/auth\/signup/);
});
});
describe('Signup Flow', () => {
test('should navigate to signup page', async ({ page }) => {
const routeManager = new WebsiteRouteManager(page);
// Navigate to /auth/signup
await page.goto(routeManager.getRoute('/auth/signup'));
// Verify signup form is displayed
await expect(page.locator('form')).toBeVisible();
// Check for required fields (email, password, displayName)
await expect(page.locator('[data-testid="email-input"]')).toBeVisible();
await expect(page.locator('[data-testid="password-input"]')).toBeVisible();
await expect(page.locator('[data-testid="display-name-input"]')).toBeVisible();
});
test('should display validation errors for empty required fields', async ({ page }) => {
const routeManager = new WebsiteRouteManager(page);
// Navigate to /auth/signup
await page.goto(routeManager.getRoute('/auth/signup'));
// Click submit without entering any data
await page.locator('[data-testid="submit-button"]').click();
// Verify validation errors for all required fields
await expect(page.locator('[data-testid="email-error"]')).toBeVisible();
await expect(page.locator('[data-testid="password-error"]')).toBeVisible();
await expect(page.locator('[data-testid="display-name-error"]')).toBeVisible();
});
test('should display validation errors for weak password', async ({ page }) => {
const routeManager = new WebsiteRouteManager(page);
// Navigate to /auth/signup
await page.goto(routeManager.getRoute('/auth/signup'));
// Enter password that doesn't meet requirements
await page.locator('[data-testid="password-input"]').fill('weak');
// Verify password strength validation error
await expect(page.locator('[data-testid="password-error"]')).toBeVisible();
await expect(page.locator('[data-testid="password-error"]')).toContainText(/password must be/i);
});
test('should successfully signup with valid data', async ({ page }) => {
const routeManager = new WebsiteRouteManager(page);
const routeContractSpec = new RouteContractSpec(page);
// Mock SignupParamsDTO and AuthSessionDTO response
const mockAuthSession = {
token: 'test-token-456',
user: {
userId: 'user-456',
email: 'newuser@example.com',
displayName: 'New User',
role: 'user',
},
};
await routeContractSpec.mockApiCall('Signup', mockAuthSession);
// Navigate to /auth/signup
await page.goto(routeManager.getRoute('/auth/signup'));
// Enter valid email, password, and display name
await page.locator('[data-testid="email-input"]').fill('newuser@example.com');
await page.locator('[data-testid="password-input"]').fill('ValidPass123!');
await page.locator('[data-testid="display-name-input"]').fill('New User');
// Click submit
await page.locator('[data-testid="submit-button"]').click();
// Verify authentication is successful
await expect(page).toHaveURL(/.*\/dashboard/);
// Verify redirect to dashboard
await expect(page.locator('[data-testid="dashboard"]')).toBeVisible();
});
test('should handle signup with optional iRacing customer ID', async ({ page }) => {
const routeManager = new WebsiteRouteManager(page);
const routeContractSpec = new RouteContractSpec(page);
// Mock SignupParamsDTO and AuthSessionDTO response
const mockAuthSession = {
token: 'test-token-789',
user: {
userId: 'user-789',
email: 'iracing@example.com',
displayName: 'iRacing User',
role: 'user',
},
};
await routeContractSpec.mockApiCall('Signup', mockAuthSession);
// Navigate to /auth/signup
await page.goto(routeManager.getRoute('/auth/signup'));
// Enter valid credentials
await page.locator('[data-testid="email-input"]').fill('iracing@example.com');
await page.locator('[data-testid="password-input"]').fill('ValidPass123!');
await page.locator('[data-testid="display-name-input"]').fill('iRacing User');
// Enter optional iRacing customer ID
await page.locator('[data-testid="iracing-customer-id-input"]').fill('123456');
// Click submit
await page.locator('[data-testid="submit-button"]').click();
// Verify authentication is successful
await expect(page).toHaveURL(/.*\/dashboard/);
});
test('should handle signup errors (email already exists)', async ({ page }) => {
const routeManager = new WebsiteRouteManager(page);
const routeContractSpec = new RouteContractSpec(page);
// Mock API to return email conflict error
await routeContractSpec.mockApiCall('Signup', {
error: 'Email already exists',
status: 409,
});
// Navigate to /auth/signup
await page.goto(routeManager.getRoute('/auth/signup'));
// Enter credentials
await page.locator('[data-testid="email-input"]').fill('existing@example.com');
await page.locator('[data-testid="password-input"]').fill('ValidPass123!');
await page.locator('[data-testid="display-name-input"]').fill('Existing User');
// Click submit
await page.locator('[data-testid="submit-button"]').click();
// Verify error message about existing account
await expect(page.locator('[data-testid="error-message"]')).toBeVisible();
await expect(page.locator('[data-testid="error-message"]')).toContainText(/already exists/i);
});
test('should handle signup errors (server error)', async ({ page }) => {
const routeManager = new WebsiteRouteManager(page);
const routeContractSpec = new RouteContractSpec(page);
const consoleErrorCapture = new ConsoleErrorCapture(page);
// Mock API to return 500 error
await routeContractSpec.mockApiCall('Signup', {
error: 'Internal Server Error',
status: 500,
});
// Navigate to /auth/signup
await page.goto(routeManager.getRoute('/auth/signup'));
// Enter valid credentials
await page.locator('[data-testid="email-input"]').fill('test@example.com');
await page.locator('[data-testid="password-input"]').fill('ValidPass123!');
await page.locator('[data-testid="display-name-input"]').fill('Test User');
// Click submit
await page.locator('[data-testid="submit-button"]').click();
// Verify generic error message is shown
await expect(page.locator('[data-testid="error-message"]')).toBeVisible();
await expect(page.locator('[data-testid="error-message"]')).toContainText(/error/i);
// Verify console error was captured
const errors = consoleErrorCapture.getErrors();
expect(errors.length).toBeGreaterThan(0);
});
test('should navigate to login from signup', async ({ page }) => {
const routeManager = new WebsiteRouteManager(page);
// Navigate to /auth/signup
await page.goto(routeManager.getRoute('/auth/signup'));
// Click login link
await page.locator('[data-testid="login-link"]').click();
// Verify navigation to /auth/login
await expect(page).toHaveURL(/.*\/auth\/login/);
});
test('should handle password visibility toggle', async ({ page }) => {
const routeManager = new WebsiteRouteManager(page);
// Navigate to /auth/signup
await page.goto(routeManager.getRoute('/auth/signup'));
// Enter password
await page.locator('[data-testid="password-input"]').fill('ValidPass123!');
// Click show/hide password toggle
await page.locator('[data-testid="password-toggle"]').click();
// Verify password visibility changes
// Check that the input type changes from password to text
const passwordInput = page.locator('[data-testid="password-input"]');
await expect(passwordInput).toHaveAttribute('type', 'text');
// Click toggle again to hide
await page.locator('[data-testid="password-toggle"]').click();
await expect(passwordInput).toHaveAttribute('type', 'password');
});
});
describe('Forgot Password Flow', () => {
test('should navigate to forgot password page', async ({ page }) => {
const routeManager = new WebsiteRouteManager(page);
// Navigate to /auth/forgot-password
await page.goto(routeManager.getRoute('/auth/forgot-password'));
// Verify forgot password form is displayed
await expect(page.locator('form')).toBeVisible();
// Check for email input field
await expect(page.locator('[data-testid="email-input"]')).toBeVisible();
});
test('should display validation error for empty email', async ({ page }) => {
const routeManager = new WebsiteRouteManager(page);
// Navigate to /auth/forgot-password
await page.goto(routeManager.getRoute('/auth/forgot-password'));
// Click submit without entering email
await page.locator('[data-testid="submit-button"]').click();
// Verify validation error is shown
await expect(page.locator('[data-testid="email-error"]')).toBeVisible();
});
test('should display validation error for invalid email format', async ({ page }) => {
const routeManager = new WebsiteRouteManager(page);
// Navigate to /auth/forgot-password
await page.goto(routeManager.getRoute('/auth/forgot-password'));
// Enter invalid email format
await page.locator('[data-testid="email-input"]').fill('invalid-email');
// Verify validation error is shown
await expect(page.locator('[data-testid="email-error"]')).toBeVisible();
await expect(page.locator('[data-testid="email-error"]')).toContainText(/invalid email/i);
});
test('should successfully submit forgot password request', async ({ page }) => {
const routeManager = new WebsiteRouteManager(page);
const routeContractSpec = new RouteContractSpec(page);
// Mock ForgotPasswordDTO response
const mockForgotPassword = {
success: true,
message: 'Password reset email sent',
};
await routeContractSpec.mockApiCall('ForgotPassword', mockForgotPassword);
// Navigate to /auth/forgot-password
await page.goto(routeManager.getRoute('/auth/forgot-password'));
// Enter valid email
await page.locator('[data-testid="email-input"]').fill('test@example.com');
// Click submit
await page.locator('[data-testid="submit-button"]').click();
// Verify success message is displayed
await expect(page.locator('[data-testid="success-message"]')).toBeVisible();
await expect(page.locator('[data-testid="success-message"]')).toContainText(/password reset email sent/i);
// Verify form is in success state
await expect(page.locator('[data-testid="submit-button"]')).toBeDisabled();
});
test('should handle forgot password errors (email not found)', async ({ page }) => {
const routeManager = new WebsiteRouteManager(page);
const routeContractSpec = new RouteContractSpec(page);
// Mock API to return email not found error
await routeContractSpec.mockApiCall('ForgotPassword', {
error: 'Email not found',
status: 404,
});
// Navigate to /auth/forgot-password
await page.goto(routeManager.getRoute('/auth/forgot-password'));
// Enter email
await page.locator('[data-testid="email-input"]').fill('nonexistent@example.com');
// Click submit
await page.locator('[data-testid="submit-button"]').click();
// Verify error message is displayed
await expect(page.locator('[data-testid="error-message"]')).toBeVisible();
await expect(page.locator('[data-testid="error-message"]')).toContainText(/email not found/i);
});
test('should handle forgot password errors (rate limit)', async ({ page }) => {
const routeManager = new WebsiteRouteManager(page);
const routeContractSpec = new RouteContractSpec(page);
// Mock API to return rate limit error
await routeContractSpec.mockApiCall('ForgotPassword', {
error: 'Rate limit exceeded',
status: 429,
});
// Navigate to /auth/forgot-password
await page.goto(routeManager.getRoute('/auth/forgot-password'));
// Enter email
await page.locator('[data-testid="email-input"]').fill('test@example.com');
// Click submit
await page.locator('[data-testid="submit-button"]').click();
// Verify rate limit message is shown
await expect(page.locator('[data-testid="error-message"]')).toBeVisible();
await expect(page.locator('[data-testid="error-message"]')).toContainText(/rate limit/i);
});
test('should navigate back to login from forgot password', async ({ page }) => {
const routeManager = new WebsiteRouteManager(page);
// Navigate to /auth/forgot-password
await page.goto(routeManager.getRoute('/auth/forgot-password'));
// Click back/login link
await page.locator('[data-testid="login-link"]').click();
// Verify navigation to /auth/login
await expect(page).toHaveURL(/.*\/auth\/login/);
});
});
describe('Reset Password Flow', () => {
test('should navigate to reset password page with token', async ({ page }) => {
const routeManager = new WebsiteRouteManager(page);
// Navigate to /auth/reset-password?token=abc123
await page.goto(routeManager.getRoute('/auth/reset-password') + '?token=abc123');
// Verify reset password form is displayed
await expect(page.locator('form')).toBeVisible();
// Check for new password and confirm password inputs
await expect(page.locator('[data-testid="new-password-input"]')).toBeVisible();
await expect(page.locator('[data-testid="confirm-password-input"]')).toBeVisible();
});
test('should display validation errors for empty password fields', async ({ page }) => {
const routeManager = new WebsiteRouteManager(page);
// Navigate to /auth/reset-password?token=abc123
await page.goto(routeManager.getRoute('/auth/reset-password') + '?token=abc123');
// Click submit without entering passwords
await page.locator('[data-testid="submit-button"]').click();
// Verify validation errors are shown
await expect(page.locator('[data-testid="new-password-error"]')).toBeVisible();
await expect(page.locator('[data-testid="confirm-password-error"]')).toBeVisible();
});
test('should display validation error for non-matching passwords', async ({ page }) => {
const routeManager = new WebsiteRouteManager(page);
// Navigate to /auth/reset-password?token=abc123
await page.goto(routeManager.getRoute('/auth/reset-password') + '?token=abc123');
// Enter different passwords in new and confirm fields
await page.locator('[data-testid="new-password-input"]').fill('ValidPass123!');
await page.locator('[data-testid="confirm-password-input"]').fill('DifferentPass456!');
// Verify validation error is shown
await expect(page.locator('[data-testid="confirm-password-error"]')).toBeVisible();
await expect(page.locator('[data-testid="confirm-password-error"]')).toContainText(/passwords do not match/i);
});
test('should display validation error for weak new password', async ({ page }) => {
const routeManager = new WebsiteRouteManager(page);
// Navigate to /auth/reset-password?token=abc123
await page.goto(routeManager.getRoute('/auth/reset-password') + '?token=abc123');
// Enter weak password
await page.locator('[data-testid="new-password-input"]').fill('weak');
// Verify password strength validation error
await expect(page.locator('[data-testid="new-password-error"]')).toBeVisible();
await expect(page.locator('[data-testid="new-password-error"]')).toContainText(/password must be/i);
});
test('should successfully reset password', async ({ page }) => {
const routeManager = new WebsiteRouteManager(page);
const routeContractSpec = new RouteContractSpec(page);
// Mock successful password reset response
const mockResetPassword = {
success: true,
message: 'Password reset successfully',
};
await routeContractSpec.mockApiCall('ResetPassword', mockResetPassword);
// Navigate to /auth/reset-password?token=abc123
await page.goto(routeManager.getRoute('/auth/reset-password') + '?token=abc123');
// Enter matching valid passwords
await page.locator('[data-testid="new-password-input"]').fill('NewPass123!');
await page.locator('[data-testid="confirm-password-input"]').fill('NewPass123!');
// Click submit
await page.locator('[data-testid="submit-button"]').click();
// Verify success message is displayed
await expect(page.locator('[data-testid="success-message"]')).toBeVisible();
await expect(page.locator('[data-testid="success-message"]')).toContainText(/password reset successfully/i);
// Verify redirect to login page
await expect(page).toHaveURL(/.*\/auth\/login/);
});
test('should handle reset password with invalid token', async ({ page }) => {
const routeManager = new WebsiteRouteManager(page);
const routeContractSpec = new RouteContractSpec(page);
// Mock API to return invalid token error
await routeContractSpec.mockApiCall('ResetPassword', {
error: 'Invalid token',
status: 400,
});
// Navigate to /auth/reset-password?token=invalid
await page.goto(routeManager.getRoute('/auth/reset-password') + '?token=invalid');
// Verify error message is displayed
await expect(page.locator('[data-testid="error-message"]')).toBeVisible();
await expect(page.locator('[data-testid="error-message"]')).toContainText(/invalid token/i);
// Verify form is disabled
await expect(page.locator('[data-testid="submit-button"]')).toBeDisabled();
});
test('should handle reset password with expired token', async ({ page }) => {
const routeManager = new WebsiteRouteManager(page);
const routeContractSpec = new RouteContractSpec(page);
// Mock API to return expired token error
await routeContractSpec.mockApiCall('ResetPassword', {
error: 'Token expired',
status: 400,
});
// Navigate to /auth/reset-password?token=expired
await page.goto(routeManager.getRoute('/auth/reset-password') + '?token=expired');
// Verify error message is displayed
await expect(page.locator('[data-testid="error-message"]')).toBeVisible();
await expect(page.locator('[data-testid="error-message"]')).toContainText(/token expired/i);
// Verify link to request new reset email
await expect(page.locator('[data-testid="request-new-link"]')).toBeVisible();
});
test('should handle reset password errors (server error)', async ({ page }) => {
const routeManager = new WebsiteRouteManager(page);
const routeContractSpec = new RouteContractSpec(page);
const consoleErrorCapture = new ConsoleErrorCapture(page);
// Mock API to return 500 error
await routeContractSpec.mockApiCall('ResetPassword', {
error: 'Internal Server Error',
status: 500,
});
// Navigate to /auth/reset-password?token=abc123
await page.goto(routeManager.getRoute('/auth/reset-password') + '?token=abc123');
// Enter valid passwords
await page.locator('[data-testid="new-password-input"]').fill('NewPass123!');
await page.locator('[data-testid="confirm-password-input"]').fill('NewPass123!');
// Click submit
await page.locator('[data-testid="submit-button"]').click();
// Verify generic error message is shown
await expect(page.locator('[data-testid="error-message"]')).toBeVisible();
await expect(page.locator('[data-testid="error-message"]')).toContainText(/error/i);
// Verify console error was captured
const errors = consoleErrorCapture.getErrors();
expect(errors.length).toBeGreaterThan(0);
});
test('should navigate to login from reset password', async ({ page }) => {
const routeManager = new WebsiteRouteManager(page);
// Navigate to /auth/reset-password?token=abc123
await page.goto(routeManager.getRoute('/auth/reset-password') + '?token=abc123');
// Click login link
await page.locator('[data-testid="login-link"]').click();
// Verify navigation to /auth/login
await expect(page).toHaveURL(/.*\/auth\/login/);
});
});
describe('Logout Flow', () => {
test('should successfully logout from authenticated session', async ({ page }) => {
const routeManager = new WebsiteRouteManager(page);
const authManager = new WebsiteAuthManager(page);
const routeContractSpec = new RouteContractSpec(page);
// Mock existing AuthSessionDTO by logging in
await authManager.loginAsUser();
// Mock logout API call
await routeContractSpec.mockApiCall('Logout', { success: true });
// Navigate to dashboard
await page.goto(routeManager.getRoute('/dashboard'));
// Click logout button
await page.locator('[data-testid="logout-button"]').click();
// Verify AuthSessionDTO is cleared
const cookies = await page.context().cookies();
const sessionCookie = cookies.find(c => c.name === 'gp_session');
expect(sessionCookie).toBeUndefined();
// Verify redirect to login page
await expect(page).toHaveURL(/.*\/auth\/login/);
});
test('should handle logout errors gracefully', async ({ page }) => {
const routeManager = new WebsiteRouteManager(page);
const authManager = new WebsiteAuthManager(page);
const routeContractSpec = new RouteContractSpec(page);
// Mock existing AuthSessionDTO by logging in
await authManager.loginAsUser();
// Mock logout API to return error
await routeContractSpec.mockApiCall('Logout', {
error: 'Logout failed',
status: 500,
});
// Navigate to dashboard
await page.goto(routeManager.getRoute('/dashboard'));
// Click logout button
await page.locator('[data-testid="logout-button"]').click();
// Verify session is still cleared locally
const cookies = await page.context().cookies();
const sessionCookie = cookies.find(c => c.name === 'gp_session');
expect(sessionCookie).toBeUndefined();
// Verify redirect to login page
await expect(page).toHaveURL(/.*\/auth\/login/);
});
test('should clear all auth-related state on logout', async ({ page }) => {
const routeManager = new WebsiteRouteManager(page);
const authManager = new WebsiteAuthManager(page);
const routeContractSpec = new RouteContractSpec(page);
// Mock existing AuthSessionDTO by logging in
await authManager.loginAsUser();
// Mock logout API call
await routeContractSpec.mockApiCall('Logout', { success: true });
// Navigate to various pages
await page.goto(routeManager.getRoute('/dashboard'));
await page.goto(routeManager.getRoute('/profile'));
// Click logout button
await page.locator('[data-testid="logout-button"]').click();
// Verify all auth state is cleared
const cookies = await page.context().cookies();
const sessionCookie = cookies.find(c => c.name === 'gp_session');
expect(sessionCookie).toBeUndefined();
// Verify no auth data persists
await expect(page).toHaveURL(/.*\/auth\/login/);
// Try to access protected route again
await page.goto(routeManager.getRoute('/dashboard'));
// Should redirect to login
await expect(page).toHaveURL(/.*\/auth\/login/);
});
});
describe('Auth Route Guards', () => {
test('should redirect unauthenticated users to login', async ({ page }) => {
const routeManager = new WebsiteRouteManager(page);
// Navigate to protected route (e.g., /dashboard)
await page.goto(routeManager.getRoute('/dashboard'));
// Verify redirect to /auth/login
await expect(page).toHaveURL(/.*\/auth\/login/);
// Check return URL parameter
const url = new URL(page.url());
expect(url.searchParams.get('returnUrl')).toBe('/dashboard');
});
test('should allow access to authenticated users', async ({ page }) => {
const routeManager = new WebsiteRouteManager(page);
const authManager = new WebsiteAuthManager(page);
// Mock existing AuthSessionDTO
await authManager.loginAsUser();
// Navigate to protected route
await page.goto(routeManager.getRoute('/dashboard'));
// Verify page loads successfully
await expect(page).toHaveURL(/.*\/dashboard/);
await expect(page.locator('[data-testid="dashboard"]')).toBeVisible();
});
test('should handle session expiration during navigation', async ({ page }) => {
const routeManager = new WebsiteRouteManager(page);
const authManager = new WebsiteAuthManager(page);
const routeContractSpec = new RouteContractSpec(page);
// Mock existing AuthSessionDTO
await authManager.loginAsUser();
// Navigate to protected route
await page.goto(routeManager.getRoute('/dashboard'));
// Mock session expiration
await routeContractSpec.mockApiCall('GetDashboardData', {
error: 'Unauthorized',
status: 401,
message: 'Session expired',
});
// Attempt navigation to another protected route
await page.goto(routeManager.getRoute('/profile'));
// Verify redirect to login
await expect(page).toHaveURL(/.*\/auth\/login/);
});
test('should maintain return URL after authentication', async ({ page }) => {
const routeManager = new WebsiteRouteManager(page);
const authManager = new WebsiteAuthManager(page);
const routeContractSpec = new RouteContractSpec(page);
// Attempt to access protected route without auth
await page.goto(routeManager.getRoute('/dashboard'));
// Verify redirect to login with return URL
await expect(page).toHaveURL(/.*\/auth\/login/);
const url = new URL(page.url());
expect(url.searchParams.get('returnUrl')).toBe('/dashboard');
// Mock dashboard data for after login
const mockDashboardData = {
overview: {
totalRaces: 10,
totalLeagues: 5,
},
};
await routeContractSpec.mockApiCall('GetDashboardData', mockDashboardData);
// Login successfully
await authManager.loginAsUser();
// Verify redirect back to original protected route
await expect(page).toHaveURL(/.*\/dashboard/);
});
test('should redirect authenticated users away from auth pages', async ({ page }) => {
const routeManager = new WebsiteRouteManager(page);
const authManager = new WebsiteAuthManager(page);
// Mock existing AuthSessionDTO
await authManager.loginAsUser();
// Navigate to /auth/login
await page.goto(routeManager.getRoute('/auth/login'));
// Verify redirect to dashboard
await expect(page).toHaveURL(/.*\/dashboard/);
});
});
describe('Auth Cross-Screen State Management', () => {
test('should preserve form data when navigating between auth pages', async ({ page }) => {
const routeManager = new WebsiteRouteManager(page);
// Navigate to /auth/login
await page.goto(routeManager.getRoute('/auth/login'));
// Enter email
await page.locator('[data-testid="email-input"]').fill('test@example.com');
// Navigate to /auth/forgot-password
await page.goto(routeManager.getRoute('/auth/forgot-password'));
// Navigate back to /auth/login
await page.goto(routeManager.getRoute('/auth/login'));
// Verify email is preserved
await expect(page.locator('[data-testid="email-input"]')).toHaveValue('test@example.com');
});
test('should clear form data after successful authentication', async ({ page }) => {
const routeManager = new WebsiteRouteManager(page);
const routeContractSpec = new RouteContractSpec(page);
// Mock AuthSessionDTO response
const mockAuthSession = {
token: 'test-token-123',
user: {
userId: 'user-123',
email: 'test@example.com',
displayName: 'Test User',
role: 'user',
},
};
await routeContractSpec.mockApiCall('Login', mockAuthSession);
// Navigate to /auth/login
await page.goto(routeManager.getRoute('/auth/login'));
// Enter credentials
await page.locator('[data-testid="email-input"]').fill('test@example.com');
await page.locator('[data-testid="password-input"]').fill('ValidPass123!');
// Login successfully
await page.locator('[data-testid="submit-button"]').click();
// Navigate back to /auth/login
await page.goto(routeManager.getRoute('/auth/login'));
// Verify form is cleared
await expect(page.locator('[data-testid="email-input"]')).toHaveValue('');
await expect(page.locator('[data-testid="password-input"]')).toHaveValue('');
});
test('should handle concurrent auth operations', async ({ page }) => {
const routeManager = new WebsiteRouteManager(page);
const routeContractSpec = new RouteContractSpec(page);
// Mock AuthSessionDTO response
const mockAuthSession = {
token: 'test-token-123',
user: {
userId: 'user-123',
email: 'test@example.com',
displayName: 'Test User',
role: 'user',
},
};
await routeContractSpec.mockApiCall('Login', mockAuthSession);
// Navigate to /auth/login
await page.goto(routeManager.getRoute('/auth/login'));
// Enter credentials
await page.locator('[data-testid="email-input"]').fill('test@example.com');
await page.locator('[data-testid="password-input"]').fill('ValidPass123!');
// Click submit multiple times quickly
await Promise.all([
page.locator('[data-testid="submit-button"]').click(),
page.locator('[data-testid="submit-button"]').click(),
page.locator('[data-testid="submit-button"]').click(),
]);
// Verify only one request is sent
// This would be verified by checking the mock call count
// For now, verify loading state is managed
await expect(page).toHaveURL(/.*\/dashboard/);
// Verify loading state is cleared
await expect(page.locator('[data-testid="loading-spinner"]')).not.toBeVisible();
});
});
describe('Auth UI State Management', () => {
test('should show loading states during auth operations', async ({ page }) => {
const routeManager = new WebsiteRouteManager(page);
const routeContractSpec = new RouteContractSpec(page);
// Mock delayed auth response
const mockAuthSession = {
token: 'test-token-123',
user: {
userId: 'user-123',
email: 'test@example.com',
displayName: 'Test User',
role: 'user',
},
};
await routeContractSpec.mockApiCall('Login', mockAuthSession, { delay: 500 });
// Navigate to /auth/login
await page.goto(routeManager.getRoute('/auth/login'));
// Enter credentials
await page.locator('[data-testid="email-input"]').fill('test@example.com');
await page.locator('[data-testid="password-input"]').fill('ValidPass123!');
// Submit login form
await page.locator('[data-testid="submit-button"]').click();
// Verify loading spinner is shown
await expect(page.locator('[data-testid="loading-spinner"]')).toBeVisible();
// Wait for loading to complete
await expect(page.locator('[data-testid="loading-spinner"]')).not.toBeVisible();
// Verify authentication is successful
await expect(page).toHaveURL(/.*\/dashboard/);
});
test('should handle error states gracefully', async ({ page }) => {
const routeManager = new WebsiteRouteManager(page);
const routeContractSpec = new RouteContractSpec(page);
const consoleErrorCapture = new ConsoleErrorCapture(page);
// Mock various auth error scenarios
await routeContractSpec.mockApiCall('Login', {
error: 'Invalid credentials',
status: 401,
});
// Navigate to /auth/login
await page.goto(routeManager.getRoute('/auth/login'));
// Enter credentials
await page.locator('[data-testid="email-input"]').fill('wrong@example.com');
await page.locator('[data-testid="password-input"]').fill('WrongPass123!');
// Click submit
await page.locator('[data-testid="submit-button"]').click();
// Verify error banner/message is displayed
await expect(page.locator('[data-testid="error-message"]')).toBeVisible();
// Verify UI remains usable after errors
await expect(page.locator('[data-testid="email-input"]')).toBeEnabled();
await expect(page.locator('[data-testid="password-input"]')).toBeEnabled();
await expect(page.locator('[data-testid="submit-button"]')).toBeEnabled();
// Verify error can be dismissed
await page.locator('[data-testid="error-dismiss"]').click();
await expect(page.locator('[data-testid="error-message"]')).not.toBeVisible();
// Verify console error was captured
const errors = consoleErrorCapture.getErrors();
expect(errors.length).toBeGreaterThan(0);
});
test('should handle network connectivity issues', async ({ page }) => {
const routeManager = new WebsiteRouteManager(page);
const routeContractSpec = new RouteContractSpec(page);
const consoleErrorCapture = new ConsoleErrorCapture(page);
// Mock network failure
await routeContractSpec.mockApiCall('Login', {
error: 'Network Error',
status: 0,
});
// Navigate to /auth/login
await page.goto(routeManager.getRoute('/auth/login'));
// Enter credentials
await page.locator('[data-testid="email-input"]').fill('test@example.com');
await page.locator('[data-testid="password-input"]').fill('ValidPass123!');
// Attempt auth operation
await page.locator('[data-testid="submit-button"]').click();
// Verify network error message is shown
await expect(page.locator('[data-testid="error-message"]')).toBeVisible();
await expect(page.locator('[data-testid="error-message"]')).toContainText(/network/i);
// Verify retry option is available
await expect(page.locator('[data-testid="retry-button"]')).toBeVisible();
// Verify console error was captured
const errors = consoleErrorCapture.getErrors();
expect(errors.length).toBeGreaterThan(0);
});
});
});