@axa-fr/react-oidc 7.27.12 → 7.27.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/OidcProvider.d.ts +2 -0
- package/dist/OidcProvider.d.ts.map +1 -1
- package/dist/core/default-component/Callback.component.d.ts +1 -0
- package/dist/core/default-component/Callback.component.d.ts.map +1 -1
- package/dist/core/default-component/LoadingTimeout.component.d.ts +4 -0
- package/dist/core/default-component/LoadingTimeout.component.d.ts.map +1 -0
- package/dist/core/default-component/index.d.ts +1 -0
- package/dist/core/default-component/index.d.ts.map +1 -1
- package/dist/core/routes/OidcRoutes.d.ts +1 -0
- package/dist/core/routes/OidcRoutes.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +147 -93
- package/dist/index.umd.cjs +1 -1
- package/package.json +3 -3
- package/src/OidcProvider.spec.tsx +279 -0
- package/src/OidcProvider.tsx +37 -0
- package/src/core/default-component/Callback.component.spec.tsx +248 -0
- package/src/core/default-component/Callback.component.tsx +62 -3
- package/src/core/default-component/LoadingTimeout.component.tsx +14 -0
- package/src/core/default-component/index.ts +1 -0
- package/src/core/routes/OidcRoutes.tsx +3 -0
- package/src/index.ts +1 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { act, render, screen } from '@testing-library/react';
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
let mockEventSubscribers: Array<{ id: string; func: (name: string, data: any) => void }> = [];
|
|
5
|
+
const mockPublishEvent = vi.fn((eventName, data) => {
|
|
6
|
+
mockEventSubscribers.forEach(sub => sub.func(eventName, data));
|
|
7
|
+
});
|
|
8
|
+
const mockSubscribeEvents = vi.fn(func => {
|
|
9
|
+
const id = Math.random().toString();
|
|
10
|
+
mockEventSubscribers.push({ id, func });
|
|
11
|
+
return id;
|
|
12
|
+
});
|
|
13
|
+
const mockRemoveEventSubscription = vi.fn(id => {
|
|
14
|
+
mockEventSubscribers = mockEventSubscribers.filter(e => e.id !== id);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
vi.mock('@axa-fr/oidc-client', () => ({
|
|
18
|
+
OidcClient: {
|
|
19
|
+
getOrCreate: vi.fn(() => () => ({
|
|
20
|
+
subscribeEvents: mockSubscribeEvents,
|
|
21
|
+
removeEventSubscription: mockRemoveEventSubscription,
|
|
22
|
+
publishEvent: mockPublishEvent,
|
|
23
|
+
configuration: {
|
|
24
|
+
redirect_uri: 'http://localhost/callback',
|
|
25
|
+
silent_redirect_uri: 'http://localhost/silent-callback',
|
|
26
|
+
silent_login_uri: 'http://localhost/silent-login',
|
|
27
|
+
},
|
|
28
|
+
tryKeepExistingSessionAsync: vi.fn().mockResolvedValue(true),
|
|
29
|
+
})),
|
|
30
|
+
get: vi.fn(() => ({
|
|
31
|
+
subscribeEvents: mockSubscribeEvents,
|
|
32
|
+
removeEventSubscription: mockRemoveEventSubscription,
|
|
33
|
+
publishEvent: mockPublishEvent,
|
|
34
|
+
configuration: {
|
|
35
|
+
redirect_uri: 'http://localhost/callback',
|
|
36
|
+
silent_redirect_uri: 'http://localhost/silent-callback',
|
|
37
|
+
silent_login_uri: 'http://localhost/silent-login',
|
|
38
|
+
},
|
|
39
|
+
tryKeepExistingSessionAsync: vi.fn().mockResolvedValue(true),
|
|
40
|
+
})),
|
|
41
|
+
eventNames: {
|
|
42
|
+
service_worker_not_supported_by_browser: 'service_worker_not_supported_by_browser',
|
|
43
|
+
token_acquired: 'token_acquired',
|
|
44
|
+
logout_from_another_tab: 'logout_from_another_tab',
|
|
45
|
+
logout_from_same_tab: 'logout_from_same_tab',
|
|
46
|
+
token_renewed: 'token_renewed',
|
|
47
|
+
token_timer: 'token_timer',
|
|
48
|
+
loginAsync_begin: 'loginAsync_begin',
|
|
49
|
+
loginAsync_error: 'loginAsync_error',
|
|
50
|
+
loginCallbackAsync_begin: 'loginCallbackAsync_begin',
|
|
51
|
+
loginCallbackAsync_end: 'loginCallbackAsync_end',
|
|
52
|
+
loginCallbackAsync_error: 'loginCallbackAsync_error',
|
|
53
|
+
refreshTokensAsync_begin: 'refreshTokensAsync_begin',
|
|
54
|
+
refreshTokensAsync: 'refreshTokensAsync',
|
|
55
|
+
refreshTokensAsync_end: 'refreshTokensAsync_end',
|
|
56
|
+
refreshTokensAsync_error: 'refreshTokensAsync_error',
|
|
57
|
+
refreshTokensAsync_silent_error: 'refreshTokensAsync_silent_error',
|
|
58
|
+
tryKeepExistingSessionAsync_begin: 'tryKeepExistingSessionAsync_begin',
|
|
59
|
+
tryKeepExistingSessionAsync_end: 'tryKeepExistingSessionAsync_end',
|
|
60
|
+
tryKeepExistingSessionAsync_error: 'tryKeepExistingSessionAsync_error',
|
|
61
|
+
silentLoginAsync_begin: 'silentLoginAsync_begin',
|
|
62
|
+
silentLoginAsync: 'silentLoginAsync',
|
|
63
|
+
silentLoginAsync_end: 'silentLoginAsync_end',
|
|
64
|
+
silentLoginAsync_error: 'silentLoginAsync_error',
|
|
65
|
+
syncTokensAsync_begin: 'syncTokensAsync_begin',
|
|
66
|
+
syncTokensAsync_lock_not_available: 'syncTokensAsync_lock_not_available',
|
|
67
|
+
syncTokensAsync_end: 'syncTokensAsync_end',
|
|
68
|
+
syncTokensAsync_error: 'syncTokensAsync_error',
|
|
69
|
+
tokensInvalidAndWaitingActionsToRefresh: 'tokensInvalidAndWaitingActionsToRefresh',
|
|
70
|
+
loadingTimeout_error: 'loadingTimeout_error',
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
OidcLocation: class {
|
|
74
|
+
getCurrentHref() {
|
|
75
|
+
return 'http://localhost/';
|
|
76
|
+
}
|
|
77
|
+
getPath() {
|
|
78
|
+
return '/';
|
|
79
|
+
}
|
|
80
|
+
open() {}
|
|
81
|
+
reload() {}
|
|
82
|
+
getOrigin() {
|
|
83
|
+
return 'http://localhost';
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
getFetchDefault: vi.fn(() => fetch),
|
|
87
|
+
}));
|
|
88
|
+
|
|
89
|
+
vi.mock('./core/routes/OidcRoutes.js', () => ({
|
|
90
|
+
default: ({ children }: any) => <div data-testid="oidc-routes">{children}</div>,
|
|
91
|
+
}));
|
|
92
|
+
|
|
93
|
+
import { OidcProvider } from './OidcProvider';
|
|
94
|
+
|
|
95
|
+
describe('OidcProvider loading timeout', () => {
|
|
96
|
+
beforeEach(() => {
|
|
97
|
+
vi.useFakeTimers();
|
|
98
|
+
vi.clearAllMocks();
|
|
99
|
+
mockEventSubscribers = [];
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
afterEach(() => {
|
|
103
|
+
vi.useRealTimers();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const baseConfiguration = {
|
|
107
|
+
client_id: 'test-client',
|
|
108
|
+
redirect_uri: 'http://localhost/callback',
|
|
109
|
+
silent_redirect_uri: 'http://localhost/silent-callback',
|
|
110
|
+
scope: 'openid',
|
|
111
|
+
authority: 'http://localhost/authority',
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
it('should fire loadingTimeout_error when stuck in initial loading state', async () => {
|
|
115
|
+
render(
|
|
116
|
+
<OidcProvider
|
|
117
|
+
configuration={{ ...baseConfiguration, loading_timeout_ms: 100 }}
|
|
118
|
+
configurationName="default"
|
|
119
|
+
>
|
|
120
|
+
<div>App</div>
|
|
121
|
+
</OidcProvider>,
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
act(() => {
|
|
125
|
+
vi.advanceTimersByTime(100);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
expect(mockPublishEvent).toHaveBeenCalledWith('loadingTimeout_error', { timeoutMs: 100 });
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should fire loadingTimeout_error when stuck in loginAsync_begin state', async () => {
|
|
132
|
+
render(
|
|
133
|
+
<OidcProvider
|
|
134
|
+
configuration={{ ...baseConfiguration, loading_timeout_ms: 200 }}
|
|
135
|
+
configurationName="default"
|
|
136
|
+
>
|
|
137
|
+
<div>App</div>
|
|
138
|
+
</OidcProvider>,
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
// Simulate loginAsync_begin event
|
|
142
|
+
act(() => {
|
|
143
|
+
mockEventSubscribers.forEach(sub => sub.func('loginAsync_begin', {}));
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Reset to track timeout event specifically
|
|
147
|
+
mockPublishEvent.mockClear();
|
|
148
|
+
|
|
149
|
+
act(() => {
|
|
150
|
+
vi.advanceTimersByTime(200);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
expect(mockPublishEvent).toHaveBeenCalledWith('loadingTimeout_error', { timeoutMs: 200 });
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('should render loadingTimeoutComponent when loadingTimeout_error is triggered', async () => {
|
|
157
|
+
const CustomTimeoutComponent = () => <div data-testid="custom-timeout">Timeout!</div>;
|
|
158
|
+
|
|
159
|
+
render(
|
|
160
|
+
<OidcProvider
|
|
161
|
+
configuration={{ ...baseConfiguration, loading_timeout_ms: 50 }}
|
|
162
|
+
configurationName="default"
|
|
163
|
+
loadingTimeoutComponent={CustomTimeoutComponent}
|
|
164
|
+
>
|
|
165
|
+
<div>App</div>
|
|
166
|
+
</OidcProvider>,
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
await act(async () => {
|
|
170
|
+
vi.advanceTimersByTime(50);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
expect(screen.getByTestId('custom-timeout')).toBeTruthy();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('should NOT fire loadingTimeout_error when loading_timeout_ms is 0 (disabled)', async () => {
|
|
177
|
+
render(
|
|
178
|
+
<OidcProvider
|
|
179
|
+
configuration={{ ...baseConfiguration, loading_timeout_ms: 0 }}
|
|
180
|
+
configurationName="default"
|
|
181
|
+
>
|
|
182
|
+
<div>App</div>
|
|
183
|
+
</OidcProvider>,
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
act(() => {
|
|
187
|
+
vi.advanceTimersByTime(60_000);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
expect(mockPublishEvent).not.toHaveBeenCalledWith('loadingTimeout_error', expect.anything());
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('should NOT fire loadingTimeout_error when loading_timeout_ms is negative (disabled)', async () => {
|
|
194
|
+
render(
|
|
195
|
+
<OidcProvider
|
|
196
|
+
configuration={{ ...baseConfiguration, loading_timeout_ms: -1 }}
|
|
197
|
+
configurationName="default"
|
|
198
|
+
>
|
|
199
|
+
<div>App</div>
|
|
200
|
+
</OidcProvider>,
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
act(() => {
|
|
204
|
+
vi.advanceTimersByTime(60_000);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
expect(mockPublishEvent).not.toHaveBeenCalledWith('loadingTimeout_error', expect.anything());
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('should NOT fire loadingTimeout_error if provider leaves loading state before deadline', async () => {
|
|
211
|
+
render(
|
|
212
|
+
<OidcProvider
|
|
213
|
+
configuration={{ ...baseConfiguration, loading_timeout_ms: 500 }}
|
|
214
|
+
configurationName="default"
|
|
215
|
+
>
|
|
216
|
+
<div>App</div>
|
|
217
|
+
</OidcProvider>,
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
// Advance partway
|
|
221
|
+
act(() => {
|
|
222
|
+
vi.advanceTimersByTime(200);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// Simulate successful callback (leaving loading state)
|
|
226
|
+
act(() => {
|
|
227
|
+
mockEventSubscribers.forEach(sub => sub.func('loginCallbackAsync_end', {}));
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
mockPublishEvent.mockClear();
|
|
231
|
+
|
|
232
|
+
// Advance past original timeout
|
|
233
|
+
act(() => {
|
|
234
|
+
vi.advanceTimersByTime(500);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
expect(mockPublishEvent).not.toHaveBeenCalledWith('loadingTimeout_error', expect.anything());
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('should use default timeout of 30000ms when loading_timeout_ms is not configured', async () => {
|
|
241
|
+
render(
|
|
242
|
+
<OidcProvider configuration={baseConfiguration} configurationName="default">
|
|
243
|
+
<div>App</div>
|
|
244
|
+
</OidcProvider>,
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
act(() => {
|
|
248
|
+
vi.advanceTimersByTime(29_999);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
expect(mockPublishEvent).not.toHaveBeenCalledWith('loadingTimeout_error', expect.anything());
|
|
252
|
+
|
|
253
|
+
act(() => {
|
|
254
|
+
vi.advanceTimersByTime(1);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
expect(mockPublishEvent).toHaveBeenCalledWith('loadingTimeout_error', { timeoutMs: 30_000 });
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('should propagate loadingTimeout_error through onEvent callback', async () => {
|
|
261
|
+
const onEvent = vi.fn();
|
|
262
|
+
|
|
263
|
+
render(
|
|
264
|
+
<OidcProvider
|
|
265
|
+
configuration={{ ...baseConfiguration, loading_timeout_ms: 100 }}
|
|
266
|
+
configurationName="default"
|
|
267
|
+
onEvent={onEvent}
|
|
268
|
+
>
|
|
269
|
+
<div>App</div>
|
|
270
|
+
</OidcProvider>,
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
act(() => {
|
|
274
|
+
vi.advanceTimersByTime(100);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
expect(onEvent).toHaveBeenCalledWith('default', 'loadingTimeout_error', { timeoutMs: 100 });
|
|
278
|
+
});
|
|
279
|
+
});
|
package/src/OidcProvider.tsx
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
Authenticating,
|
|
14
14
|
CallBackSuccess,
|
|
15
15
|
Loading,
|
|
16
|
+
LoadingTimeout,
|
|
16
17
|
SessionLost,
|
|
17
18
|
} from './core/default-component/index.js';
|
|
18
19
|
import ServiceWorkerNotSupported from './core/default-component/ServiceWorkerNotSupported.component.js';
|
|
@@ -31,6 +32,7 @@ export type OidcProviderProps = {
|
|
|
31
32
|
authenticatingComponent?: ComponentType<any>;
|
|
32
33
|
authenticatingErrorComponent?: ComponentType<any>;
|
|
33
34
|
loadingComponent?: ComponentType<any>;
|
|
35
|
+
loadingTimeoutComponent?: ComponentType<any>;
|
|
34
36
|
serviceWorkerNotSupportedComponent?: ComponentType<any>;
|
|
35
37
|
configurationName?: string;
|
|
36
38
|
configuration?: OidcConfiguration;
|
|
@@ -39,6 +41,7 @@ export type OidcProviderProps = {
|
|
|
39
41
|
onLogoutFromAnotherTab?: () => void;
|
|
40
42
|
onLogoutFromSameTab?: () => void;
|
|
41
43
|
withCustomHistory?: () => CustomHistory;
|
|
44
|
+
navigateAfterCallback?: (callbackPath: string) => Promise<void>;
|
|
42
45
|
onEvent?: (configuration: string, name: string, data: any) => void;
|
|
43
46
|
getFetch?: () => Fetch;
|
|
44
47
|
location?: ILOidcLocation;
|
|
@@ -84,6 +87,8 @@ const Switch = ({ isLoading, loadingComponent, children, configurationName }) =>
|
|
|
84
87
|
return <>{children}</>;
|
|
85
88
|
};
|
|
86
89
|
|
|
90
|
+
const DEFAULT_LOADING_TIMEOUT_MS = 30_000;
|
|
91
|
+
|
|
87
92
|
export const OidcProvider: FC<PropsWithChildren<OidcProviderProps>> = ({
|
|
88
93
|
children,
|
|
89
94
|
configuration,
|
|
@@ -91,6 +96,7 @@ export const OidcProvider: FC<PropsWithChildren<OidcProviderProps>> = ({
|
|
|
91
96
|
callbackSuccessComponent = CallBackSuccess,
|
|
92
97
|
authenticatingComponent = Authenticating,
|
|
93
98
|
loadingComponent = Loading,
|
|
99
|
+
loadingTimeoutComponent = LoadingTimeout,
|
|
94
100
|
serviceWorkerNotSupportedComponent = ServiceWorkerNotSupported,
|
|
95
101
|
authenticatingErrorComponent = AuthenticatingError,
|
|
96
102
|
sessionLostComponent = SessionLost,
|
|
@@ -98,6 +104,7 @@ export const OidcProvider: FC<PropsWithChildren<OidcProviderProps>> = ({
|
|
|
98
104
|
onLogoutFromAnotherTab = null,
|
|
99
105
|
onLogoutFromSameTab = null,
|
|
100
106
|
withCustomHistory = null,
|
|
107
|
+
navigateAfterCallback = null,
|
|
101
108
|
onEvent = null,
|
|
102
109
|
getFetch = null,
|
|
103
110
|
location = null,
|
|
@@ -155,6 +162,8 @@ export const OidcProvider: FC<PropsWithChildren<OidcProviderProps>> = ({
|
|
|
155
162
|
onLogoutFromSameTab();
|
|
156
163
|
}
|
|
157
164
|
// setEvent({name, data});
|
|
165
|
+
} else if (name === OidcClient.eventNames.loadingTimeout_error) {
|
|
166
|
+
setEvent({ name, data });
|
|
158
167
|
} else if (
|
|
159
168
|
name === OidcClient.eventNames.loginAsync_begin ||
|
|
160
169
|
name === OidcClient.eventNames.loginCallbackAsync_end ||
|
|
@@ -181,9 +190,26 @@ export const OidcProvider: FC<PropsWithChildren<OidcProviderProps>> = ({
|
|
|
181
190
|
};
|
|
182
191
|
}, [configuration, configurationName]);
|
|
183
192
|
|
|
193
|
+
useEffect(() => {
|
|
194
|
+
const timeoutMs = configuration?.loading_timeout_ms ?? DEFAULT_LOADING_TIMEOUT_MS;
|
|
195
|
+
if (timeoutMs <= 0) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const isStuck = event.name === '' || event.name === OidcClient.eventNames.loginAsync_begin;
|
|
199
|
+
if (!isStuck) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const timeoutId = setTimeout(() => {
|
|
203
|
+
const oidcInstance = getOidc(configurationName);
|
|
204
|
+
oidcInstance.publishEvent(OidcClient.eventNames.loadingTimeout_error, { timeoutMs });
|
|
205
|
+
}, timeoutMs);
|
|
206
|
+
return () => clearTimeout(timeoutId);
|
|
207
|
+
}, [event.name, configurationName, configuration]);
|
|
208
|
+
|
|
184
209
|
const SessionLostComponent = sessionLostComponent;
|
|
185
210
|
const AuthenticatingComponent = authenticatingComponent;
|
|
186
211
|
const LoadingComponent = loadingComponent;
|
|
212
|
+
const LoadingTimeoutComponent = loadingTimeoutComponent;
|
|
187
213
|
const ServiceWorkerNotSupportedComponent = serviceWorkerNotSupportedComponent;
|
|
188
214
|
const AuthenticatingErrorComponent = authenticatingErrorComponent;
|
|
189
215
|
|
|
@@ -211,6 +237,16 @@ export const OidcProvider: FC<PropsWithChildren<OidcProviderProps>> = ({
|
|
|
211
237
|
<AuthenticatingComponent configurationName={configurationName} />
|
|
212
238
|
</Switch>
|
|
213
239
|
);
|
|
240
|
+
case OidcClient.eventNames.loadingTimeout_error:
|
|
241
|
+
return (
|
|
242
|
+
<Switch
|
|
243
|
+
loadingComponent={LoadingComponent}
|
|
244
|
+
isLoading={isLoading}
|
|
245
|
+
configurationName={configurationName}
|
|
246
|
+
>
|
|
247
|
+
<LoadingTimeoutComponent configurationName={configurationName} />
|
|
248
|
+
</Switch>
|
|
249
|
+
);
|
|
214
250
|
case OidcClient.eventNames.loginAsync_error:
|
|
215
251
|
case OidcClient.eventNames.loginCallbackAsync_error:
|
|
216
252
|
return (
|
|
@@ -250,6 +286,7 @@ export const OidcProvider: FC<PropsWithChildren<OidcProviderProps>> = ({
|
|
|
250
286
|
authenticatingComponent={authenticatingComponent}
|
|
251
287
|
configurationName={configurationName}
|
|
252
288
|
withCustomHistory={withCustomHistory}
|
|
289
|
+
navigateAfterCallback={navigateAfterCallback}
|
|
253
290
|
location={location ?? new OidcLocation()}
|
|
254
291
|
>
|
|
255
292
|
<OidcSession loadingComponent={LoadingComponent} configurationName={configurationName}>
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { act, render, waitFor } from '@testing-library/react';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import CallbackManager, { CallBackSuccess, verifyNavigationCommitted } from './Callback.component';
|
|
6
|
+
|
|
7
|
+
vi.mock('@axa-fr/oidc-client', () => ({
|
|
8
|
+
OidcClient: {
|
|
9
|
+
get: vi.fn(),
|
|
10
|
+
eventNames: {
|
|
11
|
+
loginCallbackAsync_navigated: 'loginCallbackAsync_navigated',
|
|
12
|
+
loginCallbackAsync_navigation_error: 'loginCallbackAsync_navigation_error',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
vi.mock('../routes/withRouter.js', () => ({
|
|
18
|
+
getCustomHistory: vi.fn(),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
import { OidcClient } from '@axa-fr/oidc-client';
|
|
22
|
+
|
|
23
|
+
import { getCustomHistory } from '../routes/withRouter.js';
|
|
24
|
+
|
|
25
|
+
describe('verifyNavigationCommitted', () => {
|
|
26
|
+
it('should return true when current path matches target path', () => {
|
|
27
|
+
const windowMock = { location: { pathname: '/dashboard' } } as Window;
|
|
28
|
+
expect(verifyNavigationCommitted('/dashboard', windowMock)).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should return false when current path does not match target path', () => {
|
|
32
|
+
const windowMock = { location: { pathname: '/callback' } } as Window;
|
|
33
|
+
expect(verifyNavigationCommitted('/dashboard', windowMock)).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should return true when target path is root', () => {
|
|
37
|
+
const windowMock = { location: { pathname: '/anything' } } as Window;
|
|
38
|
+
expect(verifyNavigationCommitted('/', windowMock)).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('CallBackSuccess', () => {
|
|
43
|
+
it('renders the success message', () => {
|
|
44
|
+
const { getByText } = render(<CallBackSuccess />);
|
|
45
|
+
expect(getByText('Authentication complete')).toBeTruthy();
|
|
46
|
+
expect(getByText('You will be redirected to your application.')).toBeTruthy();
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('CallbackManager', () => {
|
|
51
|
+
let mockPublishEvent: ReturnType<typeof vi.fn>;
|
|
52
|
+
let mockLoginCallbackAsync: ReturnType<typeof vi.fn>;
|
|
53
|
+
let mockReplaceState: ReturnType<typeof vi.fn>;
|
|
54
|
+
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
vi.useFakeTimers();
|
|
57
|
+
mockPublishEvent = vi.fn();
|
|
58
|
+
mockLoginCallbackAsync = vi.fn();
|
|
59
|
+
mockReplaceState = vi.fn();
|
|
60
|
+
|
|
61
|
+
(OidcClient.get as ReturnType<typeof vi.fn>).mockReturnValue({
|
|
62
|
+
loginCallbackAsync: mockLoginCallbackAsync,
|
|
63
|
+
publishEvent: mockPublishEvent,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
(getCustomHistory as ReturnType<typeof vi.fn>).mockReturnValue({
|
|
67
|
+
replaceState: mockReplaceState,
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
afterEach(() => {
|
|
72
|
+
vi.useRealTimers();
|
|
73
|
+
vi.clearAllMocks();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should use navigateAfterCallback when provided and emit navigated event on success', async () => {
|
|
77
|
+
mockLoginCallbackAsync.mockResolvedValue({ callbackPath: '/dashboard' });
|
|
78
|
+
const navigateAfterCallback = vi.fn().mockResolvedValue(undefined);
|
|
79
|
+
|
|
80
|
+
await act(async () => {
|
|
81
|
+
render(
|
|
82
|
+
<CallbackManager
|
|
83
|
+
configurationName="default"
|
|
84
|
+
navigateAfterCallback={navigateAfterCallback}
|
|
85
|
+
/>,
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
expect(navigateAfterCallback).toHaveBeenCalledWith('/dashboard');
|
|
90
|
+
expect(mockPublishEvent).toHaveBeenCalledWith('loginCallbackAsync_navigated', {
|
|
91
|
+
configurationName: 'default',
|
|
92
|
+
callbackPath: '/dashboard',
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should emit navigation_error event when navigateAfterCallback rejects', async () => {
|
|
97
|
+
mockLoginCallbackAsync.mockResolvedValue({ callbackPath: '/dashboard' });
|
|
98
|
+
const navError = new Error('Navigation failed');
|
|
99
|
+
const navigateAfterCallback = vi.fn().mockRejectedValue(navError);
|
|
100
|
+
|
|
101
|
+
await act(async () => {
|
|
102
|
+
render(
|
|
103
|
+
<CallbackManager
|
|
104
|
+
configurationName="default"
|
|
105
|
+
navigateAfterCallback={navigateAfterCallback}
|
|
106
|
+
/>,
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
expect(mockPublishEvent).toHaveBeenCalledWith('loginCallbackAsync_navigation_error', {
|
|
111
|
+
configurationName: 'default',
|
|
112
|
+
callbackPath: '/dashboard',
|
|
113
|
+
error: navError,
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should render error component when navigateAfterCallback fails', async () => {
|
|
118
|
+
mockLoginCallbackAsync.mockResolvedValue({ callbackPath: '/dashboard' });
|
|
119
|
+
const navigateAfterCallback = vi.fn().mockRejectedValue(new Error('fail'));
|
|
120
|
+
|
|
121
|
+
const ErrorComponent = () => <div>Error occurred</div>;
|
|
122
|
+
|
|
123
|
+
let container;
|
|
124
|
+
await act(async () => {
|
|
125
|
+
const result = render(
|
|
126
|
+
<CallbackManager
|
|
127
|
+
configurationName="default"
|
|
128
|
+
navigateAfterCallback={navigateAfterCallback}
|
|
129
|
+
callBackError={ErrorComponent}
|
|
130
|
+
/>,
|
|
131
|
+
);
|
|
132
|
+
container = result.container;
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
expect(container.textContent).toContain('Error occurred');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should use default history navigation when navigateAfterCallback is not provided', async () => {
|
|
139
|
+
vi.useRealTimers();
|
|
140
|
+
mockLoginCallbackAsync.mockResolvedValue({ callbackPath: '/dashboard' });
|
|
141
|
+
|
|
142
|
+
// Mock window.location to simulate successful navigation
|
|
143
|
+
Object.defineProperty(window, 'location', {
|
|
144
|
+
value: { pathname: '/dashboard' },
|
|
145
|
+
writable: true,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
await act(async () => {
|
|
149
|
+
render(<CallbackManager configurationName="default" />);
|
|
150
|
+
// Wait for the verification delay to pass
|
|
151
|
+
await new Promise(resolve => setTimeout(resolve, 300));
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
expect(mockReplaceState).toHaveBeenCalledWith('/dashboard');
|
|
155
|
+
|
|
156
|
+
await waitFor(() => {
|
|
157
|
+
expect(mockPublishEvent).toHaveBeenCalledWith('loginCallbackAsync_navigated', {
|
|
158
|
+
configurationName: 'default',
|
|
159
|
+
callbackPath: '/dashboard',
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('should emit navigation_error when default navigation does not commit', async () => {
|
|
165
|
+
vi.useRealTimers();
|
|
166
|
+
mockLoginCallbackAsync.mockResolvedValue({ callbackPath: '/dashboard' });
|
|
167
|
+
|
|
168
|
+
// Mock window.location to simulate failed navigation
|
|
169
|
+
Object.defineProperty(window, 'location', {
|
|
170
|
+
value: { pathname: '/callback' },
|
|
171
|
+
writable: true,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
await act(async () => {
|
|
175
|
+
render(<CallbackManager configurationName="default" />);
|
|
176
|
+
// Wait for the verification delay to pass
|
|
177
|
+
await new Promise(resolve => setTimeout(resolve, 300));
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
await waitFor(() => {
|
|
181
|
+
expect(mockPublishEvent).toHaveBeenCalledWith(
|
|
182
|
+
'loginCallbackAsync_navigation_error',
|
|
183
|
+
expect.objectContaining({
|
|
184
|
+
configurationName: 'default',
|
|
185
|
+
callbackPath: '/dashboard',
|
|
186
|
+
}),
|
|
187
|
+
);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('should use "/" as fallback when callbackPath is empty', async () => {
|
|
192
|
+
mockLoginCallbackAsync.mockResolvedValue({ callbackPath: '' });
|
|
193
|
+
const navigateAfterCallback = vi.fn().mockResolvedValue(undefined);
|
|
194
|
+
|
|
195
|
+
await act(async () => {
|
|
196
|
+
render(
|
|
197
|
+
<CallbackManager
|
|
198
|
+
configurationName="default"
|
|
199
|
+
navigateAfterCallback={navigateAfterCallback}
|
|
200
|
+
/>,
|
|
201
|
+
);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
expect(navigateAfterCallback).toHaveBeenCalledWith('/');
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('should use withCustomHistory when provided and no navigateAfterCallback', async () => {
|
|
208
|
+
mockLoginCallbackAsync.mockResolvedValue({ callbackPath: '/profile' });
|
|
209
|
+
const customReplaceState = vi.fn();
|
|
210
|
+
const withCustomHistory = vi.fn().mockReturnValue({ replaceState: customReplaceState });
|
|
211
|
+
|
|
212
|
+
Object.defineProperty(window, 'location', {
|
|
213
|
+
value: { pathname: '/' },
|
|
214
|
+
writable: true,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
await act(async () => {
|
|
218
|
+
render(<CallbackManager configurationName="default" withCustomHistory={withCustomHistory} />);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
expect(withCustomHistory).toHaveBeenCalled();
|
|
222
|
+
expect(customReplaceState).toHaveBeenCalledWith('/profile');
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('should set error state when loginCallbackAsync throws', async () => {
|
|
226
|
+
mockLoginCallbackAsync.mockRejectedValue(new Error('login error'));
|
|
227
|
+
|
|
228
|
+
const ErrorComponent = () => <div>Login Error</div>;
|
|
229
|
+
|
|
230
|
+
let container;
|
|
231
|
+
await act(async () => {
|
|
232
|
+
const result = render(
|
|
233
|
+
<CallbackManager configurationName="default" callBackError={ErrorComponent} />,
|
|
234
|
+
);
|
|
235
|
+
container = result.container;
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
expect(container.textContent).toContain('Login Error');
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('should render success component by default', () => {
|
|
242
|
+
mockLoginCallbackAsync.mockReturnValue(new Promise(() => {})); // never resolves
|
|
243
|
+
|
|
244
|
+
const { getByText } = render(<CallbackManager configurationName="default" />);
|
|
245
|
+
|
|
246
|
+
expect(getByText('Authentication complete')).toBeTruthy();
|
|
247
|
+
});
|
|
248
|
+
});
|