Skip to content

Commit 7cfc7c2

Browse files
committed
fix(journey-client): handle errors in journey utils with RTK narrowing
1 parent 13326e0 commit 7cfc7c2

10 files changed

Lines changed: 214 additions & 91 deletions

File tree

packages/davinci-client/api-report/davinci-client.api.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -267,13 +267,11 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
267267
resume: (input: {
268268
continueToken: string;
269269
}) => Promise<InternalErrorResponse | NodeStates>;
270-
start: <QueryParams extends OutgoingQueryParams = OutgoingQueryParams>(options?: StartOptions<QueryParams> | undefined) => Promise<ContinueNode | StartNode | ErrorNode | FailureNode | SuccessNode>;
270+
start: <QueryParams extends OutgoingQueryParams = OutgoingQueryParams>(options?: StartOptions<QueryParams> | undefined) => Promise<ContinueNode | ErrorNode | FailureNode | StartNode | SuccessNode>;
271271
update: <T extends SingleValueCollectors | MultiSelectCollector | ObjectValueCollectors | AutoCollectors>(collector: T) => Updater<T>;
272272
validate: (collector: SingleValueCollectors | ObjectValueCollectors | MultiValueCollectors | AutoCollectors) => Validator;
273-
poll: (collector: PollingCollector) => Poller;
273+
pollStatus: (collector: PollingCollector) => Poller;
274274
getClient: () => {
275-
status: "start";
276-
} | {
277275
action: string;
278276
collectors: Collectors[];
279277
description?: string;
@@ -287,6 +285,8 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
287285
status: "error";
288286
} | {
289287
status: "failure";
288+
} | {
289+
status: "start";
290290
} | {
291291
authorization?: {
292292
code?: string;
@@ -297,7 +297,7 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
297297
getCollectors: () => Collectors[];
298298
getError: () => DaVinciError | null;
299299
getErrorCollectors: () => CollectorErrors[];
300-
getNode: () => ContinueNode | StartNode | ErrorNode | FailureNode | SuccessNode;
300+
getNode: () => ContinueNode | ErrorNode | FailureNode | StartNode | SuccessNode;
301301
getServer: () => {
302302
_links?: Links;
303303
id?: string;
@@ -306,8 +306,6 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
306306
href?: string;
307307
eventName?: string;
308308
status: "continue";
309-
} | {
310-
status: "start";
311309
} | {
312310
_links?: Links;
313311
eventName?: string;
@@ -323,6 +321,8 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
323321
interactionId?: string;
324322
interactionToken?: string;
325323
status: "failure";
324+
} | {
325+
status: "start";
326326
} | {
327327
_links?: Links;
328328
eventName?: string;

packages/davinci-client/api-report/davinci-client.types.api.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -267,13 +267,11 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
267267
resume: (input: {
268268
continueToken: string;
269269
}) => Promise<InternalErrorResponse | NodeStates>;
270-
start: <QueryParams extends OutgoingQueryParams = OutgoingQueryParams>(options?: StartOptions<QueryParams> | undefined) => Promise<ContinueNode | StartNode | ErrorNode | FailureNode | SuccessNode>;
270+
start: <QueryParams extends OutgoingQueryParams = OutgoingQueryParams>(options?: StartOptions<QueryParams> | undefined) => Promise<ContinueNode | ErrorNode | FailureNode | StartNode | SuccessNode>;
271271
update: <T extends SingleValueCollectors | MultiSelectCollector | ObjectValueCollectors | AutoCollectors>(collector: T) => Updater<T>;
272272
validate: (collector: SingleValueCollectors | ObjectValueCollectors | MultiValueCollectors | AutoCollectors) => Validator;
273-
poll: (collector: PollingCollector) => Poller;
273+
pollStatus: (collector: PollingCollector) => Poller;
274274
getClient: () => {
275-
status: "start";
276-
} | {
277275
action: string;
278276
collectors: Collectors[];
279277
description?: string;
@@ -287,6 +285,8 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
287285
status: "error";
288286
} | {
289287
status: "failure";
288+
} | {
289+
status: "start";
290290
} | {
291291
authorization?: {
292292
code?: string;
@@ -297,7 +297,7 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
297297
getCollectors: () => Collectors[];
298298
getError: () => DaVinciError | null;
299299
getErrorCollectors: () => CollectorErrors[];
300-
getNode: () => ContinueNode | StartNode | ErrorNode | FailureNode | SuccessNode;
300+
getNode: () => ContinueNode | ErrorNode | FailureNode | StartNode | SuccessNode;
301301
getServer: () => {
302302
_links?: Links;
303303
id?: string;
@@ -306,8 +306,6 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
306306
href?: string;
307307
eventName?: string;
308308
status: "continue";
309-
} | {
310-
status: "start";
311309
} | {
312310
_links?: Links;
313311
eventName?: string;
@@ -323,6 +321,8 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
323321
interactionId?: string;
324322
interactionToken?: string;
325323
status: "failure";
324+
} | {
325+
status: "start";
326326
} | {
327327
_links?: Links;
328328
eventName?: string;

packages/journey-client/api-report/journey-client.api.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { RequestMiddleware } from '@forgerock/sdk-request-middleware';
2424
import { Step } from '@forgerock/sdk-types';
2525
import { StepDetail } from '@forgerock/sdk-types';
2626
import { StepType } from '@forgerock/sdk-types';
27+
import { WellknownResponse } from '@forgerock/sdk-types';
2728

2829
export { ActionTypes }
2930

@@ -494,6 +495,8 @@ export class ValidatedCreateUsernameCallback extends BaseCallback {
494495
setValidateOnly(value: boolean): void;
495496
}
496497

498+
export { WellknownResponse }
499+
497500
// (No @packageDocumentation comment for this package)
498501

499502
```

packages/journey-client/api-report/journey-client.types.api.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { RequestMiddleware } from '@forgerock/sdk-request-middleware';
2323
import { Step } from '@forgerock/sdk-types';
2424
import { StepDetail } from '@forgerock/sdk-types';
2525
import { StepType } from '@forgerock/sdk-types';
26+
import { WellknownResponse } from '@forgerock/sdk-types';
2627

2728
export { ActionTypes }
2829

@@ -481,6 +482,8 @@ export class ValidatedCreateUsernameCallback extends BaseCallback {
481482
setValidateOnly(value: boolean): void;
482483
}
483484

485+
export { WellknownResponse }
486+
484487
// (No @packageDocumentation comment for this package)
485488

486489
```

packages/journey-client/src/lib/client.store.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@
66
* of the MIT license. See the LICENSE file for details.
77
*/
88

9-
import { callbackType } from '@forgerock/sdk-types';
109
import { afterEach, describe, expect, test, vi } from 'vitest';
1110

12-
import type { GenericError, Step, WellknownResponse } from '@forgerock/sdk-types';
13-
1411
import { journey } from './client.store.js';
1512
import { createJourneyStep } from './step.utils.js';
13+
14+
import { callbackType, type GenericError, type Step, type WellknownResponse } from '../index.js';
15+
1616
import { JourneyClientConfig } from './config.types.js';
1717

1818
/**
@@ -158,7 +158,7 @@ describe('journey-client', () => {
158158
}
159159
});
160160

161-
test('start_401WithCodeInBody_ReturnsLoginFailure', async () => {
161+
test('start_401WithStepPayload_ReturnsLoginFailure', async () => {
162162
const failurePayload: Step = {
163163
code: 401,
164164
message: 'Access Denied',
@@ -222,7 +222,7 @@ describe('journey-client', () => {
222222
}
223223
});
224224

225-
test('next_401WithCodeInBody_ReturnsLoginFailure', async () => {
225+
test('next_401WithStepPayload_ReturnsLoginFailure', async () => {
226226
const initialStep = createJourneyStep({
227227
authId: 'test-auth-id',
228228
callbacks: [],
@@ -434,7 +434,7 @@ describe('journey-client', () => {
434434

435435
expect(isGenericError(result)).toBe(true);
436436
if (isGenericError(result)) {
437-
expect(result.error).toBe('no_response_data');
437+
expect(result.error).toBe('request_failed');
438438
expect(result.type).toBe('unknown_error');
439439
}
440440
});

packages/journey-client/src/lib/client.store.ts

Lines changed: 7 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { createJourneyStore } from './client.store.utils.js';
2121
import { configSlice } from './config.slice.js';
2222
import { journeyApi } from './journey.api.js';
2323
import { createStorage } from '@forgerock/storage';
24-
import { createJourneyObject } from './journey.utils.js';
24+
import { createJourneyObject, resolveJourneyResult } from './journey.utils.js';
2525
import { wellknownApi } from './wellknown.api.js';
2626

2727
import type { JourneyStep } from './step.utils.js';
@@ -155,34 +155,18 @@ export async function journey<ActionType extends ActionTypes = ActionTypes>({
155155

156156
const self: JourneyClient = {
157157
start: async (options?: StartParam) => {
158-
const { data } = await store.dispatch(journeyApi.endpoints.start.initiate(options));
159-
if (data) {
160-
return createJourneyObject(data);
161-
}
162-
163-
const genericError: GenericError = {
164-
error: 'no_response_data',
165-
message: 'No data received from server when starting journey',
166-
type: 'unknown_error',
167-
};
168-
return genericError;
158+
const { data, error } = await store.dispatch(journeyApi.endpoints.start.initiate(options));
159+
return resolveJourneyResult(data, error);
169160
},
170161

171162
/**
172163
* Submits the current Step payload to the authentication API and retrieves the next JourneyStep in the journey.
173164
*/
174165
next: async (step: JourneyStep, options?: NextOptions) => {
175-
const { data } = await store.dispatch(journeyApi.endpoints.next.initiate({ step, options }));
176-
if (data) {
177-
return createJourneyObject(data);
178-
}
179-
180-
const genericError: GenericError = {
181-
error: 'no_response_data',
182-
message: 'No data received from server when submitting step',
183-
type: 'unknown_error',
184-
};
185-
return genericError;
166+
const { data, error } = await store.dispatch(
167+
journeyApi.endpoints.next.initiate({ step, options }),
168+
);
169+
return resolveJourneyResult(data, error);
186170
},
187171

188172
// TODO: Remove the actual redirect from this method and just return the URL to the caller

packages/journey-client/src/lib/journey.api.ts

Lines changed: 0 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,6 @@ interface Extras {
8989
logger: ReturnType<typeof loggerFn>;
9090
}
9191

92-
// Only treat these numeric codes as login failures coming back in the Step payload.
93-
const LOGIN_FAILURE_CODES = [400, 401, 403, 412, 423, 429];
94-
9592
export const journeyApi = createApi({
9693
reducerPath: 'journeyReducer',
9794
baseQuery: fetchBaseQuery({
@@ -136,24 +133,6 @@ export const journeyApi = createApi({
136133
return result as QueryReturnValue<unknown, FetchBaseQueryError, FetchBaseQueryMeta>;
137134
});
138135

139-
/**
140-
* If the endpoint returned an HTTP error whose body is an AM Step with a
141-
* login-failure code, treat it as successful data so callers receive the
142-
* Step via the `data` path (keeps downstream logic simpler).
143-
*/
144-
if ('error' in response) {
145-
const errorData = (response.error as FetchBaseQueryError | undefined)?.data as
146-
| Step
147-
| undefined;
148-
if (errorData && errorData.code && LOGIN_FAILURE_CODES.includes(errorData.code)) {
149-
return { data: errorData } as QueryReturnValue<
150-
Step,
151-
FetchBaseQueryError,
152-
FetchBaseQueryMeta
153-
>;
154-
}
155-
}
156-
157136
return response as QueryReturnValue<Step, FetchBaseQueryError, FetchBaseQueryMeta>;
158137
},
159138
}),
@@ -183,24 +162,6 @@ export const journeyApi = createApi({
183162
return result as QueryReturnValue<unknown, FetchBaseQueryError, FetchBaseQueryMeta>;
184163
});
185164

186-
/**
187-
* If the endpoint returned an HTTP error whose body is an AM Step with a
188-
* login-failure code, treat it as successful data so callers receive the
189-
* Step via the `data` path (keeps downstream logic simpler).
190-
*/
191-
if ('error' in response) {
192-
const errorData = (response.error as FetchBaseQueryError | undefined)?.data as
193-
| Step
194-
| undefined;
195-
if (errorData && errorData.code && LOGIN_FAILURE_CODES.includes(errorData.code)) {
196-
return { data: errorData } as QueryReturnValue<
197-
Step,
198-
FetchBaseQueryError,
199-
FetchBaseQueryMeta
200-
>;
201-
}
202-
}
203-
204165
return response as QueryReturnValue<Step, FetchBaseQueryError, FetchBaseQueryMeta>;
205166
},
206167
}),
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* Copyright (c) 2026 Ping Identity Corporation. All rights reserved.
3+
*
4+
* This software may be modified and distributed under the terms
5+
* of the MIT license. See the LICENSE file for details.
6+
*/
7+
8+
import { describe, expect, it } from 'vitest';
9+
10+
import { StepType } from '../types.js';
11+
import { type Step } from '../index.js';
12+
13+
import { createJourneyObject, resolveJourneyResult } from './journey.utils.js';
14+
import type { JourneyLoginFailure } from './login-failure.utils.js';
15+
16+
describe('createJourneyObject', () => {
17+
it('returns Step when provided a step with authId', () => {
18+
const stepPayload: Step = {
19+
authId: 'test-auth-id',
20+
callbacks: [],
21+
};
22+
23+
const result = createJourneyObject(stepPayload);
24+
25+
expect(result).not.toHaveProperty('error');
26+
expect(result).toHaveProperty('type', StepType.Step);
27+
expect(result).toHaveProperty('payload');
28+
expect((result as { payload: Step }).payload).toEqual(stepPayload);
29+
});
30+
31+
it('returns LoginSuccess when provided a step with successUrl', () => {
32+
const successPayload: Step = {
33+
successUrl: 'https://example.com/success',
34+
realm: 'root',
35+
tokenId: 'token-123',
36+
};
37+
38+
const result = createJourneyObject(successPayload);
39+
40+
expect(result).not.toHaveProperty('error');
41+
expect(result).toHaveProperty('type', StepType.LoginSuccess);
42+
expect(result).toHaveProperty('payload', successPayload);
43+
});
44+
});
45+
46+
describe('resolveJourneyResult - no_response_data', () => {
47+
it('returns no_response_data GenericError when no data and no error', () => {
48+
const result = resolveJourneyResult(undefined, undefined);
49+
50+
expect(result).toMatchObject({
51+
error: 'no_response_data',
52+
message: 'No data received from server',
53+
type: 'unknown_error',
54+
});
55+
});
56+
});
57+
58+
describe('toJourneyResult', () => {
59+
it('returns request_failed GenericError for FetchBaseQueryError without Step payload', () => {
60+
const result = resolveJourneyResult(undefined, { status: 500, data: { foo: 'bar' } });
61+
62+
expect(result).toMatchObject({
63+
error: 'request_failed',
64+
message: 'Request failed: {"foo":"bar"}',
65+
type: 'unknown_error',
66+
});
67+
});
68+
69+
it('returns request_failed GenericError for SerializedError', () => {
70+
const result = resolveJourneyResult(undefined, { message: 'Network failure' });
71+
72+
expect(result).toMatchObject({
73+
error: 'request_failed',
74+
message: 'Request failed: Network failure',
75+
type: 'unknown_error',
76+
});
77+
});
78+
79+
it('returns LoginFailure when FetchBaseQueryError contains a failure Step payload', () => {
80+
const failurePayload: Step = {
81+
code: 401,
82+
message: 'Access Denied',
83+
reason: 'Unauthorized',
84+
detail: { failureUrl: 'https://example.com/failure' },
85+
};
86+
87+
const result = resolveJourneyResult(undefined, { status: 401, data: failurePayload });
88+
89+
expect(result).not.toHaveProperty('error');
90+
expect(result).toHaveProperty('type', StepType.LoginFailure);
91+
expect(result).toHaveProperty('payload', failurePayload);
92+
93+
const failure = result as JourneyLoginFailure;
94+
expect(failure.getCode()).toBe(401);
95+
expect(failure.getMessage()).toBe('Access Denied');
96+
expect(failure.getReason()).toBe('Unauthorized');
97+
});
98+
});

0 commit comments

Comments
 (0)