1148 lines
44 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|