mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-12 09:12:40 +02:00
11 KiB
11 KiB
Complex Authentication Flow Patterns
Table of Contents
- Email Verification Flows
- Password Reset
- Session Timeout
- Remember Me Persistence
- Logout Patterns
- Tips
- Related
When to use: Testing email verification, password reset, session timeout/expiration, or remember-me functionality. For basic auth setup (storage state, OAuth mocking, MFA, role-based access), see authentication.md.
Email Verification Flows
Capturing Verification Tokens
Intercept API responses to capture verification tokens for testing:
test('completes registration with email verification', async ({ page }) => {
let capturedToken = '';
await page.route('**/api/auth/register', async (route) => {
const response = await route.fetch();
const body = await response.json();
capturedToken = body.verificationToken;
await route.fulfill({ response });
});
await page.goto('/register');
await page.getByLabel('Name').fill('New User');
await page.getByLabel('Email').fill('newuser@test.com');
await page.getByLabel('Password', { exact: true }).fill('SecurePass!');
await page.getByLabel('Confirm password').fill('SecurePass!');
await page.getByRole('button', { name: 'Create account' }).click();
await expect(page.getByText('Check your inbox')).toBeVisible();
expect(capturedToken).toBeTruthy();
await page.goto(`/verify?token=${capturedToken}`);
await expect(page.getByText('Email confirmed')).toBeVisible();
});
Fully Mocked Verification
test('verifies email with mocked endpoints', async ({ page }) => {
const mockToken = 'test-verification-abc123';
await page.route('**/api/auth/register', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ message: 'Verification sent', verificationToken: mockToken }),
});
});
await page.route(`**/api/auth/verify?token=${mockToken}`, async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ verified: true }),
});
});
await page.goto('/register');
await page.getByLabel('Email').fill('test@example.com');
await page.getByLabel('Password', { exact: true }).fill('Password123!');
await page.getByRole('button', { name: 'Sign up' }).click();
await expect(page.getByText('Check your inbox')).toBeVisible();
await page.goto(`/verify?token=${mockToken}`);
await expect(page.getByText('Email confirmed')).toBeVisible();
});
Password Reset
Complete Reset Flow
test('resets password through email link', async ({ page }) => {
let resetToken = '';
await page.route('**/api/auth/forgot-password', async (route) => {
const response = await route.fetch();
const body = await response.json();
resetToken = body.resetToken;
await route.fulfill({ response });
});
await page.goto('/forgot-password');
await page.getByLabel('Email').fill('user@test.com');
await page.getByRole('button', { name: 'Send link' }).click();
await expect(page.getByText('Reset email sent')).toBeVisible();
expect(resetToken).toBeTruthy();
await page.goto(`/reset-password?token=${resetToken}`);
await page.getByLabel('New password', { exact: true }).fill('NewPassword456!');
await page.getByLabel('Confirm password').fill('NewPassword456!');
await page.getByRole('button', { name: 'Update password' }).click();
await expect(page.getByText('Password updated')).toBeVisible();
});
Expired Token Handling
test('shows error for expired reset token', async ({ page }) => {
await page.goto('/reset-password?token=expired-token');
await page.getByLabel('New password', { exact: true }).fill('NewPass!');
await page.getByLabel('Confirm password').fill('NewPass!');
await page.getByRole('button', { name: 'Update password' }).click();
await expect(page.getByRole('alert')).toContainText(/expired|invalid/i);
});
Password Strength Validation
test('enforces password requirements on reset', async ({ page }) => {
await page.goto('/reset-password?token=valid-token');
await page.getByLabel('New password', { exact: true }).fill('weak');
await page.getByLabel('Confirm password').fill('weak');
await page.getByRole('button', { name: 'Update password' }).click();
await expect(page.getByText(/at least 8 characters/i)).toBeVisible();
});
Session Timeout
Detecting Expired Sessions
test('redirects to signin after session expires', async ({ page, context }) => {
await page.goto('/signin');
await page.getByLabel('Email').fill('user@test.com');
await page.getByLabel('Password').fill('Password!');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page).toHaveURL('/home');
const cookies = await context.cookies();
const sessionCookie = cookies.find((c) => c.name.includes('session'));
if (sessionCookie) {
await context.clearCookies({ name: sessionCookie.name });
}
await page.goto('/profile');
await expect(page).toHaveURL(/\/signin/);
await expect(page.getByText(/session.*expired|sign in again/i)).toBeVisible();
});
Session Extension Warning
test('shows warning before session expires', async ({ page }) => {
await page.route('**/api/auth/session', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ valid: true, expiresIn: 60 }),
});
});
await page.goto('/home');
await expect(page.getByText(/session.*expir/i)).toBeVisible({ timeout: 10000 });
await expect(page.getByRole('button', { name: /extend|stay signed in/i })).toBeVisible();
});
Session Extension Action
test('extends session when user clicks extend', async ({ page }) => {
let sessionExtended = false;
await page.route('**/api/auth/session', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ valid: true, expiresIn: 60 }),
});
});
await page.route('**/api/auth/refresh', async (route) => {
sessionExtended = true;
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ valid: true, expiresIn: 3600 }),
});
});
await page.goto('/home');
await expect(page.getByRole('button', { name: /extend|stay signed in/i })).toBeVisible({
timeout: 10000,
});
await page.getByRole('button', { name: /extend|stay signed in/i }).click();
expect(sessionExtended).toBe(true);
await expect(page.getByText(/session.*expir/i)).not.toBeVisible();
});
Remember Me Persistence
Persistent Session
test('persists session with remember me enabled', async ({ browser }) => {
const ctx1 = await browser.newContext();
const page1 = await ctx1.newPage();
await page1.goto('/signin');
await page1.getByLabel('Email').fill('user@test.com');
await page1.getByLabel('Password').fill('Password!');
await page1.getByLabel('Keep me signed in').check();
await page1.getByRole('button', { name: 'Sign in' }).click();
await expect(page1).toHaveURL('/home');
const state = await ctx1.storageState();
await ctx1.close();
const ctx2 = await browser.newContext({ storageState: state });
const page2 = await ctx2.newPage();
await page2.goto('/home');
await expect(page2).toHaveURL('/home');
await expect(page2.getByText('Welcome')).toBeVisible();
await ctx2.close();
});
Session-Only Login
test('session-only login does not persist across browser restarts', async ({ browser }) => {
const ctx1 = await browser.newContext();
const page1 = await ctx1.newPage();
await page1.goto('/signin');
await page1.getByLabel('Email').fill('user@test.com');
await page1.getByLabel('Password').fill('Password!');
// Leave "Remember me" unchecked
await expect(page1.getByLabel('Keep me signed in')).not.toBeChecked();
await page1.getByRole('button', { name: 'Sign in' }).click();
await expect(page1).toHaveURL('/home');
// Only keep persistent cookies (filter out session cookies)
const cookies = await ctx1.cookies();
await ctx1.close();
const persistentCookies = cookies.filter((c) => c.expires > 0);
const ctx2 = await browser.newContext();
await ctx2.addCookies(persistentCookies);
const page2 = await ctx2.newPage();
await page2.goto('/home');
// Should redirect to login since session was not persisted
await expect(page2).toHaveURL(/\/signin/);
await ctx2.close();
});
Logout Patterns
Standard Logout with Session Cleanup
test.use({ storageState: '.auth/user.json' });
test('logs out and clears session', async ({ page, context }) => {
await page.goto('/home');
await page.getByRole('button', { name: /account|menu/i }).click();
await page.getByRole('menuitem', { name: 'Sign out' }).click();
await expect(page).toHaveURL('/signin');
const cookies = await context.cookies();
const sessionCookies = cookies.filter((c) => c.name.includes('session') || c.name.includes('token'));
expect(sessionCookies).toHaveLength(0);
await page.goto('/home');
await expect(page).toHaveURL(/\/signin/);
});
Logout from All Devices
test('logs out from all devices', async ({ page }) => {
let logoutAllCalled = false;
await page.route('**/api/auth/logout-all', async (route) => {
logoutAllCalled = true;
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ message: 'Logged out everywhere' }),
});
});
await page.goto('/settings/security');
await page.getByRole('button', { name: 'Sign out everywhere' }).click();
await page.getByRole('dialog').getByRole('button', { name: 'Confirm' }).click();
expect(logoutAllCalled).toBe(true);
await expect(page).toHaveURL(/\/signin/);
});
Tips
- Configure shorter session timeouts in test environments — Enables testing timeout behavior without slow tests
- Test token expiration edge cases — Expired tokens, invalid tokens, already-used tokens
- Verify cleanup on logout — Check both cookies and localStorage are cleared
- Test the full flow end-to-end — Password reset should verify login with new password works
Related
- authentication.md — Storage state, OAuth mocking, MFA, role-based access, API login
- fixtures-hooks.md — Creating auth fixtures
- third-party.md — Mocking external auth providers