diff --git a/e2e/davinci-app/index.html b/e2e/davinci-app/index.html index 361683dc55..c42a64f324 100644 --- a/e2e/davinci-app/index.html +++ b/e2e/davinci-app/index.html @@ -15,9 +15,7 @@ -
-
-
+
diff --git a/e2e/davinci-app/main.ts b/e2e/davinci-app/main.ts index b5e3e2da8c..cd8dea7507 100644 --- a/e2e/davinci-app/main.ts +++ b/e2e/davinci-app/main.ts @@ -177,7 +177,6 @@ const urlParams = new URLSearchParams(window.location.search); formEl.innerHTML = ''; const clientInfo = davinciClient.getClient(); - //const clientInfo = node.client; let formName = ''; @@ -191,11 +190,12 @@ const urlParams = new URLSearchParams(window.location.search); const error = davinciClient.getError(); if (error) { - formEl.appendChild(document.createElement('div')).setAttribute('id', 'error-div'); - const errorDiv = formEl.querySelector('#error-div'); - if (errorDiv && clientInfo?.status === 'continue') { + const errorDiv = document.createElement('div'); + formEl.appendChild(errorDiv).setAttribute('id', 'error-div'); + if (errorDiv && clientInfo?.status === 'error') { + errorDiv.style.color = 'red'; errorDiv.innerHTML = ` -
${davinciClient.getError()?.message}
+

Error: ${davinciClient.getError()?.message}

`; } } diff --git a/e2e/davinci-app/server-configs.ts b/e2e/davinci-app/server-configs.ts index 10a49423de..2ddbc19c87 100644 --- a/e2e/davinci-app/server-configs.ts +++ b/e2e/davinci-app/server-configs.ts @@ -77,4 +77,16 @@ export const serverConfigs: Record = { 'https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/.well-known/openid-configuration', }, }, + /** + * Polling + */ + '31a587ce-9aa4-4f36-a09f-78cd8a0a74a0': { + clientId: '31a587ce-9aa4-4f36-a09f-78cd8a0a74a0', + redirectUri: window.location.origin + '/', + scope: 'openid profile email revoke', + serverConfig: { + wellknown: + 'https://auth.pingone.ca/356a254c-cba3-4ade-be1a-860136e8df01/as/.well-known/openid-configuration', + }, + }, }; diff --git a/e2e/davinci-app/style.css b/e2e/davinci-app/style.css index ced966e4a6..fcc585cb69 100644 --- a/e2e/davinci-app/style.css +++ b/e2e/davinci-app/style.css @@ -81,6 +81,10 @@ input { color: #888; } +.error { + color: red; +} + button { border-radius: 8px; border: 1px solid transparent; diff --git a/e2e/davinci-suites/src/polling.test.ts b/e2e/davinci-suites/src/polling.test.ts new file mode 100644 index 0000000000..771b0ae700 --- /dev/null +++ b/e2e/davinci-suites/src/polling.test.ts @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2026 Ping Identity Corporation. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ +import { expect, test } from '@playwright/test'; +import { asyncEvents } from './utils/async-events.js'; + +test.describe('Challenge Polling', () => { + test('should succeed when opening magic link', async ({ page, browser }) => { + const clientId = '31a587ce-9aa4-4f36-a09f-78cd8a0a74a0'; + const davinciPolicy = 'f40b544a4dfb575daa0cf5e9487c206a'; + const { navigate } = asyncEvents(page); + await navigate(`/?clientId=${clientId}&acr_values=${davinciPolicy}`); + + await expect(page.url()).toBe( + `http://localhost:5829/?clientId=${clientId}&acr_values=${davinciPolicy}`, + ); + + await page.getByRole('button', { name: 'Sign On' }).click(); + await expect(page.getByRole('heading', { name: 'Polling' })).toBeVisible(); + + // Get magic link + const linkLocator = page.getByText('Number Challenge https://auth.pingone'); + await expect(linkLocator).toBeVisible(); + + const linkLocatorText = await linkLocator.innerText(); + const magicLink = linkLocatorText.split('Number Challenge ')[1]; + expect(magicLink.startsWith('https://auth.pingone')); + + // Start polling + await page.getByRole('button', { name: 'Start polling' }).click(); + await expect(page.getByText('Polling...')).toBeVisible(); + + // Go to magic link in another browser to complete challenge + const newContext = await browser.newContext(); + const newPage = await newContext.newPage(); + await newPage.goto(magicLink); + await expect(newPage.getByText('Close me')).toBeVisible(); + await newContext.close(); + + // Check for success + await expect(page.getByText('Message: approved')).toBeVisible(); + }); + + test('should timeout when retries are exhausted', async ({ page }) => { + const clientId = '31a587ce-9aa4-4f36-a09f-78cd8a0a74a0'; + const davinciPolicy = 'f40b544a4dfb575daa0cf5e9487c206a'; + const { navigate } = asyncEvents(page); + await navigate(`/?clientId=${clientId}&acr_values=${davinciPolicy}`); + + await expect(page.url()).toBe( + `http://localhost:5829/?clientId=${clientId}&acr_values=${davinciPolicy}`, + ); + + await page.getByRole('button', { name: 'Sign On' }).click(); + await expect(page.getByRole('heading', { name: 'Polling' })).toBeVisible(); + + // Track poll retries + let numPollRequests = 0; + page.on('request', (request) => { + const method = request.method(); + const requestUrl = request.url(); + + if (method === 'POST' && requestUrl.includes('/status')) { + numPollRequests++; + } + }); + + // Start polling + await page.getByRole('button', { name: 'Start polling' }).click(); + await expect(page.getByText('Polling...')).toBeVisible(); + + // Wait for timeout + const pollInterval = 2000; // milliseconds + const maxRetries = 5; + await expect(page.getByText('Error: timedOut')).toBeVisible({ + timeout: 2 * pollInterval * maxRetries, + }); + + // Check max retry count + expect(numPollRequests === maxRetries); + }); + + test('should return expired status after challenge expires', async ({ page }) => { + const clientId = '31a587ce-9aa4-4f36-a09f-78cd8a0a74a0'; + const davinciPolicy = 'f40b544a4dfb575daa0cf5e9487c206a'; + const { navigate } = asyncEvents(page); + await navigate(`/?clientId=${clientId}&acr_values=${davinciPolicy}`); + + await expect(page.url()).toBe( + `http://localhost:5829/?clientId=${clientId}&acr_values=${davinciPolicy}`, + ); + + await page.getByRole('button', { name: 'Sign On' }).click(); + await expect(page.getByRole('heading', { name: 'Polling' })).toBeVisible(); + + // Track poll retries + let numPollRequests = 0; + page.on('request', (request) => { + const method = request.method(); + const requestUrl = request.url(); + + if (method === 'POST' && requestUrl.includes('/status')) { + numPollRequests++; + } + }); + + // Wait for challenge to expire + const challengeExpiry = 15000; // milliseconds + await page.waitForTimeout(challengeExpiry + 5000); + + // Start polling + await page.getByRole('button', { name: 'Start polling' }).click(); + await expect(page.getByText('Polling...')).toBeVisible(); + + // Check for expired status + await expect(page.getByText('Error: expired')).toBeVisible(); + + // Check poll count + expect(numPollRequests === 0); + }); +}); + +test.describe('Continue Polling', () => { + test('should succeed on QR code scan simulation', async ({ page }) => { + const clientId = '31a587ce-9aa4-4f36-a09f-78cd8a0a74a0'; + const davinciPolicy = '27aacf0efcc480dfcd00b04be8023cdc'; + const { navigate } = asyncEvents(page); + await navigate(`/?clientId=${clientId}&acr_values=${davinciPolicy}`); + + await expect(page.url()).toBe( + `http://localhost:5829/?clientId=${clientId}&acr_values=${davinciPolicy}`, + ); + + await expect(page.getByRole('heading', { name: 'Select Continue Polling Test' })).toBeVisible(); + await page.getByRole('button', { name: 'Success' }).click(); + await expect(page.getByRole('heading', { name: 'Polling' })).toBeVisible(); + + // Start polling + const numberCounterSuccess = 2; + for (let i = 0; i < numberCounterSuccess; i++) { + await page.getByRole('button', { name: 'Start polling' }).click(); + await expect(page.getByText('Polling...')).toBeVisible(); + await expect(page.getByRole('button', { name: 'Start polling' })).toBeDisabled(); + } + + // Check for success + await expect(page.getByText('Message: Done')).toBeVisible(); + }); + + test('should timeout when retries are exhausted', async ({ page }) => { + const clientId = '31a587ce-9aa4-4f36-a09f-78cd8a0a74a0'; + const davinciPolicy = '27aacf0efcc480dfcd00b04be8023cdc'; + const { navigate } = asyncEvents(page); + await navigate(`/?clientId=${clientId}&acr_values=${davinciPolicy}`); + + await expect(page.url()).toBe( + `http://localhost:5829/?clientId=${clientId}&acr_values=${davinciPolicy}`, + ); + + await expect(page.getByRole('heading', { name: 'Select Continue Polling Test' })).toBeVisible(); + await page.getByRole('button', { name: 'Timeout' }).click(); + await expect(page.getByRole('heading', { name: 'Polling' })).toBeVisible(); + + // Start polling + const maxRetries = 3; + for (let i = 0; i < maxRetries + 1; i++) { + await page.getByRole('button', { name: 'Start polling' }).click(); + await expect(page.getByText('Polling...')).toBeVisible(); + await expect(page.getByRole('button', { name: 'Start polling' })).toBeDisabled(); + } + + // Check for timeout + await expect(page.getByText('Error: timedOut')).toBeVisible(); + }); +}); diff --git a/e2e/davinci-suites/src/protect.test.ts b/e2e/davinci-suites/src/protect.test.ts index 8534a6043c..4830bb6b22 100644 --- a/e2e/davinci-suites/src/protect.test.ts +++ b/e2e/davinci-suites/src/protect.test.ts @@ -17,7 +17,7 @@ test('Test Protect collector with Custom HTML component', async ({ page }) => { await expect(page.getByText('JS Protect - Custom HTML Form')).toBeVisible(); - const requests = []; + const requests: string[] = []; page.on('request', (request) => { const method = request.method(); const requestUrl = request.url(); @@ -54,7 +54,7 @@ test('Test Protect collector with P1 Forms component', async ({ page }) => { await expect(page.getByText('Example - Sign On')).toBeVisible(); - const requests = []; + const requests: string[] = []; page.on('request', (request) => { const method = request.method(); const requestUrl = request.url(); diff --git a/lefthook.yml b/lefthook.yml index 812fd13b1d..39a02c602d 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -5,9 +5,6 @@ pre-commit: nx-check: run: pnpm nx affected -t typecheck lint build api-report --tui=false stage_fixed: true - format: - run: pnpm nx format:write - stage_fixed: true interface-mapping: glob: >- {tools/interface-mapping-validator/**/*.ts, @@ -24,7 +21,9 @@ pre-commit: echo "Interface mapping is out of sync." && echo "Run: pnpm mapping:generate" && exit 1) - + format: + run: pnpm nx format:write + stage_fixed: true commit-msg: commands: commitlint: