Add E2E tests for OAuth consent page workflows

- Added tests for OAuth consent page covering parameter validation, unauthenticated user redirects, authenticated user interactions, scope management, and consent API calls.
- Verified behaviors such as error handling, toggling scopes, loading states, and authorize/deny actions.
- Updated utility methods with `loginViaUI` for improved test setup.
This commit is contained in:
Felipe Cardoso
2025-11-26 14:06:36 +01:00
parent 803b720530
commit c63b6a4f76

View File

@@ -4,7 +4,7 @@
*/
import { test, expect } from '@playwright/test';
import { setupAuthenticatedMocks } from './helpers/auth';
import { setupAuthenticatedMocks, loginViaUI } from './helpers/auth';
test.describe('OAuth Authentication', () => {
test.describe('Login Page OAuth', () => {
@@ -167,4 +167,255 @@ test.describe('OAuth Authentication', () => {
expect(authorizationCalled).toBe(true);
});
});
test.describe('OAuth Provider Consent Page', () => {
const mockConsentParams = {
client_id: 'test-mcp-client-id',
client_name: 'Test MCP Application',
redirect_uri: 'http://localhost:3001/callback',
scope: 'openid profile email',
state: 'mock-state-token-123',
code_challenge: 'E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM',
code_challenge_method: 'S256',
};
test.beforeEach(async ({ page }) => {
// Set up API mocks
await setupAuthenticatedMocks(page);
});
test('should display error when required params are missing', async ({ page }) => {
// Navigate to consent page without params
await page.goto('/en/auth/consent');
// Should show error alert
await expect(page.getByText(/invalid authorization request/i)).toBeVisible({
timeout: 10000,
});
await expect(page.getByText(/missing required parameters/i)).toBeVisible();
});
test('should redirect unauthenticated users to login', async ({ page }) => {
// Build consent URL with params
const params = new URLSearchParams(mockConsentParams);
// Navigate to consent page without being logged in
await page.goto(`/en/auth/consent?${params.toString()}`);
// Should redirect to login page with return_to parameter
await expect(page).toHaveURL(/\/login\?return_to=/i, { timeout: 10000 });
});
test('should display consent page with client info for authenticated user', async ({
page,
}) => {
// First log in
await loginViaUI(page);
// Build consent URL with params
const params = new URLSearchParams(mockConsentParams);
// Navigate to consent page
await page.goto(`/en/auth/consent?${params.toString()}`);
// Should display authorization request header
await expect(page.getByText(/authorization request/i)).toBeVisible({ timeout: 10000 });
// Should display client name
await expect(page.getByText('Test MCP Application')).toBeVisible();
// Should display the requested scopes (use exact match to avoid duplicates)
await expect(page.getByText('OpenID Connect')).toBeVisible();
await expect(page.getByLabel('Profile')).toBeVisible();
await expect(page.getByLabel('Email')).toBeVisible();
// Should display redirect URI
await expect(page.getByText(/localhost:3001\/callback/i)).toBeVisible();
// Should display Authorize and Deny buttons
await expect(page.getByRole('button', { name: /authorize/i })).toBeVisible();
await expect(page.getByRole('button', { name: /deny/i })).toBeVisible();
});
test('should allow toggling scopes', async ({ page }) => {
// First log in
await loginViaUI(page);
// Build consent URL with params
const params = new URLSearchParams(mockConsentParams);
await page.goto(`/en/auth/consent?${params.toString()}`);
// Wait for page to load
await expect(page.getByText(/authorization request/i)).toBeVisible({ timeout: 10000 });
// Find OpenID scope checkbox and toggle it
const openidCheckbox = page.locator('#scope-openid');
await expect(openidCheckbox).toBeChecked();
// Uncheck it
await openidCheckbox.click();
await expect(openidCheckbox).not.toBeChecked();
// Check it again
await openidCheckbox.click();
await expect(openidCheckbox).toBeChecked();
});
test('should disable Authorize button when no scopes selected', async ({ page }) => {
// First log in
await loginViaUI(page);
// Build consent URL with single scope for easier testing
const params = new URLSearchParams({
...mockConsentParams,
scope: 'openid',
});
await page.goto(`/en/auth/consent?${params.toString()}`);
// Wait for page to load
await expect(page.getByText(/authorization request/i)).toBeVisible({ timeout: 10000 });
// Initially Authorize button should be enabled
const authorizeButton = page.getByRole('button', { name: /authorize/i });
await expect(authorizeButton).toBeEnabled();
// Uncheck the only scope
await page.locator('#scope-openid').click();
// Now Authorize button should be disabled
await expect(authorizeButton).toBeDisabled();
});
test('should call consent API when clicking Authorize', async ({ page }) => {
// First log in
await loginViaUI(page);
// Mock the consent submission endpoint (use wildcard to catch all variations)
let consentSubmitted = false;
await page.route('**/api/v1/oauth/provider/authorize/consent', async (route) => {
if (route.request().method() === 'POST') {
consentSubmitted = true;
// Simulate redirect response
await route.fulfill({
status: 302,
headers: {
Location: `${mockConsentParams.redirect_uri}?code=mock-auth-code&state=${mockConsentParams.state}`,
},
});
} else {
await route.continue();
}
});
// Prevent actual navigation to callback URL
await page.route('http://localhost:3001/**', (route) => route.fulfill({ status: 200 }));
// Build consent URL with params
const params = new URLSearchParams(mockConsentParams);
await page.goto(`/en/auth/consent?${params.toString()}`);
// Wait for page to load
await expect(page.getByText(/authorization request/i)).toBeVisible({ timeout: 10000 });
// Click Authorize
await page.getByRole('button', { name: /authorize/i }).click();
// Wait for API call
await page.waitForTimeout(1000);
// Verify consent was submitted
expect(consentSubmitted).toBe(true);
});
test('should call consent API with approved=false when clicking Deny', async ({ page }) => {
// First log in
await loginViaUI(page);
// Track the request
let requestMade = false;
let postDataContainsFalse = false;
// Mock the consent submission endpoint
await page.route('**/api/v1/oauth/provider/authorize/consent', async (route) => {
requestMade = true;
const postData = route.request().postData();
// FormData is multipart, so we check if "false" appears after "approved"
if (postData && postData.includes('name="approved"')) {
// The multipart format has approved value after the field name line
postDataContainsFalse = postData.includes('false');
}
// Return a simple success response
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ success: true }),
});
});
// Build consent URL with params
const params = new URLSearchParams(mockConsentParams);
await page.goto(`/en/auth/consent?${params.toString()}`);
// Wait for page to load
await expect(page.getByText(/authorization request/i)).toBeVisible({ timeout: 10000 });
// Click Deny and wait for the request
await Promise.all([
page.waitForResponse('**/api/v1/oauth/provider/authorize/consent'),
page.getByRole('button', { name: /deny/i }).click(),
]);
// Verify the request was made with approved=false
expect(requestMade).toBe(true);
expect(postDataContainsFalse).toBe(true);
});
test('should show loading state while submitting', async ({ page }) => {
// First log in
await loginViaUI(page);
// We'll use a promise that we can resolve manually to control when the request completes
let resolveRequest: () => void;
const requestComplete = new Promise<void>((resolve) => {
resolveRequest = resolve;
});
// Mock the consent submission endpoint with controlled delay
await page.route('**/api/v1/oauth/provider/authorize/consent', async (route) => {
// Wait until we've verified the loading state, then complete
await requestComplete;
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ success: true }),
});
});
// Build consent URL with params
const params = new URLSearchParams(mockConsentParams);
await page.goto(`/en/auth/consent?${params.toString()}`);
// Wait for page to load
await expect(page.getByText(/authorization request/i)).toBeVisible({ timeout: 10000 });
// Get buttons before clicking
const authorizeBtn = page.getByRole('button', { name: /authorize/i });
const denyBtn = page.getByRole('button', { name: /deny/i });
// Verify buttons are initially enabled
await expect(authorizeBtn).toBeEnabled();
await expect(denyBtn).toBeEnabled();
// Click Authorize (don't await - let it start the request)
authorizeBtn.click();
// Should show loading spinner while request is pending
await expect(page.locator('.animate-spin').first()).toBeVisible({ timeout: 5000 });
// Now resolve the request to clean up
resolveRequest!();
});
});
});