@jasperoosthoek/zustand-auth-registry 0.0.1 → 0.0.2

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.
@@ -1,975 +0,0 @@
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
- });