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