Skip to content

Commit e932be9

Browse files
committed
refactor(journey): updated journey client logic so now journey store can handle generic error case as completely generic, login failure is handled by its own branch
1 parent 05fc510 commit e932be9

5 files changed

Lines changed: 152 additions & 11 deletions

File tree

core/journey-client.config.test.ts

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/**
2+
*
3+
* Copyright © 2026 Ping Identity Corporation. All right reserved.
4+
*
5+
* This software may be modified and distributed under the terms
6+
* of the MIT license. See the LICENSE file for details.
7+
*
8+
**/
9+
10+
import { beforeEach, describe, expect, it, vi } from 'vitest';
11+
12+
import type { JourneyClient, JourneyClientConfig } from '@forgerock/journey-client/types';
13+
14+
const journeyMock = vi.fn();
15+
16+
vi.mock('@forgerock/journey-client', () => {
17+
return {
18+
journey: journeyMock,
19+
};
20+
});
21+
22+
async function importSubject() {
23+
const mod = await import('./journey-client.config');
24+
return mod;
25+
}
26+
27+
describe('journey-client.config', () => {
28+
beforeEach(() => {
29+
journeyMock.mockReset();
30+
vi.resetModules();
31+
});
32+
33+
it('throws when getJourneyClient() is called before configuration', async () => {
34+
const { getJourneyClient } = await importSubject();
35+
await expect(getJourneyClient()).rejects.toThrow(
36+
'Journey Client is not configured. Call setJourneyClientConfig() first.',
37+
);
38+
});
39+
40+
it('validates journeyClient config (wellknown must be a URL)', async () => {
41+
const { setJourneyClientConfig } = await importSubject();
42+
43+
expect(() =>
44+
setJourneyClientConfig({
45+
serverConfig: {
46+
wellknown: 'not-a-url',
47+
},
48+
} as unknown as JourneyClientConfig),
49+
).toThrow(/wellknown/i);
50+
51+
expect(() =>
52+
setJourneyClientConfig({
53+
serverConfig: {},
54+
} as unknown as JourneyClientConfig),
55+
).toThrow(/wellknown/i);
56+
57+
const config = setJourneyClientConfig({
58+
serverConfig: {
59+
wellknown: 'https://example.com/.well-known/openid-configuration',
60+
},
61+
});
62+
63+
expect(config.serverConfig.wellknown).toBe(
64+
'https://example.com/.well-known/openid-configuration',
65+
);
66+
});
67+
68+
it('caches the journey promise so concurrent calls only initialize once', async () => {
69+
const client = {} as JourneyClient;
70+
71+
let resolveClient: (value: JourneyClient) => void;
72+
const deferred = new Promise<JourneyClient>((resolve) => {
73+
resolveClient = resolve;
74+
});
75+
76+
journeyMock.mockReturnValueOnce(deferred);
77+
78+
const { getJourneyClient, setJourneyClientConfig } = await importSubject();
79+
setJourneyClientConfig({
80+
serverConfig: {
81+
wellknown: 'https://example.com/.well-known/openid-configuration',
82+
},
83+
});
84+
85+
const aPromise = getJourneyClient();
86+
const bPromise = getJourneyClient();
87+
88+
expect(journeyMock).toHaveBeenCalledTimes(1);
89+
90+
// Fulfill the mocked creation and assert both callers share the same client.
91+
resolveClient!(client);
92+
93+
const [a, b] = await Promise.all([aPromise, bPromise]);
94+
expect(a).toBe(client);
95+
expect(b).toBe(client);
96+
});
97+
98+
it('clears the cached promise on initialization failure so it can retry', async () => {
99+
const error = new Error('boom');
100+
const client = {} as JourneyClient;
101+
102+
journeyMock.mockRejectedValueOnce(error).mockResolvedValueOnce(client);
103+
104+
const { getJourneyClient, setJourneyClientConfig } = await importSubject();
105+
setJourneyClientConfig({
106+
serverConfig: {
107+
wellknown: 'https://example.com/.well-known/openid-configuration',
108+
},
109+
});
110+
111+
await expect(getJourneyClient()).rejects.toThrow('boom');
112+
await expect(getJourneyClient()).resolves.toBe(client);
113+
114+
expect(journeyMock).toHaveBeenCalledTimes(2);
115+
});
116+
117+
it('resets the cached promise when configuration changes', async () => {
118+
const client1 = { client: 1 } as unknown as JourneyClient;
119+
const client2 = { client: 2 } as unknown as JourneyClient;
120+
121+
journeyMock.mockResolvedValueOnce(client1).mockResolvedValueOnce(client2);
122+
123+
const { getJourneyClient, setJourneyClientConfig } = await importSubject();
124+
125+
setJourneyClientConfig({
126+
serverConfig: {
127+
wellknown: 'https://example.com/.well-known/openid-configuration',
128+
},
129+
});
130+
131+
await expect(getJourneyClient()).resolves.toBe(client1);
132+
133+
setJourneyClientConfig({
134+
serverConfig: {
135+
wellknown: 'https://example.com/other/.well-known/openid-configuration',
136+
},
137+
});
138+
139+
await expect(getJourneyClient()).resolves.toBe(client2);
140+
expect(journeyMock).toHaveBeenCalledTimes(2);
141+
});
142+
});

core/journey/journey.store.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -332,9 +332,13 @@ export function initialize(): JourneyStore {
332332
// Handle GenericError case
333333
const genericError = result;
334334
const errorMessage =
335-
// Journey Client can surface non-2xx responses as a missing `data` payload.
336-
// When that happens on step submission (i.e., we have a `prevStep` context),
337-
// show the standard login failure message rather than a technical error.
335+
/**
336+
* TODO: Journey Client currently does not handle JourneyLoginFailure case
337+
* It returns a GenericError type when it should be returning JourneyLoginFailure type
338+
* The hack below temporarily passes failing tests for Login journey
339+
* Remove this check when https://github.com/ForgeRock/ping-javascript-sdk/pull/574
340+
* PR has been merged and journey client is published to npm
341+
*/
338342
genericError.error === 'no_response_data' && context?.prevStep
339343
? interpolate('loginFailure')
340344
: genericError.message ?? genericError.error ?? interpolate('unknownNetworkError');

core/journey/stages/email-suspend.svelte

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,7 @@
1616
1717
// Import primitives
1818
import Alert from '$components/primitives/alert/alert.svelte';
19-
import {
20-
convertStringToKey,
21-
shouldRedirectFromStep,
22-
} from '$journey/stages/_utilities/step.utilities';
19+
import { convertStringToKey, shouldRedirectFromStep } from '$journey/stages/_utilities/step.utilities';
2320
import Form from '$components/primitives/form/form.svelte';
2421
import Sanitize from '$components/_utilities/server-strings.svelte';
2522
import EmailIcon from '$components/icons/email-icon.svelte';

core/journey/stages/generic.svelte

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,7 @@
1818
// Import primitives
1919
import Alert from '$components/primitives/alert/alert.svelte';
2020
import Button from '$components/primitives/button/button.svelte';
21-
import {
22-
convertStringToKey,
23-
shouldRedirectFromStep,
24-
} from '$journey/stages/_utilities/step.utilities';
21+
import { convertStringToKey, shouldRedirectFromStep } from '$journey/stages/_utilities/step.utilities';
2522
import Form from '$components/primitives/form/form.svelte';
2623
import Sanitize from '$components/_utilities/server-strings.svelte';
2724
import ShieldIcon from '$components/icons/shield-icon.svelte';

experimental/custom/demo/callbacks/custom-name/custom-name.story.svelte

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
isReadyForSubmission: false,
4141
isSelfSubmitting: false,
4242
isUserInputRequired: true,
43+
isPasskeyAutofillEligible: false,
4344
},
4445
idx: 0,
4546
};

0 commit comments

Comments
 (0)