From 55ef89d95b76fe3f42a39beab958b1ebee2d6594 Mon Sep 17 00:00:00 2001 From: Justin Lowery Date: Tue, 4 Nov 2025 10:34:17 -0600 Subject: [PATCH 1/4] test(journey-client): ported legacy Journey tests to new Client --- e2e/journey-app/callback-map.ts | 157 ++++++++++++++++++ e2e/journey-app/components/README.md | 84 ++++++++++ e2e/journey-app/components/attribute-input.ts | 51 ++++++ e2e/journey-app/components/choice.ts | 42 +++++ e2e/journey-app/components/confirmation.ts | 57 +++++++ e2e/journey-app/components/device-profile.ts | 32 ++++ e2e/journey-app/components/hidden-value.ts | 26 +++ e2e/journey-app/components/index.ts | 35 ++++ e2e/journey-app/components/kba-create.ts | 62 +++++++ e2e/journey-app/components/metadata.ts | 27 +++ .../components/ping-protect-evaluation.ts | 23 +++ .../components/ping-protect-initialize.ts | 23 +++ e2e/journey-app/components/polling-wait.ts | 48 ++++++ .../components/recaptcha-enterprise.ts | 43 +++++ e2e/journey-app/components/recaptcha.ts | 42 +++++ e2e/journey-app/components/redirect.ts | 45 +++++ e2e/journey-app/components/select-idp.ts | 56 +++++++ .../components/suspended-text-output.ts | 34 ++++ .../components/terms-and-conditions.ts | 36 ++++ e2e/journey-app/components/text-input.ts | 34 ++++ e2e/journey-app/components/text-output.ts | 24 +++ .../components/validated-password.ts | 29 ++++ .../{text.ts => validated-username.ts} | 8 +- e2e/journey-app/main.ts | 100 +++++------ e2e/journey-app/tsconfig.app.json | 1 + .../src/choice-confirm-poll.test.ts | 58 +++++++ e2e/journey-suites/src/custom-paths.test.ts | 38 +++++ e2e/journey-suites/src/device-profile.test.ts | 40 +++++ e2e/journey-suites/src/email-suspend.test.ts | 34 ++++ .../src/{basic.test.ts => login.test.ts} | 16 +- e2e/journey-suites/src/no-session.test.ts | 35 ++++ e2e/journey-suites/src/otp-register.ts | 46 +++++ e2e/journey-suites/src/protect.test.ts | 40 +++++ e2e/journey-suites/src/registration.test.ts | 66 ++++++++ .../src/request-middleware.test.ts | 63 +++++++ e2e/oidc-app/src/ping-am/index.html | 3 +- e2e/oidc-app/src/ping-one/index.html | 3 +- e2e/oidc-app/src/utils/oidc-app.ts | 7 +- e2e/oidc-suites/src/login.spec.ts | 47 +++--- e2e/oidc-suites/src/logout.spec.ts | 32 ++-- e2e/oidc-suites/src/token.spec.ts | 129 ++++++++------ e2e/oidc-suites/src/user.spec.ts | 30 ++-- e2e/oidc-suites/src/utils/async-events.ts | 17 +- packages/journey-client/src/index.ts | 2 +- ...ney.store.test.ts => client.store.test.ts} | 2 +- .../lib/{journey.store.ts => client.store.ts} | 2 +- ...y.store.utils.ts => client.store.utils.ts} | 0 packages/journey-client/src/types.ts | 2 + .../src/lib/request-mware.test.ts | 4 +- .../src/lib/request-mware.types.ts | 2 +- 50 files changed, 1650 insertions(+), 187 deletions(-) create mode 100644 e2e/journey-app/callback-map.ts create mode 100644 e2e/journey-app/components/README.md create mode 100644 e2e/journey-app/components/attribute-input.ts create mode 100644 e2e/journey-app/components/choice.ts create mode 100644 e2e/journey-app/components/confirmation.ts create mode 100644 e2e/journey-app/components/device-profile.ts create mode 100644 e2e/journey-app/components/hidden-value.ts create mode 100644 e2e/journey-app/components/index.ts create mode 100644 e2e/journey-app/components/kba-create.ts create mode 100644 e2e/journey-app/components/metadata.ts create mode 100644 e2e/journey-app/components/ping-protect-evaluation.ts create mode 100644 e2e/journey-app/components/ping-protect-initialize.ts create mode 100644 e2e/journey-app/components/polling-wait.ts create mode 100644 e2e/journey-app/components/recaptcha-enterprise.ts create mode 100644 e2e/journey-app/components/recaptcha.ts create mode 100644 e2e/journey-app/components/redirect.ts create mode 100644 e2e/journey-app/components/select-idp.ts create mode 100644 e2e/journey-app/components/suspended-text-output.ts create mode 100644 e2e/journey-app/components/terms-and-conditions.ts create mode 100644 e2e/journey-app/components/text-input.ts create mode 100644 e2e/journey-app/components/text-output.ts create mode 100644 e2e/journey-app/components/validated-password.ts rename e2e/journey-app/components/{text.ts => validated-username.ts} (81%) create mode 100644 e2e/journey-suites/src/choice-confirm-poll.test.ts create mode 100644 e2e/journey-suites/src/custom-paths.test.ts create mode 100644 e2e/journey-suites/src/device-profile.test.ts create mode 100644 e2e/journey-suites/src/email-suspend.test.ts rename e2e/journey-suites/src/{basic.test.ts => login.test.ts} (62%) create mode 100644 e2e/journey-suites/src/no-session.test.ts create mode 100644 e2e/journey-suites/src/otp-register.ts create mode 100644 e2e/journey-suites/src/protect.test.ts create mode 100644 e2e/journey-suites/src/registration.test.ts create mode 100644 e2e/journey-suites/src/request-middleware.test.ts rename packages/journey-client/src/lib/{journey.store.test.ts => client.store.test.ts} (99%) rename packages/journey-client/src/lib/{journey.store.ts => client.store.ts} (99%) rename packages/journey-client/src/lib/{journey.store.utils.ts => client.store.utils.ts} (100%) diff --git a/e2e/journey-app/callback-map.ts b/e2e/journey-app/callback-map.ts new file mode 100644 index 0000000000..1f7fc328dd --- /dev/null +++ b/e2e/journey-app/callback-map.ts @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2025 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 type { + AttributeInputCallback, + BaseCallback, + ChoiceCallback, + ConfirmationCallback, + DeviceProfileCallback, + HiddenValueCallback, + KbaCreateCallback, + MetadataCallback, + NameCallback, + PasswordCallback, + PingOneProtectEvaluationCallback, + PingOneProtectInitializeCallback, + PollingWaitCallback, + ReCaptchaCallback, + ReCaptchaEnterpriseCallback, + RedirectCallback, + SelectIdPCallback, + SuspendedTextOutputCallback, + TermsAndConditionsCallback, + TextInputCallback, + TextOutputCallback, + ValidatedCreatePasswordCallback, + ValidatedCreateUsernameCallback, +} from '@forgerock/journey-client/types'; + +import { + attributeInputComponent, + choiceComponent, + confirmationComponent, + deviceProfileComponent, + hiddenValueComponent, + kbaCreateComponent, + metadataComponent, + passwordComponent, + pingProtectEvaluationComponent, + pingProtectInitializeComponent, + pollingWaitComponent, + recaptchaComponent, + recaptchaEnterpriseComponent, + redirectComponent, + selectIdpComponent, + suspendedTextOutputComponent, + termsAndConditionsComponent, + textInputComponent, + textOutputComponent, + validatedPasswordComponent, + validatedUsernameComponent, +} from './components/index.js'; + +/** + * Renders a callback component based on its type + * @param journeyEl - The container element to append the component to + * @param callback - The callback instance + * @param idx - Index for generating unique IDs + */ +export function renderCallback( + journeyEl: HTMLDivElement, + callback: BaseCallback, + idx: number, +): void { + switch (callback.getType()) { + case 'BooleanAttributeInputCallback': + case 'NumberAttributeInputCallback': + case 'StringAttributeInputCallback': + attributeInputComponent( + journeyEl, + callback as AttributeInputCallback, + idx, + ); + break; + case 'ChoiceCallback': + choiceComponent(journeyEl, callback as ChoiceCallback, idx); + break; + case 'ConfirmationCallback': + confirmationComponent(journeyEl, callback as ConfirmationCallback, idx); + break; + case 'DeviceProfileCallback': + deviceProfileComponent(journeyEl, callback as DeviceProfileCallback, idx); + break; + case 'HiddenValueCallback': + hiddenValueComponent(journeyEl, callback as HiddenValueCallback, idx); + break; + case 'KbaCreateCallback': + kbaCreateComponent(journeyEl, callback as KbaCreateCallback, idx); + break; + case 'MetadataCallback': + metadataComponent(journeyEl, callback as MetadataCallback, idx); + break; + case 'NameCallback': + textInputComponent(journeyEl, callback as NameCallback, idx); + break; + case 'PasswordCallback': + passwordComponent(journeyEl, callback as PasswordCallback, idx); + break; + case 'PingOneProtectEvaluationCallback': + pingProtectEvaluationComponent(journeyEl, callback as PingOneProtectEvaluationCallback, idx); + break; + case 'PingOneProtectInitializeCallback': + pingProtectInitializeComponent(journeyEl, callback as PingOneProtectInitializeCallback, idx); + break; + case 'PollingWaitCallback': + pollingWaitComponent(journeyEl, callback as PollingWaitCallback, idx); + break; + case 'ReCaptchaCallback': + recaptchaComponent(journeyEl, callback as ReCaptchaCallback, idx); + break; + case 'ReCaptchaEnterpriseCallback': + recaptchaEnterpriseComponent(journeyEl, callback as ReCaptchaEnterpriseCallback, idx); + break; + case 'RedirectCallback': + redirectComponent(journeyEl, callback as RedirectCallback, idx); + break; + case 'SelectIdPCallback': + selectIdpComponent(journeyEl, callback as SelectIdPCallback, idx); + break; + case 'SuspendedTextOutputCallback': + suspendedTextOutputComponent(journeyEl, callback as SuspendedTextOutputCallback, idx); + break; + case 'TermsAndConditionsCallback': + termsAndConditionsComponent(journeyEl, callback as TermsAndConditionsCallback, idx); + break; + case 'TextInputCallback': + textInputComponent(journeyEl, callback as TextInputCallback, idx); + break; + case 'TextOutputCallback': + textOutputComponent(journeyEl, callback as TextOutputCallback, idx); + break; + case 'ValidatedCreatePasswordCallback': + validatedPasswordComponent(journeyEl, callback as ValidatedCreatePasswordCallback, idx); + break; + case 'ValidatedCreateUsernameCallback': + validatedUsernameComponent(journeyEl, callback as ValidatedCreateUsernameCallback, idx); + break; + default: + console.warn(`Unknown callback type: ${callback.getType()}`); + break; + } +} + +/** + * Renders all callbacks in a step + * @param journeyEl - The container element to append components to + * @param callbacks - Array of callback instances + */ +export function renderCallbacks(journeyEl: HTMLDivElement, callbacks: BaseCallback[]): void { + callbacks.forEach((callback, idx) => { + renderCallback(journeyEl, callback, idx); + }); +} diff --git a/e2e/journey-app/components/README.md b/e2e/journey-app/components/README.md new file mode 100644 index 0000000000..44c8096613 --- /dev/null +++ b/e2e/journey-app/components/README.md @@ -0,0 +1,84 @@ +# Journey App Components + +This directory contains UI components for rendering different types of journey callbacks in the e2e test application. Each component follows a consistent pattern and handles the specific requirements of its callback type. + +## Available Components + +### Input Components + +- **`attribute-input.ts`** - `AttributeInputCallback` - Handles string, number, and boolean attribute inputs with appropriate input types +- **`choice.ts`** - `ChoiceCallback` - Renders a select dropdown with available choices +- **`confirmation.ts`** - `ConfirmationCallback` - Creates radio buttons for confirmation options +- **`kba-create.ts`** - `KbaCreateCallback` - Two-field form for creating security questions and answers +- **`password.ts`** - `PasswordCallback` - Password input field +- **`text-input.ts`** - `NameCallback`, `TextInputCallback` - Generic text input component +- **`validated-password.ts`** - `ValidatedCreatePasswordCallback` - Password input with validation +- **`validated-username.ts`** - `ValidatedCreateUsernameCallback` - Username input with validation + +### Output Components + +- **`text-output.ts`** - `TextOutputCallback` - Displays text messages +- **`suspended-text-output.ts`** - `SuspendedTextOutputCallback` - Styled suspension message display + +### Interaction Components + +- **`redirect.ts`** - `RedirectCallback` - Button to trigger external redirects +- **`select-idp.ts`** - `SelectIdPCallback` - Radio buttons for identity provider selection +- **`terms-and-conditions.ts`** - `TermsAndConditionsCallback` - Terms display with acceptance checkbox + +### Security Components + +- **`recaptcha.ts`** - `ReCaptchaCallback` - reCAPTCHA challenge placeholder +- **`recaptcha-enterprise.ts`** - `ReCaptchaEnterpriseCallback` - reCAPTCHA Enterprise placeholder +- **`ping-protect-evaluation.ts`** - `PingOneProtectEvaluationCallback` - Risk assessment display +- **`ping-protect-initialize.ts`** - `PingOneProtectInitializeCallback` - Protection initialization + +### Utility Components + +- **`device-profile.ts`** - `DeviceProfileCallback` - Device profiling indicator +- **`hidden-value.ts`** - `HiddenValueCallback` - Hidden input field +- **`metadata.ts`** - `MetadataCallback` - Hidden metadata storage +- **`polling-wait.ts`** - `PollingWaitCallback` - Loading spinner with wait message + +## Component Pattern + +All components follow this consistent pattern: + +```typescript +export default function componentName( + journeyEl: HTMLDivElement, + callback: CallbackType, + idx: number, +) { + // Create DOM elements + // Set up event listeners + // Append to journeyEl +} +``` + +### Parameters + +- **`journeyEl`** - The container element to append the component to +- **`callback`** - The callback instance with data and methods +- **`idx`** - Index for generating unique IDs + +### Usage Example + +```typescript +import { choiceComponent } from './components/index.js'; + +// Render a choice callback +choiceComponent(containerDiv, choiceCallback, 0); +``` + +## Implementation Notes + +- All components handle their own styling via inline CSS or style attributes +- Event listeners are set up to call appropriate callback methods +- Components generate unique IDs using the callback's input name or a fallback +- Error handling is implemented where appropriate +- Console logging is used for debugging and demonstration + +## Component States + +Some components like reCAPTCHA and PingOne Protect include simulation timeouts for demonstration purposes in the e2e testing environment. In production, these would integrate with actual third-party services. diff --git a/e2e/journey-app/components/attribute-input.ts b/e2e/journey-app/components/attribute-input.ts new file mode 100644 index 0000000000..fcf9f5038e --- /dev/null +++ b/e2e/journey-app/components/attribute-input.ts @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2025 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 type { AttributeInputCallback } from '@forgerock/journey-client/types'; + +export default function attributeInputComponent( + journeyEl: HTMLDivElement, + callback: AttributeInputCallback, + idx: number, +) { + const collectorKey = callback?.payload?.input?.[0].name || `collector-${idx}`; + const label = document.createElement('label'); + const input = document.createElement('input'); + + label.htmlFor = collectorKey; + label.innerText = callback.getPrompt(); + + // Determine input type based on attribute type + const attributeType = callback.getType(); + if (attributeType === 'BooleanAttributeInputCallback') { + input.type = 'checkbox'; + input.checked = (callback.getInputValue() as boolean) || false; + } else if (attributeType === 'NumberAttributeInputCallback') { + input.type = 'number'; + input.value = String(callback.getInputValue() || ''); + } else { + input.type = 'text'; + input.value = String(callback.getInputValue() || ''); + } + + input.id = collectorKey; + input.name = collectorKey; + input.required = callback.isRequired(); + + journeyEl?.appendChild(label); + journeyEl?.appendChild(input); + + journeyEl?.querySelector(`#${collectorKey}`)?.addEventListener('input', (event) => { + const target = event.target as HTMLInputElement; + if (attributeType === 'BooleanAttributeInputCallback') { + callback.setInputValue(target.checked); + } else if (attributeType === 'NumberAttributeInputCallback') { + callback.setInputValue(Number(target.value)); + } else { + callback.setInputValue(target.value); + } + }); +} diff --git a/e2e/journey-app/components/choice.ts b/e2e/journey-app/components/choice.ts new file mode 100644 index 0000000000..e069054b4e --- /dev/null +++ b/e2e/journey-app/components/choice.ts @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2025 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 type { ChoiceCallback } from '@forgerock/journey-client/types'; + +export default function choiceComponent( + journeyEl: HTMLDivElement, + callback: ChoiceCallback, + idx: number, +) { + const collectorKey = callback?.payload?.input?.[0].name || `collector-${idx}`; + const label = document.createElement('label'); + const select = document.createElement('select'); + + label.htmlFor = collectorKey; + label.innerText = callback.getPrompt(); + select.id = collectorKey; + select.name = collectorKey; + + // Add choices as options + const choices = callback.getChoices(); + const defaultChoice = callback.getDefaultChoice(); + + choices.forEach((choice, index) => { + const option = document.createElement('option'); + option.value = String(index); + option.text = choice; + option.selected = index === defaultChoice; + select.appendChild(option); + }); + + journeyEl?.appendChild(label); + journeyEl?.appendChild(select); + + journeyEl?.querySelector(`#${collectorKey}`)?.addEventListener('change', (event) => { + const selectedIndex = Number((event.target as HTMLSelectElement).value); + callback.setChoiceIndex(selectedIndex); + }); +} diff --git a/e2e/journey-app/components/confirmation.ts b/e2e/journey-app/components/confirmation.ts new file mode 100644 index 0000000000..562c0e39ab --- /dev/null +++ b/e2e/journey-app/components/confirmation.ts @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2025 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 type { ConfirmationCallback } from '@forgerock/journey-client/types'; + +export default function confirmationComponent( + journeyEl: HTMLDivElement, + callback: ConfirmationCallback, + idx: number, +) { + const collectorKey = callback?.payload?.input?.[0].name || `collector-${idx}`; + const label = document.createElement('label'); + const container = document.createElement('div'); + + label.innerText = callback.getPrompt(); + container.id = collectorKey; + + // Get options and default option + const options = callback.getOptions(); + const defaultOption = callback.getDefaultOption(); + + // Create radio buttons for each option + options.forEach((option: string, index: number) => { + const radioContainer = document.createElement('div'); + const radio = document.createElement('input'); + const radioLabel = document.createElement('label'); + + radio.type = 'radio'; + radio.id = `${collectorKey}-${index}`; + radio.name = collectorKey; + radio.value = String(index); + radio.checked = index === defaultOption; + + radioLabel.htmlFor = `${collectorKey}-${index}`; + radioLabel.innerText = option; + + radioContainer.appendChild(radio); + radioContainer.appendChild(radioLabel); + container.appendChild(radioContainer); + }); + + journeyEl?.appendChild(label); + journeyEl?.appendChild(container); + + // Add event listener for radio button changes + journeyEl?.querySelectorAll(`input[name="${collectorKey}"]`)?.forEach((radio) => { + radio.addEventListener('change', (event) => { + if ((event.target as HTMLInputElement).checked) { + const selectedIndex = Number((event.target as HTMLInputElement).value); + callback.setOptionIndex(selectedIndex); + } + }); + }); +} diff --git a/e2e/journey-app/components/device-profile.ts b/e2e/journey-app/components/device-profile.ts new file mode 100644 index 0000000000..c0d62e9223 --- /dev/null +++ b/e2e/journey-app/components/device-profile.ts @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2025 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 type { DeviceProfileCallback } from '@forgerock/journey-client/types'; + +export default function deviceProfileComponent( + journeyEl: HTMLDivElement, + callback: DeviceProfileCallback, + idx: number, +) { + const collectorKey = callback?.payload?.input?.[0].name || `collector-${idx}`; + const message = document.createElement('p'); + + message.id = collectorKey; + message.innerText = 'Collecting device profile information...'; + + journeyEl?.appendChild(message); + + // Device profile callback typically runs automatically + // The callback will collect device information in the background + setTimeout(() => { + try { + // Device profile collection is typically handled automatically by the callback + console.log('Device profile collection initiated'); + } catch (error) { + console.error('Device profile collection failed:', error); + } + }, 100); +} diff --git a/e2e/journey-app/components/hidden-value.ts b/e2e/journey-app/components/hidden-value.ts new file mode 100644 index 0000000000..8bbfc3d4e5 --- /dev/null +++ b/e2e/journey-app/components/hidden-value.ts @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2025 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 type { HiddenValueCallback } from '@forgerock/journey-client/types'; + +export default function hiddenValueComponent( + journeyEl: HTMLDivElement, + callback: HiddenValueCallback, + idx: number, +) { + const collectorKey = callback?.payload?.input?.[0].name || `collector-${idx}`; + const input = document.createElement('input'); + + input.type = 'hidden'; + input.id = collectorKey; + input.name = collectorKey; + input.value = String(callback.getInputValue() || ''); + + journeyEl?.appendChild(input); + + // Hidden value callback typically doesn't require user interaction + // The value is usually set programmatically or by the server +} diff --git a/e2e/journey-app/components/index.ts b/e2e/journey-app/components/index.ts new file mode 100644 index 0000000000..425c8560be --- /dev/null +++ b/e2e/journey-app/components/index.ts @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2025 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. + */ + +/** + * Index file for journey-app callback components + * + * This file exports all the available component functions for rendering + * journey callbacks in the e2e test application. + */ + +export { default as attributeInputComponent } from './attribute-input.js'; +export { default as choiceComponent } from './choice.js'; +export { default as confirmationComponent } from './confirmation.js'; +export { default as deviceProfileComponent } from './device-profile.js'; +export { default as hiddenValueComponent } from './hidden-value.js'; +export { default as kbaCreateComponent } from './kba-create.js'; +export { default as metadataComponent } from './metadata.js'; +export { default as passwordComponent } from './password.js'; +export { default as pingProtectEvaluationComponent } from './ping-protect-evaluation.js'; +export { default as pingProtectInitializeComponent } from './ping-protect-initialize.js'; +export { default as pollingWaitComponent } from './polling-wait.js'; +export { default as recaptchaComponent } from './recaptcha.js'; +export { default as recaptchaEnterpriseComponent } from './recaptcha-enterprise.js'; +export { default as redirectComponent } from './redirect.js'; +export { default as selectIdpComponent } from './select-idp.js'; +export { default as suspendedTextOutputComponent } from './suspended-text-output.js'; +export { default as termsAndConditionsComponent } from './terms-and-conditions.js'; +export { default as textInputComponent } from './text-input.js'; +export { default as textOutputComponent } from './text-output.js'; +export { default as validatedPasswordComponent } from './validated-password.js'; +export { default as validatedUsernameComponent } from './validated-username.js'; diff --git a/e2e/journey-app/components/kba-create.ts b/e2e/journey-app/components/kba-create.ts new file mode 100644 index 0000000000..3bc3975dc9 --- /dev/null +++ b/e2e/journey-app/components/kba-create.ts @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2025 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 type { KbaCreateCallback } from '@forgerock/journey-client/types'; + +export default function kbaCreateComponent( + journeyEl: HTMLDivElement, + callback: KbaCreateCallback, + idx: number, +) { + const collectorKey = callback?.payload?.input?.[0].name || `collector-${idx}`; + const container = document.createElement('div'); + const questions = callback.getPredefinedQuestions(); + + container.id = collectorKey; + + // Question field + const questionLabel = document.createElement('label'); + questionLabel.htmlFor = `${collectorKey}-question`; + // Add index to prompt to differentiate multiple KBA callbacks + questionLabel.innerText = callback.getPrompt() + ' ' + idx; + + // Iterate through predefined questions and create a select dropdown + const questionInput = document.createElement('select'); + questionInput.id = `${collectorKey}-question`; + + questions.forEach((question) => { + const option = document.createElement('option'); + option.value = question; + option.text = question; + questionInput.appendChild(option); + }); + + // Answer field + const answerLabel = document.createElement('label'); + answerLabel.htmlFor = `${collectorKey}-answer`; + answerLabel.innerText = 'Answer ' + idx + ':'; + + const answerInput = document.createElement('input'); + answerInput.type = 'text'; + answerInput.id = `${collectorKey}-answer`; + answerInput.placeholder = 'Enter your answer'; + + container.appendChild(questionLabel); + container.appendChild(questionInput); + container.appendChild(answerLabel); + container.appendChild(answerInput); + + journeyEl?.appendChild(container); + + // Event listeners + questionInput.addEventListener('input', (event) => { + callback.setQuestion((event.target as HTMLInputElement).value); + }); + + answerInput.addEventListener('input', (event) => { + callback.setAnswer((event.target as HTMLInputElement).value); + }); +} diff --git a/e2e/journey-app/components/metadata.ts b/e2e/journey-app/components/metadata.ts new file mode 100644 index 0000000000..f8ef98fd40 --- /dev/null +++ b/e2e/journey-app/components/metadata.ts @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2025 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 type { MetadataCallback } from '@forgerock/journey-client/types'; + +export default function metadataComponent( + journeyEl: HTMLDivElement, + callback: MetadataCallback, + idx: number, +) { + const collectorKey = callback?.payload?.input?.[0].name || `collector-${idx}`; + + // Metadata callback typically doesn't render UI elements + // It's used to pass metadata information + const hiddenInput = document.createElement('input'); + hiddenInput.type = 'hidden'; + hiddenInput.id = collectorKey; + hiddenInput.name = collectorKey; + hiddenInput.value = JSON.stringify(callback.getData()); + + journeyEl?.appendChild(hiddenInput); + + console.log('Metadata callback data:', callback.getData()); +} diff --git a/e2e/journey-app/components/ping-protect-evaluation.ts b/e2e/journey-app/components/ping-protect-evaluation.ts new file mode 100644 index 0000000000..90ce809036 --- /dev/null +++ b/e2e/journey-app/components/ping-protect-evaluation.ts @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2025 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 type { PingOneProtectEvaluationCallback } from '@forgerock/journey-client/types'; + +export default function pingProtectEvaluationComponent( + journeyEl: HTMLDivElement, + callback: PingOneProtectEvaluationCallback, + idx: number, +) { + const collectorKey = callback?.payload?.input?.[0].name || `collector-${idx}`; + const message = document.createElement('p'); + + message.id = collectorKey; + message.innerText = 'Evaluating risk assessment...'; + + journeyEl?.appendChild(message); + + // TODO: Implement PingOne Protect module evaluation here +} diff --git a/e2e/journey-app/components/ping-protect-initialize.ts b/e2e/journey-app/components/ping-protect-initialize.ts new file mode 100644 index 0000000000..c45215c6a4 --- /dev/null +++ b/e2e/journey-app/components/ping-protect-initialize.ts @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2025 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 type { PingOneProtectInitializeCallback } from '@forgerock/journey-client/types'; + +export default function pingProtectInitializeComponent( + journeyEl: HTMLDivElement, + callback: PingOneProtectInitializeCallback, + idx: number, +) { + const collectorKey = callback?.payload?.input?.[0].name || `collector-${idx}`; + const message = document.createElement('p'); + + message.id = collectorKey; + message.innerText = 'Initializing PingOne Protect...'; + + journeyEl?.appendChild(message); + + // TODO: Implement PingOne Protect module initialization here +} diff --git a/e2e/journey-app/components/polling-wait.ts b/e2e/journey-app/components/polling-wait.ts new file mode 100644 index 0000000000..3c861549d0 --- /dev/null +++ b/e2e/journey-app/components/polling-wait.ts @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2025 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 type { PollingWaitCallback } from '@forgerock/journey-client/types'; + +export default function pollingWaitComponent( + journeyEl: HTMLDivElement, + callback: PollingWaitCallback, + idx: number, +) { + const collectorKey = callback?.payload?.input?.[0].name || `collector-${idx}`; + const container = document.createElement('div'); + const message = document.createElement('p'); + const spinner = document.createElement('div'); + + container.id = collectorKey; + message.innerText = callback.getMessage() || 'Please wait...'; + + // Simple spinner + spinner.style.cssText = ` + width: 20px; + height: 20px; + border: 2px solid #f3f3f3; + border-top: 2px solid #3498db; + border-radius: 50%; + animation: spin 1s linear infinite; + margin: 10px 0; + `; + + // Add CSS animation + const style = document.createElement('style'); + style.textContent = ` + @keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } + } + `; + document.head.appendChild(style); + + container.appendChild(message); + container.appendChild(spinner); + journeyEl?.appendChild(container); + + // TODO: Use set timeout to submit for after delay +} diff --git a/e2e/journey-app/components/recaptcha-enterprise.ts b/e2e/journey-app/components/recaptcha-enterprise.ts new file mode 100644 index 0000000000..1254f04ccc --- /dev/null +++ b/e2e/journey-app/components/recaptcha-enterprise.ts @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2025 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 type { ReCaptchaEnterpriseCallback } from '@forgerock/journey-client/types'; + +export default function recaptchaEnterpriseComponent( + journeyEl: HTMLDivElement, + callback: ReCaptchaEnterpriseCallback, + idx: number, +) { + const collectorKey = callback?.payload?.input?.[0].name || `collector-${idx}`; + const container = document.createElement('div'); + const message = document.createElement('p'); + + container.id = collectorKey; + message.innerText = 'Please complete the reCAPTCHA Enterprise challenge'; + + // Create placeholder div for reCAPTCHA Enterprise + const recaptchaDiv = document.createElement('div'); + recaptchaDiv.id = `recaptcha-enterprise-${collectorKey}`; + recaptchaDiv.style.cssText = ` + border: 1px solid #ccc; + padding: 20px; + text-align: center; + background-color: #f0f8ff; + margin: 10px 0; + `; + recaptchaDiv.innerText = + 'reCAPTCHA Enterprise placeholder (requires reCAPTCHA Enterprise script)'; + + container.appendChild(message); + container.appendChild(recaptchaDiv); + journeyEl?.appendChild(container); + + // In a real implementation, you would load the reCAPTCHA Enterprise script + // and initialize the widget here + console.log('reCAPTCHA Enterprise callback initialized'); + + // TODO: Implement reCAPTCHA Enterprise integration here +} diff --git a/e2e/journey-app/components/recaptcha.ts b/e2e/journey-app/components/recaptcha.ts new file mode 100644 index 0000000000..60a7903502 --- /dev/null +++ b/e2e/journey-app/components/recaptcha.ts @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2025 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 type { ReCaptchaCallback } from '@forgerock/journey-client/types'; + +export default function recaptchaComponent( + journeyEl: HTMLDivElement, + callback: ReCaptchaCallback, + idx: number, +) { + const collectorKey = callback?.payload?.input?.[0].name || `collector-${idx}`; + const container = document.createElement('div'); + const message = document.createElement('p'); + + container.id = collectorKey; + message.innerText = 'Please complete the reCAPTCHA challenge'; + + // Create placeholder div for reCAPTCHA + const recaptchaDiv = document.createElement('div'); + recaptchaDiv.id = `recaptcha-${collectorKey}`; + recaptchaDiv.style.cssText = ` + border: 1px solid #ccc; + padding: 20px; + text-align: center; + background-color: #f9f9f9; + margin: 10px 0; + `; + recaptchaDiv.innerText = 'reCAPTCHA placeholder (requires reCAPTCHA script)'; + + container.appendChild(message); + container.appendChild(recaptchaDiv); + journeyEl?.appendChild(container); + + // In a real implementation, you would load the reCAPTCHA script + // and initialize the widget here + console.log('reCAPTCHA callback initialized'); + + // TODO: Implement reCAPTCHA integration here +} diff --git a/e2e/journey-app/components/redirect.ts b/e2e/journey-app/components/redirect.ts new file mode 100644 index 0000000000..f09619d6ab --- /dev/null +++ b/e2e/journey-app/components/redirect.ts @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2025 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 type { RedirectCallback } from '@forgerock/journey-client/types'; + +export default function redirectComponent( + journeyEl: HTMLDivElement, + callback: RedirectCallback, + idx: number, +) { + const collectorKey = callback?.payload?.input?.[0].name || `collector-${idx}`; + const container = document.createElement('div'); + const message = document.createElement('p'); + const button = document.createElement('button'); + + container.id = collectorKey; + message.innerText = 'You will be redirected to complete authentication'; + button.innerText = 'Continue to External Provider'; + button.style.cssText = ` + padding: 10px 20px; + background-color: #007bff; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + margin: 10px 0; + `; + + container.appendChild(message); + container.appendChild(button); + journeyEl?.appendChild(container); + + button.addEventListener('click', () => { + try { + const redirectUrl = callback.getRedirectUrl(); + console.log('Redirecting to:', redirectUrl); + window.location.href = redirectUrl; + } catch (error) { + console.error('Redirect failed:', error); + } + }); +} diff --git a/e2e/journey-app/components/select-idp.ts b/e2e/journey-app/components/select-idp.ts new file mode 100644 index 0000000000..bf89dfac41 --- /dev/null +++ b/e2e/journey-app/components/select-idp.ts @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2025 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 type { SelectIdPCallback } from '@forgerock/journey-client/types'; + +export default function selectIdpComponent( + journeyEl: HTMLDivElement, + callback: SelectIdPCallback, + idx: number, +) { + const collectorKey = callback?.payload?.input?.[0].name || `collector-${idx}`; + const container = document.createElement('div'); + const label = document.createElement('label'); + + container.id = collectorKey; + label.innerText = 'Select Identity Provider:'; + + const providers = callback.getProviders(); + + // Create select element for provider selection + const select = document.createElement('select'); + select.id = collectorKey; + select.name = collectorKey; + + // Add default option + const defaultOption = document.createElement('option'); + defaultOption.value = ''; + defaultOption.innerText = 'Choose a provider...'; + defaultOption.disabled = true; + defaultOption.selected = true; + select.appendChild(defaultOption); + + // Create option elements for each provider + providers.forEach((provider) => { + const option = document.createElement('option'); + option.value = provider.provider; + option.innerText = provider.provider; + select.appendChild(option); + }); + + container.appendChild(select); + + journeyEl?.appendChild(label); + journeyEl?.appendChild(container); + + // Add event listener for provider selection + select.addEventListener('change', (event) => { + const selectedProvider = (event.target as HTMLSelectElement).value; + if (selectedProvider) { + callback.setProvider(selectedProvider); + } + }); +} diff --git a/e2e/journey-app/components/suspended-text-output.ts b/e2e/journey-app/components/suspended-text-output.ts new file mode 100644 index 0000000000..88e27ef9a1 --- /dev/null +++ b/e2e/journey-app/components/suspended-text-output.ts @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2025 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 type { SuspendedTextOutputCallback } from '@forgerock/journey-client/types'; + +export default function suspendedTextOutputComponent( + journeyEl: HTMLDivElement, + callback: SuspendedTextOutputCallback, + idx: number, +) { + const collectorKey = callback?.payload?.input?.[0].name || `collector-${idx}`; + const container = document.createElement('div'); + const message = document.createElement('p'); + + container.id = collectorKey; + message.innerText = + callback.getMessage() || 'Authentication is suspended. Please contact your admin.'; + message.style.cssText = ` + padding: 15px; + background-color: #fff3cd; + border: 1px solid #ffeaa7; + border-radius: 4px; + color: #856404; + margin: 10px 0; + `; + + container.appendChild(message); + journeyEl?.appendChild(container); + + console.log('Suspended text output callback:', callback.getMessage()); +} diff --git a/e2e/journey-app/components/terms-and-conditions.ts b/e2e/journey-app/components/terms-and-conditions.ts new file mode 100644 index 0000000000..3ee44f2aa0 --- /dev/null +++ b/e2e/journey-app/components/terms-and-conditions.ts @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2025 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 type { TermsAndConditionsCallback } from '@forgerock/journey-client/types'; + +export default function termsAndConditionsComponent( + journeyEl: HTMLDivElement, + callback: TermsAndConditionsCallback, + idx: number, +) { + const collectorKey = callback?.payload?.input?.[0].name || `collector-${idx}`; + const label = document.createElement('label'); + const checkbox = document.createElement('input'); + + // Terms text area + console.log(callback.getTerms); + + // Checkbox for acceptance + checkbox.type = 'checkbox'; + checkbox.id = collectorKey; + checkbox.required = true; + + label.htmlFor = collectorKey; + label.innerText = ' I accept the terms and conditions'; + + journeyEl.appendChild(label); + journeyEl.appendChild(checkbox); + + checkbox.addEventListener('change', (event) => { + const accepted = (event.target as HTMLInputElement).checked; + callback.setAccepted(accepted); + }); +} diff --git a/e2e/journey-app/components/text-input.ts b/e2e/journey-app/components/text-input.ts new file mode 100644 index 0000000000..0a45074160 --- /dev/null +++ b/e2e/journey-app/components/text-input.ts @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2025 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 type { NameCallback, TextInputCallback } from '@forgerock/journey-client/types'; + +export default function textComponent( + journeyEl: HTMLDivElement, + callback: NameCallback | TextInputCallback, + idx: number, +) { + const collectorKey = callback?.payload?.input?.[0].name || `collector-${idx}`; + const label = document.createElement('label'); + const input = document.createElement('input'); + + label.htmlFor = collectorKey; + label.innerText = callback.getPrompt(); + input.type = 'text'; + input.id = collectorKey; + input.name = collectorKey; + + journeyEl?.appendChild(label); + journeyEl?.appendChild(input); + + journeyEl?.querySelector(`#${collectorKey}`)?.addEventListener('input', (event) => { + if (callback.getType() === 'NameCallback') { + (callback as NameCallback).setName((event.target as HTMLInputElement).value); + } else { + (callback as TextInputCallback).setInputValue((event.target as HTMLInputElement).value); + } + }); +} diff --git a/e2e/journey-app/components/text-output.ts b/e2e/journey-app/components/text-output.ts new file mode 100644 index 0000000000..ab83403d6f --- /dev/null +++ b/e2e/journey-app/components/text-output.ts @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2025 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 type { TextOutputCallback } from '@forgerock/journey-client/types'; + +export default function textComponent( + journeyEl: HTMLDivElement, + callback: TextOutputCallback, + idx: number, +) { + const collectorKey = callback?.payload?.input?.[0].name || `collector-${idx}`; + const message = callback.getMessage() || ''; + const p = document.createElement('paragraph'); + + p.innerText = message; + p.id = collectorKey; + + console.log(message); + + journeyEl?.appendChild(p); +} diff --git a/e2e/journey-app/components/validated-password.ts b/e2e/journey-app/components/validated-password.ts new file mode 100644 index 0000000000..4bfb81674f --- /dev/null +++ b/e2e/journey-app/components/validated-password.ts @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2025 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 type { ValidatedCreatePasswordCallback } from '@forgerock/journey-client/types'; + +export default function validatedPasswordComponent( + journeyEl: HTMLDivElement, + callback: ValidatedCreatePasswordCallback, + idx: number, +) { + const collectorKey = callback?.payload?.input?.[0].name || `collector-${idx}`; + const label = document.createElement('label'); + const input = document.createElement('input'); + + label.htmlFor = collectorKey; + label.innerText = callback.getPrompt(); + input.type = 'password'; + input.id = collectorKey; + input.name = collectorKey; + + journeyEl?.appendChild(label); + journeyEl?.appendChild(input); + + journeyEl?.querySelector(`#${collectorKey}`)?.addEventListener('input', (event) => { + callback.setPassword((event.target as HTMLInputElement).value); + }); +} diff --git a/e2e/journey-app/components/text.ts b/e2e/journey-app/components/validated-username.ts similarity index 81% rename from e2e/journey-app/components/text.ts rename to e2e/journey-app/components/validated-username.ts index bca281d4a8..0efddfaebe 100644 --- a/e2e/journey-app/components/text.ts +++ b/e2e/journey-app/components/validated-username.ts @@ -1,14 +1,14 @@ -/* +/** * Copyright (c) 2025 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 type { NameCallback } from '@forgerock/journey-client/types'; +import type { ValidatedCreateUsernameCallback } from '@forgerock/journey-client/types'; -export default function textComponent( +export default function validatedUsernameComponent( journeyEl: HTMLDivElement, - callback: NameCallback, + callback: ValidatedCreateUsernameCallback, idx: number, ) { const collectorKey = callback?.payload?.input?.[0].name || `collector-${idx}`; diff --git a/e2e/journey-app/main.ts b/e2e/journey-app/main.ts index bc8a9eb0e4..30bb36b8d1 100644 --- a/e2e/journey-app/main.ts +++ b/e2e/journey-app/main.ts @@ -8,14 +8,9 @@ import './style.css'; import { journey } from '@forgerock/journey-client'; -import type { - RequestMiddleware, - NameCallback, - PasswordCallback, -} from '@forgerock/journey-client/types'; - -import textComponent from './components/text.js'; -import passwordComponent from './components/password.js'; +import type { RequestMiddleware } from '@forgerock/journey-client/types'; + +import { renderCallbacks } from './callback-map.js'; import { serverConfigs } from './server-configs.js'; const qs = window.location.search; @@ -23,23 +18,34 @@ const searchParams = new URLSearchParams(qs); const config = serverConfigs[searchParams.get('clientId') || 'basic']; -const requestMiddleware: RequestMiddleware[] = [ - (req, action, next) => { - switch (action.type) { - case 'JOURNEY_START': - if ((action.payload as any).type === 'service') { - console.log('Starting authentication with service'); - } - break; - case 'JOURNEY_NEXT': - if (!('type' in (action.payload as any))) { - console.log('Continuing authentication with service'); - } - break; - } - next(); - }, -]; +let requestMiddleware: RequestMiddleware[] = []; + +if (searchParams.get('middleware') === 'true') { + requestMiddleware = [ + (req, action, next) => { + switch (action.type) { + case 'JOURNEY_START': + req.url.searchParams.set('start-authenticate-middleware', 'start-authentication'); + req.headers.append('x-start-authenticate-middleware', 'start-authentication'); + break; + case 'JOURNEY_NEXT': + req.url.searchParams.set('authenticate-middleware', 'authentication'); + req.headers.append('x-authenticate-middleware', 'authentication'); + break; + } + next(); + }, + (req, action, next) => { + switch (action.type) { + case 'END_SESSION': + req.url.searchParams.set('end-session-middleware', 'end-session'); + req.headers.append('x-end-session-middleware', 'end-session'); + break; + } + next(); + }, + ]; +} (async () => { const journeyClient = await journey({ config, requestMiddleware }); @@ -48,7 +54,10 @@ const requestMiddleware: RequestMiddleware[] = [ const formEl = document.getElementById('form') as HTMLFormElement; const journeyEl = document.getElementById('journey') as HTMLDivElement; - let step = await journeyClient.start(); + let step = await journeyClient.start({ + journey: searchParams.get('journey') || '', + query: { noSession: searchParams.get('no-session') || 'false' }, + }); function renderComplete() { if (step?.type !== 'LoginSuccess') { @@ -57,10 +66,12 @@ const requestMiddleware: RequestMiddleware[] = [ const session = step.getSessionToken(); + console.log(`Session Token: ${session || 'none'}`); + journeyEl.innerHTML = ` -

Complete

- Session: -
${session}
+

Complete

+ Session: +
${session}
`; @@ -82,9 +93,12 @@ const requestMiddleware: RequestMiddleware[] = [ } const error = step.payload.message; + + console.error(`Error: ${error}`); + if (errorEl) { errorEl.innerHTML = ` -
${error}
+
${error}
`; } } @@ -105,23 +119,7 @@ const requestMiddleware: RequestMiddleware[] = [ const callbacks = step.callbacks; - callbacks.forEach((callback, idx) => { - if (callback.getType() === 'NameCallback') { - const cb = callback as NameCallback; - textComponent( - journeyEl, // You can ignore this; it's just for rendering - cb, // This callback class - idx, - ); - } else if (callback.getType() === 'PasswordCallback') { - const cb = callback as PasswordCallback; - passwordComponent( - journeyEl, // You can ignore this; it's just for rendering - cb, // This callback class - idx, - ); - } - }); + renderCallbacks(journeyEl, callbacks); const submitBtn = document.createElement('button'); submitBtn.type = 'submit'; @@ -140,17 +138,21 @@ const requestMiddleware: RequestMiddleware[] = [ /** * We can just call `next` here and not worry about passing any arguments */ - step = await journeyClient.next(step); + step = await journeyClient.next(step, { + query: { noSession: searchParams.get('no-session') || 'false' }, + }); /** * Recursively render the form with the new state */ if (step?.type === 'Step') { + console.log('Continuing journey to next step'); renderForm(); } else if (step?.type === 'LoginSuccess') { - console.log('Basic login successful'); + console.log('Journey completed successfully'); renderComplete(); } else if (step?.type === 'LoginFailure') { + console.error('Journey failed'); renderForm(); renderError(); } else { diff --git a/e2e/journey-app/tsconfig.app.json b/e2e/journey-app/tsconfig.app.json index a2b6a0685a..3000be2f63 100644 --- a/e2e/journey-app/tsconfig.app.json +++ b/e2e/journey-app/tsconfig.app.json @@ -9,6 +9,7 @@ "./main.ts", "./helper.ts", "./server-configs.ts", + "./callback-map.ts", "components/**/*.ts" ], "references": [ diff --git a/e2e/journey-suites/src/choice-confirm-poll.test.ts b/e2e/journey-suites/src/choice-confirm-poll.test.ts new file mode 100644 index 0000000000..24db060471 --- /dev/null +++ b/e2e/journey-suites/src/choice-confirm-poll.test.ts @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2025 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'; +import { username, password } from './utils/demo-user.js'; + +test('Test happy paths on test page', async ({ page }) => { + const { clickButton, navigate } = asyncEvents(page); + await navigate('/?journey=TEST_LoginWithMiscCallbacks'); + + const messageArray: string[] = []; + + // Listen for events on page + page.on('console', async (msg) => { + messageArray.push(msg.text()); + return Promise.resolve(true); + }); + + expect(page.url()).toBe('http://localhost:5829/?journey=TEST_LoginWithMiscCallbacks'); + + // Perform basic login + await page.getByLabel('User Name').fill(username); + await clickButton('Submit', '/authenticate'); + + await page.getByLabel('Password').fill(password); + await clickButton('Submit', '/authenticate'); + + // Message Node: Are you human? + await page.getByText('Are you human?').isVisible(); + await page.getByLabel('Yes').click(); + await clickButton('Submit', '/authenticate'); + + // Choice Collector: Are you sure? + await page.getByLabel('Are you sure?').selectOption('Yes'); + await clickButton('Submit', '/authenticate'); + + // Choice Collector: Are you sure? + await page.getByLabel('Are you sure?').selectOption('Yes'); + await clickButton('Submit', '/authenticate'); + + await expect(page.getByText('Please wait while we process your request.')).toBeVisible(); + await page.waitForTimeout(1500); // Simulate wait for async poll + await clickButton('Submit', '/authenticate'); + + await expect(page.getByText('Complete')).toBeVisible(); + + // Perform logout, wait for /authenticate to ensure logout completed and form is refreshed + await clickButton('Logout', '/authenticate'); + + // Test assertions + expect(messageArray.includes('Journey completed successfully')).toBe(true); + expect(messageArray.includes('Logout successful')).toBe(true); +}); diff --git a/e2e/journey-suites/src/custom-paths.test.ts b/e2e/journey-suites/src/custom-paths.test.ts new file mode 100644 index 0000000000..f848f8f1d2 --- /dev/null +++ b/e2e/journey-suites/src/custom-paths.test.ts @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2025 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'; +import { username, password } from './utils/demo-user.js'; + +// Skipping test until AM Mock API is available that supports custom paths +test.skip('Test happy paths on test page', async ({ page }) => { + const { clickButton, navigate } = asyncEvents(page); + await navigate('/?paths=true&journey=Login'); + + const messageArray: string[] = []; + + // Listen for events on page + page.on('console', async (msg) => { + messageArray.push(msg.text()); + return Promise.resolve(true); + }); + + // Perform basic login + await page.getByLabel('User Name').fill(username); + await page.getByLabel('Password').fill(password); + await clickButton('Submit', '/authenticate'); + + await expect(page.getByText('Complete')).toBeVisible(); + + // Perform logout, wait for /authenticate to ensure logout completed and form is refreshed + await clickButton('Logout', '/authenticate'); + + // Test assertions + expect(messageArray.includes('Journey completed successfully')).toBe(true); + expect(messageArray.includes('Logout successful')).toBe(true); +}); diff --git a/e2e/journey-suites/src/device-profile.test.ts b/e2e/journey-suites/src/device-profile.test.ts new file mode 100644 index 0000000000..65c8c348ee --- /dev/null +++ b/e2e/journey-suites/src/device-profile.test.ts @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2025 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'; +import { username, password } from './utils/demo-user.js'; + +test.skip('Test happy paths on test page', async ({ page }) => { + const { clickButton, navigate } = asyncEvents(page); + await navigate('/?journey=TEST_DeviceProfile'); + + const messageArray: string[] = []; + + // Listen for events on page + page.on('console', async (msg) => { + messageArray.push(msg.text()); + return Promise.resolve(true); + }); + + // Perform basic login + await page.getByLabel('User Name').fill(username); + await page.getByLabel('Password').fill(password); + await clickButton('Submit', '/authenticate'); + + await expect(page.getByText('Collecting device profile')).toBeVisible(); + + await expect(page.getByText('Complete')).toBeVisible(); + + // Perform logout + await clickButton('Logout', '/authenticate'); + + // Test assertions + expect(messageArray.includes('Device profile collected successfully')).toBe(true); + expect(messageArray.includes('Journey completed successfully')).toBe(true); + expect(messageArray.includes('Logout successful')).toBe(true); +}); diff --git a/e2e/journey-suites/src/email-suspend.test.ts b/e2e/journey-suites/src/email-suspend.test.ts new file mode 100644 index 0000000000..900d61b0a7 --- /dev/null +++ b/e2e/journey-suites/src/email-suspend.test.ts @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2025 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'; +import { username, password } from './utils/demo-user.js'; + +test('Test happy paths on test page', async ({ page }) => { + const { clickButton, navigate } = asyncEvents(page); + await navigate('/?journey=TEST_LoginSuspendEmail'); + + const messageArray: string[] = []; + + // Listen for events on page + page.on('console', async (msg) => { + messageArray.push(msg.text()); + return Promise.resolve(true); + }); + + // Perform basic login + await page.getByLabel('User Name').fill(username); + await page.getByLabel('Password').fill(password); + await clickButton('Submit', '/authenticate'); + + await expect( + page.getByText( + 'An email has been sent to the address you entered. Click the link in that email to proceed.', + ), + ).toBeVisible(); +}); diff --git a/e2e/journey-suites/src/basic.test.ts b/e2e/journey-suites/src/login.test.ts similarity index 62% rename from e2e/journey-suites/src/basic.test.ts rename to e2e/journey-suites/src/login.test.ts index ca40ceda1c..f048e6de15 100644 --- a/e2e/journey-suites/src/basic.test.ts +++ b/e2e/journey-suites/src/login.test.ts @@ -10,8 +10,8 @@ import { asyncEvents } from './utils/async-events.js'; import { username, password } from './utils/demo-user.js'; test('Test happy paths on test page', async ({ page }) => { - const { navigate } = asyncEvents(page); - await navigate('/'); + const { clickButton, navigate } = asyncEvents(page); + await navigate('/?journey=Login'); const messageArray: string[] = []; @@ -21,21 +21,17 @@ test('Test happy paths on test page', async ({ page }) => { return Promise.resolve(true); }); - expect(page.url()).toBe('http://localhost:5829/'); - // Perform basic login await page.getByLabel('User Name').fill(username); await page.getByLabel('Password').fill(password); - await page.getByRole('button', { name: 'Submit' }).click(); + await clickButton('Submit', '/authenticate'); + await expect(page.getByText('Complete')).toBeVisible(); // Perform logout - await page.getByRole('button', { name: 'Logout' }).click(); - await expect(page.getByRole('button', { name: 'Submit' })).toBeVisible(); + await clickButton('Logout', '/authenticate'); // Test assertions - expect(messageArray.includes('Basic login successful')).toBe(true); + expect(messageArray.includes('Journey completed successfully')).toBe(true); expect(messageArray.includes('Logout successful')).toBe(true); - expect(messageArray.includes('Starting authentication with service')).toBe(true); - expect(messageArray.includes('Continuing authentication with service')).toBe(true); }); diff --git a/e2e/journey-suites/src/no-session.test.ts b/e2e/journey-suites/src/no-session.test.ts new file mode 100644 index 0000000000..679e6e9cba --- /dev/null +++ b/e2e/journey-suites/src/no-session.test.ts @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2025 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'; +import { username, password } from './utils/demo-user.js'; + +test('Test happy paths on test page', async ({ page }) => { + const { clickButton, navigate } = asyncEvents(page); + await navigate('/?journey=Login&no-session=true'); + + const messageArray: string[] = []; + + // Listen for events on page + page.on('console', async (msg) => { + messageArray.push(msg.text()); + return Promise.resolve(true); + }); + + expect(page.url()).toBe('http://localhost:5829/?journey=Login&no-session=true'); + + // Perform basic login + await page.getByLabel('User Name').fill(username); + await page.getByLabel('Password').fill(password); + await clickButton('Submit', '/authenticate'); + + await expect(page.getByText('Complete')).toBeVisible(); + + // Test assertions + await expect(messageArray.includes('Session Token: none')).toBe(true); +}); diff --git a/e2e/journey-suites/src/otp-register.ts b/e2e/journey-suites/src/otp-register.ts new file mode 100644 index 0000000000..19a5b43e64 --- /dev/null +++ b/e2e/journey-suites/src/otp-register.ts @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2025 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'; +import { username, password } from './utils/demo-user.js'; + +test('Test happy paths on test page', async ({ page }) => { + const { clickButton, navigate } = asyncEvents(page); + await navigate('/?journey=TEST_OTPRegistration'); + + const messageArray: string[] = []; + + // Listen for events on page + page.on('console', async (msg) => { + messageArray.push(msg.text()); + return Promise.resolve(true); + }); + + // Perform basic login + await page.getByLabel('User Name').fill(username); + await page.getByLabel('Password').fill(password); + await clickButton('Submit', '/authenticate'); + + await expect(page.getByText('Complete')).toBeVisible(); + + // Test assertions + expect( + messageArray.includes( + 'Scan the QR code image below with the ForgeRock Authenticator app to register your device with your login.', + ), + ).toBe(true); + expect(messageArray.includes('otp')).toBe(true); + + // TODO: Use when AM Mock API is available + // expect( + // messageArray.includes( + // 'otpauth://totp/ForgeRock:jlowery?secret=QITSTC234FRIU8DD987DW3VPICFY======&issuer=ForgeRock&period=30&digits=6&b=032b75', + // ), + // ).toBe(true); + // expect(messageArray.includes('Basic login with OTP registration step successful')).toBe(true); +}); diff --git a/e2e/journey-suites/src/protect.test.ts b/e2e/journey-suites/src/protect.test.ts new file mode 100644 index 0000000000..bb7eb3ee11 --- /dev/null +++ b/e2e/journey-suites/src/protect.test.ts @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2025 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'; +import { username, password } from './utils/demo-user.js'; + +test.skip('Test happy paths on test page', async ({ page }) => { + const { clickButton, navigate } = asyncEvents(page); + await navigate('/?journey=TEST_Protect'); + + const messageArray: string[] = []; + + // Listen for events on page + page.on('console', async (msg) => { + messageArray.push(msg.text()); + return Promise.resolve(true); + }); + + // Perform basic login + await page.getByLabel('User Name').fill(username); + await page.getByLabel('Password').fill(password); + await clickButton('Submit', '/authenticate'); + + await expect(page.getByText('Collecting protect data')).toBeVisible(); + + await expect(page.getByText('Complete')).toBeVisible(); + + // Perform logout + await clickButton('Logout', '/authenticate'); + + // Test assertions + expect(messageArray.includes('Protect data collected successfully')).toBe(true); + expect(messageArray.includes('Journey completed successfully')).toBe(true); + expect(messageArray.includes('Logout successful')).toBe(true); +}); diff --git a/e2e/journey-suites/src/registration.test.ts b/e2e/journey-suites/src/registration.test.ts new file mode 100644 index 0000000000..242ae04270 --- /dev/null +++ b/e2e/journey-suites/src/registration.test.ts @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2025 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'; +import { password } from './utils/demo-user.js'; + +test('Test happy paths on test page', async ({ page }) => { + const { clickButton, navigate } = asyncEvents(page); + await navigate('/?journey=Registration'); + + // generate ID, 3 sections of random numbers: "714524572-2799534390-3707617532" + const id = globalThis.crypto.getRandomValues(new Uint32Array(3)).join('-'); + const messageArray: string[] = []; + + // Listen for events on page + page.on('console', async (msg) => { + messageArray.push(msg.text()); + return Promise.resolve(true); + }); + + expect(page.url()).toBe('http://localhost:5829/?journey=Registration'); + + // Perform registration + await page.getByLabel('Username').fill('testuser+' + id); + + // Select email and fill with "testuser+@example.com" + await page.getByLabel('Email Address').fill('testuser+' + id + '@example.com'); + // Select first name and fill with "Sally" + await page.getByLabel('First Name').fill('Sally'); + // Select last name and fill with "Tester" + await page.getByLabel('Last Name').fill('Tester'); + // Select "Send me special offers and services" and leave check + await page.getByLabel('Send me special offers and services').check(); + // Select "Send me news and updates" and uncheck + await page.getByLabel('Send me news and updates').check(); + // Fill password + await page.getByLabel('Password').fill(password); + // Select "Select a security question 7" dropdown and choose "What's your favorite color?" + await page.getByLabel('Select a security question 7').selectOption("What's your favorite color?"); + // Fill answer with "Red" + await page.getByLabel('Answer 7').fill('Red'); + // Select "Select a security question 8" dropdown and choose "Who was your first employer?" + await page + .getByLabel('Select a security question 8') + .selectOption('Who was your first employer?'); + // Fill answer with "Pizza" + await page.getByLabel('Answer 8').fill('AAA Engineering'); + + await page.getByLabel('I accept the terms and conditions').check(); + + await clickButton('Submit', '/authenticate'); + + await expect(page.getByText('Complete')).toBeVisible(); + + // Perform logout + await clickButton('Logout', '/authenticate'); + + // Test assertions + expect(messageArray.includes('Journey completed successfully')).toBe(true); + expect(messageArray.includes('Logout successful')).toBe(true); +}); diff --git a/e2e/journey-suites/src/request-middleware.test.ts b/e2e/journey-suites/src/request-middleware.test.ts new file mode 100644 index 0000000000..be0e269113 --- /dev/null +++ b/e2e/journey-suites/src/request-middleware.test.ts @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2025 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'; +import { username, password } from './utils/demo-user.js'; + +test.skip('Test happy paths on test page', async ({ page }) => { + const { clickButton, navigate } = asyncEvents(page); + await navigate('/?middleware=true&journey=Login'); + + const headerArray: Headers[] = []; + const messageArray: string[] = []; + const networkArray: string[] = []; + + // Listen for events on page + page.on('console', async (msg) => { + messageArray.push(msg.text()); + return Promise.resolve(true); + }); + + page.on('request', async (req) => { + networkArray.push(`${new URL(req.url()).pathname}, ${req.resourceType()}`); + }); + + page.on('request', async (req) => { + const headers = req.headers(); + + headerArray.push(new Headers(headers)); + }); + + expect(page.url()).toBe('http://localhost:5829/?middleware=true&journey=Login'); + + // Perform basic login + await page.getByLabel('User Name').fill(username); + await page.getByLabel('Password').fill(password); + await clickButton('Submit', '/authenticate'); + + await expect(page.getByText('Complete')).toBeVisible(); + + // Perform logout + await clickButton('Logout', '/authenticate'); + + // Test assertions + // test URL query parameters added to URL on networkArray + + expect(networkArray).toContain('start-authenticate-middleware, fetch'); + expect(networkArray).toContain('authenticate-middleware, fetch'); + expect(networkArray).toContain('end-session-middleware, fetch'); + + expect( + headerArray.find((headers) => headers.get('x-start-authenticate-middleware')), + ).toBeTruthy(); + expect(headerArray.find((headers) => headers.get('x-authenticate-middleware'))).toBeTruthy(); + expect(headerArray.find((headers) => headers.get('x-end-session-middleware'))).toBeTruthy(); + + expect(messageArray.includes('Journey completed successfully')).toBe(true); + expect(messageArray.includes('Logout successful')).toBe(true); +}); diff --git a/e2e/oidc-app/src/ping-am/index.html b/e2e/oidc-app/src/ping-am/index.html index f1021f29e4..269c9ce9a4 100644 --- a/e2e/oidc-app/src/ping-am/index.html +++ b/e2e/oidc-app/src/ping-am/index.html @@ -15,7 +15,8 @@

OIDC App | PingAM Login

- + + diff --git a/e2e/oidc-app/src/ping-one/index.html b/e2e/oidc-app/src/ping-one/index.html index f1a50104e1..08c30a4735 100644 --- a/e2e/oidc-app/src/ping-one/index.html +++ b/e2e/oidc-app/src/ping-one/index.html @@ -15,7 +15,8 @@

OIDC App | P1 Login

- + + diff --git a/e2e/oidc-app/src/utils/oidc-app.ts b/e2e/oidc-app/src/utils/oidc-app.ts index eaa099bfff..a95f7daf4e 100644 --- a/e2e/oidc-app/src/utils/oidc-app.ts +++ b/e2e/oidc-app/src/utils/oidc-app.ts @@ -103,11 +103,16 @@ export async function oidcApp({ config, urlParams }) { displayTokenResponse(response); }); - document.getElementById('renew-tokens').addEventListener('click', async () => { + document.getElementById('get-tokens-background').addEventListener('click', async () => { const response = await oidcClient.token.get({ backgroundRenew: true }); displayTokenResponse(response); }); + document.getElementById('renew-tokens').addEventListener('click', async () => { + const response = await oidcClient.token.get({ backgroundRenew: true, forceRenew: true }); + displayTokenResponse(response); + }); + document.getElementById('user-info-btn').addEventListener('click', async () => { const userInfo = await oidcClient.user.info(); diff --git a/e2e/oidc-suites/src/login.spec.ts b/e2e/oidc-suites/src/login.spec.ts index 1443be4c3d..1d1523bc13 100644 --- a/e2e/oidc-suites/src/login.spec.ts +++ b/e2e/oidc-suites/src/login.spec.ts @@ -17,41 +17,40 @@ import { asyncEvents } from './utils/async-events.js'; test.describe('PingAM login and get token tests', () => { test('background login with valid credentials', async ({ page }) => { - const { navigate, clickButton } = asyncEvents(page); + const { clickWithRedirect, navigate } = asyncEvents(page); await navigate('/ping-am/'); - expect(page.url()).toBe('http://localhost:8443/ping-am/'); - await clickButton('Login (Background)', '/authorize'); + await clickWithRedirect('Login (Background)', '**/am/XUI/**'); await page.getByLabel('User Name').fill(pingAmUsername); await page.getByRole('textbox', { name: 'Password' }).fill(pingAmPassword); - await page.getByRole('button', { name: 'Next' }).click(); + await clickWithRedirect('Next', 'http://localhost:8443/ping-am/**'); expect(page.url()).toContain('code'); expect(page.url()).toContain('state'); + await expect(page.locator('#accessToken-0')).not.toBeEmpty(); }); test('redirect login with valid credentials', async ({ page }) => { - const { navigate, clickButton } = asyncEvents(page); + const { clickWithRedirect, navigate } = asyncEvents(page); await navigate('/ping-am/'); - expect(page.url()).toBe('http://localhost:8443/ping-am/'); - await clickButton('Login (Redirect)', '/authorize'); + await clickWithRedirect('Login (Redirect)', '**/am/XUI/**'); await page.getByLabel('User Name').fill(pingAmUsername); await page.getByRole('textbox', { name: 'Password' }).fill(pingAmPassword); - await page.getByRole('button', { name: 'Next' }).click(); + await clickWithRedirect('Next', 'http://localhost:8443/ping-am/**'); expect(page.url()).toContain('code'); expect(page.url()).toContain('state'); + await expect(page.locator('#accessToken-0')).not.toBeEmpty(); }); test('background login with invalid client id fails', async ({ page }) => { const { navigate } = asyncEvents(page); await navigate('/ping-am/?clientid=bad-id'); - expect(page.url()).toBe('http://localhost:8443/ping-am/?clientid=bad-id'); await page.getByRole('button', { name: 'Login (Background)' }).click(); @@ -65,43 +64,40 @@ test.describe('PingAM login and get token tests', () => { test.describe('PingOne login and get token tests', () => { test('background login with valid credentials', async ({ page }) => { - const { navigate, clickButton } = asyncEvents(page); + const { clickWithRedirect, navigate } = asyncEvents(page); await navigate('/ping-one/'); - expect(page.url()).toBe('http://localhost:8443/ping-one/'); - await clickButton('Login (Background)', '/authorize'); + await clickWithRedirect('Login (Background)', '**/signon/**'); await page.getByLabel('Username').fill(pingOneUsername); await page.getByRole('textbox', { name: 'Password' }).fill(pingOnePassword); - await page.getByRole('button', { name: 'Sign On' }).click(); + await clickWithRedirect('Sign On', 'http://localhost:8443/ping-one/**'); - await page.waitForURL('http://localhost:8443/ping-one/**'); expect(page.url()).toContain('code'); expect(page.url()).toContain('state'); + await expect(page.locator('#accessToken-0')).not.toBeEmpty(); }); test('redirect login with valid credentials', async ({ page }) => { - const { navigate, clickButton } = asyncEvents(page); + const { clickWithRedirect, navigate } = asyncEvents(page); await navigate('/ping-one/'); - expect(page.url()).toBe('http://localhost:8443/ping-one/'); - await clickButton('Login (Redirect)', '/authorize'); + await clickWithRedirect('Login (Background)', '**/signon/**'); await page.getByLabel('Username').fill(pingOneUsername); await page.getByRole('textbox', { name: 'Password' }).fill(pingOnePassword); - await page.getByRole('button', { name: 'Sign On' }).click(); + await clickWithRedirect('Sign On', 'http://localhost:8443/ping-one/**'); - await page.waitForURL('http://localhost:8443/ping-one/**'); expect(page.url()).toContain('code'); expect(page.url()).toContain('state'); + await expect(page.locator('#accessToken-0')).not.toBeEmpty(); }); test('login with invalid client id fails', async ({ page }) => { const { navigate } = asyncEvents(page); await navigate('/ping-one/?clientid=bad-id'); - expect(page.url()).toBe('http://localhost:8443/ping-one/?clientid=bad-id'); await page.getByRole('button', { name: 'Login (Background)' }).click(); @@ -113,9 +109,8 @@ test.describe('PingOne login and get token tests', () => { }); test('login with pi.flow response mode', async ({ page }) => { - const { navigate, clickButton } = asyncEvents(page); + const { clickWithRedirect, navigate } = asyncEvents(page); await navigate('/ping-one/?piflow=true'); - expect(page.url()).toBe('http://localhost:8443/ping-one/?piflow=true'); await page.on('request', (request) => { const method = request.method(); @@ -126,15 +121,15 @@ test.describe('PingOne login and get token tests', () => { } }); - await clickButton('Login (Background)', '/authorize'); + await clickWithRedirect('Login (Background)', '**/signon/**'); await page.getByLabel('Username').fill(pingOneUsername); await page.getByRole('textbox', { name: 'Password' }).fill(pingOnePassword); - await page.getByRole('button', { name: 'Sign On' }).click(); + await clickWithRedirect('Sign On', 'http://localhost:8443/ping-one/**'); - await page.waitForURL('http://localhost:8443/ping-one/**'); expect(page.url()).toContain('code'); expect(page.url()).toContain('state'); + await expect(page.locator('#accessToken-0')).not.toBeEmpty(); }); }); @@ -142,7 +137,6 @@ test.describe('PingOne login and get token tests', () => { test('login with invalid state fails with error', async ({ page }) => { const { navigate } = asyncEvents(page); await navigate('/ping-am/?code=12345&state=abcxyz'); - expect(page.url()).toBe('http://localhost:8443/ping-am/?code=12345&state=abcxyz'); await expect(page.locator('.error')).toContainText(`"error": "State mismatch"`); await expect(page.locator('.error')).toContainText(`"type": "state_error"`); @@ -154,7 +148,6 @@ test('login with invalid state fails with error', async ({ page }) => { test('oidc client fails to initialize with bad wellknown', async ({ page }) => { const { navigate } = asyncEvents(page); await navigate('/ping-am/?wellknown=bad-wellknown'); - expect(page.url()).toBe('http://localhost:8443/ping-am/?wellknown=bad-wellknown'); await page.getByRole('button', { name: 'Login (Background)' }).click(); diff --git a/e2e/oidc-suites/src/logout.spec.ts b/e2e/oidc-suites/src/logout.spec.ts index e62f4eda9d..ae4065301e 100644 --- a/e2e/oidc-suites/src/logout.spec.ts +++ b/e2e/oidc-suites/src/logout.spec.ts @@ -17,11 +17,11 @@ import { asyncEvents } from './utils/async-events.js'; test.describe('Logout tests', () => { test('PingAM login then logout', async ({ page }) => { - const { navigate, clickButton } = asyncEvents(page); + const { clickButton, clickWithRedirect, navigate } = asyncEvents(page); await navigate('/ping-am/'); - expect(page.url()).toBe('http://localhost:8443/ping-am/'); let endSessionStatus, revokeStatus; + page.on('response', (response) => { const responseUrl = response.url(); const status = response.ok(); @@ -34,31 +34,27 @@ test.describe('Logout tests', () => { } }); - await clickButton('Login (Background)', 'https://openam-sdks.forgeblocks.com/'); + await clickWithRedirect('Login (Background)', '**/am/XUI/**'); await page.getByLabel('User Name').fill(pingAmUsername); await page.getByRole('textbox', { name: 'Password' }).fill(pingAmPassword); - await Promise.all([ - page.waitForURL('http://localhost:8443/ping-am/**'), - page.getByRole('button', { name: 'Next' }).click(), - ]); + await clickWithRedirect('Next', 'http://localhost:8443/ping-am/**'); + expect(page.url()).toContain('code'); expect(page.url()).toContain('state'); - await expect(page.getByRole('button', { name: 'Login (Background)' })).toBeHidden(); - await page.getByRole('button', { name: 'Logout' }).click(); - await expect(page.getByRole('button', { name: 'Login (Background)' })).toBeVisible(); + await clickButton('Logout', '/revoke'); expect(endSessionStatus).toBeTruthy(); expect(revokeStatus).toBeTruthy(); }); test('PingOne login then logout', async ({ page }) => { - const { navigate, clickButton } = asyncEvents(page); + const { clickButton, clickWithRedirect, navigate } = asyncEvents(page); await navigate('/ping-one/'); - expect(page.url()).toBe('http://localhost:8443/ping-one/'); let endSessionStatus, revokeStatus; + page.on('response', (response) => { const responseUrl = response.url(); const status = response.ok(); @@ -71,20 +67,16 @@ test.describe('Logout tests', () => { } }); - await clickButton('Login (Background)', 'https://apps.pingone.ca/'); + await clickWithRedirect('Login (Background)', '**/signon/**'); await page.getByLabel('Username').fill(pingOneUsername); await page.getByRole('textbox', { name: 'Password' }).fill(pingOnePassword); - await Promise.all([ - page.waitForURL('http://localhost:8443/ping-one/**'), - page.getByRole('button', { name: 'Sign On' }).click(), - ]); + await clickWithRedirect('Sign On', 'http://localhost:8443/ping-one/**'); + expect(page.url()).toContain('code'); expect(page.url()).toContain('state'); - await expect(page.getByRole('button', { name: 'Login (Background)' })).toBeHidden(); - await page.getByRole('button', { name: 'Logout' }).click(); - await expect(page.getByRole('button', { name: 'Login (Background)' })).toBeVisible(); + await clickButton('Logout', '/revoke'); expect(endSessionStatus).toBeTruthy(); expect(revokeStatus).toBeTruthy(); diff --git a/e2e/oidc-suites/src/token.spec.ts b/e2e/oidc-suites/src/token.spec.ts index 6905eb4f1b..083d14be1d 100644 --- a/e2e/oidc-suites/src/token.spec.ts +++ b/e2e/oidc-suites/src/token.spec.ts @@ -17,21 +17,21 @@ import { asyncEvents } from './utils/async-events.js'; test.describe('PingAM tokens', () => { test('login and get tokens', async ({ page }) => { - const { navigate, clickButton } = asyncEvents(page); + const { clickWithRedirect, navigate } = asyncEvents(page); await navigate('/ping-am/'); - expect(page.url()).toBe('http://localhost:8443/ping-am/'); - await clickButton('Login (Background)', 'https://openam-sdks.forgeblocks.com/'); + await clickWithRedirect('Login (Background)', '**/am/XUI/**'); await page.getByLabel('User Name').fill(pingAmUsername); await page.getByRole('textbox', { name: 'Password' }).fill(pingAmPassword); - await page.getByRole('button', { name: 'Next' }).click(); + await clickWithRedirect('Next', 'http://localhost:8443/ping-am/**'); - await page.waitForURL('http://localhost:8443/ping-am/**', { waitUntil: 'networkidle' }); expect(page.url()).toContain('code'); expect(page.url()).toContain('state'); - await page.getByRole('button', { name: 'Get Tokens' }).click(); + await expect(page.locator('#accessToken-0')).not.toBeEmpty(); + + await page.getByRole('button', { name: 'Get Tokens (Local)' }).click(); await expect(page.locator('#accessToken-1')).not.toBeEmpty(); const accessToken0 = await page.locator('#accessToken-0').textContent(); @@ -39,22 +39,46 @@ test.describe('PingAM tokens', () => { await expect(accessToken0).toBe(accessToken1); }); - test('login and renew tokens', async ({ page }) => { - const { navigate, clickButton } = asyncEvents(page); + test('login and renew missing tokens', async ({ page }) => { + const { clickWithRedirect, navigate } = asyncEvents(page); await navigate('/ping-am/'); - expect(page.url()).toBe('http://localhost:8443/ping-am/'); - await clickButton('Login (Background)', 'https://openam-sdks.forgeblocks.com/'); + await clickWithRedirect('Login (Background)', '**/am/XUI/**'); await page.getByLabel('User Name').fill(pingAmUsername); await page.getByRole('textbox', { name: 'Password' }).fill(pingAmPassword); - await page.getByRole('button', { name: 'Next' }).click(); + await clickWithRedirect('Next', 'http://localhost:8443/ping-am/**'); - await page.waitForURL('http://localhost:8443/ping-am/**', { waitUntil: 'networkidle' }); expect(page.url()).toContain('code'); expect(page.url()).toContain('state'); + await expect(page.locator('#accessToken-0')).not.toBeEmpty(); + await page.evaluate(() => window.localStorage.clear()); + await page.getByRole('button', { name: 'Get Tokens (Background)' }).click(); + + await expect(page.locator('#accessToken-1')).not.toBeEmpty(); + + const accessToken0 = await page.locator('#accessToken-0').textContent(); + const accessToken1 = await page.locator('#accessToken-1').textContent(); + await expect(accessToken0).not.toBe(accessToken1); + }); + + test('login and renew existing tokens', async ({ page }) => { + const { clickWithRedirect, navigate } = asyncEvents(page); + await navigate('/ping-am/'); + + await clickWithRedirect('Login (Background)', '**/am/XUI/**'); + + await page.getByLabel('User Name').fill(pingAmUsername); + await page.getByRole('textbox', { name: 'Password' }).fill(pingAmPassword); + await clickWithRedirect('Next', 'http://localhost:8443/ping-am/**'); + + expect(page.url()).toContain('code'); + expect(page.url()).toContain('state'); + + await expect(page.locator('#accessToken-0')).not.toBeEmpty(); + await page.getByRole('button', { name: 'Renew Tokens' }).click(); await expect(page.locator('#accessToken-1')).not.toBeEmpty(); @@ -65,17 +89,14 @@ test.describe('PingAM tokens', () => { }); test('login and revoke tokens', async ({ page }) => { - const { navigate, clickButton } = asyncEvents(page); + const { clickWithRedirect, navigate } = asyncEvents(page); await navigate('/ping-am/'); - expect(page.url()).toBe('http://localhost:8443/ping-am/'); - await clickButton('Login (Background)', 'https://openam-sdks.forgeblocks.com/'); + await clickWithRedirect('Login (Background)', '**/am/XUI/**'); await page.getByLabel('User Name').fill(pingAmUsername); await page.getByRole('textbox', { name: 'Password' }).fill(pingAmPassword); - await page.getByRole('button', { name: 'Next' }).click(); - - await page.waitForURL('http://localhost:8443/ping-am/**'); + await clickWithRedirect('Next', 'http://localhost:8443/ping-am/**'); await expect(page.locator('#accessToken-0')).not.toBeEmpty(); @@ -91,7 +112,6 @@ test.describe('PingAM tokens', () => { test('renew tokens without logging in should error', async ({ page }) => { const { navigate } = asyncEvents(page); await navigate('/ping-am/'); - expect(page.url()).toBe('http://localhost:8443/ping-am/'); await page.getByRole('button', { name: 'Renew Tokens' }).click(); @@ -105,21 +125,18 @@ test.describe('PingAM tokens', () => { test.describe('PingOne tokens', () => { test('login and get tokens', async ({ page }) => { - const { navigate, clickButton } = asyncEvents(page); + const { clickWithRedirect, navigate } = asyncEvents(page); await navigate('/ping-one/'); - expect(page.url()).toBe('http://localhost:8443/ping-one/'); - await clickButton('Login (Background)', 'https://apps.pingone.ca/'); + await clickWithRedirect('Login (Background)', '**/signon/**'); await page.getByLabel('Username').fill(pingOneUsername); await page.getByRole('textbox', { name: 'Password' }).fill(pingOnePassword); - await page.getByRole('button', { name: 'Sign On' }).click(); + await clickWithRedirect('Sign On', 'http://localhost:8443/ping-one/**'); - await page.waitForURL('http://localhost:8443/ping-one/**', { waitUntil: 'networkidle' }); - expect(page.url()).toContain('code'); - expect(page.url()).toContain('state'); + await expect(page.locator('#accessToken-0')).not.toBeEmpty(); - await page.getByRole('button', { name: 'Get Tokens' }).click(); + await page.getByRole('button', { name: 'Get Tokens (Local)' }).click(); await expect(page.locator('#accessToken-1')).not.toBeEmpty(); const accessToken0 = await page.locator('#accessToken-0').textContent(); @@ -127,23 +144,20 @@ test.describe('PingOne tokens', () => { await expect(accessToken0).toBe(accessToken1); }); - test('login and renew tokens', async ({ page }) => { - const { navigate, clickButton } = asyncEvents(page); + test('login and renew missing tokens', async ({ page }) => { + const { clickButton, clickWithRedirect, navigate } = asyncEvents(page); await navigate('/ping-one/'); - expect(page.url()).toBe('http://localhost:8443/ping-one/'); - await clickButton('Login (Background)', 'https://apps.pingone.ca/'); + await clickWithRedirect('Login (Background)', '**/signon/**'); await page.getByLabel('Username').fill(pingOneUsername); await page.getByRole('textbox', { name: 'Password' }).fill(pingOnePassword); - await page.getByRole('button', { name: 'Sign On' }).click(); + await clickWithRedirect('Sign On', 'http://localhost:8443/ping-one/**'); - await page.waitForURL('http://localhost:8443/ping-one/**', { waitUntil: 'networkidle' }); - expect(page.url()).toContain('code'); - expect(page.url()).toContain('state'); + await expect(page.locator('#accessToken-0')).not.toBeEmpty(); await page.evaluate(() => window.localStorage.clear()); - await page.getByRole('button', { name: 'Renew Tokens' }).click(); + await clickButton('Get Tokens (Background)', '/as/token'); await expect(page.locator('#accessToken-1')).not.toBeEmpty(); @@ -152,24 +166,40 @@ test.describe('PingOne tokens', () => { await expect(accessToken0).not.toBe(accessToken1); }); - test('login and revoke tokens', async ({ page }) => { - const { navigate, clickButton } = asyncEvents(page); + test('login and renew existing tokens', async ({ page }) => { + const { clickButton, clickWithRedirect, navigate } = asyncEvents(page); await navigate('/ping-one/'); - expect(page.url()).toBe('http://localhost:8443/ping-one/'); - await clickButton('Login (Background)', 'https://apps.pingone.ca/'); + await clickWithRedirect('Login (Background)', '**/signon/**'); await page.getByLabel('Username').fill(pingOneUsername); await page.getByRole('textbox', { name: 'Password' }).fill(pingOnePassword); - await page.getByRole('button', { name: 'Sign On' }).click(); + await clickWithRedirect('Sign On', 'http://localhost:8443/ping-one/**'); - await page.waitForURL('http://localhost:8443/ping-one/**', { waitUntil: 'networkidle' }); - expect(page.url()).toContain('code'); - expect(page.url()).toContain('state'); + await expect(page.locator('#accessToken-0')).not.toBeEmpty(); + + await clickButton('Renew Tokens', '/as/token'); + + await expect(page.locator('#accessToken-1')).not.toBeEmpty(); + + const accessToken0 = await page.locator('#accessToken-0').textContent(); + const accessToken1 = await page.locator('#accessToken-1').textContent(); + await expect(accessToken0).not.toBe(accessToken1); + }); + + test('login and revoke tokens', async ({ page }) => { + const { clickButton, clickWithRedirect, navigate } = asyncEvents(page); + await navigate('/ping-one/'); + + await clickWithRedirect('Login (Background)', '**/signon/**'); + + await page.getByLabel('Username').fill(pingOneUsername); + await page.getByRole('textbox', { name: 'Password' }).fill(pingOnePassword); + await clickWithRedirect('Sign On', 'http://localhost:8443/ping-one/**'); await expect(page.locator('#accessToken-0')).not.toBeEmpty(); - await page.getByRole('button', { name: 'Revoke Token' }).click(); + await clickButton('Revoke Token', '/as/revoke'); await expect(page.getByText('Token successfully revoked')).toBeVisible(); const token = await page.evaluate(() => localStorage.getItem('pic-oidcTokens')); await expect(token).toBeFalsy(); @@ -178,9 +208,12 @@ test.describe('PingOne tokens', () => { test('renew tokens without logging in should error', async ({ page }) => { const { navigate } = asyncEvents(page); await navigate('/ping-one/'); - expect(page.url()).toBe('http://localhost:8443/ping-one/'); + const responsePromise = page.waitForResponse((response) => { + return response.url().includes('/authorize') && !response.ok(); + }); await page.getByRole('button', { name: 'Renew Tokens' }).click(); + await responsePromise; await expect(page.locator('.error')).toContainText(`"error": "LOGIN_REQUIRED"`); await expect(page.locator('.error')).toContainText(`"type": "auth_error"`); @@ -191,9 +224,8 @@ test.describe('PingOne tokens', () => { test('get tokens without logging in should error', async ({ page }) => { const { navigate } = asyncEvents(page); await navigate('/ping-am/'); - expect(page.url()).toBe('http://localhost:8443/ping-am/'); - await page.getByRole('button', { name: 'Get Tokens' }).click(); + await page.getByRole('button', { name: 'Get Tokens (Local)' }).click(); await expect(page.locator('.error')).toContainText(`"error": "No tokens found"`); await expect(page.locator('.error')).toContainText(`"type": "state_error"`); @@ -202,7 +234,6 @@ test('get tokens without logging in should error', async ({ page }) => { test('revoke tokens should error with missing token', async ({ page }) => { const { navigate } = asyncEvents(page); await navigate('/ping-am/'); - expect(page.url()).toBe('http://localhost:8443/ping-am/'); await page.getByRole('button', { name: 'Revoke Token' }).click(); diff --git a/e2e/oidc-suites/src/user.spec.ts b/e2e/oidc-suites/src/user.spec.ts index 5012b49f49..1fbb847840 100644 --- a/e2e/oidc-suites/src/user.spec.ts +++ b/e2e/oidc-suites/src/user.spec.ts @@ -17,45 +17,44 @@ import { asyncEvents } from './utils/async-events.js'; test.describe('User tests', () => { test('get user info from PingAM', async ({ page }) => { - const { navigate, clickButton } = asyncEvents(page); + const { clickButton, clickWithRedirect, navigate } = asyncEvents(page); await navigate('/ping-am/'); - expect(page.url()).toBe('http://localhost:8443/ping-am/'); - await clickButton('Login (Background)', 'https://openam-sdks.forgeblocks.com/'); + await clickWithRedirect('Login (Background)', '**/am/XUI/**'); await page.getByLabel('User Name').fill(pingAmUsername); await page.getByRole('textbox', { name: 'Password' }).fill(pingAmPassword); - await page.getByRole('button', { name: 'Next' }).click(); + await clickWithRedirect('Next', 'http://localhost:8443/ping-am/**'); - await page.waitForURL('http://localhost:8443/ping-am/**', { waitUntil: 'networkidle' }); expect(page.url()).toContain('code'); expect(page.url()).toContain('state'); - await clickButton('User Info', 'https://openam-sdks.forgeblocks.com/am/oauth2/alpha/userinfo'); + await expect(page.locator('#accessToken-0')).not.toBeEmpty(); + + await clickButton('User Info', '/userinfo'); + await expect(page.locator('#userInfo')).not.toBeEmpty(); await expect(page.getByText('Sdk User')).toBeVisible(); await expect(page.getByText('sdkuser@example.com')).toBeVisible(); }); test('get user info from PingOne', async ({ page }) => { - const { navigate, clickButton } = asyncEvents(page); + const { clickButton, clickWithRedirect, navigate } = asyncEvents(page); await navigate('/ping-one/'); - expect(page.url()).toBe('http://localhost:8443/ping-one/'); - await clickButton('Login (Background)', 'https://apps.pingone.ca/'); + await clickWithRedirect('Login (Background)', '**/signon/**'); await page.getByLabel('Username').fill(pingOneUsername); await page.getByRole('textbox', { name: 'Password' }).fill(pingOnePassword); - await page.getByRole('button', { name: 'Sign On' }).click(); + await clickWithRedirect('Sign On', 'http://localhost:8443/ping-one/**'); - await page.waitForURL('http://localhost:8443/ping-one/**', { waitUntil: 'networkidle' }); expect(page.url()).toContain('code'); expect(page.url()).toContain('state'); - await clickButton( - 'User Info', - 'https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/userinfo', - ); + await expect(page.locator('#accessToken-0')).not.toBeEmpty(); + + await clickButton('User Info', '/userinfo'); + await expect(page.locator('#userInfo')).not.toBeEmpty(); await expect(page.getByText('demouser')).toBeVisible(); await expect(page.getByText('demouser@user.com')).toBeVisible(); @@ -64,7 +63,6 @@ test.describe('User tests', () => { test('get user info should error with missing token', async ({ page }) => { const { navigate } = asyncEvents(page); await navigate('/ping-am/'); - expect(page.url()).toBe('http://localhost:8443/ping-am/'); await page.getByRole('button', { name: 'User Info' }).click(); diff --git a/e2e/oidc-suites/src/utils/async-events.ts b/e2e/oidc-suites/src/utils/async-events.ts index b0874e9145..e62f14a8d9 100644 --- a/e2e/oidc-suites/src/utils/async-events.ts +++ b/e2e/oidc-suites/src/utils/async-events.ts @@ -10,15 +10,24 @@ export function asyncEvents(page) { if (!endpoint) throw new Error('Must provide endpoint argument, type string, e.g. "/authenticate"'); await Promise.all([ - page.waitForResponse((response) => response.url().includes(endpoint)), + page.waitForResponse((response) => { + return response.url().includes(endpoint); + }), page.getByRole('button', { name: text }).click(), ]); }, + async clickWithRedirect(text, url) { + if (!url) + throw new Error('Must provide endpoint argument, type string, e.g. "/authenticate"'); + await Promise.all([page.waitForURL(url), page.getByRole('button', { name: text }).click()]); + }, async clickLink(text, endpoint) { if (!endpoint) throw new Error('Must provide endpoint argument, type string, e.g. "/authenticate"'); await Promise.all([ - page.waitForResponse((response) => response.url().includes(endpoint)), + page.waitForResponse((response) => { + return response.url().includes(endpoint) && response.ok(); + }), page.getByRole('link', { name: text }).click(), ]); }, @@ -49,7 +58,9 @@ export function asyncEvents(page) { if (!endpoint) throw new Error('Must provide endpoint argument, type string, e.g. "/authenticate"'); await Promise.all([ - page.waitForResponse((response) => response.url().includes(endpoint)), + page.waitForResponse((response) => { + return response.url().includes(endpoint) && response.ok(); + }), page.keyboard.press('Enter'), ]); }, diff --git a/packages/journey-client/src/index.ts b/packages/journey-client/src/index.ts index 2a3fb2e92d..c51b07963d 100644 --- a/packages/journey-client/src/index.ts +++ b/packages/journey-client/src/index.ts @@ -5,4 +5,4 @@ * of the MIT license. See the LICENSE file for details. */ -export * from './lib/journey.store.js'; +export * from './lib/client.store.js'; diff --git a/packages/journey-client/src/lib/journey.store.test.ts b/packages/journey-client/src/lib/client.store.test.ts similarity index 99% rename from packages/journey-client/src/lib/journey.store.test.ts rename to packages/journey-client/src/lib/client.store.test.ts index 30865be247..ea7038c307 100644 --- a/packages/journey-client/src/lib/journey.store.test.ts +++ b/packages/journey-client/src/lib/client.store.test.ts @@ -10,7 +10,7 @@ import { afterEach, describe, expect, test, vi } from 'vitest'; import type { Step } from '@forgerock/sdk-types'; -import { journey } from './journey.store.js'; +import { journey } from './client.store.js'; import { createJourneyStep } from './step.utils.js'; import { JourneyClientConfig } from './config.types.js'; diff --git a/packages/journey-client/src/lib/journey.store.ts b/packages/journey-client/src/lib/client.store.ts similarity index 99% rename from packages/journey-client/src/lib/journey.store.ts rename to packages/journey-client/src/lib/client.store.ts index f9ba9e9ce3..531675417a 100644 --- a/packages/journey-client/src/lib/journey.store.ts +++ b/packages/journey-client/src/lib/client.store.ts @@ -11,7 +11,7 @@ import { callbackType } from '@forgerock/sdk-types'; import type { RequestMiddleware } from '@forgerock/sdk-request-middleware'; import type { GenericError, Step } from '@forgerock/sdk-types'; -import { createJourneyStore } from './journey.store.utils.js'; +import { createJourneyStore } from './client.store.utils.js'; import { journeyApi } from './journey.api.js'; import { setConfig } from './journey.slice.js'; import { createStorage } from '@forgerock/storage'; diff --git a/packages/journey-client/src/lib/journey.store.utils.ts b/packages/journey-client/src/lib/client.store.utils.ts similarity index 100% rename from packages/journey-client/src/lib/journey.store.utils.ts rename to packages/journey-client/src/lib/client.store.utils.ts diff --git a/packages/journey-client/src/types.ts b/packages/journey-client/src/types.ts index c17806c680..9c1ef38f22 100644 --- a/packages/journey-client/src/types.ts +++ b/packages/journey-client/src/types.ts @@ -12,6 +12,7 @@ export * from './lib/step.types.js'; export * from './lib/callbacks/attribute-input-callback.js'; export * from './lib/callbacks/base-callback.js'; export * from './lib/callbacks/choice-callback.js'; +export * from './lib/callbacks/confirmation-callback.js'; export * from './lib/callbacks/device-profile-callback.js'; export * from './lib/callbacks/factory.js'; export * from './lib/callbacks/hidden-value-callback.js'; @@ -27,6 +28,7 @@ export * from './lib/callbacks/recaptcha-enterprise-callback.js'; export * from './lib/callbacks/redirect-callback.js'; export * from './lib/callbacks/select-idp-callback.js'; export * from './lib/callbacks/suspended-text-output-callback.js'; +export * from './lib/callbacks/text-input-callback.js'; export * from './lib/callbacks/text-output-callback.js'; export * from './lib/callbacks/terms-and-conditions-callback.js'; export * from './lib/callbacks/validated-create-password-callback.js'; diff --git a/packages/sdk-effects/sdk-request-middleware/src/lib/request-mware.test.ts b/packages/sdk-effects/sdk-request-middleware/src/lib/request-mware.test.ts index 7c367a9ce8..885fc47123 100644 --- a/packages/sdk-effects/sdk-request-middleware/src/lib/request-mware.test.ts +++ b/packages/sdk-effects/sdk-request-middleware/src/lib/request-mware.test.ts @@ -48,7 +48,7 @@ describe('Middleware should be called with an action', () => { }); it('should run all middleware testing action for no match', () => { const runMiddleware = middlewareWrapper( - { url: new URL('https://www.example.com') }, + { url: new URL('https://www.example.com'), headers: new Headers() }, { type: 'z' as ActionTypes, }, @@ -74,7 +74,7 @@ describe('Middleware should be called with an action', () => { it('should not allow middleware to mutate `action`', () => { try { const runMiddleware = middlewareWrapper( - { url: new URL('https://www.example.com') }, + { url: new URL('https://www.example.com'), headers: new Headers() }, { type: 'MUTATE-ACTION' as ActionTypes, }, diff --git a/packages/sdk-effects/sdk-request-middleware/src/lib/request-mware.types.ts b/packages/sdk-effects/sdk-request-middleware/src/lib/request-mware.types.ts index 17c785e75d..55973eecde 100644 --- a/packages/sdk-effects/sdk-request-middleware/src/lib/request-mware.types.ts +++ b/packages/sdk-effects/sdk-request-middleware/src/lib/request-mware.types.ts @@ -36,7 +36,7 @@ export interface Action { url: URL; - headers?: Headers; + headers: Headers; } export interface RequestObj { From 26b7dfae078f27e8e4ff34ebb212a1701fa8c2f6 Mon Sep 17 00:00:00 2001 From: Ryan Bas Date: Thu, 6 Nov 2025 12:44:23 -0700 Subject: [PATCH 2/4] chore: update-filename --- e2e/journey-suites/src/{otp-register.ts => otp-register.test.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename e2e/journey-suites/src/{otp-register.ts => otp-register.test.ts} (100%) diff --git a/e2e/journey-suites/src/otp-register.ts b/e2e/journey-suites/src/otp-register.test.ts similarity index 100% rename from e2e/journey-suites/src/otp-register.ts rename to e2e/journey-suites/src/otp-register.test.ts From c5dbebf9fe4acc6723dc755091615f8637f48258 Mon Sep 17 00:00:00 2001 From: Ryan Bas Date: Thu, 6 Nov 2025 12:46:10 -0700 Subject: [PATCH 3/4] chore: update-unittest --- .../sdk-request-middleware/src/lib/request-mware.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sdk-effects/sdk-request-middleware/src/lib/request-mware.test.ts b/packages/sdk-effects/sdk-request-middleware/src/lib/request-mware.test.ts index 885fc47123..acecbdc391 100644 --- a/packages/sdk-effects/sdk-request-middleware/src/lib/request-mware.test.ts +++ b/packages/sdk-effects/sdk-request-middleware/src/lib/request-mware.test.ts @@ -54,7 +54,7 @@ describe('Middleware should be called with an action', () => { }, ); const newReq = runMiddleware(middleware); - expect(newReq.headers).toBeUndefined(); + expect(newReq.headers).toStrictEqual(new Headers()); expect(newReq.url.toString()).toBe('https://www.example.com/'); }); it('should run all middleware testing add action with payload', () => { From b02ef84e8561271e8f8e4654354feb1b9fa9733a Mon Sep 17 00:00:00 2001 From: Ryan Bas Date: Thu, 6 Nov 2025 13:09:45 -0700 Subject: [PATCH 4/4] chore: update-e2e --- e2e/journey-suites/src/otp-register.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/e2e/journey-suites/src/otp-register.test.ts b/e2e/journey-suites/src/otp-register.test.ts index 19a5b43e64..fc21076e02 100644 --- a/e2e/journey-suites/src/otp-register.test.ts +++ b/e2e/journey-suites/src/otp-register.test.ts @@ -26,7 +26,7 @@ test('Test happy paths on test page', async ({ page }) => { await page.getByLabel('Password').fill(password); await clickButton('Submit', '/authenticate'); - await expect(page.getByText('Complete')).toBeVisible(); + await expect(() => expect(page.getByText('Scan the QR code')).toBeVisible()).toPass(); // Test assertions expect( @@ -34,7 +34,6 @@ test('Test happy paths on test page', async ({ page }) => { 'Scan the QR code image below with the ForgeRock Authenticator app to register your device with your login.', ), ).toBe(true); - expect(messageArray.includes('otp')).toBe(true); // TODO: Use when AM Mock API is available // expect(