Skip to content

Commit f8dba5d

Browse files
committed
chore: initial-jc-tests
1 parent 51cefda commit f8dba5d

8 files changed

Lines changed: 265 additions & 23 deletions

File tree

e2e/am-mock-api/src/app/routes.auth.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ import {
5050
newPiWellKnown,
5151
} from './responses.js';
5252
import initialRegResponse from './response.registration.js';
53+
import {
54+
webAuthnRegistrationInit,
55+
getRecoveryCodesDisplay,
56+
authSuccess as webAuthnSuccess,
57+
} from './response.webauthn.js';
5358
import wait from './wait.js';
5459

5560
console.log(`Your user password from 'env.config' file: ${USERS[0].pw}`);
@@ -93,6 +98,8 @@ export default function (app) {
9398
res.json(MetadataMarketPlaceInitialize);
9499
} else if (req.query.authIndexValue === 'AMSocialLogin') {
95100
res.json(idpChoiceCallback);
101+
} else if (req.query.authIndexValue === 'TEST_WebAuthnWithRecoveryCodes') {
102+
res.json(webAuthnRegistrationInit);
96103
} else if (req.query.authIndexValue === 'RecaptchaEnterprise') {
97104
res.json(initialBasicLogin);
98105
} else {
@@ -414,6 +421,28 @@ export default function (app) {
414421
// an additional auth callback would be sent, like OTP
415422
res.json(authFail);
416423
}
424+
} else if (
425+
req.query.authIndexValue === 'TEST_WebAuthnWithRecoveryCodes' ||
426+
req.body.authId?.startsWith('webauthn-registration') ||
427+
req.body.authId?.startsWith('recovery-codes')
428+
) {
429+
// Handle WebAuthn registration with recovery codes journey
430+
// Identify by authIndexValue (initial) or authId (subsequent)
431+
const metadataCb = req.body.callbacks.find((cb) => cb.type === 'MetadataCallback');
432+
const hiddenCb = req.body.callbacks.find((cb) => cb.type === 'HiddenValueCallback');
433+
const confirmationCb = req.body.callbacks.find((cb) => cb.type === 'ConfirmationCallback');
434+
435+
if (metadataCb && hiddenCb && hiddenCb.input[0].value) {
436+
// WebAuthn credential has been submitted, show recovery codes
437+
res.json(getRecoveryCodesDisplay());
438+
} else if (confirmationCb) {
439+
// Recovery codes have been acknowledged, complete authentication
440+
res.cookie('iPlanetDirectoryPro', 'mock-webauthn-session-' + Date.now(), { domain: 'localhost' });
441+
res.json(webAuthnSuccess);
442+
} else {
443+
// Invalid state
444+
res.status(401).json(authFail);
445+
}
417446
}
418447
});
419448

e2e/journey-app/components/ping-protect-evaluation.ts

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@
55
* of the MIT license. See the LICENSE file for details.
66
*/
77
import type { PingOneProtectEvaluationCallback } from '@forgerock/journey-client/types';
8+
import { getProtectInstance } from './ping-protect-initialize.js';
89

10+
/**
11+
* PingOne Protect Evaluation Component
12+
* Automatically collects device and behavioral signals using the Protect SDK
13+
*/
914
export default function pingProtectEvaluationComponent(
1015
journeyEl: HTMLDivElement,
1116
callback: PingOneProtectEvaluationCallback,
@@ -19,5 +24,49 @@ export default function pingProtectEvaluationComponent(
1924

2025
journeyEl?.appendChild(message);
2126

22-
// TODO: Implement PingOne Protect module evaluation here
27+
// Automatically trigger Protect data collection
28+
setTimeout(async () => {
29+
try {
30+
// Get the protect instance created during initialization
31+
const protectInstance = getProtectInstance();
32+
33+
if (!protectInstance) {
34+
throw new Error('Protect instance not initialized. Initialize callback must be called first.');
35+
}
36+
37+
console.log('Collecting Protect signals...');
38+
39+
// Collect device and behavioral data
40+
const result = await protectInstance.getData();
41+
42+
// Check if result is an error object
43+
if (typeof result !== 'string' && 'error' in result) {
44+
console.error('Error collecting Protect data:', result.error);
45+
callback.setClientError(result.error);
46+
message.innerText = `Data collection failed: ${result.error}`;
47+
message.style.color = 'red';
48+
return;
49+
}
50+
51+
// Set the collected data on the callback
52+
console.log('Protect data collected successfully');
53+
callback.setData(result);
54+
message.innerText = 'Risk assessment completed successfully!';
55+
message.style.color = 'green';
56+
57+
// Auto-submit the form after successful data collection
58+
setTimeout(() => {
59+
const submitButton = document.getElementById('submitButton') as HTMLButtonElement;
60+
if (submitButton) {
61+
submitButton.click();
62+
}
63+
}, 500);
64+
} catch (error) {
65+
console.error('Protect evaluation failed:', error);
66+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
67+
callback.setClientError(errorMessage);
68+
message.innerText = `Evaluation failed: ${errorMessage}`;
69+
message.style.color = 'red';
70+
}
71+
}, 100);
2372
}

e2e/journey-app/components/ping-protect-initialize.ts

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,24 @@
44
* This software may be modified and distributed under the terms
55
* of the MIT license. See the LICENSE file for details.
66
*/
7+
import { protect } from '@forgerock/protect';
78
import type { PingOneProtectInitializeCallback } from '@forgerock/journey-client/types';
89

10+
// Global storage for protect instance to be used by evaluation component
11+
let protectInstance: ReturnType<typeof protect> | null = null;
12+
13+
/**
14+
* Gets the stored protect instance
15+
* @returns The protect instance or null if not initialized
16+
*/
17+
export function getProtectInstance() {
18+
return protectInstance;
19+
}
20+
21+
/**
22+
* PingOne Protect Initialize Component
23+
* Automatically initializes the Protect SDK using configuration from the callback
24+
*/
925
export default function pingProtectInitializeComponent(
1026
journeyEl: HTMLDivElement,
1127
callback: PingOneProtectInitializeCallback,
@@ -19,5 +35,58 @@ export default function pingProtectInitializeComponent(
1935

2036
journeyEl?.appendChild(message);
2137

22-
// TODO: Implement PingOne Protect module initialization here
38+
// Automatically trigger Protect initialization
39+
setTimeout(async () => {
40+
try {
41+
// Get configuration from callback
42+
const config = callback.getConfig();
43+
console.log('Protect callback config:', config);
44+
45+
if (!config?.envId) {
46+
const error = 'Missing envId in Protect configuration';
47+
console.error(error);
48+
callback.setClientError(error);
49+
message.innerText = `Initialization failed: ${error}`;
50+
message.style.color = 'red';
51+
return;
52+
}
53+
54+
console.log('Initializing Protect with envId:', config.envId);
55+
56+
// Create and store protect instance
57+
protectInstance = protect({ envId: config.envId });
58+
console.log('Protect instance created');
59+
60+
// Initialize the Protect SDK
61+
console.log('Calling protect.start()...');
62+
const result = await protectInstance.start();
63+
console.log('protect.start() result:', result);
64+
65+
if (result?.error) {
66+
console.error('Error initializing Protect:', result.error);
67+
callback.setClientError(result.error);
68+
message.innerText = `Initialization failed: ${result.error}`;
69+
message.style.color = 'red';
70+
return;
71+
}
72+
73+
console.log('Protect initialized successfully - no errors');
74+
message.innerText = 'PingOne Protect initialized successfully!';
75+
message.style.color = 'green';
76+
77+
// Auto-submit the form after successful initialization
78+
setTimeout(() => {
79+
const submitButton = document.getElementById('submitButton') as HTMLButtonElement;
80+
if (submitButton) {
81+
submitButton.click();
82+
}
83+
}, 500);
84+
} catch (error) {
85+
console.error('Protect initialization failed:', error);
86+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
87+
callback.setClientError(errorMessage);
88+
message.innerText = `Initialization failed: ${errorMessage}`;
89+
message.style.color = 'red';
90+
}
91+
}, 100);
2392
}

e2e/journey-app/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"dependencies": {
1717
"@forgerock/journey-client": "workspace:*",
1818
"@forgerock/oidc-client": "workspace:*",
19+
"@forgerock/protect": "workspace:*",
1920
"@forgerock/sdk-logger": "workspace:*"
2021
}
2122
}

e2e/journey-app/tsconfig.app.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
{
2020
"path": "../../packages/oidc-client/tsconfig.lib.json"
2121
},
22+
{
23+
"path": "../../packages/protect/tsconfig.lib.json"
24+
},
2225
{
2326
"path": "../../packages/journey-client/tsconfig.lib.json"
2427
}

e2e/journey-app/tsconfig.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
{
2121
"path": "../../packages/oidc-client"
2222
},
23+
{
24+
"path": "../../packages/protect"
25+
},
2326
{
2427
"path": "../../packages/journey-client"
2528
},

e2e/journey-suites/src/protect.test.ts

Lines changed: 68 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,32 +9,79 @@ import { expect, test } from '@playwright/test';
99
import { asyncEvents } from './utils/async-events.js';
1010
import { username, password } from './utils/demo-user.js';
1111

12-
test.skip('Test happy paths on test page', async ({ page }) => {
13-
const { clickButton, navigate } = asyncEvents(page);
14-
await navigate('/?journey=TEST_Protect');
12+
test.describe('PingOne Protect Journey', () => {
13+
test('should complete journey with Protect initialization and evaluation', async ({ page }) => {
14+
const { clickButton } = asyncEvents(page);
1515

16-
const messageArray: string[] = [];
16+
const messageArray: string[] = [];
1717

18-
// Listen for events on page
19-
page.on('console', async (msg) => {
20-
messageArray.push(msg.text());
21-
return Promise.resolve(true);
22-
});
18+
// Listen for console messages
19+
page.on('console', async (msg) => {
20+
messageArray.push(msg.text());
21+
return Promise.resolve(true);
22+
});
23+
24+
// Navigate to PingOne Protect journey
25+
// Use 'load' instead of 'networkidle' because Protect SDK makes continuous requests
26+
await page.goto('/?journey=TEST_LoginPingProtect&clientId=basic', { waitUntil: 'load' });
27+
28+
// Step 1: Wait for Protect initialization to display and complete
29+
await expect(page.getByText('Initializing PingOne Protect...')).toBeVisible({ timeout: 10000 });
30+
31+
// Wait for initialization success message
32+
await expect(page.getByText('PingOne Protect initialized successfully!')).toBeVisible({
33+
timeout: 15000,
34+
});
35+
36+
// Submit the form to proceed to next step
37+
await page.getByRole('button', { name: 'Submit' }).click();
38+
39+
// Wait for the journey to progress
40+
await page.waitForTimeout(2000);
41+
42+
// Debug: Print console messages
43+
console.log('Console messages so far:', messageArray);
44+
45+
// Step 2: Perform login with username and password
46+
await expect(page.getByLabel('User Name')).toBeVisible({ timeout: 15000 });
47+
await page.getByLabel('User Name').fill(username);
48+
await page.getByLabel('Password').fill(password);
2349

24-
// Perform basic login
25-
await page.getByLabel('User Name').fill(username);
26-
await page.getByLabel('Password').fill(password);
27-
await clickButton('Submit', '/authenticate');
50+
// Wait a bit to ensure input events have been processed
51+
await page.waitForTimeout(500);
2852

29-
await expect(page.getByText('Collecting protect data')).toBeVisible();
53+
await clickButton('Submit', '/authenticate');
3054

31-
await expect(page.getByText('Complete')).toBeVisible();
55+
// Step 3: Wait for Protect evaluation to display and complete
56+
await expect(page.getByText('Evaluating risk assessment...')).toBeVisible({ timeout: 10000 });
3257

33-
// Perform logout
34-
await clickButton('Logout', '/authenticate');
58+
// Wait for evaluation success message
59+
await expect(page.getByText('Risk assessment completed successfully!')).toBeVisible({
60+
timeout: 15000,
61+
});
3562

36-
// Test assertions
37-
expect(messageArray.includes('Protect data collected successfully')).toBe(true);
38-
expect(messageArray.includes('Journey completed successfully')).toBe(true);
39-
expect(messageArray.includes('Logout successful')).toBe(true);
63+
// Submit the form to complete the journey
64+
await page.getByRole('button', { name: 'Submit' }).click();
65+
66+
// Wait for the journey to complete
67+
await page.waitForTimeout(2000);
68+
69+
// Step 4: Verify journey completion
70+
await expect(page.getByText('Complete')).toBeVisible({ timeout: 10000 });
71+
72+
// Verify session token is present
73+
const sessionToken = await page.locator('#sessionToken').textContent();
74+
expect(sessionToken).toBeTruthy();
75+
76+
// Step 5: Perform logout
77+
await clickButton('Logout', '/authenticate');
78+
79+
// Verify we're back at the beginning
80+
await expect(page.getByText('Initializing PingOne Protect...')).toBeVisible({ timeout: 10000 });
81+
82+
// Test console log assertions
83+
expect(messageArray.some((msg) => msg.includes('Protect initialized successfully'))).toBe(true);
84+
expect(messageArray.some((msg) => msg.includes('Protect data collected successfully'))).toBe(true);
85+
expect(messageArray.some((msg) => msg.includes('Logout successful'))).toBe(true);
86+
});
4087
});

pnpm-lock.yaml

Lines changed: 41 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)