import { test, expect } from '@playwright/test'; /** * E2E Tests for Authentication Flow with Keycloak OIDC and Club Switching * * Prerequisites: * - Docker Compose stack running: postgres, keycloak, dotnet-api, nextjs * - Keycloak realm imported with test users * - Frontend accessible at http://localhost:3000 * * Test User Credentials (from Task 3): * - admin@test.com / testpass123 (member of club-1 AND club-2) */ test.describe('Authentication Flow', () => { test.beforeEach(async ({ context }) => { // Clear all cookies and storage before each test await context.clearCookies(); await context.clearPermissions(); }); test('Scenario 1: Full auth flow E2E - redirect → Keycloak → club picker → dashboard', async ({ page }) => { // Step 1: Navigate to protected route await page.goto('/dashboard'); // Step 2: Assert redirected to login page await page.waitForURL(/\/login/, { timeout: 10000 }); await expect(page).toHaveURL(/\/login/); // Step 3: Click "Sign in with Keycloak" button await page.click('button:has-text("Sign in with Keycloak")'); // Step 4: Assert redirected to Keycloak login await page.waitForURL(/localhost:8080.*realms\/workclub/, { timeout: 15000 }); await expect(page).toHaveURL(/keycloak/); // Step 5: Fill Keycloak credentials await page.fill('#username', 'admin@test.com'); await page.fill('#password', 'testpass123'); // Step 6: Submit login form await page.click('#kc-login'); // Step 7: Wait for redirect back to app - should land on select-club (multi-club user) await page.waitForURL(/localhost:3000/, { timeout: 15000 }); await page.waitForURL(/\/select-club/, { timeout: 10000 }); await expect(page).toHaveURL(/\/select-club/); // Step 8: Verify club picker shows clubs await expect(page.locator('h1:has-text("Select Your Club")')).toBeVisible(); // Step 9: Click first club card (should be "Sunrise Tennis Club" or club-1) const firstClubCard = page.locator('[data-testid="club-card"]').first(); // Fallback if no test id: click any card const clubCard = (await firstClubCard.count()) > 0 ? firstClubCard : page.locator('div.cursor-pointer').first(); await clubCard.click(); // Step 10: Assert redirected to dashboard await page.waitForURL(/\/dashboard/, { timeout: 10000 }); await expect(page).toHaveURL(/\/dashboard/); // Step 11: Assert club name visible in header (via ClubSwitcher) // ClubSwitcher shows active club name await expect(page.locator('button:has-text("Sunrise Tennis Club"), button:has-text("Valley Cycling Club")')).toBeVisible({ timeout: 10000 }); // Step 12: Take screenshot as evidence await page.screenshot({ path: '.sisyphus/evidence/task-26-auth-flow.png', fullPage: true }); console.log('✅ Full auth flow completed successfully'); }); test('Scenario 2: Club switching refreshes data', async ({ page }) => { // Pre-requisite: Authenticate first await authenticateUser(page, 'admin@test.com', 'testpass123'); // Select first club await page.goto('/select-club'); await page.waitForLoadState('networkidle'); const firstClub = page.locator('div.cursor-pointer').first(); const firstClubName = await firstClub.locator('h3, [class*="CardTitle"]').first().textContent(); await firstClub.click(); // Step 1: Navigate to tasks page await page.goto('/tasks'); await page.waitForURL(/\/tasks/); await page.waitForLoadState('networkidle'); // Step 2: Assert tasks visible (club-1 data) const initialTasksCount = await page.locator('table tbody tr').count(); console.log(`Initial tasks count for first club: ${initialTasksCount}`); // Step 3: Open club switcher dropdown await page.click('button:has-text("' + (firstClubName || 'Select Club').trim() + '")'); // Wait for dropdown to open await page.waitForSelector('[role="menu"], [role="menuitem"]', { timeout: 5000 }); // Step 4: Click second club in dropdown (Valley Cycling Club or club-2) const dropdownItems = page.locator('[role="menuitem"]'); const secondClubItem = dropdownItems.nth(1); // Ensure there's a second club const itemCount = await dropdownItems.count(); expect(itemCount).toBeGreaterThan(1); const secondClubName = await secondClubItem.textContent(); await secondClubItem.click(); // Wait for page to refresh (TanStack Query invalidates all queries) await page.waitForTimeout(1000); // Allow time for query invalidation await page.waitForLoadState('networkidle'); // Step 5: Assert tasks list updates (different data) const updatedTasksCount = await page.locator('table tbody tr').count(); console.log(`Updated tasks count for second club: ${updatedTasksCount}`); // Tasks should be different between clubs (may be same count but different data) // We verify the club switcher updated await expect(page.locator('button:has-text("' + (secondClubName || '').trim() + '")')).toBeVisible(); // Step 6: Assert header shows new club name const clubSwitcherButton = page.locator('header button[class*="outline"]').first(); await expect(clubSwitcherButton).toContainText(secondClubName || ''); // Step 7: Take screenshot as evidence await page.screenshot({ path: '.sisyphus/evidence/task-26-club-switch.png', fullPage: true }); console.log('✅ Club switching completed successfully'); }); test('Scenario 3: Logout flow - clears session and blocks protected routes', async ({ page }) => { // Pre-requisite: Authenticate first await authenticateUser(page, 'admin@test.com', 'testpass123'); // Navigate to dashboard await page.goto('/dashboard'); await page.waitForURL(/\/dashboard/); // Step 1: Click logout button (SignOutButton component) await page.click('button:has-text("Sign Out"), button:has-text("Logout"), [aria-label="Sign out"]'); // Step 2: Assert redirected to login page await page.waitForURL(/\/login/, { timeout: 10000 }); await expect(page).toHaveURL(/\/login/); // Step 3: Attempt to navigate to protected route await page.goto('/dashboard'); // Step 4: Assert redirected back to login (session cleared) await page.waitForURL(/\/login/, { timeout: 10000 }); await expect(page).toHaveURL(/\/login/); console.log('✅ Logout flow completed successfully'); }); test('Unauthenticated user blocked from protected routes', async ({ page }) => { // Attempt to access protected route directly await page.goto('/tasks'); // Assert redirected to login with callbackUrl await page.waitForURL(/\/login/, { timeout: 10000 }); await expect(page).toHaveURL(/\/login/); // Verify callbackUrl query param exists const url = new URL(page.url()); expect(url.searchParams.get('callbackUrl')).toBe('/tasks'); console.log('✅ Protected route correctly blocked'); }); test('Single-club user bypasses club picker', async ({ page }) => { // Authenticate with single-club user (manager@test.com - only in club-1) await page.goto('/login'); await page.click('button:has-text("Sign in with Keycloak")'); await page.waitForURL(/localhost:8080.*realms\/workclub/, { timeout: 15000 }); await page.fill('#username', 'manager@test.com'); await page.fill('#password', 'testpass123'); await page.click('#kc-login'); // Should redirect directly to dashboard (no club picker) await page.waitForURL(/localhost:3000/, { timeout: 15000 }); // Wait a moment for potential redirect await page.waitForTimeout(2000); // Assert on dashboard, NOT select-club await expect(page).toHaveURL(/\/dashboard/); await expect(page).not.toHaveURL(/\/select-club/); console.log('✅ Single-club user bypassed club picker'); }); test('Keycloak login with invalid credentials fails', async ({ page }) => { await page.goto('/login'); await page.click('button:has-text("Sign in with Keycloak")'); await page.waitForURL(/localhost:8080.*realms\/workclub/, { timeout: 15000 }); // Enter invalid credentials await page.fill('#username', 'invalid@test.com'); await page.fill('#password', 'wrongpassword'); await page.click('#kc-login'); // Assert error message from Keycloak await expect(page.locator('#input-error, .kc-feedback-text, [class*="error"]')).toBeVisible({ timeout: 5000 }); console.log('✅ Invalid credentials correctly rejected'); }); }); /** * Helper function to authenticate a user through the full Keycloak flow */ async function authenticateUser(page: any, email: string, password: string) { await page.goto('/login'); await page.click('button:has-text("Sign in with Keycloak")'); await page.waitForURL(/localhost:8080.*realms\/workclub/, { timeout: 15000 }); await page.fill('#username', email); await page.fill('#password', password); await page.click('#kc-login'); // Wait for redirect back await page.waitForURL(/localhost:3000/, { timeout: 15000 }); // If club picker appears, select first club const isClubPicker = await page.url().includes('/select-club'); if (isClubPicker) { await page.waitForTimeout(1000); const clubCard = page.locator('div.cursor-pointer').first(); await clubCard.click(); await page.waitForURL(/\/dashboard/, { timeout: 10000 }); } }