mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-10-01 07:19:05 +02:00
tests(e2e): use custom Playwright route for callback URL checks
This commit is contained in:
+100
-149
@@ -10,36 +10,21 @@ test.beforeEach(async () => await cleanupBackend());
|
||||
test('Authorize existing client', async ({ page }) => {
|
||||
const oidcClient = oidcClients.nextcloud;
|
||||
const urlParams = createUrlParams(oidcClient);
|
||||
await page.goto(`/authorize?${urlParams.toString()}`);
|
||||
|
||||
// Ignore DNS resolution error as the callback URL is not reachable
|
||||
await page.waitForURL(oidcClient.callbackUrl).catch((e) => {
|
||||
if (
|
||||
!e.message.includes('net::ERR_NAME_NOT_RESOLVED') &&
|
||||
!e.message.includes('net::ERR_CERT_AUTHORITY_INVALID')
|
||||
) {
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
await expectCallbackRedirect(page, oidcClient.callbackUrl, () =>
|
||||
page.goto(`/authorize?${urlParams.toString()}`)
|
||||
);
|
||||
});
|
||||
|
||||
test('Authorize existing client while not signed in', async ({ page }) => {
|
||||
const oidcClient = oidcClients.nextcloud;
|
||||
const urlParams = createUrlParams(oidcClient);
|
||||
await page.context().clearCookies();
|
||||
await page.goto(`/authorize?${urlParams.toString()}`);
|
||||
|
||||
await (await passkeyUtil.init(page)).addPasskey();
|
||||
await page.getByRole('button', { name: 'Sign in' }).click();
|
||||
await expectCallbackRedirect(page, oidcClient.callbackUrl, async () => {
|
||||
await page.goto(`/authorize?${urlParams.toString()}`);
|
||||
|
||||
// Ignore DNS resolution error as the callback URL is not reachable
|
||||
await page.waitForURL(oidcClient.callbackUrl).catch((e) => {
|
||||
if (
|
||||
!e.message.includes('net::ERR_NAME_NOT_RESOLVED') &&
|
||||
!e.message.includes('net::ERR_CERT_AUTHORITY_INVALID')
|
||||
) {
|
||||
throw e;
|
||||
}
|
||||
await (await passkeyUtil.init(page)).addPasskey();
|
||||
await page.getByRole('button', { name: 'Sign in' }).click();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -51,17 +36,9 @@ test('Authorize new client', async ({ page }) => {
|
||||
await expect(page.getByTestId('scopes').getByRole('heading', { name: 'Email' })).toBeVisible();
|
||||
await expect(page.getByTestId('scopes').getByRole('heading', { name: 'Profile' })).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'Sign in' }).click();
|
||||
|
||||
// Ignore DNS resolution error as the callback URL is not reachable
|
||||
await page.waitForURL(oidcClient.callbackUrl).catch((e) => {
|
||||
if (
|
||||
!e.message.includes('net::ERR_NAME_NOT_RESOLVED') &&
|
||||
!e.message.includes('net::ERR_CERT_AUTHORITY_INVALID')
|
||||
) {
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
await expectCallbackRedirect(page, oidcClient.callbackUrl, () =>
|
||||
page.getByRole('button', { name: 'Sign in' }).click()
|
||||
);
|
||||
});
|
||||
|
||||
test('Authorize new client while not signed in', async ({ page }) => {
|
||||
@@ -76,17 +53,9 @@ test('Authorize new client while not signed in', async ({ page }) => {
|
||||
await expect(page.getByTestId('scopes').getByRole('heading', { name: 'Email' })).toBeVisible();
|
||||
await expect(page.getByTestId('scopes').getByRole('heading', { name: 'Profile' })).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'Sign in' }).click();
|
||||
|
||||
// Ignore DNS resolution error as the callback URL is not reachable
|
||||
await page.waitForURL(oidcClient.callbackUrl).catch((e) => {
|
||||
if (
|
||||
!e.message.includes('net::ERR_NAME_NOT_RESOLVED') &&
|
||||
!e.message.includes('net::ERR_CERT_AUTHORITY_INVALID')
|
||||
) {
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
await expectCallbackRedirect(page, oidcClient.callbackUrl, () =>
|
||||
page.getByRole('button', { name: 'Sign in' }).click()
|
||||
);
|
||||
});
|
||||
|
||||
test('Authorize new client fails with user group not allowed', async ({ page }) => {
|
||||
@@ -130,24 +99,16 @@ test('End session without id token hint shows confirmation page', async ({ page
|
||||
|
||||
test('End session with id token hint redirects to callback URL', async ({ page }) => {
|
||||
const client = oidcClients.nextcloud;
|
||||
const idToken = await generateIdToken("fe81c12a-7336-4aee-bebc-d901a873bf48", users.tim, client.id);
|
||||
let redirectedCorrectly = false;
|
||||
await page
|
||||
.goto(
|
||||
const idToken = await generateIdToken(
|
||||
'fe81c12a-7336-4aee-bebc-d901a873bf48',
|
||||
users.tim,
|
||||
client.id
|
||||
);
|
||||
await expectCallbackRedirect(page, client.logoutCallbackUrl, () =>
|
||||
page.goto(
|
||||
`/api/oidc/end-session?id_token_hint=${idToken}&post_logout_redirect_uri=${client.logoutCallbackUrl}`
|
||||
)
|
||||
.catch((e) => {
|
||||
if (
|
||||
e.message.includes('net::ERR_NAME_NOT_RESOLVED') ||
|
||||
e.message.includes('net::ERR_CERT_AUTHORITY_INVALID')
|
||||
) {
|
||||
redirectedCorrectly = true;
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
||||
expect(redirectedCorrectly).toBeTruthy();
|
||||
);
|
||||
});
|
||||
|
||||
test('Successfully refresh tokens with valid refresh token', async ({ request }) => {
|
||||
@@ -627,17 +588,9 @@ test('Forces reauthentication when client requires it', async ({ page, request }
|
||||
await (await passkeyUtil.init(page)).addPasskey();
|
||||
|
||||
const urlParams = createUrlParams(oidcClients.nextcloud);
|
||||
await page.goto(`/authorize?${urlParams.toString()}`);
|
||||
|
||||
await expect(page.getByTestId('scopes')).not.toBeVisible();
|
||||
|
||||
await page.waitForURL(oidcClients.nextcloud.callbackUrl).catch((e) => {
|
||||
if (
|
||||
!e.message.includes('net::ERR_NAME_NOT_RESOLVED') &&
|
||||
!e.message.includes('net::ERR_CERT_AUTHORITY_INVALID')
|
||||
) {
|
||||
throw e;
|
||||
}
|
||||
await expectCallbackRedirect(page, oidcClients.nextcloud.callbackUrl, async () => {
|
||||
await page.goto(`/authorize?${urlParams.toString()}`);
|
||||
await expect(page.getByTestId('scopes')).not.toBeVisible();
|
||||
});
|
||||
|
||||
expect(webauthnStartCalled).toBe(true);
|
||||
@@ -676,9 +629,9 @@ test('Authorize existing client with response_mode=fragment', async ({ page }) =
|
||||
const urlParams = createUrlParams(oidcClient);
|
||||
urlParams.set('response_mode', 'fragment');
|
||||
|
||||
await page.goto(`/authorize?${urlParams.toString()}`);
|
||||
|
||||
const redirectUrl = await waitForCallbackURL(page, oidcClient.callbackUrl);
|
||||
const redirectUrl = await expectCallbackRedirect(page, oidcClient.callbackUrl, () =>
|
||||
page.goto(`/authorize?${urlParams.toString()}`)
|
||||
);
|
||||
expect(redirectUrl.search).toBe('');
|
||||
|
||||
const fragmentParams = new URLSearchParams(redirectUrl.hash.slice(1));
|
||||
@@ -693,23 +646,6 @@ function waitForFormPostRequest(page: Page, callbackUrl: string): Promise<Reques
|
||||
);
|
||||
}
|
||||
|
||||
async function waitForCallbackURL(page: Page, callbackUrl: string): Promise<URL> {
|
||||
const expectedUrl = new URL(callbackUrl);
|
||||
|
||||
await page
|
||||
.waitForURL((url) => url.origin === expectedUrl.origin && url.pathname === expectedUrl.pathname)
|
||||
.catch((e) => {
|
||||
if (
|
||||
!e.message.includes('net::ERR_NAME_NOT_RESOLVED') &&
|
||||
!e.message.includes('net::ERR_CERT_AUTHORITY_INVALID')
|
||||
) {
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
||||
return new URL(page.url());
|
||||
}
|
||||
|
||||
async function expectFormPostRequest(formPostRequestPromise: Promise<Request>) {
|
||||
const request = await formPostRequestPromise;
|
||||
const formData = new URLSearchParams(request.postData() ?? '');
|
||||
@@ -744,9 +680,9 @@ test.describe('OIDC prompt parameter', () => {
|
||||
urlParams.set('prompt', 'none');
|
||||
urlParams.set('response_mode', 'fragment');
|
||||
|
||||
await page.goto(`/authorize?${urlParams.toString()}`).then(() => {});
|
||||
|
||||
const redirectUrl = await waitForCallbackURL(page, oidcClient.callbackUrl);
|
||||
const redirectUrl = await expectCallbackRedirect(page, oidcClient.callbackUrl, () =>
|
||||
page.goto(`/authorize?${urlParams.toString()}`)
|
||||
);
|
||||
expect(redirectUrl.search).toBe('');
|
||||
|
||||
const fragmentParams = new URLSearchParams(redirectUrl.hash.slice(1));
|
||||
@@ -775,17 +711,9 @@ test.describe('OIDC prompt parameter', () => {
|
||||
const urlParams = createUrlParams(oidcClient);
|
||||
urlParams.set('prompt', 'none');
|
||||
|
||||
await page.goto(`/authorize?${urlParams.toString()}`);
|
||||
|
||||
// Should redirect successfully to callback URL with code
|
||||
await page.waitForURL(oidcClient.callbackUrl).catch((e) => {
|
||||
if (
|
||||
!e.message.includes('net::ERR_NAME_NOT_RESOLVED') &&
|
||||
!e.message.includes('net::ERR_CERT_AUTHORITY_INVALID')
|
||||
) {
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
await expectCallbackRedirect(page, oidcClient.callbackUrl, () =>
|
||||
page.goto(`/authorize?${urlParams.toString()}`)
|
||||
);
|
||||
});
|
||||
|
||||
test('prompt=consent forces consent display even for authorized client', async ({ page }) => {
|
||||
@@ -801,17 +729,9 @@ test.describe('OIDC prompt parameter', () => {
|
||||
).toBeVisible();
|
||||
await expect(page.getByTestId('scopes').getByRole('heading', { name: 'Email' })).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'Sign in' }).click();
|
||||
|
||||
// Should redirect successfully after consent
|
||||
await page.waitForURL(oidcClient.callbackUrl).catch((e) => {
|
||||
if (
|
||||
!e.message.includes('net::ERR_NAME_NOT_RESOLVED') &&
|
||||
!e.message.includes('net::ERR_CERT_AUTHORITY_INVALID')
|
||||
) {
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
await expectCallbackRedirect(page, oidcClient.callbackUrl, () =>
|
||||
page.getByRole('button', { name: 'Sign in' }).click()
|
||||
);
|
||||
});
|
||||
|
||||
test('prompt=login forces reauthentication', async ({ page }) => {
|
||||
@@ -826,17 +746,9 @@ test.describe('OIDC prompt parameter', () => {
|
||||
});
|
||||
|
||||
await (await passkeyUtil.init(page)).addPasskey();
|
||||
await page.goto(`/authorize?${urlParams.toString()}`);
|
||||
|
||||
// Should require reauthentication even though user is signed in
|
||||
await page.waitForURL(oidcClient.callbackUrl).catch((e) => {
|
||||
if (
|
||||
!e.message.includes('net::ERR_NAME_NOT_RESOLVED') &&
|
||||
!e.message.includes('net::ERR_CERT_AUTHORITY_INVALID')
|
||||
) {
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
await expectCallbackRedirect(page, oidcClient.callbackUrl, () =>
|
||||
page.goto(`/authorize?${urlParams.toString()}`)
|
||||
);
|
||||
|
||||
expect(reauthCalled).toBe(true);
|
||||
});
|
||||
@@ -853,17 +765,9 @@ test.describe('OIDC prompt parameter', () => {
|
||||
await expect(selectionCard).toBeVisible();
|
||||
await expect(selectionCard).toContainText('Tim Cook');
|
||||
|
||||
await page.getByRole('button', { name: 'Sign In' }).click();
|
||||
|
||||
// Should redirect successfully to callback URL with code
|
||||
await page.waitForURL(oidcClient.callbackUrl).catch((e) => {
|
||||
if (
|
||||
!e.message.includes('net::ERR_NAME_NOT_RESOLVED') &&
|
||||
!e.message.includes('net::ERR_CERT_AUTHORITY_INVALID')
|
||||
) {
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
await expectCallbackRedirect(page, oidcClient.callbackUrl, () =>
|
||||
page.getByRole('button', { name: 'Sign In' }).click()
|
||||
);
|
||||
});
|
||||
|
||||
test('prompt=select_account account can be changed', async ({ page }) => {
|
||||
@@ -880,17 +784,9 @@ test.describe('OIDC prompt parameter', () => {
|
||||
|
||||
await page.getByRole('button', { name: 'Sign In' }).click();
|
||||
|
||||
await page.getByRole('button', { name: 'Sign In' }).click();
|
||||
|
||||
// Should redirect successfully to callback URL with code
|
||||
await page.waitForURL(oidcClient.callbackUrl).catch((e) => {
|
||||
if (
|
||||
!e.message.includes('net::ERR_NAME_NOT_RESOLVED') &&
|
||||
!e.message.includes('net::ERR_CERT_AUTHORITY_INVALID')
|
||||
) {
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
await expectCallbackRedirect(page, oidcClient.callbackUrl, () =>
|
||||
page.getByRole('button', { name: 'Sign In' }).click()
|
||||
);
|
||||
});
|
||||
|
||||
test('prompt=none with prompt=consent returns interaction_required', async ({ page }) => {
|
||||
@@ -935,3 +831,58 @@ test.describe('OIDC prompt parameter', () => {
|
||||
expect(redirectUrl.searchParams.get('state')).toBe('nXx-6Qr-owc1SHBa');
|
||||
});
|
||||
});
|
||||
|
||||
async function waitForCallbackURL(page: Page, callbackUrl: string): Promise<URL> {
|
||||
const expectedUrl = new URL(callbackUrl);
|
||||
const isCallbackURL = (url: URL) =>
|
||||
url.origin === expectedUrl.origin && url.pathname === expectedUrl.pathname;
|
||||
|
||||
const callbackRequest = await page.waitForRequest((request) =>
|
||||
isCallbackURL(new URL(request.url()))
|
||||
);
|
||||
await page.waitForURL(isCallbackURL, { waitUntil: 'commit' }).catch(() => {});
|
||||
|
||||
const currentURL = new URL(page.url());
|
||||
if (isCallbackURL(currentURL)) {
|
||||
return currentURL;
|
||||
}
|
||||
|
||||
return new URL(callbackRequest.url());
|
||||
}
|
||||
|
||||
async function expectCallbackRedirect(
|
||||
page: Page,
|
||||
callbackUrl: string,
|
||||
action: () => Promise<unknown>
|
||||
): Promise<URL> {
|
||||
const callbackRouteMatcher = await routeCallbackPage(page, callbackUrl);
|
||||
|
||||
try {
|
||||
const callbackURLPromise = waitForCallbackURL(page, callbackUrl);
|
||||
const actionPromise = action().then(
|
||||
() => undefined,
|
||||
(error) => error
|
||||
);
|
||||
const callbackURL = await callbackURLPromise;
|
||||
await actionPromise;
|
||||
return callbackURL;
|
||||
} finally {
|
||||
await page.unroute(callbackRouteMatcher);
|
||||
}
|
||||
}
|
||||
|
||||
async function routeCallbackPage(page: Page, callbackUrl: string): Promise<(url: URL) => boolean> {
|
||||
const expectedUrl = new URL(callbackUrl);
|
||||
const callbackRouteMatcher = (url: URL) =>
|
||||
url.origin === expectedUrl.origin && url.pathname === expectedUrl.pathname;
|
||||
|
||||
await page.route(callbackRouteMatcher, async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'text/html',
|
||||
body: '<!doctype html><title>OIDC callback</title>'
|
||||
});
|
||||
});
|
||||
|
||||
return callbackRouteMatcher;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user