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:
@@ -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!();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user