Skip to content

Commit 5ee7baa

Browse files
authored
Merge pull request #545 from ForgeRock/fix-jc-return-types
fix(journey-client): remove undefined from JourneyResult return type
2 parents 8502426 + 70b6781 commit 5ee7baa

7 files changed

Lines changed: 91 additions & 116 deletions

File tree

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

Lines changed: 13 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -99,13 +99,10 @@ describe('journey-client', () => {
9999
});
100100

101101
test('journey_WellknownConfig_ReturnsClientWithAllMethods', async () => {
102-
// Arrange
103102
setupMockFetch();
104103

105-
// Act
106104
const client = await journey({ config: mockConfig });
107105

108-
// Assert
109106
expect(client.start).toBeInstanceOf(Function);
110107
expect(client.next).toBeInstanceOf(Function);
111108
expect(client.redirect).toBeInstanceOf(Function);
@@ -114,39 +111,32 @@ describe('journey-client', () => {
114111
});
115112

116113
test('journey_InvalidWellknownUrl_ThrowsError', async () => {
117-
// Arrange
118114
const invalidConfig: JourneyClientConfig = {
119115
serverConfig: {
120116
wellknown: 'not-a-valid-url',
121117
},
122118
};
123119

124-
// Act & Assert
125120
await expect(journey({ config: invalidConfig })).rejects.toThrow('Invalid wellknown URL');
126121
});
127122

128123
test('journey_MissingWellknownPath_ThrowsError', async () => {
129-
// Arrange — valid HTTPS URL but missing /.well-known/openid-configuration
130124
const badPathConfig: JourneyClientConfig = {
131125
serverConfig: {
132126
wellknown: 'https://am.example.com/am/oauth2/alpha',
133127
},
134128
};
135129

136-
// Act & Assert
137130
await expect(journey({ config: badPathConfig })).rejects.toThrow('Invalid wellknown URL');
138131
});
139132

140133
test('start_WellknownConfig_FetchesFirstStep', async () => {
141-
// Arrange
142134
const mockStepResponse: Step = { authId: 'test-auth-id', callbacks: [] };
143135
setupMockFetch(mockStepResponse);
144136

145-
// Act
146137
const client = await journey({ config: mockConfig });
147138
const step = await client.start();
148139

149-
// Assert
150140
expect(step).toBeDefined();
151141
expect(isGenericError(step)).toBe(false);
152142

@@ -163,7 +153,6 @@ describe('journey-client', () => {
163153
});
164154

165155
test('next_WellknownConfig_SendsStepAndReturnsNext', async () => {
166-
// Arrange
167156
const initialStep = createJourneyStep({
168157
authId: 'test-auth-id',
169158
callbacks: [
@@ -186,11 +175,9 @@ describe('journey-client', () => {
186175
};
187176
setupMockFetch(nextStepPayload);
188177

189-
// Act
190178
const client = await journey({ config: mockConfig });
191179
const nextStep = await client.next(initialStep, {});
192180

193-
// Assert
194181
expect(nextStep).toBeDefined();
195182
expect(isGenericError(nextStep)).toBe(false);
196183

@@ -206,7 +193,6 @@ describe('journey-client', () => {
206193
});
207194

208195
test('redirect_WellknownConfig_StoresStepAndCallsLocationAssign', async () => {
209-
// Arrange
210196
const mockStepPayload: Step = {
211197
callbacks: [
212198
{
@@ -224,11 +210,9 @@ describe('journey-client', () => {
224210
});
225211
setupMockFetch();
226212

227-
// Act
228213
const client = await journey({ config: mockConfig });
229214
await client.redirect(step);
230215

231-
// Assert
232216
expect(mockStorageInstance.set).toHaveBeenCalledWith({ step: step.payload });
233217
expect(assignMock).toHaveBeenCalledWith('https://sso.com/redirect');
234218

@@ -237,20 +221,17 @@ describe('journey-client', () => {
237221

238222
describe('resume()', () => {
239223
test('resume_WithPreviousStepInStorage_CallsNextWithUrlParams', async () => {
240-
// Arrange
241224
const previousStepPayload: Step = {
242225
callbacks: [{ type: callbackType.RedirectCallback, input: [], output: [] }],
243226
};
244227
mockStorageInstance.get.mockResolvedValue({ step: previousStepPayload });
245228
const nextStepPayload: Step = { authId: 'test-auth-id', callbacks: [] };
246229
setupMockFetch(nextStepPayload);
247230

248-
// Act
249231
const client = await journey({ config: mockConfig });
250232
const resumeUrl = 'https://app.com/callback?code=123&state=abc';
251233
const step = await client.resume(resumeUrl, {});
252234

253-
// Assert
254235
expect(step).toBeDefined();
255236
expect(mockStorageInstance.get).toHaveBeenCalledTimes(1);
256237
expect(mockStorageInstance.remove).toHaveBeenCalledTimes(1);
@@ -269,7 +250,6 @@ describe('journey-client', () => {
269250
});
270251

271252
test('resume_WithPlainStepObjectInStorage_CorrectlyResumes', async () => {
272-
// Arrange
273253
const plainStepPayload: Step = {
274254
callbacks: [
275255
{ type: callbackType.TextOutputCallback, output: [{ name: 'message', value: 'Hello' }] },
@@ -280,12 +260,10 @@ describe('journey-client', () => {
280260
const nextStepPayload: Step = { authId: 'test-auth-id', callbacks: [] };
281261
setupMockFetch(nextStepPayload);
282262

283-
// Act
284263
const client = await journey({ config: mockConfig });
285264
const resumeUrl = 'https://app.com/callback?code=123&state=abc';
286265
const step = await client.resume(resumeUrl, {});
287266

288-
// Assert
289267
expect(step).toBeDefined();
290268
expect(mockStorageInstance.get).toHaveBeenCalledTimes(1);
291269
expect(mockStorageInstance.remove).toHaveBeenCalledTimes(1);
@@ -300,15 +278,12 @@ describe('journey-client', () => {
300278
});
301279

302280
test('resume_PreviousStepRequiredButNotFound_ThrowsError', async () => {
303-
// Arrange
304281
mockStorageInstance.get.mockResolvedValue(undefined);
305282
setupMockFetch();
306283

307-
// Act
308284
const client = await journey({ config: mockConfig });
309285
const resumeUrl = 'https://app.com/callback?code=123&state=abc';
310286

311-
// Assert
312287
await expect(client.resume(resumeUrl)).rejects.toThrow(
313288
'Error: previous step information not found in storage for resume operation.',
314289
);
@@ -317,17 +292,14 @@ describe('journey-client', () => {
317292
});
318293

319294
test('resume_NoPreviousStepRequired_CallsStartWithUrlParams', async () => {
320-
// Arrange
321295
mockStorageInstance.get.mockResolvedValue(undefined);
322296
const mockStepResponse: Step = { authId: 'test-auth-id', callbacks: [] };
323297
setupMockFetch(mockStepResponse);
324298

325-
// Act
326299
const client = await journey({ config: mockConfig });
327300
const resumeUrl = 'https://app.com/callback?foo=bar';
328301
const step = await client.resume(resumeUrl, {});
329302

330-
// Assert
331303
expect(step).toBeDefined();
332304
expect(mockStorageInstance.get).not.toHaveBeenCalled();
333305
expect(mockFetch).toHaveBeenCalledTimes(2); // wellknown + start
@@ -341,9 +313,21 @@ describe('journey-client', () => {
341313
});
342314
});
343315

316+
test('start_NoDataFromServer_ReturnsGenericError', async () => {
317+
setupMockFetch(null);
318+
319+
const client = await journey({ config: mockConfig });
320+
const result = await client.start();
321+
322+
expect(isGenericError(result)).toBe(true);
323+
if (isGenericError(result)) {
324+
expect(result.error).toBe('no_response_data');
325+
expect(result.type).toBe('unknown_error');
326+
}
327+
});
328+
344329
describe('baseUrl from convertWellknown', () => {
345330
test('journey_LocalhostWellknown_ConstructsCorrectUrls', async () => {
346-
// Arrange
347331
const localhostConfig: JourneyClientConfig = {
348332
serverConfig: {
349333
wellknown: 'http://localhost:9443/am/oauth2/realms/root/.well-known/openid-configuration',
@@ -366,11 +350,9 @@ describe('journey-client', () => {
366350
return Promise.resolve(new Response(JSON.stringify(mockStepResponse)));
367351
});
368352

369-
// Act
370353
const client = await journey({ config: localhostConfig });
371354
await client.start();
372355

373-
// Assert
374356
expect(mockFetch).toHaveBeenCalledTimes(2);
375357
const request = mockFetch.mock.calls[1][0] as Request;
376358
expect(request.url).toBe('http://localhost:9443/am/json/realms/root/authenticate');
@@ -379,7 +361,6 @@ describe('journey-client', () => {
379361

380362
describe('subrealm inference', () => {
381363
test('journey_WellknownWithSubrealm_DerivesCorrectPaths', async () => {
382-
// Arrange
383364
const alphaConfig: JourneyClientConfig = {
384365
serverConfig: {
385366
wellknown:
@@ -404,11 +385,9 @@ describe('journey-client', () => {
404385
return Promise.resolve(new Response(JSON.stringify(mockStepResponse)));
405386
});
406387

407-
// Act
408388
const client = await journey({ config: alphaConfig });
409389
await client.start();
410390

411-
// Assert
412391
const request = mockFetch.mock.calls[1][0] as Request;
413392
expect(request.url).toBe('https://test.com/am/json/realms/root/realms/alpha/authenticate');
414393
});

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

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,7 @@ import type { JourneyLoginFailure } from './login-failure.utils.js';
3232
import type { JourneyLoginSuccess } from './login-success.utils.js';
3333

3434
/** Result type for journey client methods. */
35-
type JourneyResult =
36-
| JourneyStep
37-
| JourneyLoginSuccess
38-
| JourneyLoginFailure
39-
| GenericError
40-
| undefined;
35+
type JourneyResult = JourneyStep | JourneyLoginSuccess | JourneyLoginFailure | GenericError;
4136

4237
/** The journey client instance returned by the `journey()` function. */
4338
export interface JourneyClient {
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright (c) 2025 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+
/* eslint-disable @typescript-eslint/no-unused-vars */
8+
import { describe, it } from 'vitest';
9+
10+
import type { GenericError } from '@forgerock/sdk-types';
11+
12+
import type { JourneyClient } from './client.types.js';
13+
import type { JourneyStep } from './step.utils.js';
14+
import type { JourneyLoginSuccess } from './login-success.utils.js';
15+
import type { JourneyLoginFailure } from './login-failure.utils.js';
16+
17+
/**
18+
* Resolves to `true` if `U` is a member of union `T`, `false` otherwise.
19+
* Uses the distributive-conditional-type trick: when `U` is a union member
20+
* of `T`, `U extends T` distributes and resolves to `true`.
21+
*/
22+
type HasMember<T, U> = U extends T ? true : false;
23+
24+
/** Compile-time assertion: `T` must be exactly `true`. */
25+
type AssertTrue<T extends true> = T;
26+
27+
/** Unwrap Promise<T> → T. */
28+
type Awaited<T> = T extends Promise<infer U> ? U : T;
29+
30+
type StartResult = Awaited<ReturnType<JourneyClient['start']>>;
31+
type NextResult = Awaited<ReturnType<JourneyClient['next']>>;
32+
type ResumeResult = Awaited<ReturnType<JourneyClient['resume']>>;
33+
type TerminateResult = Awaited<ReturnType<JourneyClient['terminate']>>;
34+
35+
describe('JourneyClient return types', () => {
36+
it('start includes all expected members and excludes undefined', () => {
37+
type _hasStep = AssertTrue<HasMember<StartResult, JourneyStep>>;
38+
type _hasSuccess = AssertTrue<HasMember<StartResult, JourneyLoginSuccess>>;
39+
type _hasFailure = AssertTrue<HasMember<StartResult, JourneyLoginFailure>>;
40+
type _hasError = AssertTrue<HasMember<StartResult, GenericError>>;
41+
type _noUndefined = AssertTrue<HasMember<StartResult, undefined> extends false ? true : false>;
42+
});
43+
44+
it('next includes all expected members and excludes undefined', () => {
45+
type _hasStep = AssertTrue<HasMember<NextResult, JourneyStep>>;
46+
type _hasSuccess = AssertTrue<HasMember<NextResult, JourneyLoginSuccess>>;
47+
type _hasFailure = AssertTrue<HasMember<NextResult, JourneyLoginFailure>>;
48+
type _hasError = AssertTrue<HasMember<NextResult, GenericError>>;
49+
type _noUndefined = AssertTrue<HasMember<NextResult, undefined> extends false ? true : false>;
50+
});
51+
52+
it('resume includes all expected members and excludes undefined', () => {
53+
type _hasStep = AssertTrue<HasMember<ResumeResult, JourneyStep>>;
54+
type _hasSuccess = AssertTrue<HasMember<ResumeResult, JourneyLoginSuccess>>;
55+
type _hasFailure = AssertTrue<HasMember<ResumeResult, JourneyLoginFailure>>;
56+
type _hasError = AssertTrue<HasMember<ResumeResult, GenericError>>;
57+
type _noUndefined = AssertTrue<HasMember<ResumeResult, undefined> extends false ? true : false>;
58+
});
59+
60+
it('terminate returns void | GenericError', () => {
61+
type _hasVoid = AssertTrue<HasMember<TerminateResult, void>>;
62+
type _hasError = AssertTrue<HasMember<TerminateResult, GenericError>>;
63+
});
64+
});

packages/journey-client/src/lib/config.slice.test.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,12 @@ function createMockWellknown(overrides: Partial<WellknownResponse> = {}): Wellkn
2727
describe('journey-client config.slice', () => {
2828
describe('configSlice_ValidAmWellknown_SetsResolvedServerConfig', () => {
2929
it('should derive baseUrl and paths from a standard AM well-known response', () => {
30-
// Arrange
3130
const payload: ResolvedConfig = {
3231
wellknownResponse: createMockWellknown(),
3332
};
3433

35-
// Act
3634
const state = configSlice.reducer(undefined, configSlice.actions.set(payload));
3735

38-
// Assert
3936
expect(state.serverConfig).toEqual({
4037
baseUrl: 'https://am.example.com',
4138
paths: {
@@ -49,18 +46,15 @@ describe('journey-client config.slice', () => {
4946

5047
describe('configSlice_NonAmIssuer_SetsError', () => {
5148
it('should set a GenericError when the issuer is not a ForgeRock AM issuer', () => {
52-
// Arrange
5349
const payload: ResolvedConfig = {
5450
wellknownResponse: createMockWellknown({
5551
issuer: 'https://auth.pingone.com/env-id/as',
5652
authorization_endpoint: 'https://auth.pingone.com/env-id/as/authorize',
5753
}),
5854
};
5955

60-
// Act
6156
const state = configSlice.reducer(undefined, configSlice.actions.set(payload));
6257

63-
// Assert
6458
expect(state.error).toBeDefined();
6559
expect(state.error?.type).toBe('wellknown_error');
6660
expect(state.error?.message).toContain('ForgeRock AM issuer');
@@ -69,17 +63,14 @@ describe('journey-client config.slice', () => {
6963

7064
describe('configSlice_MissingAuthEndpoint_SetsError', () => {
7165
it('should set a GenericError when authorization_endpoint is empty', () => {
72-
// Arrange
7366
const payload: ResolvedConfig = {
7467
wellknownResponse: createMockWellknown({
7568
authorization_endpoint: '',
7669
}),
7770
};
7871

79-
// Act
8072
const state = configSlice.reducer(undefined, configSlice.actions.set(payload));
8173

82-
// Assert
8374
expect(state.error).toBeDefined();
8475
expect(state.error?.type).toBe('wellknown_error');
8576
expect(state.error?.message).toContain('authorization_endpoint');

0 commit comments

Comments
 (0)