@jmruthers/pace-core 0.5.4 → 0.5.6

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.
Files changed (158) hide show
  1. package/dist/{DataTable-ZQDRE46Q.js → DataTable-BEMN72L5.js} +2 -2
  2. package/dist/{chunk-5H3C2SWM.js → chunk-4EIBJ6DF.js} +2 -2
  3. package/dist/{chunk-M4RW7PIP.js → chunk-SFGUMWEE.js} +105 -81
  4. package/dist/chunk-SFGUMWEE.js.map +1 -0
  5. package/dist/components.js +2 -2
  6. package/dist/index.js +2 -2
  7. package/dist/utils.js +1 -1
  8. package/docs/api/classes/ErrorBoundary.md +1 -1
  9. package/docs/api/classes/InvalidScopeError.md +1 -1
  10. package/docs/api/classes/MissingUserContextError.md +1 -1
  11. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  12. package/docs/api/classes/PermissionDeniedError.md +1 -1
  13. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  14. package/docs/api/classes/RBACAuditManager.md +1 -1
  15. package/docs/api/classes/RBACCache.md +1 -1
  16. package/docs/api/classes/RBACEngine.md +1 -1
  17. package/docs/api/classes/RBACError.md +1 -1
  18. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  19. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  20. package/docs/api/interfaces/AggregateConfig.md +1 -1
  21. package/docs/api/interfaces/ButtonProps.md +1 -1
  22. package/docs/api/interfaces/CardProps.md +1 -1
  23. package/docs/api/interfaces/ColorPalette.md +1 -1
  24. package/docs/api/interfaces/ColorShade.md +1 -1
  25. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  26. package/docs/api/interfaces/DataTableAction.md +1 -1
  27. package/docs/api/interfaces/DataTableColumn.md +1 -1
  28. package/docs/api/interfaces/DataTableProps.md +34 -34
  29. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  30. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  31. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  32. package/docs/api/interfaces/EventContextType.md +1 -1
  33. package/docs/api/interfaces/EventLogoProps.md +1 -1
  34. package/docs/api/interfaces/EventProviderProps.md +1 -1
  35. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  36. package/docs/api/interfaces/FileUploadProps.md +1 -1
  37. package/docs/api/interfaces/FooterProps.md +1 -1
  38. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  39. package/docs/api/interfaces/InputProps.md +1 -1
  40. package/docs/api/interfaces/LabelProps.md +1 -1
  41. package/docs/api/interfaces/LoginFormProps.md +1 -1
  42. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  43. package/docs/api/interfaces/NavigationContextType.md +1 -1
  44. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  45. package/docs/api/interfaces/NavigationItem.md +1 -1
  46. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  47. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  48. package/docs/api/interfaces/Organisation.md +1 -1
  49. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  50. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  51. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  52. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  53. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  54. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  55. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  56. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  57. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  58. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  59. package/docs/api/interfaces/PaletteData.md +1 -1
  60. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  61. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  62. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  63. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  64. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  65. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  66. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  67. package/docs/api/interfaces/RBACConfig.md +1 -1
  68. package/docs/api/interfaces/RBACContextType.md +1 -1
  69. package/docs/api/interfaces/RBACLogger.md +1 -1
  70. package/docs/api/interfaces/RBACProviderProps.md +1 -1
  71. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  72. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  73. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  74. package/docs/api/interfaces/RouteConfig.md +1 -1
  75. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  76. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  77. package/docs/api/interfaces/StorageConfig.md +1 -1
  78. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  79. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  80. package/docs/api/interfaces/StorageListOptions.md +1 -1
  81. package/docs/api/interfaces/StorageListResult.md +1 -1
  82. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  83. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  84. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  85. package/docs/api/interfaces/StyleImport.md +1 -1
  86. package/docs/api/interfaces/ToastActionElement.md +1 -1
  87. package/docs/api/interfaces/ToastProps.md +1 -1
  88. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  89. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  90. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  91. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  92. package/docs/api/interfaces/UsePublicEventLogoOptions.md +1 -1
  93. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  94. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  95. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  96. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  97. package/docs/api/interfaces/UserEventAccess.md +1 -1
  98. package/docs/api/interfaces/UserMenuProps.md +1 -1
  99. package/docs/api/interfaces/UserProfile.md +1 -1
  100. package/docs/api/modules.md +3 -3
  101. package/docs/implementation-guides/data-tables.md +20 -0
  102. package/docs/quick-reference.md +9 -0
  103. package/docs/rbac/examples.md +4 -0
  104. package/package.json +1 -1
  105. package/src/__tests__/helpers/test-utils.tsx +147 -1
  106. package/src/components/DataTable/DataTable.tsx +20 -0
  107. package/src/components/DataTable/__tests__/DataTable.hooks.test 2.tsx +191 -0
  108. package/src/components/DataTable/__tests__/DataTable.hooks.test.tsx +191 -0
  109. package/src/components/DataTable/components/DataTableCore.tsx +164 -131
  110. package/src/hooks/__tests__/hooks.integration.test.tsx +575 -0
  111. package/src/hooks/__tests__/useApiFetch.unit.test.ts +115 -0
  112. package/src/hooks/__tests__/useComponentPerformance.unit.test.tsx +133 -0
  113. package/src/hooks/__tests__/useDebounce.unit.test.ts +82 -0
  114. package/src/hooks/__tests__/useFocusTrap.unit.test.tsx +293 -0
  115. package/src/hooks/__tests__/useInactivityTracker.unit.test.ts +385 -0
  116. package/src/hooks/__tests__/useOrganisationPermissions.unit.test.tsx +286 -0
  117. package/src/hooks/__tests__/useOrganisationSecurity.unit.test.tsx +838 -0
  118. package/src/hooks/__tests__/usePermissionCache.simple.test.ts +104 -0
  119. package/src/hooks/__tests__/usePermissionCache.unit.test.ts +633 -0
  120. package/src/hooks/__tests__/useRBAC.unit.test.ts +856 -0
  121. package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +537 -0
  122. package/src/hooks/__tests__/useToast.unit.test.tsx +62 -0
  123. package/src/hooks/__tests__/useZodForm.unit.test.tsx +37 -0
  124. package/src/rbac/utils/__tests__/eventContext.test.ts +428 -0
  125. package/src/rbac/utils/__tests__/eventContext.unit.test.ts +428 -0
  126. package/src/utils/__tests__/appConfig.unit.test.ts +55 -0
  127. package/src/utils/__tests__/audit.unit.test.ts +69 -0
  128. package/src/utils/__tests__/auth-utils.unit.test.ts +70 -0
  129. package/src/utils/__tests__/bundleAnalysis.unit.test.ts +317 -0
  130. package/src/utils/__tests__/cn.unit.test.ts +34 -0
  131. package/src/utils/__tests__/deviceFingerprint.unit.test.ts +503 -0
  132. package/src/utils/__tests__/dynamicUtils.unit.test.ts +322 -0
  133. package/src/utils/__tests__/formatDate.unit.test.ts +109 -0
  134. package/src/utils/__tests__/formatting.unit.test.ts +66 -0
  135. package/src/utils/__tests__/index.unit.test.ts +251 -0
  136. package/src/utils/__tests__/lazyLoad.unit.test.tsx +309 -0
  137. package/src/utils/__tests__/organisationContext.unit.test.ts +192 -0
  138. package/src/utils/__tests__/performanceBudgets.unit.test.ts +259 -0
  139. package/src/utils/__tests__/permissionTypes.unit.test.ts +250 -0
  140. package/src/utils/__tests__/permissionUtils.unit.test.ts +362 -0
  141. package/src/utils/__tests__/sanitization.unit.test.ts +346 -0
  142. package/src/utils/__tests__/schemaUtils.unit.test.ts +441 -0
  143. package/src/utils/__tests__/secureDataAccess.unit.test.ts +334 -0
  144. package/src/utils/__tests__/secureErrors.unit.test.ts +377 -0
  145. package/src/utils/__tests__/secureStorage.unit.test.ts +293 -0
  146. package/src/utils/__tests__/security.unit.test.ts +127 -0
  147. package/src/utils/__tests__/securityMonitor.unit.test.ts +280 -0
  148. package/src/utils/__tests__/sessionTracking.unit.test.ts +356 -0
  149. package/src/utils/__tests__/validation.unit.test.ts +84 -0
  150. package/src/utils/__tests__/validationUtils.unit.test.ts +571 -0
  151. package/src/validation/__tests__/common.unit.test.ts +101 -0
  152. package/src/validation/__tests__/csrf.unit.test.ts +302 -0
  153. package/src/validation/__tests__/passwordSchema.unit.test 2.ts +98 -0
  154. package/src/validation/__tests__/passwordSchema.unit.test.ts +98 -0
  155. package/src/validation/__tests__/sqlInjectionProtection.unit.test.ts +466 -0
  156. package/dist/chunk-M4RW7PIP.js.map +0 -1
  157. /package/dist/{DataTable-ZQDRE46Q.js.map → DataTable-BEMN72L5.js.map} +0 -0
  158. /package/dist/{chunk-5H3C2SWM.js.map → chunk-4EIBJ6DF.js.map} +0 -0
@@ -0,0 +1,293 @@
1
+ /**
2
+ * @file Secure Storage Unit Tests
3
+ * @package @jmruthers/pace-core
4
+ * @module Utils/SecureStorage
5
+ * @since 0.4.0
6
+ */
7
+
8
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
9
+ import { secureStorage } from '../secureStorage';
10
+
11
+ // Mock localStorage
12
+ const mockLocalStorage = {
13
+ getItem: vi.fn(),
14
+ setItem: vi.fn(),
15
+ removeItem: vi.fn(),
16
+ clear: vi.fn(),
17
+ key: vi.fn(),
18
+ length: 0
19
+ };
20
+
21
+ // Mock crypto API
22
+ const mockCrypto = {
23
+ subtle: {
24
+ generateKey: vi.fn(),
25
+ importKey: vi.fn(),
26
+ exportKey: vi.fn(),
27
+ encrypt: vi.fn(),
28
+ decrypt: vi.fn()
29
+ },
30
+ getRandomValues: vi.fn()
31
+ };
32
+
33
+ describe('Secure Storage', () => {
34
+ beforeEach(() => {
35
+ vi.clearAllMocks();
36
+
37
+ // Mock global objects
38
+ Object.defineProperty(global, 'localStorage', {
39
+ value: mockLocalStorage,
40
+ writable: true
41
+ });
42
+
43
+ Object.defineProperty(global, 'crypto', {
44
+ value: mockCrypto,
45
+ writable: true
46
+ });
47
+
48
+ // Mock window.crypto
49
+ Object.defineProperty(global, 'window', {
50
+ value: { crypto: mockCrypto },
51
+ writable: true
52
+ });
53
+
54
+ // Mock getRandomValues
55
+ mockCrypto.getRandomValues.mockReturnValue(new Uint8Array(12));
56
+ });
57
+
58
+ afterEach(() => {
59
+ vi.restoreAllMocks();
60
+ });
61
+
62
+ describe('initialization', () => {
63
+ it('should initialize without existing key', async () => {
64
+ mockLocalStorage.getItem.mockReturnValue(null);
65
+ mockCrypto.subtle.generateKey.mockResolvedValue({} as CryptoKey);
66
+ mockCrypto.subtle.exportKey.mockResolvedValue(new ArrayBuffer(32));
67
+
68
+ await secureStorage.init();
69
+
70
+ expect(mockCrypto.subtle.generateKey).toHaveBeenCalled();
71
+ });
72
+
73
+ it('should initialize with existing key', async () => {
74
+ // Reset the initialized state
75
+ (secureStorage as any).initialized = false;
76
+
77
+ const mockKeyData = 'dGVzdC1rZXktZGF0YQ=='; // Valid base64 for "test-key-data"
78
+ mockLocalStorage.getItem.mockReturnValue(mockKeyData);
79
+ mockCrypto.subtle.importKey.mockResolvedValue({} as CryptoKey);
80
+
81
+ // Mock the base64ToArrayBuffer function to avoid base64 decoding issues
82
+ const originalBase64ToArrayBuffer = (secureStorage as any).base64ToArrayBuffer;
83
+ (secureStorage as any).base64ToArrayBuffer = vi.fn().mockReturnValue(new ArrayBuffer(32));
84
+
85
+ await secureStorage.init();
86
+
87
+ expect(mockLocalStorage.getItem).toHaveBeenCalledWith('_sec_key');
88
+ expect(mockCrypto.subtle.importKey).toHaveBeenCalled();
89
+
90
+ // Restore original function
91
+ (secureStorage as any).base64ToArrayBuffer = originalBase64ToArrayBuffer;
92
+ });
93
+
94
+ it('should handle initialization errors gracefully', async () => {
95
+ mockLocalStorage.getItem.mockImplementation(() => {
96
+ throw new Error('Storage error');
97
+ });
98
+
99
+ await expect(secureStorage.init()).resolves.not.toThrow();
100
+ });
101
+ });
102
+
103
+ describe('setItem', () => {
104
+ it('should store item without encryption by default', async () => {
105
+ await secureStorage.setItem('test-key', 'test-value');
106
+
107
+ expect(mockLocalStorage.setItem).toHaveBeenCalledWith('test-key', expect.any(String));
108
+ });
109
+
110
+ it('should store item with encryption when enabled', async () => {
111
+ // Setup encryption key
112
+ mockCrypto.subtle.generateKey.mockResolvedValue({} as CryptoKey);
113
+ mockCrypto.subtle.exportKey.mockResolvedValue(new ArrayBuffer(32));
114
+ await secureStorage.init();
115
+
116
+ mockCrypto.subtle.encrypt.mockResolvedValue(new ArrayBuffer(8));
117
+
118
+ await secureStorage.setItem('test-key', 'test-value', { encrypt: true });
119
+
120
+ expect(mockCrypto.subtle.encrypt).toHaveBeenCalled();
121
+ expect(mockLocalStorage.setItem).toHaveBeenCalledWith('_sec_test-key', expect.any(String));
122
+ });
123
+
124
+ it('should handle encryption failure gracefully', async () => {
125
+ // Setup encryption key
126
+ mockCrypto.subtle.generateKey.mockResolvedValue({} as CryptoKey);
127
+ mockCrypto.subtle.exportKey.mockResolvedValue(new ArrayBuffer(32));
128
+ await secureStorage.init();
129
+
130
+ mockCrypto.subtle.encrypt.mockRejectedValue(new Error('Encryption failed'));
131
+
132
+ await secureStorage.setItem('test-key', 'test-value', { encrypt: true });
133
+
134
+ expect(mockLocalStorage.setItem).toHaveBeenCalledWith('test-key', expect.any(String));
135
+ });
136
+
137
+ it('should store item with expiry', async () => {
138
+ await secureStorage.setItem('test-key', 'test-value', { expiry: 1000 });
139
+
140
+ const storedData = JSON.parse(mockLocalStorage.setItem.mock.calls[0][1]);
141
+ expect(storedData.expiry).toBeDefined();
142
+ });
143
+ });
144
+
145
+ describe('getItem', () => {
146
+ it('should retrieve unencrypted item', async () => {
147
+ // Mock getItem to return null for encrypted data, then return plain data
148
+ mockLocalStorage.getItem
149
+ .mockReturnValueOnce(null) // No encrypted data
150
+ .mockReturnValueOnce(JSON.stringify({ value: 'test', timestamp: Date.now() })); // Plain data
151
+
152
+ const result = await secureStorage.getItem('test-key');
153
+
154
+ expect(result).toBe('test');
155
+ });
156
+
157
+ it('should retrieve encrypted item', async () => {
158
+ // Setup encryption key
159
+ mockCrypto.subtle.generateKey.mockResolvedValue({} as CryptoKey);
160
+ mockCrypto.subtle.exportKey.mockResolvedValue(new ArrayBuffer(32));
161
+ await secureStorage.init();
162
+
163
+ const encryptedData = 'dGVzdC1lbmNyeXB0ZWQ='; // Valid base64 for "test-encrypted"
164
+ // Mock getItem to return encrypted data for the _sec_ key
165
+ mockLocalStorage.getItem.mockReturnValue(encryptedData);
166
+ mockCrypto.subtle.decrypt.mockResolvedValue(new TextEncoder().encode(JSON.stringify({ value: 'test' })));
167
+
168
+ // Mock the base64ToArrayBuffer function to avoid base64 decoding issues
169
+ const originalBase64ToArrayBuffer = (secureStorage as any).base64ToArrayBuffer;
170
+ (secureStorage as any).base64ToArrayBuffer = vi.fn().mockReturnValue(new ArrayBuffer(32));
171
+
172
+ const result = await secureStorage.getItem('test-key');
173
+
174
+ expect(mockCrypto.subtle.decrypt).toHaveBeenCalled();
175
+ expect(result).toBe('test');
176
+
177
+ // Restore original function
178
+ (secureStorage as any).base64ToArrayBuffer = originalBase64ToArrayBuffer;
179
+ });
180
+
181
+ it('should handle decryption failure gracefully', async () => {
182
+ // Setup encryption key
183
+ mockCrypto.subtle.generateKey.mockResolvedValue({} as CryptoKey);
184
+ mockCrypto.subtle.exportKey.mockResolvedValue(new ArrayBuffer(32));
185
+ await secureStorage.init();
186
+
187
+ const encryptedData = 'encrypted-data';
188
+ mockLocalStorage.getItem.mockReturnValue(encryptedData);
189
+ mockCrypto.subtle.decrypt.mockRejectedValue(new Error('Decryption failed'));
190
+
191
+ const result = await secureStorage.getItem('test-key');
192
+
193
+ expect(result).toBe('encrypted-data');
194
+ });
195
+
196
+ it('should handle malformed stored data', async () => {
197
+ // Mock getItem to return null for encrypted data, then return malformed data
198
+ mockLocalStorage.getItem
199
+ .mockReturnValueOnce(null) // No encrypted data
200
+ .mockReturnValueOnce('invalid-json'); // Malformed plain data
201
+
202
+ const result = await secureStorage.getItem('test-key');
203
+
204
+ expect(result).toBe('invalid-json');
205
+ });
206
+
207
+ it('should return null for non-existent item', async () => {
208
+ // Mock getItem to return null for both encrypted and plain data
209
+ mockLocalStorage.getItem
210
+ .mockReturnValueOnce(null) // No encrypted data
211
+ .mockReturnValueOnce(null); // No plain data
212
+
213
+ const result = await secureStorage.getItem('test-key');
214
+
215
+ expect(result).toBeNull();
216
+ });
217
+
218
+ it('should handle expired items', async () => {
219
+ const expiredData = JSON.stringify({
220
+ value: 'test',
221
+ timestamp: Date.now(),
222
+ expiry: Date.now() - 1000 // Expired
223
+ });
224
+
225
+ // Mock getItem to return null for encrypted data, then return expired data
226
+ mockLocalStorage.getItem
227
+ .mockReturnValueOnce(null) // No encrypted data
228
+ .mockReturnValueOnce(expiredData); // Expired plain data
229
+
230
+ const result = await secureStorage.getItem('test-key');
231
+
232
+ expect(result).toBeNull();
233
+ });
234
+ });
235
+
236
+ describe('removeItem', () => {
237
+ it('should remove both encrypted and unencrypted items', async () => {
238
+ await secureStorage.removeItem('test-key');
239
+
240
+ expect(mockLocalStorage.removeItem).toHaveBeenCalledWith('test-key');
241
+ expect(mockLocalStorage.removeItem).toHaveBeenCalledWith('_sec_test-key');
242
+ });
243
+ });
244
+
245
+ describe('clear', () => {
246
+ it('should clear all secure storage', async () => {
247
+ // Mock Object.keys to return secure keys
248
+ const originalKeys = Object.keys;
249
+ Object.keys = vi.fn().mockReturnValue(['_sec_key1', '_sec_key2', 'normal_key']);
250
+
251
+ await secureStorage.clear();
252
+
253
+ expect(mockLocalStorage.removeItem).toHaveBeenCalledWith('_sec_key1');
254
+ expect(mockLocalStorage.removeItem).toHaveBeenCalledWith('_sec_key2');
255
+
256
+ // Restore original Object.keys
257
+ Object.keys = originalKeys;
258
+ });
259
+ });
260
+
261
+ describe('Edge Cases', () => {
262
+ it('should handle multiple initializations', async () => {
263
+ mockLocalStorage.getItem.mockReturnValue(null);
264
+ mockCrypto.subtle.generateKey.mockResolvedValue({} as CryptoKey);
265
+ mockCrypto.subtle.exportKey.mockResolvedValue(new ArrayBuffer(32));
266
+
267
+ await secureStorage.init();
268
+
269
+ // Reset the initialized state to test the second call
270
+ (secureStorage as any).initialized = false;
271
+ await secureStorage.init(); // Should not reinitialize
272
+
273
+ expect(mockCrypto.subtle.generateKey).toHaveBeenCalledTimes(1);
274
+ });
275
+
276
+ it('should handle retrieval errors gracefully', async () => {
277
+ // Mock getItem to throw an error
278
+ mockLocalStorage.getItem.mockImplementation(() => {
279
+ throw new Error('Retrieval failed');
280
+ });
281
+
282
+ await expect(secureStorage.getItem('test-key')).rejects.toThrow('Retrieval failed');
283
+ });
284
+
285
+ it('should handle storage errors gracefully', async () => {
286
+ mockLocalStorage.setItem.mockImplementation(() => {
287
+ throw new Error('Storage failed');
288
+ });
289
+
290
+ await expect(secureStorage.setItem('test-key', 'test-value')).rejects.toThrow();
291
+ });
292
+ });
293
+ });
@@ -0,0 +1,127 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import {
3
+ logSecurityEvent,
4
+ validateUserSession,
5
+ createSecureSession,
6
+ invalidateSession,
7
+ getSecurityHeaders,
8
+ validateSecurityHeaders,
9
+ generateDeviceFingerprint,
10
+ validateDeviceFingerprint
11
+ } from '../security';
12
+
13
+ describe('Security Utils', () => {
14
+ beforeEach(() => {
15
+ vi.restoreAllMocks();
16
+ });
17
+
18
+ it('logSecurityEvent logs to console.warn', () => {
19
+ const spy = vi.spyOn(console, 'warn').mockImplementation(() => {});
20
+ const event = { type: 'test', timestamp: new Date(), details: { foo: 1 } };
21
+ logSecurityEvent(event);
22
+ expect(spy).toHaveBeenCalledWith('[SECURITY EVENT]', expect.objectContaining({ type: 'test', details: { foo: 1 }, timestamp: event.timestamp.toISOString() }));
23
+ });
24
+
25
+ it('validateUserSession returns false and logs for invalid userId', async () => {
26
+ const spy = vi.spyOn(console, 'warn').mockImplementation(() => {});
27
+ const result = await validateUserSession('', 'sometoken');
28
+ expect(result).toBe(false);
29
+ expect(spy).toHaveBeenCalledWith('[SECURITY EVENT]', expect.objectContaining({ type: 'invalid_session_validation' }));
30
+ });
31
+
32
+ it('validateUserSession returns false and logs for short sessionToken', async () => {
33
+ const spy = vi.spyOn(console, 'warn').mockImplementation(() => {});
34
+ const result = await validateUserSession('user1', 'short');
35
+ expect(result).toBe(false);
36
+ expect(spy).toHaveBeenCalledWith('[SECURITY EVENT]', expect.objectContaining({ type: 'suspicious_session_token' }));
37
+ });
38
+
39
+ it('validateUserSession returns true for valid input', async () => {
40
+ const result = await validateUserSession('user1', 'longenoughsessiontoken');
41
+ expect(result).toBe(true);
42
+ });
43
+
44
+ it('createSecureSession returns a session ID and logs', async () => {
45
+ const spy = vi.spyOn(console, 'warn').mockImplementation(() => {});
46
+ const fakeClient = {} as any;
47
+ const sessionId = await createSecureSession(fakeClient, { userId: 'user1', deviceFingerprint: 'abc' });
48
+ expect(typeof sessionId).toBe('string');
49
+ expect(sessionId).toMatch(/^sess_/);
50
+ expect(spy).toHaveBeenCalledWith('[SECURITY EVENT]', expect.objectContaining({ type: 'session_created' }));
51
+ });
52
+
53
+ it('invalidateSession logs and resolves', async () => {
54
+ const spy = vi.spyOn(console, 'warn').mockImplementation(() => {});
55
+ const fakeClient = {} as any;
56
+ await expect(invalidateSession(fakeClient, 'sess_123')).resolves.toBeUndefined();
57
+ expect(spy).toHaveBeenCalledWith('[SECURITY EVENT]', expect.objectContaining({ type: 'session_invalidated' }));
58
+ });
59
+
60
+ it('getSecurityHeaders returns required headers', () => {
61
+ const headers = getSecurityHeaders();
62
+ expect(headers['X-Content-Type-Options']).toBe('nosniff');
63
+ expect(headers['X-Frame-Options']).toBe('DENY');
64
+ expect(headers['X-XSS-Protection']).toBe('1; mode=block');
65
+ expect(headers['Referrer-Policy']).toBe('strict-origin-when-cross-origin');
66
+ expect(headers['Strict-Transport-Security']).toMatch(/max-age/);
67
+ });
68
+
69
+ it('validateSecurityHeaders returns true for valid headers', () => {
70
+ const headers = getSecurityHeaders();
71
+ expect(validateSecurityHeaders(headers)).toBe(true);
72
+ });
73
+
74
+ it('validateSecurityHeaders returns false and logs for missing headers', () => {
75
+ const spy = vi.spyOn(console, 'warn').mockImplementation(() => {});
76
+ const headers = { 'X-Content-Type-Options': 'nosniff' };
77
+ expect(validateSecurityHeaders(headers)).toBe(false);
78
+ expect(spy).toHaveBeenCalledWith('[SECURITY EVENT]', expect.objectContaining({ type: 'missing_security_headers' }));
79
+ });
80
+
81
+ it('generateDeviceFingerprint returns a string', () => {
82
+ // Mock browser APIs
83
+ globalThis.navigator = { userAgent: 'test', language: 'en-US' } as any;
84
+ globalThis.screen = { width: 100, height: 200 } as any;
85
+
86
+ // Mock HTMLCanvasElement for JSDOM environment
87
+ const mockCanvas = {
88
+ getContext: vi.fn().mockReturnValue({
89
+ fillText: vi.fn(),
90
+ measureText: vi.fn().mockReturnValue({ width: 100 })
91
+ }),
92
+ toDataURL: vi.fn().mockReturnValue('data:image/png;base64,mockdata')
93
+ };
94
+
95
+ // Mock document.createElement to return our mock canvas
96
+ const originalCreateElement = document.createElement;
97
+ document.createElement = vi.fn().mockImplementation((tagName) => {
98
+ if (tagName === 'canvas') {
99
+ return mockCanvas as any;
100
+ }
101
+ return originalCreateElement.call(document, tagName);
102
+ });
103
+
104
+ const fingerprint = generateDeviceFingerprint();
105
+ expect(typeof fingerprint).toBe('string');
106
+ expect(fingerprint.length).toBeGreaterThan(0);
107
+
108
+ // Restore original createElement
109
+ document.createElement = originalCreateElement;
110
+ });
111
+
112
+ it('validateDeviceFingerprint returns true for valid match', () => {
113
+ expect(validateDeviceFingerprint('abc', 'abc')).toBe(true);
114
+ });
115
+
116
+ it('validateDeviceFingerprint returns false and logs for invalid input', () => {
117
+ const spy = vi.spyOn(console, 'warn').mockImplementation(() => {});
118
+ expect(validateDeviceFingerprint('', 'abc')).toBe(false);
119
+ expect(spy).toHaveBeenCalledWith('[SECURITY EVENT]', expect.objectContaining({ type: 'invalid_device_fingerprint' }));
120
+ });
121
+
122
+ it('validateDeviceFingerprint returns false and logs for mismatch', () => {
123
+ const spy = vi.spyOn(console, 'warn').mockImplementation(() => {});
124
+ expect(validateDeviceFingerprint('abc', 'def')).toBe(false);
125
+ expect(spy).toHaveBeenCalledWith('[SECURITY EVENT]', expect.objectContaining({ type: 'device_fingerprint_mismatch' }));
126
+ });
127
+ });
@@ -0,0 +1,280 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { securityMonitor } from '../securityMonitor';
3
+
4
+ describe('securityMonitor', () => {
5
+ beforeEach(() => {
6
+ // Reset the monitor before each test
7
+ securityMonitor.clearEvents();
8
+ });
9
+
10
+ afterEach(() => {
11
+ vi.restoreAllMocks();
12
+ });
13
+
14
+ describe('logEvent', () => {
15
+ it('should log a security event with generated ID and timestamp', () => {
16
+ const event = {
17
+ action: 'login_attempt',
18
+ details: { userId: 'user-123', ip: '192.168.1.1' }
19
+ };
20
+
21
+ securityMonitor.logEvent(event);
22
+
23
+ const events = securityMonitor.getEvents();
24
+ expect(events).toHaveLength(1);
25
+ expect(events[0].action).toBe('login_attempt');
26
+ expect(events[0].details).toEqual({ userId: 'user-123', ip: '192.168.1.1' });
27
+ expect(events[0].id).toBeDefined();
28
+ expect(events[0].timestamp).toBeDefined();
29
+ expect(typeof events[0].id).toBe('string');
30
+ expect(typeof events[0].timestamp).toBe('number');
31
+ });
32
+
33
+ it('should log multiple events', () => {
34
+ const event1 = { action: 'login_attempt', details: { userId: 'user-123' } };
35
+ const event2 = { action: 'logout', details: { userId: 'user-123' } };
36
+
37
+ securityMonitor.logEvent(event1);
38
+ securityMonitor.logEvent(event2);
39
+
40
+ const events = securityMonitor.getEvents();
41
+ expect(events).toHaveLength(2);
42
+ expect(events[0].action).toBe('login_attempt');
43
+ expect(events[1].action).toBe('logout');
44
+ });
45
+
46
+ it('should generate unique IDs for each event', () => {
47
+ const event1 = { action: 'event1', details: {} };
48
+ const event2 = { action: 'event2', details: {} };
49
+
50
+ securityMonitor.logEvent(event1);
51
+ securityMonitor.logEvent(event2);
52
+
53
+ const events = securityMonitor.getEvents();
54
+ expect(events[0].id).not.toBe(events[1].id);
55
+ });
56
+
57
+ it('should always generate new ID and timestamp', () => {
58
+ const event = {
59
+ id: 'custom-id',
60
+ action: 'custom_action',
61
+ details: { custom: 'data' },
62
+ timestamp: 1234567890
63
+ };
64
+
65
+ securityMonitor.logEvent(event);
66
+
67
+ const events = securityMonitor.getEvents();
68
+ expect(events[0].id).not.toBe('custom-id');
69
+ expect(events[0].timestamp).not.toBe(1234567890);
70
+ expect(typeof events[0].id).toBe('string');
71
+ expect(typeof events[0].timestamp).toBe('number');
72
+ });
73
+
74
+ it('should handle events with complex details', () => {
75
+ const event = {
76
+ action: 'data_access',
77
+ details: {
78
+ userId: 'user-123',
79
+ resource: 'sensitive_data',
80
+ permissions: ['read', 'write'],
81
+ metadata: {
82
+ browser: 'Chrome',
83
+ ip: '192.168.1.1',
84
+ userAgent: 'Mozilla/5.0...'
85
+ }
86
+ }
87
+ };
88
+
89
+ securityMonitor.logEvent(event);
90
+
91
+ const events = securityMonitor.getEvents();
92
+ expect(events[0].details).toEqual(event.details);
93
+ });
94
+ });
95
+
96
+ describe('getEvents', () => {
97
+ it('should return empty array when no events logged', () => {
98
+ const events = securityMonitor.getEvents();
99
+ expect(events).toEqual([]);
100
+ });
101
+
102
+ it('should return copy of events array', () => {
103
+ const event = { action: 'test', details: {} };
104
+ securityMonitor.logEvent(event);
105
+
106
+ const events1 = securityMonitor.getEvents();
107
+ const events2 = securityMonitor.getEvents();
108
+
109
+ expect(events1).toEqual(events2);
110
+ expect(events1).not.toBe(events2); // Should be different references
111
+ });
112
+
113
+ it('should return all logged events in order', () => {
114
+ const events = [
115
+ { action: 'event1', details: {} },
116
+ { action: 'event2', details: {} },
117
+ { action: 'event3', details: {} }
118
+ ];
119
+
120
+ events.forEach(event => securityMonitor.logEvent(event));
121
+
122
+ const retrievedEvents = securityMonitor.getEvents();
123
+ expect(retrievedEvents).toHaveLength(3);
124
+ expect(retrievedEvents[0].action).toBe('event1');
125
+ expect(retrievedEvents[1].action).toBe('event2');
126
+ expect(retrievedEvents[2].action).toBe('event3');
127
+ });
128
+ });
129
+
130
+ describe('clearEvents', () => {
131
+ it('should clear all logged events', () => {
132
+ const event = { action: 'test', details: {} };
133
+ securityMonitor.logEvent(event);
134
+
135
+ expect(securityMonitor.getEvents()).toHaveLength(1);
136
+
137
+ securityMonitor.clearEvents();
138
+
139
+ expect(securityMonitor.getEvents()).toHaveLength(0);
140
+ });
141
+
142
+ it('should clear events multiple times', () => {
143
+ const event = { action: 'test', details: {} };
144
+ securityMonitor.logEvent(event);
145
+ securityMonitor.clearEvents();
146
+ securityMonitor.logEvent(event);
147
+ securityMonitor.clearEvents();
148
+
149
+ expect(securityMonitor.getEvents()).toHaveLength(0);
150
+ });
151
+ });
152
+
153
+ describe('createAlert', () => {
154
+ it('should create an alert with generated ID and timestamp', () => {
155
+ const alertData = {
156
+ type: 'suspicious_activity',
157
+ message: 'Multiple failed login attempts detected'
158
+ };
159
+
160
+ const alert = securityMonitor.createAlert(alertData);
161
+
162
+ expect(alert.id).toBeDefined();
163
+ expect(alert.type).toBe('suspicious_activity');
164
+ expect(alert.message).toBe('Multiple failed login attempts detected');
165
+ expect(alert.timestamp).toBeInstanceOf(Date);
166
+ expect(typeof alert.id).toBe('string');
167
+ });
168
+
169
+ it('should generate unique IDs for each alert', () => {
170
+ const alert1 = securityMonitor.createAlert({
171
+ type: 'alert1',
172
+ message: 'First alert'
173
+ });
174
+
175
+ const alert2 = securityMonitor.createAlert({
176
+ type: 'alert2',
177
+ message: 'Second alert'
178
+ });
179
+
180
+ expect(alert1.id).not.toBe(alert2.id);
181
+ });
182
+
183
+ it('should create alerts with different types', () => {
184
+ const alertTypes = [
185
+ { type: 'suspicious_activity', message: 'Suspicious activity detected' },
186
+ { type: 'authentication_failure', message: 'Authentication failed' },
187
+ { type: 'data_breach', message: 'Potential data breach detected' }
188
+ ];
189
+
190
+ const alerts = alertTypes.map(data => securityMonitor.createAlert(data));
191
+
192
+ alerts.forEach((alert, index) => {
193
+ expect(alert.type).toBe(alertTypes[index].type);
194
+ expect(alert.message).toBe(alertTypes[index].message);
195
+ expect(alert.id).toBeDefined();
196
+ expect(alert.timestamp).toBeInstanceOf(Date);
197
+ });
198
+ });
199
+
200
+ it('should create alerts with complex messages', () => {
201
+ const alertData = {
202
+ type: 'security_violation',
203
+ message: 'User admin@example.com attempted to access restricted resource /api/admin/users from IP 192.168.1.100 without proper permissions'
204
+ };
205
+
206
+ const alert = securityMonitor.createAlert(alertData);
207
+
208
+ expect(alert.message).toBe(alertData.message);
209
+ expect(alert.type).toBe('security_violation');
210
+ });
211
+ });
212
+
213
+ describe('Integration tests', () => {
214
+ it('should handle complete security monitoring workflow', () => {
215
+ // Log some security events
216
+ securityMonitor.logEvent({
217
+ action: 'login_attempt',
218
+ details: { userId: 'user-123', success: false }
219
+ });
220
+
221
+ securityMonitor.logEvent({
222
+ action: 'login_attempt',
223
+ details: { userId: 'user-123', success: false }
224
+ });
225
+
226
+ securityMonitor.logEvent({
227
+ action: 'login_attempt',
228
+ details: { userId: 'user-123', success: false }
229
+ });
230
+
231
+ // Create an alert for multiple failed attempts
232
+ const alert = securityMonitor.createAlert({
233
+ type: 'multiple_failed_logins',
234
+ message: 'User user-123 has 3 failed login attempts'
235
+ });
236
+
237
+ // Verify events and alert
238
+ const events = securityMonitor.getEvents();
239
+ expect(events).toHaveLength(3);
240
+ expect(events.every(e => e.action === 'login_attempt')).toBe(true);
241
+ expect(alert.type).toBe('multiple_failed_logins');
242
+ expect(alert.message).toBe('User user-123 has 3 failed login attempts');
243
+
244
+ // Clear events
245
+ securityMonitor.clearEvents();
246
+ expect(securityMonitor.getEvents()).toHaveLength(0);
247
+ });
248
+
249
+ it('should handle high-volume event logging', () => {
250
+ const events = Array.from({ length: 100 }, (_, i) => ({
251
+ action: `event_${i}`,
252
+ details: { index: i, timestamp: Date.now() }
253
+ }));
254
+
255
+ events.forEach(event => securityMonitor.logEvent(event));
256
+
257
+ const retrievedEvents = securityMonitor.getEvents();
258
+ expect(retrievedEvents).toHaveLength(100);
259
+ expect(retrievedEvents[0].action).toBe('event_0');
260
+ expect(retrievedEvents[99].action).toBe('event_99');
261
+ });
262
+
263
+ it('should maintain event order under concurrent access simulation', () => {
264
+ const events = [
265
+ { action: 'start_session', details: { userId: 'user-123' } },
266
+ { action: 'access_resource', details: { resource: '/api/data' } },
267
+ { action: 'end_session', details: { userId: 'user-123' } }
268
+ ];
269
+
270
+ // Simulate concurrent logging
271
+ events.forEach(event => securityMonitor.logEvent(event));
272
+
273
+ const retrievedEvents = securityMonitor.getEvents();
274
+ expect(retrievedEvents).toHaveLength(3);
275
+ expect(retrievedEvents[0].action).toBe('start_session');
276
+ expect(retrievedEvents[1].action).toBe('access_resource');
277
+ expect(retrievedEvents[2].action).toBe('end_session');
278
+ });
279
+ });
280
+ });