@jasperoosthoek/zustand-auth-registry 0.0.1
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/LICENSE +21 -0
- package/README.md +407 -0
- package/dist/authConfig.d.ts +67 -0
- package/dist/authStore.d.ts +17 -0
- package/dist/createAuthRegistry.d.ts +3 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/setupTests.d.ts +1 -0
- package/dist/useAuth.d.ts +7 -0
- package/package.json +50 -0
- package/src/__tests__/authConfig.test.ts +463 -0
- package/src/__tests__/authStore.test.ts +608 -0
- package/src/__tests__/createAuthRegistry.test.ts +202 -0
- package/src/__tests__/testHelpers.ts +92 -0
- package/src/__tests__/testUtils.ts +142 -0
- package/src/__tests__/useAuth.test.ts +975 -0
- package/src/authConfig.ts +184 -0
- package/src/authStore.ts +160 -0
- package/src/createAuthRegistry.ts +22 -0
- package/src/index.ts +4 -0
- package/src/setupTests.ts +46 -0
- package/src/useAuth.ts +157 -0
|
@@ -0,0 +1,975 @@
|
|
|
1
|
+
import { renderHook, act } from '@testing-library/react';
|
|
2
|
+
import { useAuth } from '../useAuth';
|
|
3
|
+
import { createAuthRegistry } from '../createAuthRegistry';
|
|
4
|
+
import { TestUser, createMockAxios, mockUser, resetAllMocks, extractAuthHeader } from './testHelpers';
|
|
5
|
+
import { testConfigs, mockResponses, createAxiosError } from './testUtils';
|
|
6
|
+
|
|
7
|
+
// Mock axios module
|
|
8
|
+
const mockAxios = require('axios');
|
|
9
|
+
|
|
10
|
+
describe('useAuth', () => {
|
|
11
|
+
let mockAxiosInstance: any;
|
|
12
|
+
let getAuthStore: any;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
resetAllMocks();
|
|
16
|
+
mockAxiosInstance = createMockAxios();
|
|
17
|
+
getAuthStore = createAuthRegistry<{ main: TestUser }>();
|
|
18
|
+
|
|
19
|
+
// Reset axios mocks
|
|
20
|
+
mockAxios.isAxiosError.mockReset();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('hook interface', () => {
|
|
24
|
+
it('should return auth interface with correct methods', () => {
|
|
25
|
+
const store = getAuthStore('main', {
|
|
26
|
+
...testConfigs.basic,
|
|
27
|
+
axios: mockAxiosInstance
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const { result } = renderHook(() => useAuth(store));
|
|
31
|
+
|
|
32
|
+
expect(result.current).toHaveProperty('login');
|
|
33
|
+
expect(result.current).toHaveProperty('logout');
|
|
34
|
+
expect(result.current).toHaveProperty('getCurrentUser');
|
|
35
|
+
expect(typeof result.current.login).toBe('function');
|
|
36
|
+
expect(typeof result.current.logout).toBe('function');
|
|
37
|
+
expect(typeof result.current.getCurrentUser).toBe('function');
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('login functionality', () => {
|
|
42
|
+
it('should login successfully and update state', async () => {
|
|
43
|
+
mockAxiosInstance.post.mockResolvedValue(mockResponses.loginSuccess);
|
|
44
|
+
mockAxiosInstance.get.mockResolvedValue(mockResponses.userSuccess); // Need this for getCurrentUser
|
|
45
|
+
|
|
46
|
+
const store = getAuthStore('main', {
|
|
47
|
+
...testConfigs.basic,
|
|
48
|
+
axios: mockAxiosInstance
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const { result } = renderHook(() => useAuth(store));
|
|
52
|
+
|
|
53
|
+
await act(async () => {
|
|
54
|
+
await result.current.login({ email: 'test@example.com', password: 'password' });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
expect(mockAxiosInstance.post).toHaveBeenCalledWith('/auth/login', {
|
|
58
|
+
email: 'test@example.com',
|
|
59
|
+
password: 'password'
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const state = store.getState();
|
|
63
|
+
expect(state.token).toBe('mock-jwt-token-12345');
|
|
64
|
+
expect(extractAuthHeader(mockAxiosInstance)).toBe('Bearer mock-jwt-token-12345');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should fetch current user after login when getUserUrl is provided', async () => {
|
|
68
|
+
mockAxiosInstance.post.mockResolvedValue(mockResponses.loginSuccess);
|
|
69
|
+
mockAxiosInstance.get.mockResolvedValue(mockResponses.userSuccess);
|
|
70
|
+
|
|
71
|
+
const store = getAuthStore('main', {
|
|
72
|
+
...testConfigs.basic,
|
|
73
|
+
axios: mockAxiosInstance
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const { result } = renderHook(() => useAuth(store));
|
|
77
|
+
|
|
78
|
+
await act(async () => {
|
|
79
|
+
await result.current.login({ email: 'test@example.com', password: 'password' });
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
expect(mockAxiosInstance.get).toHaveBeenCalledWith('/auth/me');
|
|
83
|
+
|
|
84
|
+
const state = store.getState();
|
|
85
|
+
expect(state.user).toEqual(mockUser);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should not fetch user when getUserUrl is not provided', async () => {
|
|
89
|
+
mockAxiosInstance.post.mockResolvedValue(mockResponses.loginSuccess);
|
|
90
|
+
|
|
91
|
+
const store = getAuthStore('main', {
|
|
92
|
+
...testConfigs.withoutGetUser,
|
|
93
|
+
axios: mockAxiosInstance
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const { result } = renderHook(() => useAuth(store));
|
|
97
|
+
|
|
98
|
+
await act(async () => {
|
|
99
|
+
await result.current.login({ email: 'test@example.com', password: 'password' });
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
expect(mockAxiosInstance.get).not.toHaveBeenCalled();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should call onLogin callback with user data', async () => {
|
|
106
|
+
const onLogin = jest.fn();
|
|
107
|
+
mockAxiosInstance.post.mockResolvedValue(mockResponses.loginSuccess);
|
|
108
|
+
mockAxiosInstance.get.mockResolvedValue(mockResponses.userSuccess);
|
|
109
|
+
|
|
110
|
+
const store = getAuthStore('main', {
|
|
111
|
+
...testConfigs.basic,
|
|
112
|
+
axios: mockAxiosInstance,
|
|
113
|
+
onLogin
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Set user in store first (simulating getCurrentUser success)
|
|
117
|
+
store.getState().setUser(mockUser);
|
|
118
|
+
|
|
119
|
+
const { result } = renderHook(() => useAuth(store));
|
|
120
|
+
|
|
121
|
+
await act(async () => {
|
|
122
|
+
await result.current.login({ email: 'test@example.com', password: 'password' });
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
expect(onLogin).toHaveBeenCalledWith(mockUser);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should call login callback when provided', async () => {
|
|
129
|
+
const callback = jest.fn();
|
|
130
|
+
mockAxiosInstance.post.mockResolvedValue(mockResponses.loginSuccess);
|
|
131
|
+
|
|
132
|
+
const store = getAuthStore('main', {
|
|
133
|
+
...testConfigs.basic,
|
|
134
|
+
axios: mockAxiosInstance
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const { result } = renderHook(() => useAuth(store));
|
|
138
|
+
|
|
139
|
+
await act(async () => {
|
|
140
|
+
await result.current.login({ email: 'test@example.com', password: 'password' }, callback);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
expect(callback).toHaveBeenCalled();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should handle login errors', async () => {
|
|
147
|
+
const onError = jest.fn();
|
|
148
|
+
const loginError = createAxiosError('Invalid credentials', 401);
|
|
149
|
+
mockAxiosInstance.post.mockRejectedValue(loginError);
|
|
150
|
+
|
|
151
|
+
const store = getAuthStore('main', {
|
|
152
|
+
...testConfigs.basic,
|
|
153
|
+
axios: mockAxiosInstance,
|
|
154
|
+
onError
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const { result } = renderHook(() => useAuth(store));
|
|
158
|
+
|
|
159
|
+
await act(async () => {
|
|
160
|
+
await result.current.login({ email: 'wrong@example.com', password: 'wrong' });
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
expect(onError).toHaveBeenCalledWith(loginError);
|
|
164
|
+
expect(store.getState().isAuthenticated).toBe(false);
|
|
165
|
+
expect(extractAuthHeader(mockAxiosInstance)).toBeUndefined();
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('logout functionality', () => {
|
|
170
|
+
it('should logout successfully and clear state', async () => {
|
|
171
|
+
mockAxiosInstance.post.mockResolvedValue(mockResponses.logoutSuccess);
|
|
172
|
+
|
|
173
|
+
const store = getAuthStore('main', {
|
|
174
|
+
...testConfigs.basic,
|
|
175
|
+
axios: mockAxiosInstance
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Set initial authenticated state
|
|
179
|
+
store.getState().setToken('token');
|
|
180
|
+
store.getState().setUser(mockUser);
|
|
181
|
+
|
|
182
|
+
const { result } = renderHook(() => useAuth(store));
|
|
183
|
+
|
|
184
|
+
await act(async () => {
|
|
185
|
+
await result.current.logout();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
expect(mockAxiosInstance.post).toHaveBeenCalledWith('/auth/logout');
|
|
189
|
+
|
|
190
|
+
const state = store.getState();
|
|
191
|
+
expect(state.user).toBeNull();
|
|
192
|
+
expect(state.token).toBe('');
|
|
193
|
+
expect(state.isAuthenticated).toBe(false);
|
|
194
|
+
expect(extractAuthHeader(mockAxiosInstance)).toBeUndefined();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('should call onLogout callback', async () => {
|
|
198
|
+
const onLogout = jest.fn();
|
|
199
|
+
mockAxiosInstance.post.mockResolvedValue(mockResponses.logoutSuccess);
|
|
200
|
+
|
|
201
|
+
const store = getAuthStore('main', {
|
|
202
|
+
...testConfigs.basic,
|
|
203
|
+
axios: mockAxiosInstance,
|
|
204
|
+
onLogout
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const { result } = renderHook(() => useAuth(store));
|
|
208
|
+
|
|
209
|
+
await act(async () => {
|
|
210
|
+
await result.current.logout();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
expect(onLogout).toHaveBeenCalled();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('should clear state even when logout API fails', async () => {
|
|
217
|
+
const onError = jest.fn();
|
|
218
|
+
const logoutError = createAxiosError('Server error', 500);
|
|
219
|
+
mockAxiosInstance.post.mockRejectedValue(logoutError);
|
|
220
|
+
|
|
221
|
+
const store = getAuthStore('main', {
|
|
222
|
+
...testConfigs.basic,
|
|
223
|
+
axios: mockAxiosInstance,
|
|
224
|
+
onError
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// Set initial authenticated state
|
|
228
|
+
store.getState().setToken('token');
|
|
229
|
+
store.getState().setUser(mockUser);
|
|
230
|
+
|
|
231
|
+
const { result } = renderHook(() => useAuth(store));
|
|
232
|
+
|
|
233
|
+
await act(async () => {
|
|
234
|
+
await result.current.logout();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
expect(onError).toHaveBeenCalledWith(logoutError);
|
|
238
|
+
|
|
239
|
+
const state = store.getState();
|
|
240
|
+
expect(state.user).toBeNull();
|
|
241
|
+
expect(state.token).toBe('');
|
|
242
|
+
expect(state.isAuthenticated).toBe(false);
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
describe('getCurrentUser functionality', () => {
|
|
247
|
+
it('should fetch current user successfully', async () => {
|
|
248
|
+
mockAxiosInstance.get.mockResolvedValue(mockResponses.userSuccess);
|
|
249
|
+
|
|
250
|
+
const store = getAuthStore('main', {
|
|
251
|
+
...testConfigs.basic,
|
|
252
|
+
axios: mockAxiosInstance
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
const { result } = renderHook(() => useAuth(store));
|
|
256
|
+
|
|
257
|
+
await act(async () => {
|
|
258
|
+
await result.current.getCurrentUser();
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
expect(mockAxiosInstance.get).toHaveBeenCalledWith('/auth/me');
|
|
262
|
+
expect(store.getState().user).toEqual(mockUser);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('should not make request when getUserUrl is not configured', async () => {
|
|
266
|
+
const store = getAuthStore('main', {
|
|
267
|
+
...testConfigs.withoutGetUser,
|
|
268
|
+
axios: mockAxiosInstance
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const { result } = renderHook(() => useAuth(store));
|
|
272
|
+
|
|
273
|
+
await act(async () => {
|
|
274
|
+
await result.current.getCurrentUser();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
expect(mockAxiosInstance.get).not.toHaveBeenCalled();
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('should handle getCurrentUser errors', async () => {
|
|
281
|
+
const onError = jest.fn();
|
|
282
|
+
const userError = createAxiosError('Unauthorized', 401);
|
|
283
|
+
mockAxiosInstance.get.mockRejectedValue(userError);
|
|
284
|
+
|
|
285
|
+
const store = getAuthStore('main', {
|
|
286
|
+
...testConfigs.basic,
|
|
287
|
+
axios: mockAxiosInstance,
|
|
288
|
+
onError
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// Set initial state
|
|
292
|
+
store.getState().setToken('token');
|
|
293
|
+
|
|
294
|
+
const { result } = renderHook(() => useAuth(store));
|
|
295
|
+
|
|
296
|
+
await act(async () => {
|
|
297
|
+
await result.current.getCurrentUser();
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
expect(onError).toHaveBeenCalledWith(userError);
|
|
301
|
+
expect(store.getState().isAuthenticated).toBe(false);
|
|
302
|
+
expect(extractAuthHeader(mockAxiosInstance)).toBeUndefined();
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
describe('axios header management', () => {
|
|
307
|
+
it('should set Bearer authorization header by default', async () => {
|
|
308
|
+
mockAxiosInstance.post.mockResolvedValue(mockResponses.loginSuccess);
|
|
309
|
+
mockAxiosInstance.get.mockResolvedValue(mockResponses.userSuccess);
|
|
310
|
+
|
|
311
|
+
const store = getAuthStore('main', {
|
|
312
|
+
...testConfigs.basic,
|
|
313
|
+
axios: mockAxiosInstance
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
const { result } = renderHook(() => useAuth(store));
|
|
317
|
+
|
|
318
|
+
await act(async () => {
|
|
319
|
+
await result.current.login({ email: 'test@example.com', password: 'password' });
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
expect(extractAuthHeader(mockAxiosInstance)).toBe('Bearer mock-jwt-token-12345');
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('should use custom auth header format', async () => {
|
|
326
|
+
mockAxiosInstance.post.mockResolvedValue(mockResponses.loginSuccess);
|
|
327
|
+
mockAxiosInstance.get.mockResolvedValue(mockResponses.userSuccess);
|
|
328
|
+
|
|
329
|
+
const store = getAuthStore('main', {
|
|
330
|
+
...testConfigs.withTokenFormat,
|
|
331
|
+
axios: mockAxiosInstance
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
const { result } = renderHook(() => useAuth(store));
|
|
335
|
+
|
|
336
|
+
await act(async () => {
|
|
337
|
+
await result.current.login({ email: 'test@example.com', password: 'password' });
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
expect(extractAuthHeader(mockAxiosInstance)).toBe('Token mock-jwt-token-12345');
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it('should remove authorization header on logout', async () => {
|
|
344
|
+
mockAxiosInstance.post.mockResolvedValue(mockResponses.logoutSuccess);
|
|
345
|
+
|
|
346
|
+
const store = getAuthStore('main', {
|
|
347
|
+
...testConfigs.basic,
|
|
348
|
+
axios: mockAxiosInstance
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// Set initial header
|
|
352
|
+
mockAxiosInstance.defaults.headers.common['Authorization'] = 'Bearer token';
|
|
353
|
+
|
|
354
|
+
const { result } = renderHook(() => useAuth(store));
|
|
355
|
+
|
|
356
|
+
await act(async () => {
|
|
357
|
+
await result.current.logout();
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
expect(extractAuthHeader(mockAxiosInstance)).toBeUndefined();
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
describe('useEffect behavior', () => {
|
|
365
|
+
it('should set axios headers when token exists on mount', () => {
|
|
366
|
+
const mockStorage = window.localStorage as jest.Mocked<Storage>;
|
|
367
|
+
mockStorage.getItem.mockImplementation((key: string) => {
|
|
368
|
+
if (key === 'token') return 'existing-token';
|
|
369
|
+
if (key === 'user') return JSON.stringify(mockUser);
|
|
370
|
+
return null;
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
const store = getAuthStore('main', {
|
|
374
|
+
...testConfigs.basic,
|
|
375
|
+
axios: mockAxiosInstance
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
renderHook(() => useAuth(store));
|
|
379
|
+
|
|
380
|
+
expect(extractAuthHeader(mockAxiosInstance)).toBe('Bearer existing-token');
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it('should fetch current user when token exists but user is missing', async () => {
|
|
384
|
+
mockAxiosInstance.get.mockResolvedValue(mockResponses.userSuccess);
|
|
385
|
+
|
|
386
|
+
const mockStorage = window.localStorage as jest.Mocked<Storage>;
|
|
387
|
+
mockStorage.getItem.mockImplementation((key: string) => {
|
|
388
|
+
if (key === 'token') return 'existing-token';
|
|
389
|
+
return null;
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
const store = getAuthStore('main', {
|
|
393
|
+
...testConfigs.basic,
|
|
394
|
+
axios: mockAxiosInstance
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
renderHook(() => useAuth(store));
|
|
398
|
+
|
|
399
|
+
// Wait for useEffect to complete
|
|
400
|
+
await act(async () => {
|
|
401
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
expect(mockAxiosInstance.get).toHaveBeenCalledWith('/auth/me');
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it('should handle 403 errors during initialization', async () => {
|
|
408
|
+
mockAxios.isAxiosError.mockReturnValue(true);
|
|
409
|
+
const forbiddenError = createAxiosError('Forbidden', 403);
|
|
410
|
+
mockAxiosInstance.get.mockRejectedValue(forbiddenError);
|
|
411
|
+
|
|
412
|
+
const mockStorage = window.localStorage as jest.Mocked<Storage>;
|
|
413
|
+
mockStorage.getItem.mockImplementation((key: string) => {
|
|
414
|
+
if (key === 'token') return 'expired-token';
|
|
415
|
+
return null;
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
const store = getAuthStore('main', {
|
|
419
|
+
...testConfigs.basic,
|
|
420
|
+
axios: mockAxiosInstance
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
renderHook(() => useAuth(store));
|
|
424
|
+
|
|
425
|
+
// Wait for useEffect to complete
|
|
426
|
+
await act(async () => {
|
|
427
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
expect(store.getState().isAuthenticated).toBe(false);
|
|
431
|
+
expect(extractAuthHeader(mockAxiosInstance)).toBeUndefined();
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
describe('OAuth 2.0 Features', () => {
|
|
436
|
+
describe('token refresh functionality', () => {
|
|
437
|
+
it('should return false when no refresh token available', async () => {
|
|
438
|
+
const store = getAuthStore('main', {
|
|
439
|
+
...testConfigs.basic,
|
|
440
|
+
axios: mockAxiosInstance
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
// Set tokens without refresh token
|
|
444
|
+
store.getState().setTokens({
|
|
445
|
+
accessToken: 'current-token',
|
|
446
|
+
tokenType: 'Bearer'
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
const { result } = renderHook(() => useAuth(store));
|
|
450
|
+
|
|
451
|
+
let refreshResult: boolean;
|
|
452
|
+
await act(async () => {
|
|
453
|
+
refreshResult = await result.current.refreshTokens();
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
expect(refreshResult!).toBe(false);
|
|
457
|
+
expect(mockAxiosInstance.post).not.toHaveBeenCalled();
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it('should successfully refresh tokens and update state', async () => {
|
|
461
|
+
const refreshResponse = {
|
|
462
|
+
data: {
|
|
463
|
+
access_token: 'new-access-token',
|
|
464
|
+
refresh_token: 'new-refresh-token',
|
|
465
|
+
expires_in: 3600,
|
|
466
|
+
token_type: 'Bearer'
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
mockAxiosInstance.post.mockResolvedValue(refreshResponse);
|
|
470
|
+
|
|
471
|
+
const store = getAuthStore('main', {
|
|
472
|
+
...testConfigs.basic,
|
|
473
|
+
axios: mockAxiosInstance,
|
|
474
|
+
tokenUrl: '/oauth/token'
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
// Set tokens with refresh token
|
|
478
|
+
store.getState().setTokens({
|
|
479
|
+
accessToken: 'old-access-token',
|
|
480
|
+
refreshToken: 'old-refresh-token',
|
|
481
|
+
tokenType: 'Bearer',
|
|
482
|
+
expiresAt: Date.now() + 300000
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
const { result } = renderHook(() => useAuth(store));
|
|
486
|
+
|
|
487
|
+
let refreshResult: boolean;
|
|
488
|
+
await act(async () => {
|
|
489
|
+
refreshResult = await result.current.refreshTokens();
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
expect(refreshResult!).toBe(true);
|
|
493
|
+
expect(mockAxiosInstance.post).toHaveBeenCalledWith('/oauth/token', {
|
|
494
|
+
grant_type: 'refresh_token',
|
|
495
|
+
refresh_token: 'old-refresh-token'
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
const state = store.getState();
|
|
499
|
+
expect(state.tokens?.accessToken).toBe('new-access-token');
|
|
500
|
+
expect(state.tokens?.refreshToken).toBe('new-refresh-token');
|
|
501
|
+
expect(state.token).toBe('new-access-token'); // Backward compatibility
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
it('should call onTokenRefresh callback after successful refresh', async () => {
|
|
505
|
+
const onTokenRefresh = jest.fn();
|
|
506
|
+
const refreshResponse = {
|
|
507
|
+
data: {
|
|
508
|
+
access_token: 'new-token',
|
|
509
|
+
refresh_token: 'new-refresh',
|
|
510
|
+
expires_in: 3600,
|
|
511
|
+
token_type: 'Bearer'
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
mockAxiosInstance.post.mockResolvedValue(refreshResponse);
|
|
515
|
+
|
|
516
|
+
const store = getAuthStore('main', {
|
|
517
|
+
...testConfigs.basic,
|
|
518
|
+
axios: mockAxiosInstance,
|
|
519
|
+
tokenUrl: '/oauth/token',
|
|
520
|
+
onTokenRefresh
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
store.getState().setTokens({
|
|
524
|
+
accessToken: 'old-token',
|
|
525
|
+
refreshToken: 'old-refresh',
|
|
526
|
+
tokenType: 'Bearer'
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
const { result } = renderHook(() => useAuth(store));
|
|
530
|
+
|
|
531
|
+
await act(async () => {
|
|
532
|
+
await result.current.refreshTokens();
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
expect(onTokenRefresh).toHaveBeenCalledWith({
|
|
536
|
+
accessToken: 'new-token',
|
|
537
|
+
refreshToken: 'new-refresh',
|
|
538
|
+
expiresAt: expect.any(Number),
|
|
539
|
+
tokenType: 'Bearer'
|
|
540
|
+
});
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
it('should handle refresh failure and clear state', async () => {
|
|
544
|
+
const refreshError = new Error('Refresh failed');
|
|
545
|
+
mockAxiosInstance.post.mockRejectedValue(refreshError);
|
|
546
|
+
|
|
547
|
+
const store = getAuthStore('main', {
|
|
548
|
+
...testConfigs.basic,
|
|
549
|
+
axios: mockAxiosInstance,
|
|
550
|
+
tokenUrl: '/oauth/token'
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
// Set initial state with user and tokens
|
|
554
|
+
store.getState().setTokens({
|
|
555
|
+
accessToken: 'old-token',
|
|
556
|
+
refreshToken: 'old-refresh',
|
|
557
|
+
tokenType: 'Bearer'
|
|
558
|
+
});
|
|
559
|
+
store.getState().setUser(mockUser);
|
|
560
|
+
|
|
561
|
+
const { result } = renderHook(() => useAuth(store));
|
|
562
|
+
|
|
563
|
+
let refreshResult: boolean;
|
|
564
|
+
await act(async () => {
|
|
565
|
+
refreshResult = await result.current.refreshTokens();
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
expect(refreshResult!).toBe(false);
|
|
569
|
+
|
|
570
|
+
const state = store.getState();
|
|
571
|
+
expect(state.user).toBeNull();
|
|
572
|
+
expect(state.tokens).toBeNull();
|
|
573
|
+
expect(state.isAuthenticated).toBe(false);
|
|
574
|
+
expect(mockAxiosInstance.defaults.headers.common['Authorization']).toBeUndefined();
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
it('should call onError callback when refresh fails', async () => {
|
|
578
|
+
const onError = jest.fn();
|
|
579
|
+
const refreshError = new Error('Network error');
|
|
580
|
+
mockAxiosInstance.post.mockRejectedValue(refreshError);
|
|
581
|
+
|
|
582
|
+
const store = getAuthStore('main', {
|
|
583
|
+
...testConfigs.basic,
|
|
584
|
+
axios: mockAxiosInstance,
|
|
585
|
+
tokenUrl: '/oauth/token',
|
|
586
|
+
onError
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
store.getState().setTokens({
|
|
590
|
+
accessToken: 'old-token',
|
|
591
|
+
refreshToken: 'old-refresh',
|
|
592
|
+
tokenType: 'Bearer'
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
const { result } = renderHook(() => useAuth(store));
|
|
596
|
+
|
|
597
|
+
await act(async () => {
|
|
598
|
+
await result.current.refreshTokens();
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
expect(onError).toHaveBeenCalledWith(refreshError);
|
|
602
|
+
});
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
describe('auto-refresh timing', () => {
|
|
606
|
+
beforeEach(() => {
|
|
607
|
+
jest.useFakeTimers();
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
afterEach(() => {
|
|
611
|
+
jest.useRealTimers();
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
it('should refresh tokens immediately when already expired', async () => {
|
|
615
|
+
const refreshResponse = {
|
|
616
|
+
data: {
|
|
617
|
+
access_token: 'refreshed-token',
|
|
618
|
+
refresh_token: 'new-refresh',
|
|
619
|
+
expires_in: 3600,
|
|
620
|
+
token_type: 'Bearer'
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
mockAxiosInstance.post.mockResolvedValue(refreshResponse);
|
|
624
|
+
|
|
625
|
+
const store = getAuthStore('main', {
|
|
626
|
+
...testConfigs.basic,
|
|
627
|
+
axios: mockAxiosInstance,
|
|
628
|
+
tokenUrl: '/oauth/token',
|
|
629
|
+
autoRefresh: true
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
// Set expired tokens
|
|
633
|
+
const expiredTime = Date.now() - 1000; // 1 second ago
|
|
634
|
+
store.getState().setTokens({
|
|
635
|
+
accessToken: 'expired-token',
|
|
636
|
+
refreshToken: 'valid-refresh',
|
|
637
|
+
tokenType: 'Bearer',
|
|
638
|
+
expiresAt: expiredTime
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
await act(async () => {
|
|
642
|
+
renderHook(() => useAuth(store));
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
// Should attempt refresh immediately
|
|
646
|
+
expect(mockAxiosInstance.post).toHaveBeenCalledWith('/oauth/token', {
|
|
647
|
+
grant_type: 'refresh_token',
|
|
648
|
+
refresh_token: 'valid-refresh'
|
|
649
|
+
});
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
it('should clear state when token expired and no refresh available', async () => {
|
|
653
|
+
const store = getAuthStore('main', {
|
|
654
|
+
...testConfigs.basic,
|
|
655
|
+
axios: mockAxiosInstance,
|
|
656
|
+
autoRefresh: true
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
// Set expired tokens without refresh token
|
|
660
|
+
const expiredTime = Date.now() - 1000;
|
|
661
|
+
store.getState().setTokens({
|
|
662
|
+
accessToken: 'expired-token',
|
|
663
|
+
tokenType: 'Bearer',
|
|
664
|
+
expiresAt: expiredTime
|
|
665
|
+
});
|
|
666
|
+
store.getState().setUser(mockUser);
|
|
667
|
+
|
|
668
|
+
await act(async () => {
|
|
669
|
+
renderHook(() => useAuth(store));
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
const state = store.getState();
|
|
673
|
+
expect(state.user).toBeNull();
|
|
674
|
+
expect(state.tokens).toBeNull();
|
|
675
|
+
expect(state.isAuthenticated).toBe(false);
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
it('should set up auto-refresh timer before token expiry', async () => {
|
|
679
|
+
const setTimeoutSpy = jest.spyOn(global, 'setTimeout');
|
|
680
|
+
const refreshResponse = {
|
|
681
|
+
data: {
|
|
682
|
+
access_token: 'refreshed-token',
|
|
683
|
+
refresh_token: 'new-refresh',
|
|
684
|
+
expires_in: 3600,
|
|
685
|
+
token_type: 'Bearer'
|
|
686
|
+
}
|
|
687
|
+
};
|
|
688
|
+
mockAxiosInstance.post.mockResolvedValue(refreshResponse);
|
|
689
|
+
|
|
690
|
+
const store = getAuthStore('main', {
|
|
691
|
+
...testConfigs.basic,
|
|
692
|
+
axios: mockAxiosInstance,
|
|
693
|
+
tokenUrl: '/oauth/token',
|
|
694
|
+
autoRefresh: true,
|
|
695
|
+
refreshThreshold: 300000 // 5 minutes
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
// Set tokens that expire in 10 minutes
|
|
699
|
+
const futureExpiry = Date.now() + 600000; // 10 minutes
|
|
700
|
+
store.getState().setTokens({
|
|
701
|
+
accessToken: 'valid-token',
|
|
702
|
+
refreshToken: 'valid-refresh',
|
|
703
|
+
tokenType: 'Bearer',
|
|
704
|
+
expiresAt: futureExpiry
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
await act(async () => {
|
|
708
|
+
renderHook(() => useAuth(store));
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
// Timer should be set for 5 minutes from now (10 - 5 threshold)
|
|
712
|
+
expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), 300000);
|
|
713
|
+
|
|
714
|
+
// Fast-forward to when refresh should happen
|
|
715
|
+
await act(async () => {
|
|
716
|
+
jest.advanceTimersByTime(300000);
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
expect(mockAxiosInstance.post).toHaveBeenCalledWith('/oauth/token', {
|
|
720
|
+
grant_type: 'refresh_token',
|
|
721
|
+
refresh_token: 'valid-refresh'
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
setTimeoutSpy.mockRestore();
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
it('should respect refreshThreshold configuration', async () => {
|
|
728
|
+
const setTimeoutSpy = jest.spyOn(global, 'setTimeout');
|
|
729
|
+
const store = getAuthStore('main', {
|
|
730
|
+
...testConfigs.basic,
|
|
731
|
+
axios: mockAxiosInstance,
|
|
732
|
+
autoRefresh: true,
|
|
733
|
+
refreshThreshold: 120000 // 2 minutes
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
// Set tokens that expire in 5 minutes
|
|
737
|
+
const futureExpiry = Date.now() + 300000;
|
|
738
|
+
store.getState().setTokens({
|
|
739
|
+
accessToken: 'valid-token',
|
|
740
|
+
refreshToken: 'valid-refresh',
|
|
741
|
+
tokenType: 'Bearer',
|
|
742
|
+
expiresAt: futureExpiry
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
await act(async () => {
|
|
746
|
+
renderHook(() => useAuth(store));
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
// Timer should be set for 3 minutes from now (5 - 2 threshold)
|
|
750
|
+
expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), 180000);
|
|
751
|
+
|
|
752
|
+
setTimeoutSpy.mockRestore();
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
it('should cleanup timers when component unmounts', async () => {
|
|
756
|
+
const clearTimeoutSpy = jest.spyOn(global, 'clearTimeout');
|
|
757
|
+
const store = getAuthStore('main', {
|
|
758
|
+
...testConfigs.basic,
|
|
759
|
+
axios: mockAxiosInstance,
|
|
760
|
+
autoRefresh: true
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
const futureExpiry = Date.now() + 600000;
|
|
764
|
+
store.getState().setTokens({
|
|
765
|
+
accessToken: 'valid-token',
|
|
766
|
+
refreshToken: 'valid-refresh',
|
|
767
|
+
tokenType: 'Bearer',
|
|
768
|
+
expiresAt: futureExpiry
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
const { unmount } = renderHook(() => useAuth(store));
|
|
772
|
+
|
|
773
|
+
await act(async () => {
|
|
774
|
+
unmount();
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
expect(clearTimeoutSpy).toHaveBeenCalled();
|
|
778
|
+
|
|
779
|
+
clearTimeoutSpy.mockRestore();
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
it('should not set up timer when autoRefresh is disabled', async () => {
|
|
783
|
+
const setTimeoutSpy = jest.spyOn(global, 'setTimeout');
|
|
784
|
+
const store = getAuthStore('main', {
|
|
785
|
+
...testConfigs.basic,
|
|
786
|
+
axios: mockAxiosInstance,
|
|
787
|
+
autoRefresh: false
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
const futureExpiry = Date.now() + 600000;
|
|
791
|
+
store.getState().setTokens({
|
|
792
|
+
accessToken: 'valid-token',
|
|
793
|
+
refreshToken: 'valid-refresh',
|
|
794
|
+
tokenType: 'Bearer',
|
|
795
|
+
expiresAt: futureExpiry
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
await act(async () => {
|
|
799
|
+
renderHook(() => useAuth(store));
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
expect(setTimeoutSpy).not.toHaveBeenCalled();
|
|
803
|
+
|
|
804
|
+
setTimeoutSpy.mockRestore();
|
|
805
|
+
});
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
describe('enhanced error handling', () => {
|
|
809
|
+
it('should handle 403 errors during token validation', async () => {
|
|
810
|
+
const error403 = {
|
|
811
|
+
response: { status: 403 },
|
|
812
|
+
isAxiosError: true
|
|
813
|
+
};
|
|
814
|
+
|
|
815
|
+
mockAxios.isAxiosError.mockReturnValue(true);
|
|
816
|
+
mockAxiosInstance.get.mockRejectedValue(error403);
|
|
817
|
+
|
|
818
|
+
const store = getAuthStore('main', {
|
|
819
|
+
...testConfigs.basic,
|
|
820
|
+
axios: mockAxiosInstance,
|
|
821
|
+
userInfoUrl: '/oauth/userinfo'
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
// Set tokens and trigger user info fetch
|
|
825
|
+
store.getState().setTokens({
|
|
826
|
+
accessToken: 'invalid-token',
|
|
827
|
+
tokenType: 'Bearer'
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
await act(async () => {
|
|
831
|
+
renderHook(() => useAuth(store));
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
const state = store.getState();
|
|
835
|
+
expect(state.user).toBeNull();
|
|
836
|
+
expect(state.tokens).toBeNull();
|
|
837
|
+
expect(state.isAuthenticated).toBe(false);
|
|
838
|
+
expect(mockAxiosInstance.defaults.headers.common['Authorization']).toBeUndefined();
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
it('should call onError callback for useEffect errors', async () => {
|
|
842
|
+
const onError = jest.fn();
|
|
843
|
+
const networkError = new Error('Network error');
|
|
844
|
+
|
|
845
|
+
mockAxios.isAxiosError.mockReturnValue(false);
|
|
846
|
+
mockAxiosInstance.get.mockRejectedValue(networkError);
|
|
847
|
+
|
|
848
|
+
const store = getAuthStore('main', {
|
|
849
|
+
...testConfigs.basic,
|
|
850
|
+
axios: mockAxiosInstance,
|
|
851
|
+
userInfoUrl: '/oauth/userinfo',
|
|
852
|
+
onError
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
store.getState().setTokens({
|
|
856
|
+
accessToken: 'valid-token',
|
|
857
|
+
tokenType: 'Bearer'
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
await act(async () => {
|
|
861
|
+
renderHook(() => useAuth(store));
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
expect(onError).toHaveBeenCalledWith(networkError);
|
|
865
|
+
});
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
describe('OAuth logout functionality', () => {
|
|
869
|
+
it('should call OAuth revoke endpoint with refresh token', async () => {
|
|
870
|
+
mockAxiosInstance.post.mockResolvedValue({ data: {} });
|
|
871
|
+
|
|
872
|
+
const store = getAuthStore('main', {
|
|
873
|
+
...testConfigs.basic,
|
|
874
|
+
axios: mockAxiosInstance,
|
|
875
|
+
revokeUrl: '/oauth/revoke'
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
store.getState().setTokens({
|
|
879
|
+
accessToken: 'access-token',
|
|
880
|
+
refreshToken: 'refresh-token',
|
|
881
|
+
tokenType: 'Bearer'
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
const { result } = renderHook(() => useAuth(store));
|
|
885
|
+
|
|
886
|
+
await act(async () => {
|
|
887
|
+
await result.current.logout();
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
expect(mockAxiosInstance.post).toHaveBeenCalledWith('/oauth/revoke', {
|
|
891
|
+
token: 'refresh-token',
|
|
892
|
+
token_type_hint: 'refresh_token'
|
|
893
|
+
});
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
it('should send proper token_type_hint for revocation', async () => {
|
|
897
|
+
mockAxiosInstance.post.mockResolvedValue({ data: {} });
|
|
898
|
+
|
|
899
|
+
const store = getAuthStore('main', {
|
|
900
|
+
...testConfigs.basic,
|
|
901
|
+
axios: mockAxiosInstance,
|
|
902
|
+
revokeUrl: '/oauth/revoke'
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
store.getState().setTokens({
|
|
906
|
+
accessToken: 'access-token',
|
|
907
|
+
refreshToken: 'refresh-token',
|
|
908
|
+
tokenType: 'Bearer'
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
const { result } = renderHook(() => useAuth(store));
|
|
912
|
+
|
|
913
|
+
await act(async () => {
|
|
914
|
+
await result.current.logout();
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
expect(mockAxiosInstance.post).toHaveBeenCalledWith('/oauth/revoke',
|
|
918
|
+
expect.objectContaining({
|
|
919
|
+
token_type_hint: 'refresh_token'
|
|
920
|
+
})
|
|
921
|
+
);
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
it('should fallback to simple logout when no refresh token', async () => {
|
|
925
|
+
mockAxiosInstance.post.mockResolvedValue({ data: {} });
|
|
926
|
+
|
|
927
|
+
const store = getAuthStore('main', {
|
|
928
|
+
...testConfigs.basic,
|
|
929
|
+
axios: mockAxiosInstance,
|
|
930
|
+
logoutUrl: '/auth/logout'
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
store.getState().setTokens({
|
|
934
|
+
accessToken: 'access-token',
|
|
935
|
+
tokenType: 'Bearer'
|
|
936
|
+
// No refresh token
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
const { result } = renderHook(() => useAuth(store));
|
|
940
|
+
|
|
941
|
+
await act(async () => {
|
|
942
|
+
await result.current.logout();
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
expect(mockAxiosInstance.post).toHaveBeenCalledWith('/auth/logout');
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
it('should fallback to simple logout when revokeUrl not configured', async () => {
|
|
949
|
+
mockAxiosInstance.post.mockResolvedValue({ data: {} });
|
|
950
|
+
|
|
951
|
+
const store = getAuthStore('main', {
|
|
952
|
+
...testConfigs.basic,
|
|
953
|
+
axios: mockAxiosInstance,
|
|
954
|
+
logoutUrl: '/auth/logout'
|
|
955
|
+
// No revokeUrl
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
store.getState().setTokens({
|
|
959
|
+
accessToken: 'access-token',
|
|
960
|
+
refreshToken: 'refresh-token',
|
|
961
|
+
tokenType: 'Bearer'
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
const { result } = renderHook(() => useAuth(store));
|
|
965
|
+
|
|
966
|
+
await act(async () => {
|
|
967
|
+
await result.current.logout();
|
|
968
|
+
});
|
|
969
|
+
|
|
970
|
+
// Since config.revokeUrl is undefined, it should use simple logout
|
|
971
|
+
expect(mockAxiosInstance.post).toHaveBeenCalledWith('/auth/logout');
|
|
972
|
+
});
|
|
973
|
+
});
|
|
974
|
+
});
|
|
975
|
+
});
|