@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.
- package/dist/{DataTable-ZQDRE46Q.js → DataTable-BEMN72L5.js} +2 -2
- package/dist/{chunk-5H3C2SWM.js → chunk-4EIBJ6DF.js} +2 -2
- package/dist/{chunk-M4RW7PIP.js → chunk-SFGUMWEE.js} +105 -81
- package/dist/chunk-SFGUMWEE.js.map +1 -0
- package/dist/components.js +2 -2
- package/dist/index.js +2 -2
- package/dist/utils.js +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +1 -1
- package/docs/api/classes/MissingUserContextError.md +1 -1
- package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
- package/docs/api/classes/PermissionDeniedError.md +1 -1
- package/docs/api/classes/PublicErrorBoundary.md +1 -1
- package/docs/api/classes/RBACAuditManager.md +1 -1
- package/docs/api/classes/RBACCache.md +1 -1
- package/docs/api/classes/RBACEngine.md +1 -1
- package/docs/api/classes/RBACError.md +1 -1
- package/docs/api/classes/RBACNotInitializedError.md +1 -1
- package/docs/api/classes/SecureSupabaseClient.md +1 -1
- package/docs/api/interfaces/AggregateConfig.md +1 -1
- package/docs/api/interfaces/ButtonProps.md +1 -1
- package/docs/api/interfaces/CardProps.md +1 -1
- package/docs/api/interfaces/ColorPalette.md +1 -1
- package/docs/api/interfaces/ColorShade.md +1 -1
- package/docs/api/interfaces/DataAccessRecord.md +1 -1
- package/docs/api/interfaces/DataTableAction.md +1 -1
- package/docs/api/interfaces/DataTableColumn.md +1 -1
- package/docs/api/interfaces/DataTableProps.md +34 -34
- package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
- package/docs/api/interfaces/EmptyStateConfig.md +1 -1
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
- package/docs/api/interfaces/EventContextType.md +1 -1
- package/docs/api/interfaces/EventLogoProps.md +1 -1
- package/docs/api/interfaces/EventProviderProps.md +1 -1
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadProps.md +1 -1
- package/docs/api/interfaces/FooterProps.md +1 -1
- package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
- package/docs/api/interfaces/InputProps.md +1 -1
- package/docs/api/interfaces/LabelProps.md +1 -1
- package/docs/api/interfaces/LoginFormProps.md +1 -1
- package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
- package/docs/api/interfaces/NavigationContextType.md +1 -1
- package/docs/api/interfaces/NavigationGuardProps.md +1 -1
- package/docs/api/interfaces/NavigationItem.md +1 -1
- package/docs/api/interfaces/NavigationMenuProps.md +1 -1
- package/docs/api/interfaces/NavigationProviderProps.md +1 -1
- package/docs/api/interfaces/Organisation.md +1 -1
- package/docs/api/interfaces/OrganisationContextType.md +1 -1
- package/docs/api/interfaces/OrganisationMembership.md +1 -1
- package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
- package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
- package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
- package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
- package/docs/api/interfaces/PageAccessRecord.md +1 -1
- package/docs/api/interfaces/PagePermissionContextType.md +1 -1
- package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
- package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
- package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
- package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
- package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
- package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
- package/docs/api/interfaces/RBACConfig.md +1 -1
- package/docs/api/interfaces/RBACContextType.md +1 -1
- package/docs/api/interfaces/RBACLogger.md +1 -1
- package/docs/api/interfaces/RBACProviderProps.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
- package/docs/api/interfaces/RouteAccessRecord.md +1 -1
- package/docs/api/interfaces/RouteConfig.md +1 -1
- package/docs/api/interfaces/SecureDataContextType.md +1 -1
- package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
- package/docs/api/interfaces/StorageConfig.md +1 -1
- package/docs/api/interfaces/StorageFileInfo.md +1 -1
- package/docs/api/interfaces/StorageFileMetadata.md +1 -1
- package/docs/api/interfaces/StorageListOptions.md +1 -1
- package/docs/api/interfaces/StorageListResult.md +1 -1
- package/docs/api/interfaces/StorageUploadOptions.md +1 -1
- package/docs/api/interfaces/StorageUploadResult.md +1 -1
- package/docs/api/interfaces/StorageUrlOptions.md +1 -1
- package/docs/api/interfaces/StyleImport.md +1 -1
- package/docs/api/interfaces/ToastActionElement.md +1 -1
- package/docs/api/interfaces/ToastProps.md +1 -1
- package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventLogoOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UserEventAccess.md +1 -1
- package/docs/api/interfaces/UserMenuProps.md +1 -1
- package/docs/api/interfaces/UserProfile.md +1 -1
- package/docs/api/modules.md +3 -3
- package/docs/implementation-guides/data-tables.md +20 -0
- package/docs/quick-reference.md +9 -0
- package/docs/rbac/examples.md +4 -0
- package/package.json +1 -1
- package/src/__tests__/helpers/test-utils.tsx +147 -1
- package/src/components/DataTable/DataTable.tsx +20 -0
- package/src/components/DataTable/__tests__/DataTable.hooks.test 2.tsx +191 -0
- package/src/components/DataTable/__tests__/DataTable.hooks.test.tsx +191 -0
- package/src/components/DataTable/components/DataTableCore.tsx +164 -131
- package/src/hooks/__tests__/hooks.integration.test.tsx +575 -0
- package/src/hooks/__tests__/useApiFetch.unit.test.ts +115 -0
- package/src/hooks/__tests__/useComponentPerformance.unit.test.tsx +133 -0
- package/src/hooks/__tests__/useDebounce.unit.test.ts +82 -0
- package/src/hooks/__tests__/useFocusTrap.unit.test.tsx +293 -0
- package/src/hooks/__tests__/useInactivityTracker.unit.test.ts +385 -0
- package/src/hooks/__tests__/useOrganisationPermissions.unit.test.tsx +286 -0
- package/src/hooks/__tests__/useOrganisationSecurity.unit.test.tsx +838 -0
- package/src/hooks/__tests__/usePermissionCache.simple.test.ts +104 -0
- package/src/hooks/__tests__/usePermissionCache.unit.test.ts +633 -0
- package/src/hooks/__tests__/useRBAC.unit.test.ts +856 -0
- package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +537 -0
- package/src/hooks/__tests__/useToast.unit.test.tsx +62 -0
- package/src/hooks/__tests__/useZodForm.unit.test.tsx +37 -0
- package/src/rbac/utils/__tests__/eventContext.test.ts +428 -0
- package/src/rbac/utils/__tests__/eventContext.unit.test.ts +428 -0
- package/src/utils/__tests__/appConfig.unit.test.ts +55 -0
- package/src/utils/__tests__/audit.unit.test.ts +69 -0
- package/src/utils/__tests__/auth-utils.unit.test.ts +70 -0
- package/src/utils/__tests__/bundleAnalysis.unit.test.ts +317 -0
- package/src/utils/__tests__/cn.unit.test.ts +34 -0
- package/src/utils/__tests__/deviceFingerprint.unit.test.ts +503 -0
- package/src/utils/__tests__/dynamicUtils.unit.test.ts +322 -0
- package/src/utils/__tests__/formatDate.unit.test.ts +109 -0
- package/src/utils/__tests__/formatting.unit.test.ts +66 -0
- package/src/utils/__tests__/index.unit.test.ts +251 -0
- package/src/utils/__tests__/lazyLoad.unit.test.tsx +309 -0
- package/src/utils/__tests__/organisationContext.unit.test.ts +192 -0
- package/src/utils/__tests__/performanceBudgets.unit.test.ts +259 -0
- package/src/utils/__tests__/permissionTypes.unit.test.ts +250 -0
- package/src/utils/__tests__/permissionUtils.unit.test.ts +362 -0
- package/src/utils/__tests__/sanitization.unit.test.ts +346 -0
- package/src/utils/__tests__/schemaUtils.unit.test.ts +441 -0
- package/src/utils/__tests__/secureDataAccess.unit.test.ts +334 -0
- package/src/utils/__tests__/secureErrors.unit.test.ts +377 -0
- package/src/utils/__tests__/secureStorage.unit.test.ts +293 -0
- package/src/utils/__tests__/security.unit.test.ts +127 -0
- package/src/utils/__tests__/securityMonitor.unit.test.ts +280 -0
- package/src/utils/__tests__/sessionTracking.unit.test.ts +356 -0
- package/src/utils/__tests__/validation.unit.test.ts +84 -0
- package/src/utils/__tests__/validationUtils.unit.test.ts +571 -0
- package/src/validation/__tests__/common.unit.test.ts +101 -0
- package/src/validation/__tests__/csrf.unit.test.ts +302 -0
- package/src/validation/__tests__/passwordSchema.unit.test 2.ts +98 -0
- package/src/validation/__tests__/passwordSchema.unit.test.ts +98 -0
- package/src/validation/__tests__/sqlInjectionProtection.unit.test.ts +466 -0
- package/dist/chunk-M4RW7PIP.js.map +0 -1
- /package/dist/{DataTable-ZQDRE46Q.js.map → DataTable-BEMN72L5.js.map} +0 -0
- /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
|
+
});
|