SurfSense/.cursor/skills/playwright-testing/advanced/authentication-flows.md
2026-05-10 04:19:55 +05:30

11 KiB

Complex Authentication Flow Patterns

Table of Contents

  1. Email Verification Flows
  2. Password Reset
  3. Session Timeout
  4. Remember Me Persistence
  5. Logout Patterns
  6. Tips
  7. 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

  1. Configure shorter session timeouts in test environments — Enables testing timeout behavior without slow tests
  2. Test token expiration edge cases — Expired tokens, invalid tokens, already-used tokens
  3. Verify cleanup on logout — Check both cookies and localStorage are cleared
  4. Test the full flow end-to-end — Password reset should verify login with new password works