Skip to content
Closed
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
7 changes: 7 additions & 0 deletions .changeset/tiny-signals-import.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@forgerock/protect': major
---

Replace the bundled Signals SDK with `@ping-identity/pingone-signals-web-sdk`.

The package no longer declares `Window._pingOneSignals`; consumers that reference this global directly must use the type declaration supplied by `@ping-identity/pingone-signals-web-sdk`.
6 changes: 4 additions & 2 deletions packages/protect/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"description": "Behavioral risk signals collector for the Ping JavaScript SDK",
"license": "MIT",
"author": "ForgeRock",
"sideEffects": ["dist/src/lib/signals-sdk.js"],
"sideEffects": false,
"type": "module",
"exports": {
".": {
Expand All @@ -28,7 +28,9 @@
"test": "pnpm nx nxTest",
"test:watch": "pnpm nx nxTest --watch"
},
"dependencies": {},
"dependencies": {
"@ping-identity/pingone-signals-web-sdk": "^5.6.11"
},
"publishConfig": {
"access": "public"
},
Expand Down
112 changes: 58 additions & 54 deletions packages/protect/src/lib/protect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,38 +20,28 @@ const config: ProtectConfig = {
universalDeviceIdentification: false,
};

describe('protect (with successfully loaded signals sdk)', () => {
beforeAll(() => {
vi.doMock('./signals-sdk.js', () => {
return {
default: {
init: vi.fn(),
getData: vi.fn(),
pauseBehavioralData: vi.fn(),
resumeBehavioralData: vi.fn(),
},
};
});
// Mock the SDK module
const mockSdk = {
init: vi.fn().mockResolvedValue(undefined),
getData: vi.fn().mockResolvedValue('mocked-data'),
pauseBehavioralData: vi.fn(),
resumeBehavioralData: vi.fn(),
};

if (typeof window === 'undefined') {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
global.window = {} as any;
}

window._pingOneSignals = {
init: vi.fn().mockResolvedValue(undefined),
getData: vi.fn().mockResolvedValue('mocked-data'),
pauseBehavioralData: vi.fn(),
resumeBehavioralData: vi.fn(),
};
});
vi.mock('@ping-identity/pingone-signals-web-sdk', () => ({
default: mockSdk,
}));

afterEach(() => {
describe('protect (with successfully loaded signals sdk)', () => {
beforeEach(() => {
// Reset mocks before each test
vi.clearAllMocks();
});

afterAll(() => {
vi.doUnmock('./signals-sdk.js');
mockSdk.init.mockResolvedValue(undefined);
mockSdk.getData.mockResolvedValue('mocked-data');
// eslint-disable-next-line @typescript-eslint/no-empty-function
mockSdk.pauseBehavioralData.mockImplementation(() => {});
// eslint-disable-next-line @typescript-eslint/no-empty-function
mockSdk.resumeBehavioralData.mockImplementation(() => {});
});

it('should be defined', () => {
Expand All @@ -77,45 +67,48 @@ describe('protect (with successfully loaded signals sdk)', () => {
};
const protectApi = protect(passthroughConfig);
await protectApi.start();
expect(window._pingOneSignals.init).toHaveBeenCalledWith(passthroughConfig);
expect(mockSdk.init).toHaveBeenCalledWith(passthroughConfig);
});

it('should resume behavioralData when behavioralDataCollection is string "true"', async () => {
const protectApi = protect({ envId: '12345', behavioralDataCollection: 'true' });
await protectApi.start();
expect(window._pingOneSignals.resumeBehavioralData).toHaveBeenCalled();
expect(mockSdk.resumeBehavioralData).toHaveBeenCalled();
});

it('should not resume behavioralData when behavioralDataCollection is string "false"', async () => {
const protectApi = protect({ envId: '12345', behavioralDataCollection: 'false' });
await protectApi.start();
expect(window._pingOneSignals.resumeBehavioralData).not.toHaveBeenCalled();
expect(mockSdk.resumeBehavioralData).not.toHaveBeenCalled();
});

it('should call start', async () => {
const protectApi = protect(config);
const protectMock = vi.spyOn(protectApi, 'start');
await protectApi.start();
expect(protectMock).toHaveBeenCalled();
expect(window._pingOneSignals.init).toHaveBeenCalledWith(config);
expect(mockSdk.init).toHaveBeenCalledWith(config);
});

it('should call getData', async () => {
const protectApi = protect(config);
await protectApi.start();
const protectMock = vi.spyOn(protectApi, 'getData');
await protectApi.getData();
expect(protectMock).toHaveBeenCalled();
});

it('should call pauseBehavioralData', () => {
it('should call pauseBehavioralData', async () => {
const protectApi = protect(config);
await protectApi.start();
const protectMock = vi.spyOn(protectApi, 'pauseBehavioralData');
protectApi.pauseBehavioralData();
expect(protectMock).toHaveBeenCalled();
});

it('should call resumeBehavioralData', () => {
it('should call resumeBehavioralData', async () => {
const protectApi = protect(config);
await protectApi.start();
const protectMock = vi.spyOn(protectApi, 'resumeBehavioralData');
protectApi.resumeBehavioralData();
expect(protectMock).toHaveBeenCalled();
Expand All @@ -141,41 +134,52 @@ describe('protect (with successfully loaded signals sdk)', () => {
});
});

describe('protect (with failed signals sdk load)', () => {
beforeAll(() => {
vi.doMock('./signals-sdk.js', () => {
throw new Error('Failed to load PingOne Signals SDK');
});
});

afterAll(() => {
vi.doUnmock('./signals-sdk.js');
describe('protect error handling', () => {
beforeEach(() => {
vi.clearAllMocks();
mockSdk.init.mockResolvedValue(undefined);
mockSdk.getData.mockResolvedValue('mocked-data');
// eslint-disable-next-line @typescript-eslint/no-empty-function
mockSdk.pauseBehavioralData.mockImplementation(() => {});
// eslint-disable-next-line @typescript-eslint/no-empty-function
mockSdk.resumeBehavioralData.mockImplementation(() => {});
});

it('start method should error', async () => {
it('start should return error when init fails', async () => {
mockSdk.init.mockRejectedValue(new Error('Init failed'));
const protectApi = protect(config);
const error = await protectApi.start();
await expect(error).toEqual({ error: 'Failed to load PingOne Signals SDK' });
expect(error).toEqual({ error: 'Failed to initialize PingOne Signals SDK' });
});

it('getData method should error', async () => {
it('getData should return error when SDK throws', async () => {
mockSdk.init.mockResolvedValue(undefined);
mockSdk.getData.mockRejectedValue(new Error('getData failed'));
const protectApi = protect(config);
await protectApi.start();
const error = await protectApi.getData();
await expect(error).toEqual({ error: 'PingOne Signals SDK is not initialized' });
expect(error).toEqual({ error: 'Failed to get data from Protect' });
});

it('pauseBehavioralData method should error', async () => {
it('pauseBehavioralData should return error when SDK throws', async () => {
mockSdk.init.mockResolvedValue(undefined);
mockSdk.pauseBehavioralData.mockImplementation(() => {
throw new Error('pause failed');
});
const protectApi = protect(config);
await protectApi.start();
const error = await protectApi.pauseBehavioralData();
await expect(error).toEqual({ error: 'PingOne Signals SDK is not initialized' });
const error = protectApi.pauseBehavioralData();
expect(error).toEqual({ error: 'Failed to pause behavioral data in Protect' });
});

it('resumeBehavioralData method should error', async () => {
it('resumeBehavioralData should return error when SDK throws', async () => {
mockSdk.init.mockResolvedValue(undefined);
mockSdk.resumeBehavioralData.mockImplementation(() => {
throw new Error('resume failed');
});
const protectApi = protect(config);
await protectApi.start();
const error = await protectApi.resumeBehavioralData();
await expect(error).toEqual({ error: 'PingOne Signals SDK is not initialized' });
const error = protectApi.resumeBehavioralData();
expect(error).toEqual({ error: 'Failed to resume behavioral data in Protect' });
});
});
40 changes: 16 additions & 24 deletions packages/protect/src/lib/protect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,9 @@
*
*/

import type { Protect, ProtectConfig, SignalsInitializationOptions } from './protect.types.js';
import type { PingOneSignals } from '@ping-identity/pingone-signals-web-sdk';

// Add Signals SDK namespace to the window object
declare global {
interface Window {
_pingOneSignals: {
init: (initParams?: ProtectConfig | SignalsInitializationOptions) => Promise<void>;
getData: () => Promise<string>;
pauseBehavioralData: () => void;
resumeBehavioralData: () => void;
};
}
}
import type { Protect, ProtectConfig, SignalsInitializationOptions } from './protect.types.js';

/**
* @async
Expand All @@ -29,67 +19,69 @@ declare global {
*/
export function protect(options: ProtectConfig | SignalsInitializationOptions): Protect {
let protectApiInitialized = false;
let sdk: PingOneSignals | null = null;

return {
start: async (): Promise<void | { error: string }> => {
try {
/*
* Load the Ping Signals SDK
* this automatically pollutes the window
* there are no exports of this module
* Load the PingOne Signals SDK
* The SDK attaches itself to window._pingOneSignals and also exports a default
*/
await import('./signals-sdk.js' as string);
const signalsModule = await import('@ping-identity/pingone-signals-web-sdk');
sdk = signalsModule.default as PingOneSignals;
protectApiInitialized = true;
} catch (err) {
console.error('error loading ping signals', err);
return { error: 'Failed to load PingOne Signals SDK' };
}

try {
await window._pingOneSignals.init(options);
await sdk?.init(options);

if (
options.behavioralDataCollection === true ||
options.behavioralDataCollection === 'true'
) {
window._pingOneSignals.resumeBehavioralData();
sdk?.resumeBehavioralData();
}
} catch (err) {
console.error('error initializing ping protect', err);
return { error: 'Failed to initialize PingOne Signals SDK' };
}
},
getData: async (): Promise<string | { error: string }> => {
if (!protectApiInitialized) {
if (!protectApiInitialized || !sdk) {
return { error: 'PingOne Signals SDK is not initialized' };
}

try {
return await window._pingOneSignals.getData();
// SDK returns string despite typed as SignalsData
return (await sdk.getData()) as unknown as string;
} catch (err) {
console.error('error getting data from ping protect', err);
return { error: 'Failed to get data from Protect' };
}
},
pauseBehavioralData: (): void | { error: string } => {
if (!protectApiInitialized) {
if (!protectApiInitialized || !sdk) {
return { error: 'PingOne Signals SDK is not initialized' };
}

try {
window._pingOneSignals.pauseBehavioralData();
sdk.pauseBehavioralData();
} catch (err) {
console.error('error pausing behavioral data in ping protect', err);
return { error: 'Failed to pause behavioral data in Protect' };
}
},
resumeBehavioralData: (): void | { error: string } => {
if (!protectApiInitialized) {
if (!protectApiInitialized || !sdk) {
return { error: 'PingOne Signals SDK is not initialized' };
}

try {
window._pingOneSignals.resumeBehavioralData();
sdk.resumeBehavioralData();
} catch (err) {
console.error('error resuming behavioral data in ping protect', err);
return { error: 'Failed to resume behavioral data in Protect' };
Expand Down
Loading
Loading