Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@
"@forgerock/oidc-app",
"@forgerock/oidc-suites",
"@forgerock/local-release-tool",
"@forgerock/protect-app",
"@forgerock/protect-suites",
"@forgerock/journey-app",
"@forgerock/journey-suites",
"@forgerock/recognize-app",
Expand Down
1 change: 0 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,6 @@ e2e/
├── davinci-suites/ # Playwright e2e for DaVinci flows
├── journey-suites/ # Playwright e2e for Journey flows
├── oidc-suites/ # Playwright e2e for OIDC flows
├── protect-suites/
├── am-mock-api/ # Mock AM server for journey e2e
└── mock-api-v2/ # Mock API v2
```
Expand Down
2 changes: 1 addition & 1 deletion e2e/davinci-app/server-configs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export const serverConfigs: Record<string, DaVinciConfig> = {
},
},
/**
* AutoCollectors: Polling, Metadata, FIDO
* AutoCollectors: Polling, Metadata, FIDO, Protect
*/
'31a587ce-9aa4-4f36-a09f-78cd8a0a74a0': {
clientId: '31a587ce-9aa4-4f36-a09f-78cd8a0a74a0',
Expand Down
72 changes: 45 additions & 27 deletions e2e/davinci-suites/src/protect.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Ping Identity Corporation. All rights reserved.
* Copyright (c) 2025 - 2026 Ping Identity Corporation. All rights reserved.
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
Expand All @@ -8,76 +8,94 @@ import { expect, test } from '@playwright/test';
import { asyncEvents } from './utils/async-events.js';
import { username, password } from './utils/demo-user.js';

const clientId = '31a587ce-9aa4-4f36-a09f-78cd8a0a74a0';

test('Test Protect collector with Custom HTML component', async ({ page }) => {
const davinciFlow = 'ea02bcbfb2112e051c94ee9b08083d2d';
const davinciFlow = '244e9bbec113931ae61fd962f0a1fe6c';
const { navigate } = asyncEvents(page);
await navigate(`/?acr_values=${davinciFlow}`);
await navigate(`/?clientId=${clientId}&acr_values=${davinciFlow}`);

await expect(page.url()).toBe(`http://localhost:5829/?acr_values=${davinciFlow}`);
await expect(page.url()).toBe(
`http://localhost:5829/?clientId=${clientId}&acr_values=${davinciFlow}`,
);

await expect(page.getByText('JS Protect - Custom HTML Form')).toBeVisible();

const requests: string[] = [];
let riskData;
page.on('request', (request) => {
const method = request.method();
const requestUrl = request.url();
const payload = request.postDataJSON();
const data = payload.parameters.data.formData.riskSDK;

requests.push(requestUrl);

if (method === 'POST' && requestUrl.includes('customHTMLTemplate')) {
expect(data).toBeDefined();
expect(data).toMatch(/^R\/o\//);
// Only process POST requests with JSON payloads
if (method === 'POST' && payload && requestUrl.includes('customHTMLTemplate')) {
const data = payload.parameters?.data?.formData?.riskSDK;
if (data) {
riskData = data;
}
}
});

const protectPromise = page.waitForRequest(
(req) =>
req.method() === 'POST' &&
req.url().includes('customHTMLTemplate') &&
req.postDataJSON()?.parameters?.data?.formData?.riskSDK,
);
await page.getByLabel('Username').fill(username);
await page.getByLabel('Password').fill(password);

await page.getByRole('button', { name: 'Sign On' }).click();
await protectPromise;

await expect(
page.getByText(/Sorry Bot, we cannot let you in this time.|You were blocked by PingOne Risk/),
).toBeVisible();

const protectRequest = requests.some((url) => url.includes('customHTMLTemplate'));
await expect(protectRequest).toBeTruthy();
expect(riskData).toBeDefined();
expect(riskData).toMatch(/^R\/o\//);
});

test('Test Protect collector with P1 Forms component', async ({ page }) => {
const davinciFlow = '908858ce3a809b579f11f49c4283b7a6';
const davinciFlow = '99ccced66a6ad160b48d339c3d219d9c';
const { navigate } = asyncEvents(page);
await navigate(`/?acr_values=${davinciFlow}`);
await navigate(`/?clientId=${clientId}&acr_values=${davinciFlow}`);

await expect(page.url()).toBe(`http://localhost:5829/?acr_values=${davinciFlow}`);
await expect(page.url()).toBe(
`http://localhost:5829/?clientId=${clientId}&acr_values=${davinciFlow}`,
);

await expect(page.getByText('Example - Sign On')).toBeVisible();

const requests: string[] = [];
let riskData;
page.on('request', (request) => {
const method = request.method();
const requestUrl = request.url();
const payload = request.postDataJSON();
const data = payload.parameters.data.formData.deviceRisk;

requests.push(requestUrl);

if (method === 'POST' && requestUrl.includes('customForm')) {
expect(data).toBeDefined();
expect(data).toMatch(/^R\/o\//);
// Only process POST requests with JSON payloads
if (method === 'POST' && payload && requestUrl.includes('customForm')) {
const data = payload.parameters?.data?.formData?.deviceRisk;
if (data) {
riskData = data;
}
}
});

const protectPromise = page.waitForRequest(
(req) =>
req.method() === 'POST' &&
req.url().includes('customForm') &&
req.postDataJSON()?.parameters?.data?.formData?.deviceRisk,
);
await page.getByLabel('Username').fill(username);
await page.getByLabel('Password').fill(password);

await page.getByRole('button', { name: 'Sign On' }).click();
await protectPromise;

await expect(
page.getByText(/Sorry Bot, we cannot let you in this time.|You were blocked by PingOne Risk/),
).toBeVisible();

const protectRequest = requests.some((url) => url.includes('customForm'));
await expect(protectRequest).toBeTruthy();
expect(riskData).toBeDefined();
expect(riskData).toMatch(/^R\/o\//);
});
9 changes: 8 additions & 1 deletion e2e/journey-suites/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,14 @@
"author": "",
"type": "module",
"main": "src/index.js",
"dependencies": {
"@forgerock/journey-client": "workspace:*"
},
"nx": {
"implicitDependencies": ["@forgerock/journey-app", "@forgerock/mock-api-v2"]
"implicitDependencies": [
"@forgerock/journey-app",
"@forgerock/mock-api-v2"
],
"tags": ["scope:e2e"]
}
}
72 changes: 37 additions & 35 deletions e2e/journey-suites/src/protect.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2025 Ping Identity Corporation. All rights reserved.
* Copyright (c) 2025 - 2026 Ping Identity Corporation. All rights reserved.
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
Expand All @@ -8,49 +8,58 @@
import { expect, test } from '@playwright/test';
import { asyncEvents } from './utils/async-events.js';
import { username, password } from './utils/demo-user.js';
import type { Callback, NameValue } from '@forgerock/journey-client';

test('Test PingOne Protect journey flow', async ({ page }) => {
const { clickButton } = asyncEvents(page);
const messageArray: string[] = [];
let protectSignalsData: string | null = null;

page.on('console', async (msg) => {
messageArray.push(msg.text());
return Promise.resolve(true);
});

let riskData: string | null = null;

page.on('request', (request) => {
if (request.url().includes('/authenticate') && request.method() === 'POST') {
try {
const postData = request.postData();
if (postData) {
const body = JSON.parse(postData);
const callbacks = body.callbacks || [];
for (const callback of callbacks) {
if (callback.type === 'PingOneProtectEvaluationCallback') {
const inputs = callback.input || [];
for (const input of inputs) {
if (input.name === 'IDToken1signals' && input.value) {
protectSignalsData = input.value;
}
}
}
}
}
} catch {
// Ignore parsing errors
const method = request.method();
const requestUrl = request.url();
const payload = request.postDataJSON();

// Only process POST requests with JSON payloads
if (method === 'POST' && payload && requestUrl.includes('/authenticate')) {
Comment on lines 24 to +30

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

For the Playwright version declared in this repository, does Request.postDataJSON() throw when the request body is not valid JSON?

💡 Result:

Yes, the Request.postDataJSON method in Playwright throws an error if the request body is present but cannot be parsed as valid JSON [1]. Internally, the method first checks if the request has body data [1]. If it does, and the Content-Type is not application/x-www-form-urlencoded, it attempts to parse the data using JSON.parse [1]. If this parsing fails, it explicitly throws an Error with the message "POST data is not a valid JSON object: " followed by the content of the request body [1]. This behavior is confirmed by Playwright's internal source code [1] and its test suite, which includes test cases explicitly verifying that the method throws an exception when provided with invalid JSON [2].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- Playwright declarations ---'
rg -n --hidden -S '"@?playwright/test"|"playwright"|playwright' \
  package.json package-lock.json yarn.lock pnpm-lock.yaml \
  e2e 2>/dev/null | head -200 || true

printf '%s\n' '--- Target listener implementations ---'
for f in \
  e2e/journey-suites/src/protect.test.ts \
  e2e/davinci-suites/src/protect.test.ts \
  e2e/protect-suites/src/protect-native.test.ts
do
  echo "--- $f ---"
  sed -n '1,115p' "$f"
done

Repository: ForgeRock/ping-javascript-sdk

Length of output: 15908


🏁 Script executed:

#!/bin/bash
set -eu

python3 - <<'PY'
from urllib.request import urlopen

url = "https://raw.githubusercontent.com/microsoft/playwright/v1.59.1/packages/playwright-core/src/client/network.ts"
text = urlopen(url, timeout=10).read().decode()
needle = "postDataJSON()"
start = text.index(needle)
print(text[start:start + 900])
PY

printf '%s\n' '--- Exact lockfile resolution ---'
sed -n '11115,11130p' pnpm-lock.yaml
sed -n '16035,16045p' pnpm-lock.yaml

Repository: ForgeRock/ping-javascript-sdk

Length of output: 2453


Filter each request before parsing its body.

request.postDataJSON() can throw when an unrelated request has a non-empty, invalid JSON body. Check the method and target URL before calling it in all four listeners:

  • e2e/journey-suites/src/protect.test.ts
  • e2e/davinci-suites/src/protect.test.ts (customHTMLTemplate and customForm)
  • e2e/protect-suites/src/protect-native.test.ts
📍 Affects 3 files
  • e2e/journey-suites/src/protect.test.ts#L24-L30 (this comment)
  • e2e/davinci-suites/src/protect.test.ts#L26-L34
  • e2e/davinci-suites/src/protect.test.ts#L71-L79
  • e2e/protect-suites/src/protect-native.test.ts#L28-L34
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/journey-suites/src/protect.test.ts` around lines 24 - 30, Move
request.postDataJSON() in each request listener after checking that the method
is POST and the URL targets /authenticate (or the listener’s existing target),
so unrelated requests are filtered before body parsing. Apply this in
e2e/journey-suites/src/protect.test.ts:24-30, both customHTMLTemplate and
customForm listeners in e2e/davinci-suites/src/protect.test.ts:26-34 and 71-79,
and e2e/protect-suites/src/protect-native.test.ts:28-34; preserve the existing
payload validation and handling after the filter.

const callback: Callback = payload.callbacks?.find(
(callback: Callback) => callback.type === 'PingOneProtectEvaluationCallback',
);

if (callback) {
const data = callback.input?.find((input: NameValue) => input.name === 'IDToken1signals')
?.value as string | undefined;
riskData = data ?? null;
}
}
});

await page.goto('/?journey=TEST_LoginPingProtect&clientId=basic', { waitUntil: 'load' });
await page.goto('/?journey=TEST_LoginPingProtect&clientId=basic');

await expect(page.getByText('Initializing PingOne Protect...')).toBeVisible({ timeout: 10000 });
await expect(page.getByText('PingOne Protect initialized successfully!')).toBeVisible({
timeout: 15000,
});

await expect(page.getByLabel('User Name')).toBeVisible({ timeout: 15000 });
const protectPromise = page.waitForRequest((req) => {
return (
req.method() === 'POST' &&
req.url().includes('/authenticate') &&
req
.postDataJSON()
?.callbacks?.some(
(callback: Callback) => callback.type === 'PingOneProtectEvaluationCallback',
)
);
});

await expect(page.getByLabel('User Name')).toBeVisible();
await page.getByLabel('User Name').fill(username);
await page.getByLabel('Password').fill(password);
await clickButton('Submit', '/authenticate');
Expand All @@ -60,24 +69,17 @@ test('Test PingOne Protect journey flow', async ({ page }) => {
timeout: 15000,
});

// Wait for the evaluation callback to auto-submit and complete
await page.waitForResponse((response) => response.url().includes('/authenticate'));

await expect(page.getByText('Complete')).toBeVisible({ timeout: 15000 });
// Wait for risk data to be evaluated
await protectPromise;

// Verify signals were captured from the request
expect(protectSignalsData).not.toBeNull();
expect(typeof protectSignalsData).toBe('string');
expect(protectSignalsData?.length).toBeGreaterThan(0);

await clickButton('Logout', '/sessions');

await expect(page.getByText('Initializing PingOne Protect...')).toBeVisible({ timeout: 10000 });
expect(riskData).not.toBeNull();
expect(typeof riskData).toBe('string');
expect(riskData).toMatch(/^R\/o\//);

// Verify the protect SDK flow through console logs
expect(messageArray.some((msg) => msg.includes('Protect initialized successfully'))).toBe(true);
expect(messageArray.some((msg) => msg.includes('Protect data collected successfully'))).toBe(
true,
);
expect(messageArray.some((msg) => msg.includes('Logout successful'))).toBe(true);
});
3 changes: 3 additions & 0 deletions e2e/journey-suites/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
"files": [],
"include": [],
"references": [
{
"path": "../../packages/journey-client"
},
{
"path": "./tsconfig.e2e.json"
}
Expand Down
24 changes: 0 additions & 24 deletions e2e/protect-app/.gitignore

This file was deleted.

37 changes: 0 additions & 37 deletions e2e/protect-app/eslint.config.mjs

This file was deleted.

20 changes: 0 additions & 20 deletions e2e/protect-app/package.json

This file was deleted.

9 changes: 0 additions & 9 deletions e2e/protect-app/public/callback.html

This file was deleted.

1 change: 0 additions & 1 deletion e2e/protect-app/public/typescript.svg

This file was deleted.

1 change: 0 additions & 1 deletion e2e/protect-app/public/vite.svg

This file was deleted.

Loading
Loading