235 lines
7.9 KiB
TypeScript
235 lines
7.9 KiB
TypeScript
import { test, expect } from '@playwright/test';
|
|
|
|
/**
|
|
* E2E Test: Session Persistence Across Browser Restarts
|
|
*
|
|
* User Story: US4 - Session Persistence
|
|
* Task: T011
|
|
*
|
|
* **Purpose**: Verify that authenticated users remain logged in after page reload
|
|
*
|
|
* **Acceptance Criteria**:
|
|
* - AC-1: After login, user reloads page and remains on /dashboard (not redirected to /login)
|
|
* - AC-2: After reload, user name is still visible in navbar
|
|
* - AC-3: After reload, user can still access protected pages
|
|
*
|
|
* **Technical Implementation**:
|
|
* - Tests rely on Pocketbase authStore persistence via localStorage
|
|
* - initAuth() composable function restores session on mount via auth plugin
|
|
* - Auth state is synchronized via pb.authStore.onChange listener
|
|
*
|
|
* **Test Flow**:
|
|
* 1. Navigate to /login
|
|
* 2. Authenticate via OAuth (mock provider)
|
|
* 3. Verify redirect to /dashboard
|
|
* 4. Verify user name visible in navbar
|
|
* 5. Reload page
|
|
* 6. Verify still on /dashboard (no redirect to /login)
|
|
* 7. Verify user name still visible in navbar
|
|
*
|
|
* **Notes**:
|
|
* - Requires Pocketbase OAuth mock provider configured for testing
|
|
* - May need to configure test-specific OAuth provider or use Pocketbase test mode
|
|
* - Session persistence depends on valid auth token not being expired
|
|
*/
|
|
|
|
test.describe('Session Persistence', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
// Clear any existing auth state before each test
|
|
await page.context().clearCookies();
|
|
await page.evaluate(() => localStorage.clear());
|
|
});
|
|
|
|
test('should maintain authentication after page reload', async ({ page }) => {
|
|
// Step 1: Navigate to login page
|
|
await page.goto('/login');
|
|
await expect(page).toHaveURL('/login');
|
|
|
|
// Step 2: Authenticate via OAuth
|
|
// NOTE: This requires a mock OAuth provider configured in Pocketbase for testing
|
|
// The actual implementation will depend on the test environment setup
|
|
// For now, this is a placeholder that documents the expected flow
|
|
|
|
// TODO: Replace with actual OAuth flow once test provider is configured
|
|
// Example mock implementation:
|
|
// await page.click('[data-testid="oauth-google"]');
|
|
// await page.waitForURL('/dashboard');
|
|
|
|
// Placeholder: Simulate successful OAuth by setting auth state directly
|
|
await page.evaluate(() => {
|
|
// Mock Pocketbase authStore for testing
|
|
const mockAuthData = {
|
|
token: 'mock-jwt-token',
|
|
record: {
|
|
id: 'test-user-id',
|
|
email: 'test@example.com',
|
|
name: 'Test User',
|
|
emailVisibility: false,
|
|
verified: true,
|
|
avatar: '',
|
|
created: new Date().toISOString(),
|
|
updated: new Date().toISOString()
|
|
}
|
|
};
|
|
localStorage.setItem('pocketbase_auth', JSON.stringify(mockAuthData));
|
|
});
|
|
|
|
// Navigate to dashboard to trigger auth restoration
|
|
await page.goto('/dashboard');
|
|
await expect(page).toHaveURL('/dashboard');
|
|
|
|
// Step 3: Verify user is authenticated - check for user name in navbar
|
|
// NOTE: Adjust selector based on actual navbar implementation
|
|
const userNameElement = page.locator('[data-testid="user-name"]').or(
|
|
page.locator('text=Test User')
|
|
);
|
|
await expect(userNameElement).toBeVisible({ timeout: 5000 });
|
|
|
|
// Step 4: Reload the page
|
|
await page.reload();
|
|
|
|
// Step 5: Verify still on /dashboard (session persisted)
|
|
await expect(page).toHaveURL('/dashboard');
|
|
|
|
// Step 6: Verify user name still visible after reload
|
|
await expect(userNameElement).toBeVisible({ timeout: 5000 });
|
|
});
|
|
|
|
test('should persist session across browser context restarts', async ({ browser }) => {
|
|
// Create initial context and authenticate
|
|
const context1 = await browser.newContext();
|
|
const page1 = await context1.newPage();
|
|
|
|
await page1.goto('/login');
|
|
|
|
// Simulate OAuth authentication
|
|
await page1.evaluate(() => {
|
|
const mockAuthData = {
|
|
token: 'mock-jwt-token',
|
|
record: {
|
|
id: 'test-user-id',
|
|
email: 'test@example.com',
|
|
name: 'Test User',
|
|
emailVisibility: false,
|
|
verified: true,
|
|
avatar: '',
|
|
created: new Date().toISOString(),
|
|
updated: new Date().toISOString()
|
|
}
|
|
};
|
|
localStorage.setItem('pocketbase_auth', JSON.stringify(mockAuthData));
|
|
});
|
|
|
|
await page1.goto('/dashboard');
|
|
await expect(page1).toHaveURL('/dashboard');
|
|
|
|
// Extract storage state to simulate browser restart
|
|
const storageState = await context1.storageState();
|
|
await context1.close();
|
|
|
|
// Create new context with stored state (simulates browser restart)
|
|
const context2 = await browser.newContext({ storageState });
|
|
const page2 = await context2.newPage();
|
|
|
|
// Navigate directly to dashboard
|
|
await page2.goto('/dashboard');
|
|
|
|
// Verify session persisted - should not redirect to /login
|
|
await expect(page2).toHaveURL('/dashboard');
|
|
|
|
// Verify user info still available
|
|
const userNameElement = page2.locator('[data-testid="user-name"]').or(
|
|
page2.locator('text=Test User')
|
|
);
|
|
await expect(userNameElement).toBeVisible({ timeout: 5000 });
|
|
|
|
await context2.close();
|
|
});
|
|
|
|
test('should redirect to login when accessing protected page without session', async ({ page }) => {
|
|
// Ensure no auth state exists
|
|
await page.goto('/');
|
|
await page.evaluate(() => localStorage.clear());
|
|
|
|
// Try to access protected page
|
|
await page.goto('/dashboard');
|
|
|
|
// Should redirect to login with redirect parameter
|
|
await expect(page).toHaveURL(/\/login\?redirect=.*dashboard/);
|
|
});
|
|
|
|
test('should restore session and allow access to other protected pages', async ({ page }) => {
|
|
// Set up authenticated session
|
|
await page.goto('/login');
|
|
await page.evaluate(() => {
|
|
const mockAuthData = {
|
|
token: 'mock-jwt-token',
|
|
record: {
|
|
id: 'test-user-id',
|
|
email: 'test@example.com',
|
|
name: 'Test User',
|
|
emailVisibility: false,
|
|
verified: true,
|
|
avatar: '',
|
|
created: new Date().toISOString(),
|
|
updated: new Date().toISOString()
|
|
}
|
|
};
|
|
localStorage.setItem('pocketbase_auth', JSON.stringify(mockAuthData));
|
|
});
|
|
|
|
await page.goto('/dashboard');
|
|
await expect(page).toHaveURL('/dashboard');
|
|
|
|
// Reload to test session persistence
|
|
await page.reload();
|
|
await expect(page).toHaveURL('/dashboard');
|
|
|
|
// Navigate to other protected pages
|
|
// NOTE: Adjust these based on actual application routes
|
|
// await page.goto('/projects');
|
|
// await expect(page).toHaveURL('/projects');
|
|
|
|
// await page.goto('/tasks');
|
|
// await expect(page).toHaveURL('/tasks');
|
|
});
|
|
});
|
|
|
|
/**
|
|
* IMPLEMENTATION NOTES FOR TEST SETUP:
|
|
*
|
|
* 1. **Playwright Configuration Required**:
|
|
* - Create `playwright.config.ts` in project root
|
|
* - Configure base URL to match dev server (http://localhost:3000)
|
|
* - Set up test fixtures for Pocketbase mock
|
|
*
|
|
* 2. **Pocketbase Test Provider Setup**:
|
|
* Option A: Use Pocketbase test mode with mock OAuth provider
|
|
* Option B: Create test-specific OAuth provider (e.g., test-provider)
|
|
* Option C: Mock OAuth at network level using Playwright's route interception
|
|
*
|
|
* 3. **Mock OAuth Flow** (Recommended Approach):
|
|
* ```typescript
|
|
* await page.route('**/api/collections/users/auth-with-oauth2', async route => {
|
|
* await route.fulfill({
|
|
* status: 200,
|
|
* body: JSON.stringify({
|
|
* token: 'mock-jwt',
|
|
* record: { ... }
|
|
* })
|
|
* });
|
|
* });
|
|
* ```
|
|
*
|
|
* 4. **Data Test IDs**:
|
|
* Add the following to components:
|
|
* - Navbar user name: `data-testid="user-name"`
|
|
* - OAuth provider buttons: `data-testid="oauth-{provider}"`
|
|
* - Logout button: `data-testid="logout-button"`
|
|
*
|
|
* 5. **Running Tests**:
|
|
* - Install Playwright: `pnpm add -D @playwright/test`
|
|
* - Add to package.json: `"test:e2e": "playwright test"`
|
|
* - Run: `pnpm test:e2e`
|
|
*/
|