@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.
- package/README.md +107 -293
- package/dist/authConfig.d.ts +25 -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 +119 -112
- package/src/authStore.ts +46 -57
- package/src/createAuthRegistry.ts +1 -1
- package/src/errors.ts +104 -0
- package/src/index.ts +2 -1
- package/src/useAuth.ts +181 -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,202 +0,0 @@
|
|
|
1
|
-
import { createAuthRegistry } from '../createAuthRegistry';
|
|
2
|
-
import { TestUser, createMockAxios } from './testHelpers';
|
|
3
|
-
import { testConfigs } from './testUtils';
|
|
4
|
-
|
|
5
|
-
describe('createAuthRegistry', () => {
|
|
6
|
-
interface TestModels {
|
|
7
|
-
main: TestUser;
|
|
8
|
-
admin: TestUser;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
beforeEach(() => {
|
|
12
|
-
jest.clearAllMocks();
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it('should create a registry function', () => {
|
|
16
|
-
const getAuthStore = createAuthRegistry<TestModels>();
|
|
17
|
-
expect(typeof getAuthStore).toBe('function');
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
describe('store creation', () => {
|
|
21
|
-
it('should create stores with correct configuration', () => {
|
|
22
|
-
const getAuthStore = createAuthRegistry<TestModels>();
|
|
23
|
-
const mockAxios = createMockAxios();
|
|
24
|
-
|
|
25
|
-
const config = {
|
|
26
|
-
axios: mockAxios,
|
|
27
|
-
loginUrl: '/auth/login',
|
|
28
|
-
logoutUrl: '/auth/logout',
|
|
29
|
-
getUserUrl: '/auth/me',
|
|
30
|
-
extractToken: (data: any) => data.auth_token
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
const store = getAuthStore('main', config);
|
|
34
|
-
|
|
35
|
-
expect(store).toBeDefined();
|
|
36
|
-
expect(store.config).toBeDefined();
|
|
37
|
-
expect(store.config.axios).toBe(mockAxios);
|
|
38
|
-
expect(store.config.loginUrl).toBe('/auth/login');
|
|
39
|
-
expect(store.config.logoutUrl).toBe('/auth/logout');
|
|
40
|
-
expect(store.config.getUserUrl).toBe('/auth/me');
|
|
41
|
-
expect(store.config.extractToken).toBe(config.extractToken);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('should return same store instance for same key', () => {
|
|
45
|
-
const getAuthStore = createAuthRegistry<TestModels>();
|
|
46
|
-
const config = testConfigs.basic;
|
|
47
|
-
|
|
48
|
-
const store1 = getAuthStore('main', config);
|
|
49
|
-
const store2 = getAuthStore('main', config);
|
|
50
|
-
|
|
51
|
-
expect(store1).toBe(store2);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it('should create different stores for different keys', () => {
|
|
55
|
-
const getAuthStore = createAuthRegistry<TestModels>();
|
|
56
|
-
const config = testConfigs.basic;
|
|
57
|
-
|
|
58
|
-
const mainStore = getAuthStore('main', config);
|
|
59
|
-
const adminStore = getAuthStore('admin', config);
|
|
60
|
-
|
|
61
|
-
expect(mainStore).not.toBe(adminStore);
|
|
62
|
-
expect(mainStore.config.loginUrl).toBe(adminStore.config.loginUrl);
|
|
63
|
-
expect(mainStore.config.logoutUrl).toBe(adminStore.config.logoutUrl);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('should validate config before store creation', () => {
|
|
67
|
-
const getAuthStore = createAuthRegistry<TestModels>();
|
|
68
|
-
|
|
69
|
-
expect(() => {
|
|
70
|
-
getAuthStore('main', {
|
|
71
|
-
loginUrl: '/login',
|
|
72
|
-
logoutUrl: '/logout',
|
|
73
|
-
extractToken: (data: any) => data.token
|
|
74
|
-
} as any);
|
|
75
|
-
}).toThrow('AuthConfig: axios instance is required');
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
describe('store functionality', () => {
|
|
80
|
-
it('should create store with initial empty state', () => {
|
|
81
|
-
const getAuthStore = createAuthRegistry<TestModels>();
|
|
82
|
-
const store = getAuthStore('main', testConfigs.basic);
|
|
83
|
-
|
|
84
|
-
const state = store.getState();
|
|
85
|
-
expect(state.user).toBeNull();
|
|
86
|
-
expect(state.token).toBe('');
|
|
87
|
-
expect(state.isAuthenticated).toBe(false);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it('should create store with working setters', () => {
|
|
91
|
-
const getAuthStore = createAuthRegistry<TestModels>();
|
|
92
|
-
const store = getAuthStore('main', testConfigs.basic);
|
|
93
|
-
|
|
94
|
-
const { setToken, setUser } = store.getState();
|
|
95
|
-
|
|
96
|
-
expect(typeof setToken).toBe('function');
|
|
97
|
-
expect(typeof setUser).toBe('function');
|
|
98
|
-
expect(typeof store.getState().unsetUser).toBe('function');
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it('should create store with config attached', () => {
|
|
102
|
-
const getAuthStore = createAuthRegistry<TestModels>();
|
|
103
|
-
const config = testConfigs.basic;
|
|
104
|
-
const store = getAuthStore('main', config);
|
|
105
|
-
|
|
106
|
-
expect(store.config).toBeDefined();
|
|
107
|
-
expect(store.config.loginUrl).toBe(config.loginUrl);
|
|
108
|
-
expect(store.config.persistence.enabled).toBe(true);
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
describe('type safety', () => {
|
|
113
|
-
it('should enforce correct model types for keys', () => {
|
|
114
|
-
const getAuthStore = createAuthRegistry<TestModels>();
|
|
115
|
-
const config = testConfigs.basic;
|
|
116
|
-
|
|
117
|
-
// These should compile without errors
|
|
118
|
-
const mainStore = getAuthStore('main', config);
|
|
119
|
-
const adminStore = getAuthStore('admin', config);
|
|
120
|
-
|
|
121
|
-
expect(mainStore).toBeDefined();
|
|
122
|
-
expect(adminStore).toBeDefined();
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it('should support multiple user types in same registry', () => {
|
|
126
|
-
interface MultiModel {
|
|
127
|
-
user: TestUser;
|
|
128
|
-
admin: TestUser & { permissions: string[] };
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const getAuthStore = createAuthRegistry<MultiModel>();
|
|
132
|
-
const config = testConfigs.basic;
|
|
133
|
-
|
|
134
|
-
const userStore = getAuthStore('user', config);
|
|
135
|
-
const adminStore = getAuthStore('admin', config);
|
|
136
|
-
|
|
137
|
-
expect(userStore).not.toBe(adminStore);
|
|
138
|
-
expect(typeof userStore.getState().setUser).toBe('function');
|
|
139
|
-
expect(typeof adminStore.getState().setUser).toBe('function');
|
|
140
|
-
});
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
describe('config validation integration', () => {
|
|
144
|
-
it('should apply default persistence settings', () => {
|
|
145
|
-
const getAuthStore = createAuthRegistry<TestModels>();
|
|
146
|
-
const config = {
|
|
147
|
-
axios: createMockAxios(),
|
|
148
|
-
loginUrl: '/login',
|
|
149
|
-
logoutUrl: '/logout',
|
|
150
|
-
extractToken: (data: any) => data.token
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
const store = getAuthStore('main', config);
|
|
154
|
-
|
|
155
|
-
expect(store.config.persistence.enabled).toBe(true);
|
|
156
|
-
expect(store.config.persistence.tokenKey).toBe('token');
|
|
157
|
-
expect(store.config.persistence.userKey).toBe('user');
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it('should apply default auth header format', () => {
|
|
161
|
-
const getAuthStore = createAuthRegistry<TestModels>();
|
|
162
|
-
const store = getAuthStore('main', testConfigs.basic);
|
|
163
|
-
|
|
164
|
-
const headerValue = store.config.formatAuthHeader('test-token');
|
|
165
|
-
expect(headerValue).toBe('Bearer test-token');
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
it('should preserve custom configurations', () => {
|
|
169
|
-
const getAuthStore = createAuthRegistry<TestModels>();
|
|
170
|
-
const store = getAuthStore('main', testConfigs.withTokenFormat);
|
|
171
|
-
|
|
172
|
-
const headerValue = store.config.formatAuthHeader('test-token');
|
|
173
|
-
expect(headerValue).toBe('Token test-token');
|
|
174
|
-
});
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
describe('registry isolation', () => {
|
|
178
|
-
it('should isolate different registry instances', () => {
|
|
179
|
-
const registry1 = createAuthRegistry<TestModels>();
|
|
180
|
-
const registry2 = createAuthRegistry<TestModels>();
|
|
181
|
-
|
|
182
|
-
const store1 = registry1('main', testConfigs.basic);
|
|
183
|
-
const store2 = registry2('main', testConfigs.basic);
|
|
184
|
-
|
|
185
|
-
expect(store1).not.toBe(store2);
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
it('should maintain store state independently', () => {
|
|
189
|
-
const getAuthStore = createAuthRegistry<TestModels>();
|
|
190
|
-
|
|
191
|
-
const mainStore = getAuthStore('main', testConfigs.basic);
|
|
192
|
-
const adminStore = getAuthStore('admin', testConfigs.basic);
|
|
193
|
-
|
|
194
|
-
// Set user in main store
|
|
195
|
-
mainStore.getState().setUser({ id: 1, email: 'main@test.com', name: 'Main User' });
|
|
196
|
-
|
|
197
|
-
// Admin store should remain empty
|
|
198
|
-
expect(mainStore.getState().user).not.toBeNull();
|
|
199
|
-
expect(adminStore.getState().user).toBeNull();
|
|
200
|
-
});
|
|
201
|
-
});
|
|
202
|
-
});
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import { act } from '@testing-library/react';
|
|
2
|
-
|
|
3
|
-
// Mock axios instance for auth testing
|
|
4
|
-
export const createMockAxios = () => ({
|
|
5
|
-
get: jest.fn(),
|
|
6
|
-
post: jest.fn(),
|
|
7
|
-
put: jest.fn(),
|
|
8
|
-
patch: jest.fn(),
|
|
9
|
-
delete: jest.fn(),
|
|
10
|
-
defaults: {
|
|
11
|
-
headers: {
|
|
12
|
-
common: {} as Record<string, string>
|
|
13
|
-
}
|
|
14
|
-
},
|
|
15
|
-
interceptors: {
|
|
16
|
-
request: { use: jest.fn(), eject: jest.fn() },
|
|
17
|
-
response: { use: jest.fn(), eject: jest.fn() }
|
|
18
|
-
}
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
// Mock user types for testing
|
|
22
|
-
export interface TestUser {
|
|
23
|
-
id: number;
|
|
24
|
-
email: string;
|
|
25
|
-
name: string;
|
|
26
|
-
role?: string;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export const mockUser: TestUser = {
|
|
30
|
-
id: 1,
|
|
31
|
-
email: 'test@example.com',
|
|
32
|
-
name: 'Test User',
|
|
33
|
-
role: 'admin'
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
// Mock API responses
|
|
37
|
-
export const mockLoginResponse = {
|
|
38
|
-
auth_token: 'mock-jwt-token-12345',
|
|
39
|
-
user: mockUser
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
// Mock storage for persistence testing
|
|
43
|
-
export const createMockStorage = () => {
|
|
44
|
-
const storage: Record<string, string> = {};
|
|
45
|
-
return {
|
|
46
|
-
getItem: jest.fn((key: string) => storage[key] || null),
|
|
47
|
-
setItem: jest.fn((key: string, value: string) => { storage[key] = value; }),
|
|
48
|
-
removeItem: jest.fn((key: string) => { delete storage[key]; }),
|
|
49
|
-
clear: jest.fn(() => Object.keys(storage).forEach(key => delete storage[key]))
|
|
50
|
-
};
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
// Helper for async operations in tests
|
|
54
|
-
export const waitFor = (condition: () => boolean, timeout: number = 5000) => {
|
|
55
|
-
return new Promise<void>((resolve, reject) => {
|
|
56
|
-
const startTime = Date.now();
|
|
57
|
-
const check = () => {
|
|
58
|
-
if (condition()) resolve();
|
|
59
|
-
else if (Date.now() - startTime > timeout) reject(new Error('Timeout'));
|
|
60
|
-
else setTimeout(check, 10);
|
|
61
|
-
};
|
|
62
|
-
check();
|
|
63
|
-
});
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
// Helper for simulating user actions
|
|
67
|
-
export const simulateUserAction = async (action: () => Promise<void> | void) => {
|
|
68
|
-
await act(async () => {
|
|
69
|
-
await action();
|
|
70
|
-
});
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
// Mock error creator for testing error handling
|
|
74
|
-
export const createMockError = (message: string, status: number = 400) => {
|
|
75
|
-
const error = new Error(message);
|
|
76
|
-
(error as any).response = { status, data: { detail: message } };
|
|
77
|
-
return error;
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
// Helper for testing auth header formats
|
|
81
|
-
export const extractAuthHeader = (mockAxios: any): string | undefined => {
|
|
82
|
-
return mockAxios.defaults.headers.common['Authorization'];
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
// Helper to reset all mocks
|
|
86
|
-
export const resetAllMocks = () => {
|
|
87
|
-
jest.clearAllMocks();
|
|
88
|
-
(window.localStorage.getItem as jest.Mock).mockClear();
|
|
89
|
-
(window.localStorage.setItem as jest.Mock).mockClear();
|
|
90
|
-
(window.localStorage.removeItem as jest.Mock).mockClear();
|
|
91
|
-
(window.localStorage.clear as jest.Mock).mockClear();
|
|
92
|
-
};
|
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
import { TestUser, createMockAxios, mockUser } from './testHelpers';
|
|
2
|
-
|
|
3
|
-
// Extended test user types for complex scenarios
|
|
4
|
-
export interface ExtendedTestUser extends TestUser {
|
|
5
|
-
permissions?: string[];
|
|
6
|
-
lastLogin?: string;
|
|
7
|
-
isActive?: boolean;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export const mockUsers: ExtendedTestUser[] = [
|
|
11
|
-
{
|
|
12
|
-
id: 1,
|
|
13
|
-
email: 'admin@example.com',
|
|
14
|
-
name: 'Admin User',
|
|
15
|
-
role: 'admin',
|
|
16
|
-
permissions: ['read', 'write', 'delete'],
|
|
17
|
-
isActive: true
|
|
18
|
-
},
|
|
19
|
-
{
|
|
20
|
-
id: 2,
|
|
21
|
-
email: 'user@example.com',
|
|
22
|
-
name: 'Regular User',
|
|
23
|
-
role: 'user',
|
|
24
|
-
permissions: ['read'],
|
|
25
|
-
isActive: true
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
id: 3,
|
|
29
|
-
email: 'inactive@example.com',
|
|
30
|
-
name: 'Inactive User',
|
|
31
|
-
role: 'user',
|
|
32
|
-
permissions: [],
|
|
33
|
-
isActive: false
|
|
34
|
-
}
|
|
35
|
-
];
|
|
36
|
-
|
|
37
|
-
// Different auth configurations for testing
|
|
38
|
-
export const createTestAuthConfig = (overrides: any = {}) => ({
|
|
39
|
-
axios: createMockAxios(),
|
|
40
|
-
loginUrl: '/auth/login',
|
|
41
|
-
logoutUrl: '/auth/logout',
|
|
42
|
-
getUserUrl: '/auth/me',
|
|
43
|
-
extractToken: (data: any) => data.auth_token,
|
|
44
|
-
...overrides
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
// Mock responses for different scenarios
|
|
48
|
-
export const mockResponses = {
|
|
49
|
-
loginSuccess: {
|
|
50
|
-
data: {
|
|
51
|
-
auth_token: 'mock-jwt-token-12345',
|
|
52
|
-
user: mockUser
|
|
53
|
-
}
|
|
54
|
-
},
|
|
55
|
-
loginFailure: {
|
|
56
|
-
response: {
|
|
57
|
-
status: 401,
|
|
58
|
-
data: { detail: 'Invalid credentials' }
|
|
59
|
-
}
|
|
60
|
-
},
|
|
61
|
-
userSuccess: {
|
|
62
|
-
data: mockUser
|
|
63
|
-
},
|
|
64
|
-
userFailure: {
|
|
65
|
-
response: {
|
|
66
|
-
status: 403,
|
|
67
|
-
data: { detail: 'Unauthorized' }
|
|
68
|
-
}
|
|
69
|
-
},
|
|
70
|
-
logoutSuccess: {
|
|
71
|
-
data: { message: 'Logged out successfully' }
|
|
72
|
-
},
|
|
73
|
-
logoutFailure: {
|
|
74
|
-
response: {
|
|
75
|
-
status: 500,
|
|
76
|
-
data: { detail: 'Server error' }
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
// Test auth configurations for different scenarios
|
|
82
|
-
export const testConfigs = {
|
|
83
|
-
basic: createTestAuthConfig(),
|
|
84
|
-
withTokenFormat: createTestAuthConfig({
|
|
85
|
-
formatAuthHeader: (token: string) => `Token ${token}`
|
|
86
|
-
}),
|
|
87
|
-
withoutGetUser: createTestAuthConfig({
|
|
88
|
-
getUserUrl: undefined
|
|
89
|
-
}),
|
|
90
|
-
withCustomStorage: createTestAuthConfig({
|
|
91
|
-
persistence: {
|
|
92
|
-
enabled: true,
|
|
93
|
-
storage: window.sessionStorage,
|
|
94
|
-
tokenKey: 'custom_token',
|
|
95
|
-
userKey: 'custom_user'
|
|
96
|
-
}
|
|
97
|
-
}),
|
|
98
|
-
withoutPersistence: createTestAuthConfig({
|
|
99
|
-
persistence: {
|
|
100
|
-
enabled: false
|
|
101
|
-
}
|
|
102
|
-
}),
|
|
103
|
-
withCallbacks: createTestAuthConfig({
|
|
104
|
-
onError: jest.fn(),
|
|
105
|
-
onLogin: jest.fn(),
|
|
106
|
-
onLogout: jest.fn()
|
|
107
|
-
})
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
// Helper to create error objects that match Axios error structure
|
|
111
|
-
export const createAxiosError = (message: string, status: number = 400, code?: string) => {
|
|
112
|
-
const error = new Error(message) as any;
|
|
113
|
-
error.response = {
|
|
114
|
-
status,
|
|
115
|
-
data: { detail: message },
|
|
116
|
-
statusText: getStatusText(status)
|
|
117
|
-
};
|
|
118
|
-
error.isAxiosError = true;
|
|
119
|
-
if (code) error.code = code;
|
|
120
|
-
return error;
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
// Helper to get HTTP status text
|
|
124
|
-
const getStatusText = (status: number): string => {
|
|
125
|
-
const statusTexts: Record<number, string> = {
|
|
126
|
-
200: 'OK',
|
|
127
|
-
201: 'Created',
|
|
128
|
-
400: 'Bad Request',
|
|
129
|
-
401: 'Unauthorized',
|
|
130
|
-
403: 'Forbidden',
|
|
131
|
-
404: 'Not Found',
|
|
132
|
-
500: 'Internal Server Error'
|
|
133
|
-
};
|
|
134
|
-
return statusTexts[status] || 'Unknown';
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
// Helper to simulate storage quota exceeded
|
|
138
|
-
export const createStorageQuotaError = () => {
|
|
139
|
-
const error = new Error('QuotaExceededError');
|
|
140
|
-
error.name = 'QuotaExceededError';
|
|
141
|
-
return error;
|
|
142
|
-
};
|