SurfSense/.cursor/skills/playwright-testing/advanced/authentication-flows.md

361 lines
11 KiB
Markdown
Raw Normal View History

2026-05-10 04:19:55 +05:30
# Complex Authentication Flow Patterns
## Table of Contents
1. [Email Verification Flows](#email-verification-flows)
2. [Password Reset](#password-reset)
3. [Session Timeout](#session-timeout)
4. [Remember Me Persistence](#remember-me-persistence)
5. [Logout Patterns](#logout-patterns)
6. [Tips](#tips)
7. [Related](#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](authentication.md).
---
## Email Verification Flows
### Capturing Verification Tokens
Intercept API responses to capture verification tokens for testing:
```typescript
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
```typescript
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
```typescript
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
```typescript
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
```typescript
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
```typescript
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
```typescript
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
```typescript
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
```typescript
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
```typescript
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
```typescript
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
```typescript
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
---
## Related
- [authentication.md](authentication.md) — Storage state, OAuth mocking, MFA, role-based access, API login
- [fixtures-hooks.md](../core/fixtures-hooks.md) — Creating auth fixtures
- [third-party.md](./third-party.md) — Mocking external auth providers