From fcbcff99e961f9ef8d0d7596d7eddb44d0cfcd21 Mon Sep 17 00:00:00 2001 From: Felipe Cardoso Date: Tue, 25 Nov 2025 10:40:37 +0100 Subject: [PATCH] Add E2E tests for OAuth authentication flows and provider integrations - Implemented comprehensive E2E tests for OAuth buttons on login and register pages, including Google and GitHub provider interactions. - Verified OAuth provider buttons' visibility, icons, and proper API integration with mock endpoints. - Added button interaction tests to ensure correct API calls for authorization and state handling. - Updated `playwright.config.ts` to include the new `auth-oauth.spec.ts` in test configurations. - Extended mock handlers in `overrides.ts` and `auth.ts` to support OAuth-specific API workflows and demo scenarios. --- frontend/e2e/auth-oauth.spec.ts | 170 +++++++++++++++++++++++ frontend/e2e/helpers/auth.ts | 74 ++++++++++ frontend/playwright.config.ts | 1 + frontend/src/mocks/handlers/overrides.ts | 81 +++++++++++ 4 files changed, 326 insertions(+) create mode 100644 frontend/e2e/auth-oauth.spec.ts diff --git a/frontend/e2e/auth-oauth.spec.ts b/frontend/e2e/auth-oauth.spec.ts new file mode 100644 index 0000000..1138588 --- /dev/null +++ b/frontend/e2e/auth-oauth.spec.ts @@ -0,0 +1,170 @@ +/** + * E2E Tests for OAuth Authentication + * Tests OAuth button display and interaction on login/register pages + */ + +import { test, expect } from '@playwright/test'; +import { setupAuthenticatedMocks } from './helpers/auth'; + +test.describe('OAuth Authentication', () => { + test.describe('Login Page OAuth', () => { + test.beforeEach(async ({ page }) => { + // Set up API mocks including OAuth providers + await setupAuthenticatedMocks(page); + }); + + test('should display OAuth provider buttons on login page', async ({ page }) => { + await page.goto('/en/login'); + + // Wait for OAuth buttons to load (they fetch providers from API) + await page.waitForSelector('text=Continue with Google', { timeout: 10000 }); + + // Verify both OAuth buttons are visible + await expect(page.getByRole('button', { name: /continue with google/i })).toBeVisible(); + await expect(page.getByRole('button', { name: /continue with github/i })).toBeVisible(); + }); + + test('should display divider between form and OAuth buttons', async ({ page }) => { + await page.goto('/en/login'); + + // Wait for OAuth buttons to load + await page.waitForSelector('text=Continue with Google', { timeout: 10000 }); + + // Verify divider text is present + await expect(page.getByText(/or continue with/i)).toBeVisible(); + }); + + test('should have Google OAuth button with correct icon', async ({ page }) => { + await page.goto('/en/login'); + + // Wait for OAuth buttons to load + const googleButton = page.getByRole('button', { name: /continue with google/i }); + await googleButton.waitFor({ state: 'visible', timeout: 10000 }); + + // Verify button contains an SVG icon + const svg = googleButton.locator('svg'); + await expect(svg).toBeVisible(); + }); + + test('should have GitHub OAuth button with correct icon', async ({ page }) => { + await page.goto('/en/login'); + + // Wait for OAuth buttons to load + const githubButton = page.getByRole('button', { name: /continue with github/i }); + await githubButton.waitFor({ state: 'visible', timeout: 10000 }); + + // Verify button contains an SVG icon + const svg = githubButton.locator('svg'); + await expect(svg).toBeVisible(); + }); + }); + + test.describe('Register Page OAuth', () => { + test.beforeEach(async ({ page }) => { + // Set up API mocks including OAuth providers + await setupAuthenticatedMocks(page); + }); + + test('should display OAuth provider buttons on register page', async ({ page }) => { + await page.goto('/en/register'); + + // Wait for OAuth buttons to load + await page.waitForSelector('text=Sign up with Google', { timeout: 10000 }); + + // Verify both OAuth buttons are visible with register-specific text + await expect(page.getByRole('button', { name: /sign up with google/i })).toBeVisible(); + await expect(page.getByRole('button', { name: /sign up with github/i })).toBeVisible(); + }); + + test('should display divider between form and OAuth buttons on register page', async ({ + page, + }) => { + await page.goto('/en/register'); + + // Wait for OAuth buttons to load + await page.waitForSelector('text=Sign up with Google', { timeout: 10000 }); + + // Verify divider text is present + await expect(page.getByText(/or continue with/i)).toBeVisible(); + }); + }); + + test.describe('OAuth Button Interaction', () => { + test.beforeEach(async ({ page }) => { + // Set up API mocks including OAuth providers + await setupAuthenticatedMocks(page); + }); + + test('should call OAuth authorization endpoint when clicking Google button', async ({ + page, + }) => { + // Track API calls + let authorizationCalled = false; + await page.route('**/api/v1/oauth/authorize/google*', async (route) => { + authorizationCalled = true; + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + authorization_url: 'https://accounts.google.com/o/oauth2/auth?mock=true', + state: 'mock-state', + }), + }); + }); + + // Prevent actual navigation to external URL + await page.route('https://accounts.google.com/**', (route) => route.abort()); + + await page.goto('/en/login'); + + // Wait for OAuth buttons to load + const googleButton = page.getByRole('button', { name: /continue with google/i }); + await googleButton.waitFor({ state: 'visible', timeout: 10000 }); + + // Click Google OAuth button + await googleButton.click(); + + // Wait for API call to complete + await page.waitForTimeout(500); + + // Verify authorization endpoint was called + expect(authorizationCalled).toBe(true); + }); + + test('should call OAuth authorization endpoint when clicking GitHub button', async ({ + page, + }) => { + // Track API calls + let authorizationCalled = false; + await page.route('**/api/v1/oauth/authorize/github*', async (route) => { + authorizationCalled = true; + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + authorization_url: 'https://github.com/login/oauth/authorize?mock=true', + state: 'mock-state', + }), + }); + }); + + // Prevent actual navigation to external URL + await page.route('https://github.com/**', (route) => route.abort()); + + await page.goto('/en/login'); + + // Wait for OAuth buttons to load + const githubButton = page.getByRole('button', { name: /continue with github/i }); + await githubButton.waitFor({ state: 'visible', timeout: 10000 }); + + // Click GitHub OAuth button + await githubButton.click(); + + // Wait for API call to complete + await page.waitForTimeout(500); + + // Verify authorization endpoint was called + expect(authorizationCalled).toBe(true); + }); + }); +}); diff --git a/frontend/e2e/helpers/auth.ts b/frontend/e2e/helpers/auth.ts index f2c969f..e18fba8 100644 --- a/frontend/e2e/helpers/auth.ts +++ b/frontend/e2e/helpers/auth.ts @@ -227,6 +227,43 @@ export async function setupAuthenticatedMocks(page: Page): Promise { } }); + // Mock GET /api/v1/oauth/providers - Get available OAuth providers + await page.route(`${baseURL}/api/v1/oauth/providers`, async (route: Route) => { + if (route.request().method() === 'GET') { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + enabled: true, + providers: [ + { provider: 'google', name: 'Google' }, + { provider: 'github', name: 'GitHub' }, + ], + }), + }); + } else { + await route.continue(); + } + }); + + // Mock GET /api/v1/oauth/authorize/:provider - Get OAuth authorization URL + await page.route(`${baseURL}/api/v1/oauth/authorize/*`, async (route: Route) => { + if (route.request().method() === 'GET') { + const url = route.request().url(); + const provider = url.match(/authorize\/(\w+)/)?.[1] || 'google'; + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + authorization_url: `https://mock-oauth.example.com/authorize?provider=${provider}`, + state: 'mock-state-token', + }), + }); + } else { + await route.continue(); + } + }); + /** * E2E tests now use the REAL auth store with mocked API routes. * We inject authentication by calling setAuth() directly in the page context. @@ -579,4 +616,41 @@ export async function setupSuperuserMocks(page: Page): Promise { await route.continue(); } }); + + // Mock GET /api/v1/oauth/providers - Get available OAuth providers + await page.route(`${baseURL}/api/v1/oauth/providers`, async (route: Route) => { + if (route.request().method() === 'GET') { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + enabled: true, + providers: [ + { provider: 'google', name: 'Google' }, + { provider: 'github', name: 'GitHub' }, + ], + }), + }); + } else { + await route.continue(); + } + }); + + // Mock GET /api/v1/oauth/authorize/:provider - Get OAuth authorization URL + await page.route(`${baseURL}/api/v1/oauth/authorize/*`, async (route: Route) => { + if (route.request().method() === 'GET') { + const url = route.request().url(); + const provider = url.match(/authorize\/(\w+)/)?.[1] || 'google'; + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + authorization_url: `https://mock-oauth.example.com/authorize?provider=${provider}`, + state: 'mock-state-token', + }), + }); + } else { + await route.continue(); + } + }); } diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts index 20e61b6..a7cf0d3 100644 --- a/frontend/playwright.config.ts +++ b/frontend/playwright.config.ts @@ -111,6 +111,7 @@ export default defineConfig({ /auth-register\.spec\.ts/, /auth-password-reset\.spec\.ts/, /auth-flows\.spec\.ts/, + /auth-oauth\.spec\.ts/, /theme-toggle\.spec\.ts/, ], use: { ...devices['Desktop Chrome'] }, diff --git a/frontend/src/mocks/handlers/overrides.ts b/frontend/src/mocks/handlers/overrides.ts index bb151e1..8fcb500 100644 --- a/frontend/src/mocks/handlers/overrides.ts +++ b/frontend/src/mocks/handlers/overrides.ts @@ -14,6 +14,7 @@ import { http, HttpResponse, delay } from 'msw'; import { generateMockToken } from '../utils/tokens'; import { validateCredentials, setCurrentUser, currentUser } from '../data/users'; +import config from '@/config/app.config'; const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8000'; const NETWORK_DELAY = 300; // ms - simulate realistic network delay @@ -101,4 +102,84 @@ export const overrideHandlers = [ expires_in: 900, }); }), + + /** + * OAuth Providers Override + * Returns list of available OAuth providers for demo mode + */ + http.get(`${API_BASE_URL}/api/v1/oauth/providers`, async () => { + await delay(NETWORK_DELAY); + + return HttpResponse.json({ + enabled: true, + providers: [ + { provider: 'google', name: 'Google' }, + { provider: 'github', name: 'GitHub' }, + ], + }); + }), + + /** + * OAuth Authorization URL Override + * Returns mock authorization URL (in demo mode, this won't actually redirect) + */ + http.get(`${API_BASE_URL}/api/v1/oauth/authorize/:provider`, async ({ params }) => { + await delay(NETWORK_DELAY); + + const { provider } = params; + + // In demo mode, we return a mock URL that will show a demo message + return HttpResponse.json({ + authorization_url: `${config.app.url}/en/login?demo_oauth=${provider}`, + state: `demo-state-${Date.now()}`, + }); + }), + + /** + * OAuth Callback Override + * Handles mock OAuth callback in demo mode + */ + http.post(`${API_BASE_URL}/api/v1/oauth/callback/:provider`, async ({ params }) => { + await delay(NETWORK_DELAY); + + const { provider } = params; + + // Create a demo user based on the provider + const demoOAuthUser = { + id: `oauth-demo-${Date.now()}`, + email: `demo.${provider}@example.com`, + first_name: 'Demo', + last_name: `${provider === 'google' ? 'Google' : 'GitHub'} User`, + phone_number: null, + is_active: true, + is_superuser: false, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + last_login: new Date().toISOString(), + organization_count: 0, + }; + + setCurrentUser(demoOAuthUser); + + return HttpResponse.json({ + access_token: generateMockToken('access', demoOAuthUser.id), + refresh_token: generateMockToken('refresh', demoOAuthUser.id), + token_type: 'bearer', + expires_in: 900, + is_new_user: true, + }); + }), + + /** + * OAuth Accounts Override + * Returns linked OAuth accounts for the current user + */ + http.get(`${API_BASE_URL}/api/v1/oauth/accounts`, async () => { + await delay(NETWORK_DELAY); + + // In demo mode, return empty accounts (user can "link" them) + return HttpResponse.json({ + accounts: [], + }); + }), ];