@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.
- package/README.md +107 -293
- package/dist/authConfig.d.ts +26 -20
- package/dist/authStore.d.ts +3 -3
- package/dist/errors.d.ts +39 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/useAuth.d.ts +3 -2
- package/package.json +2 -1
- package/src/authConfig.ts +132 -111
- package/src/authStore.ts +71 -57
- package/src/createAuthRegistry.ts +1 -1
- package/src/errors.ts +104 -0
- package/src/index.ts +2 -1
- package/src/useAuth.ts +169 -101
- package/dist/setupTests.d.ts +0 -1
- package/src/__tests__/authConfig.test.ts +0 -463
- package/src/__tests__/authStore.test.ts +0 -608
- package/src/__tests__/createAuthRegistry.test.ts +0 -202
- package/src/__tests__/testHelpers.ts +0 -92
- package/src/__tests__/testUtils.ts +0 -142
- package/src/__tests__/useAuth.test.ts +0 -975
- package/src/setupTests.ts +0 -46
|
@@ -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
|
-
});
|