@jasperoosthoek/zustand-auth-registry 0.0.1 → 0.0.3

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,463 +0,0 @@
1
- import { validateAuthConfig } from '../authConfig';
2
- import { createMockAxios } from './testHelpers';
3
- import { testConfigs } from './testUtils';
4
-
5
- describe('validateAuthConfig', () => {
6
- let mockAxios: any;
7
-
8
- beforeEach(() => {
9
- mockAxios = createMockAxios();
10
- });
11
-
12
- describe('required field validation', () => {
13
- it('should validate that axios is required', () => {
14
- expect(() => {
15
- validateAuthConfig({
16
- loginUrl: '/login',
17
- logoutUrl: '/logout',
18
- extractToken: (data) => data.token
19
- } as any);
20
- }).toThrow('AuthConfig: axios instance is required');
21
- });
22
-
23
- it('should validate that loginUrl is required', () => {
24
- expect(() => {
25
- validateAuthConfig({
26
- axios: mockAxios,
27
- logoutUrl: '/logout',
28
- extractToken: (data) => data.token
29
- } as any);
30
- }).toThrow('AuthConfig: tokenUrl or loginUrl is required');
31
- });
32
-
33
- it('should accept missing logoutUrl (OAuth compatible)', () => {
34
- expect(() => {
35
- validateAuthConfig({
36
- axios: mockAxios,
37
- loginUrl: '/login',
38
- extractToken: (data) => data.token
39
- } as any);
40
- }).not.toThrow();
41
- });
42
-
43
- it('should accept missing extractToken (OAuth compatible)', () => {
44
- expect(() => {
45
- validateAuthConfig({
46
- axios: mockAxios,
47
- loginUrl: '/login',
48
- logoutUrl: '/logout'
49
- } as any);
50
- }).not.toThrow();
51
- });
52
-
53
- it('should pass validation with all required fields', () => {
54
- const config = {
55
- axios: mockAxios,
56
- loginUrl: '/login',
57
- logoutUrl: '/logout',
58
- extractToken: (data: any) => data.token
59
- };
60
-
61
- expect(() => validateAuthConfig(config)).not.toThrow();
62
- });
63
- });
64
-
65
- describe('default values', () => {
66
- it('should apply default persistence settings', () => {
67
- const config = {
68
- axios: mockAxios,
69
- loginUrl: '/login',
70
- logoutUrl: '/logout',
71
- extractToken: (data: any) => data.token
72
- };
73
-
74
- const validated = validateAuthConfig(config);
75
-
76
- expect(validated.persistence).toEqual({
77
- enabled: true,
78
- storage: expect.any(Object),
79
- tokenKey: 'token',
80
- refreshTokenKey: 'refresh_token',
81
- userKey: 'user',
82
- expiryKey: 'expires_at'
83
- });
84
- });
85
-
86
- it('should apply default Bearer auth header format', () => {
87
- const config = {
88
- axios: mockAxios,
89
- loginUrl: '/login',
90
- logoutUrl: '/logout',
91
- extractToken: (data: any) => data.token
92
- };
93
-
94
- const validated = validateAuthConfig(config);
95
- const headerValue = validated.formatAuthHeader('test-token');
96
-
97
- expect(headerValue).toBe('Bearer test-token');
98
- });
99
-
100
- it('should preserve custom formatAuthHeader', () => {
101
- const config = {
102
- axios: mockAxios,
103
- loginUrl: '/login',
104
- logoutUrl: '/logout',
105
- extractToken: (data: any) => data.token,
106
- formatAuthHeader: (token: string) => `Token ${token}`
107
- };
108
-
109
- const validated = validateAuthConfig(config);
110
- const headerValue = validated.formatAuthHeader('test-token');
111
-
112
- expect(headerValue).toBe('Token test-token');
113
- });
114
-
115
- it('should preserve custom persistence settings', () => {
116
- const customStorage = window.sessionStorage;
117
- const config = {
118
- axios: mockAxios,
119
- loginUrl: '/login',
120
- logoutUrl: '/logout',
121
- extractToken: (data: any) => data.token,
122
- persistence: {
123
- enabled: false,
124
- storage: customStorage,
125
- tokenKey: 'custom_token',
126
- userKey: 'custom_user'
127
- }
128
- };
129
-
130
- const validated = validateAuthConfig(config);
131
-
132
- expect(validated.persistence).toEqual({
133
- enabled: false,
134
- storage: customStorage,
135
- tokenKey: 'custom_token',
136
- refreshTokenKey: 'refresh_token', // OAuth defaults are still applied
137
- userKey: 'custom_user',
138
- expiryKey: 'expires_at' // OAuth defaults are still applied
139
- });
140
- });
141
- });
142
-
143
- describe('optional fields', () => {
144
- it('should handle missing getUserUrl', () => {
145
- const config = {
146
- axios: mockAxios,
147
- loginUrl: '/login',
148
- logoutUrl: '/logout',
149
- extractToken: (data: any) => data.token
150
- };
151
-
152
- const validated = validateAuthConfig(config);
153
- expect(validated.getUserUrl).toBeUndefined();
154
- });
155
-
156
- it('should preserve getUserUrl when provided', () => {
157
- const config = {
158
- axios: mockAxios,
159
- loginUrl: '/login',
160
- logoutUrl: '/logout',
161
- getUserUrl: '/me',
162
- extractToken: (data: any) => data.token
163
- };
164
-
165
- const validated = validateAuthConfig(config);
166
- expect(validated.getUserUrl).toBe('/me');
167
- });
168
-
169
- it('should preserve callback functions', () => {
170
- const onError = jest.fn();
171
- const onLogin = jest.fn();
172
- const onLogout = jest.fn();
173
-
174
- const config = {
175
- axios: mockAxios,
176
- loginUrl: '/login',
177
- logoutUrl: '/logout',
178
- extractToken: (data: any) => data.token,
179
- onError,
180
- onLogin,
181
- onLogout
182
- };
183
-
184
- const validated = validateAuthConfig(config);
185
- expect(validated.onError).toBe(onError);
186
- expect(validated.onLogin).toBe(onLogin);
187
- expect(validated.onLogout).toBe(onLogout);
188
- });
189
- });
190
-
191
- describe('OAuth token extraction', () => {
192
- it('should use custom extractTokens function when provided', () => {
193
- const customExtractTokens = jest.fn().mockReturnValue({
194
- accessToken: 'custom-access',
195
- refreshToken: 'custom-refresh',
196
- tokenType: 'Bearer',
197
- expiresAt: Date.now() + 3600000
198
- });
199
-
200
- const config = {
201
- axios: mockAxios,
202
- tokenUrl: '/oauth/token',
203
- extractTokens: customExtractTokens
204
- };
205
-
206
- const validated = validateAuthConfig(config);
207
- const testData = { custom_field: 'test-data' };
208
- const result = validated.extractTokens(testData);
209
-
210
- expect(customExtractTokens).toHaveBeenCalledWith(testData);
211
- expect(result.accessToken).toBe('custom-access');
212
- expect(result.refreshToken).toBe('custom-refresh');
213
- });
214
-
215
- it('should use custom OAuth field extractors', () => {
216
- const extractAccessToken = jest.fn().mockReturnValue('custom-access-token');
217
- const extractRefreshToken = jest.fn().mockReturnValue('custom-refresh-token');
218
- const extractExpiresIn = jest.fn().mockReturnValue(7200);
219
- const extractTokenType = jest.fn().mockReturnValue('Custom');
220
- const extractScope = jest.fn().mockReturnValue(['read', 'write']);
221
-
222
- const config = {
223
- axios: mockAxios,
224
- tokenUrl: '/oauth/token',
225
- extractAccessToken,
226
- extractRefreshToken,
227
- extractExpiresIn,
228
- extractTokenType,
229
- extractScope
230
- };
231
-
232
- const validated = validateAuthConfig(config);
233
- const testData = {
234
- access_token: 'standard-access',
235
- refresh_token: 'standard-refresh',
236
- expires_in: 3600,
237
- token_type: 'Bearer',
238
- scope: 'read write'
239
- };
240
-
241
- const result = validated.extractTokens(testData);
242
-
243
- expect(extractAccessToken).toHaveBeenCalledWith(testData);
244
- expect(extractRefreshToken).toHaveBeenCalledWith(testData);
245
- expect(extractExpiresIn).toHaveBeenCalledWith(testData);
246
- expect(extractTokenType).toHaveBeenCalledWith(testData);
247
- expect(extractScope).toHaveBeenCalledWith(testData);
248
-
249
- expect(result.accessToken).toBe('custom-access-token');
250
- expect(result.refreshToken).toBe('custom-refresh-token');
251
- expect(result.tokenType).toBe('Custom');
252
- expect(result.scope).toEqual(['read', 'write']);
253
- expect(result.expiresAt).toBeGreaterThan(Date.now() + 7199000); // ~7200 seconds from now
254
- });
255
-
256
- it('should handle OAuth response with standard fields', () => {
257
- const config = {
258
- axios: mockAxios,
259
- tokenUrl: '/oauth/token'
260
- };
261
-
262
- const validated = validateAuthConfig(config);
263
- const oauthResponse = {
264
- access_token: 'oauth-access-token',
265
- refresh_token: 'oauth-refresh-token',
266
- expires_in: 3600,
267
- token_type: 'Bearer',
268
- scope: 'read write profile'
269
- };
270
-
271
- const result = validated.extractTokens(oauthResponse);
272
-
273
- expect(result.accessToken).toBe('oauth-access-token');
274
- expect(result.refreshToken).toBe('oauth-refresh-token');
275
- expect(result.tokenType).toBe('Bearer');
276
- expect(result.scope).toEqual(['read', 'write', 'profile']);
277
- expect(result.expiresAt).toBeGreaterThan(Date.now() + 3599000);
278
- });
279
-
280
- it('should handle OAuth response without optional fields', () => {
281
- const config = {
282
- axios: mockAxios,
283
- tokenUrl: '/oauth/token'
284
- };
285
-
286
- const validated = validateAuthConfig(config);
287
- const minimalResponse = {
288
- access_token: 'minimal-token'
289
- };
290
-
291
- const result = validated.extractTokens(minimalResponse);
292
-
293
- expect(result.accessToken).toBe('minimal-token');
294
- expect(result.refreshToken).toBeUndefined();
295
- expect(result.tokenType).toBe('Bearer'); // Default
296
- expect(result.scope).toBeUndefined();
297
- expect(result.expiresAt).toBeUndefined();
298
- });
299
-
300
- it('should fallback to legacy token extraction', () => {
301
- const extractToken = jest.fn().mockReturnValue('legacy-token');
302
-
303
- const config = {
304
- axios: mockAxios,
305
- loginUrl: '/auth/login',
306
- extractToken
307
- };
308
-
309
- const validated = validateAuthConfig(config);
310
- const legacyResponse = {
311
- auth_token: 'django-token',
312
- user: { id: 1, name: 'Test User' }
313
- };
314
-
315
- const result = validated.extractTokens(legacyResponse);
316
-
317
- expect(extractToken).toHaveBeenCalledWith(legacyResponse);
318
- expect(result.accessToken).toBe('legacy-token');
319
- expect(result.tokenType).toBe('Bearer'); // Standard default
320
- expect(result.refreshToken).toBeUndefined();
321
- });
322
-
323
- it('should handle auth_token field without extractToken function', () => {
324
- const config = {
325
- axios: mockAxios,
326
- loginUrl: '/auth/login'
327
- };
328
-
329
- const validated = validateAuthConfig(config);
330
- const response = {
331
- auth_token: 'auto-extracted-token'
332
- };
333
-
334
- const result = validated.extractTokens(response);
335
-
336
- expect(result.accessToken).toBe('auto-extracted-token');
337
- expect(result.tokenType).toBe('Bearer');
338
- });
339
-
340
- it('should handle generic token field', () => {
341
- const config = {
342
- axios: mockAxios,
343
- loginUrl: '/auth/login'
344
- };
345
-
346
- const validated = validateAuthConfig(config);
347
- const response = {
348
- token: 'generic-token'
349
- };
350
-
351
- const result = validated.extractTokens(response);
352
-
353
- expect(result.accessToken).toBe('generic-token');
354
- expect(result.tokenType).toBe('Bearer');
355
- });
356
-
357
- it('should throw error when no valid token fields found', () => {
358
- const config = {
359
- axios: mockAxios,
360
- tokenUrl: '/oauth/token'
361
- };
362
-
363
- const validated = validateAuthConfig(config);
364
- const invalidResponse = {
365
- user: { id: 1, name: 'Test' },
366
- message: 'Success'
367
- };
368
-
369
- expect(() => validated.extractTokens(invalidResponse)).toThrow(
370
- 'No valid token found in response. Provide extractTokens, extractToken, or ensure response contains access_token/auth_token field.'
371
- );
372
- });
373
-
374
- it('should provide helpful error message for invalid responses', () => {
375
- const config = {
376
- axios: mockAxios,
377
- loginUrl: '/auth/login'
378
- };
379
-
380
- const validated = validateAuthConfig(config);
381
- const emptyResponse = {};
382
-
383
- expect(() => validated.extractTokens(emptyResponse)).toThrow(
384
- /No valid token found in response/
385
- );
386
- });
387
- });
388
-
389
- describe('storage interface compliance', () => {
390
- it('should work with localStorage', () => {
391
- const config = {
392
- axios: mockAxios,
393
- loginUrl: '/login',
394
- logoutUrl: '/logout',
395
- extractToken: (data: any) => data.token,
396
- persistence: {
397
- storage: window.localStorage
398
- }
399
- };
400
-
401
- const validated = validateAuthConfig(config);
402
- expect(validated.persistence.storage).toBe(window.localStorage);
403
- });
404
-
405
- it('should work with sessionStorage', () => {
406
- const config = {
407
- axios: mockAxios,
408
- loginUrl: '/login',
409
- logoutUrl: '/logout',
410
- extractToken: (data: any) => data.token,
411
- persistence: {
412
- storage: window.sessionStorage
413
- }
414
- };
415
-
416
- const validated = validateAuthConfig(config);
417
- expect(validated.persistence.storage).toBe(window.sessionStorage);
418
- });
419
-
420
- it('should work with custom storage implementation', () => {
421
- const customStorage = {
422
- getItem: jest.fn(),
423
- setItem: jest.fn(),
424
- removeItem: jest.fn(),
425
- clear: jest.fn()
426
- };
427
-
428
- const config = {
429
- axios: mockAxios,
430
- loginUrl: '/login',
431
- logoutUrl: '/logout',
432
- extractToken: (data: any) => data.token,
433
- persistence: {
434
- storage: customStorage as Storage
435
- }
436
- };
437
-
438
- const validated = validateAuthConfig(config);
439
- expect(validated.persistence.storage).toBe(customStorage);
440
- });
441
- });
442
-
443
- describe('SSR compatibility', () => {
444
- it('should handle missing window object gracefully', () => {
445
- // Temporarily remove window object
446
- const originalWindow = global.window;
447
- delete (global as any).window;
448
-
449
- const config = {
450
- axios: mockAxios,
451
- loginUrl: '/login',
452
- logoutUrl: '/logout',
453
- extractToken: (data: any) => data.token
454
- };
455
-
456
- const validated = validateAuthConfig(config);
457
- expect(validated.persistence.storage).toEqual({});
458
-
459
- // Restore window
460
- global.window = originalWindow;
461
- });
462
- });
463
- });