forked from cardosofelipe/fast-next-template
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.
This commit is contained in:
170
frontend/e2e/auth-oauth.spec.ts
Normal file
170
frontend/e2e/auth-oauth.spec.ts
Normal file
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -227,6 +227,43 @@ export async function setupAuthenticatedMocks(page: Page): Promise<void> {
|
||||
}
|
||||
});
|
||||
|
||||
// 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<void> {
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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'] },
|
||||
|
||||
@@ -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: [],
|
||||
});
|
||||
}),
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user