@jmruthers/pace-core 0.5.3 → 0.5.5

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 (170) hide show
  1. package/dist/{DataTable-ZQDRE46Q.js → DataTable-3SSI644S.js} +2 -2
  2. package/dist/{chunk-M4RW7PIP.js → chunk-2BJFM2JC.js} +105 -81
  3. package/dist/chunk-2BJFM2JC.js.map +1 -0
  4. package/dist/{chunk-5H3C2SWM.js → chunk-RTCA5ZNK.js} +2 -2
  5. package/dist/components.js +2 -2
  6. package/dist/index.js +2 -2
  7. package/dist/styles/core.css +3 -0
  8. package/dist/utils.js +1 -1
  9. package/docs/api/classes/ErrorBoundary.md +1 -1
  10. package/docs/api/classes/InvalidScopeError.md +1 -1
  11. package/docs/api/classes/MissingUserContextError.md +1 -1
  12. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  13. package/docs/api/classes/PermissionDeniedError.md +1 -1
  14. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  15. package/docs/api/classes/RBACAuditManager.md +1 -1
  16. package/docs/api/classes/RBACCache.md +1 -1
  17. package/docs/api/classes/RBACEngine.md +1 -1
  18. package/docs/api/classes/RBACError.md +1 -1
  19. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  20. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  21. package/docs/api/interfaces/AggregateConfig.md +1 -1
  22. package/docs/api/interfaces/ButtonProps.md +1 -1
  23. package/docs/api/interfaces/CardProps.md +1 -1
  24. package/docs/api/interfaces/ColorPalette.md +1 -1
  25. package/docs/api/interfaces/ColorShade.md +1 -1
  26. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  27. package/docs/api/interfaces/DataTableAction.md +1 -1
  28. package/docs/api/interfaces/DataTableColumn.md +1 -1
  29. package/docs/api/interfaces/DataTableProps.md +34 -34
  30. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  31. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  32. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  33. package/docs/api/interfaces/EventContextType.md +1 -1
  34. package/docs/api/interfaces/EventLogoProps.md +1 -1
  35. package/docs/api/interfaces/EventProviderProps.md +1 -1
  36. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  37. package/docs/api/interfaces/FileUploadProps.md +1 -1
  38. package/docs/api/interfaces/FooterProps.md +1 -1
  39. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  40. package/docs/api/interfaces/InputProps.md +1 -1
  41. package/docs/api/interfaces/LabelProps.md +1 -1
  42. package/docs/api/interfaces/LoginFormProps.md +1 -1
  43. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  44. package/docs/api/interfaces/NavigationContextType.md +1 -1
  45. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  46. package/docs/api/interfaces/NavigationItem.md +1 -1
  47. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  48. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  49. package/docs/api/interfaces/Organisation.md +1 -1
  50. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  51. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  52. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  53. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  54. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  55. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  56. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  57. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  58. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  59. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  60. package/docs/api/interfaces/PaletteData.md +1 -1
  61. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  62. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  63. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  64. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  65. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  66. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  67. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  68. package/docs/api/interfaces/RBACConfig.md +1 -1
  69. package/docs/api/interfaces/RBACContextType.md +1 -1
  70. package/docs/api/interfaces/RBACLogger.md +1 -1
  71. package/docs/api/interfaces/RBACProviderProps.md +1 -1
  72. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  73. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  74. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  75. package/docs/api/interfaces/RouteConfig.md +1 -1
  76. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  77. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  78. package/docs/api/interfaces/StorageConfig.md +1 -1
  79. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  80. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  81. package/docs/api/interfaces/StorageListOptions.md +1 -1
  82. package/docs/api/interfaces/StorageListResult.md +1 -1
  83. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  84. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  85. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  86. package/docs/api/interfaces/StyleImport.md +1 -1
  87. package/docs/api/interfaces/ToastActionElement.md +1 -1
  88. package/docs/api/interfaces/ToastProps.md +1 -1
  89. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  90. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  91. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  92. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  93. package/docs/api/interfaces/UsePublicEventLogoOptions.md +1 -1
  94. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  95. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  96. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  97. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  98. package/docs/api/interfaces/UserEventAccess.md +1 -1
  99. package/docs/api/interfaces/UserMenuProps.md +1 -1
  100. package/docs/api/interfaces/UserProfile.md +1 -1
  101. package/docs/api/modules.md +3 -3
  102. package/docs/implementation-guides/data-tables.md +20 -0
  103. package/docs/quick-reference.md +9 -0
  104. package/docs/rbac/examples.md +4 -0
  105. package/package.json +1 -1
  106. package/src/__tests__/helpers/test-utils.tsx +147 -1
  107. package/src/components/DataTable/DataTable.tsx +20 -0
  108. package/src/components/DataTable/__tests__/DataTable.hooks.test 2.tsx +191 -0
  109. package/src/components/DataTable/__tests__/DataTable.hooks.test.tsx +191 -0
  110. package/src/components/DataTable/components/DataTableCore.tsx +167 -138
  111. package/src/components/Header/Header.test.tsx +1 -1
  112. package/src/components/OrganisationSelector/OrganisationSelector.test.tsx +1 -1
  113. package/src/hooks/__tests__/hooks.integration.test.tsx +575 -0
  114. package/src/hooks/__tests__/useApiFetch.unit.test.ts +115 -0
  115. package/src/hooks/__tests__/useComponentPerformance.unit.test.tsx +133 -0
  116. package/src/hooks/__tests__/useDebounce.unit.test.ts +82 -0
  117. package/src/hooks/__tests__/useFocusTrap.unit.test.tsx +293 -0
  118. package/src/hooks/__tests__/useInactivityTracker.unit.test.ts +385 -0
  119. package/src/hooks/__tests__/useOrganisationPermissions.unit.test.tsx +286 -0
  120. package/src/hooks/__tests__/useOrganisationSecurity.unit.test.tsx +838 -0
  121. package/src/hooks/__tests__/usePermissionCache.simple.test.ts +104 -0
  122. package/src/hooks/__tests__/usePermissionCache.unit.test.ts +633 -0
  123. package/src/hooks/__tests__/useRBAC.unit.test.ts +856 -0
  124. package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +537 -0
  125. package/src/hooks/__tests__/useToast.unit.test.tsx +62 -0
  126. package/src/hooks/__tests__/useZodForm.unit.test.tsx +37 -0
  127. package/src/rbac/api.test.ts +511 -0
  128. package/src/rbac/components/__tests__/NavigationGuard.test.tsx +843 -0
  129. package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +1007 -0
  130. package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +806 -0
  131. package/src/rbac/components/__tests__/RoleBasedRouter.test.tsx +741 -0
  132. package/src/rbac/hooks/useCan.test.ts +1 -1
  133. package/src/rbac/hooks/usePermissions.test.ts +10 -5
  134. package/src/rbac/hooks/useRBAC.test.ts +141 -93
  135. package/src/rbac/utils/__tests__/eventContext.test.ts +428 -0
  136. package/src/rbac/utils/__tests__/eventContext.unit.test.ts +428 -0
  137. package/src/styles/core.css +3 -0
  138. package/src/utils/__tests__/appConfig.unit.test.ts +55 -0
  139. package/src/utils/__tests__/audit.unit.test.ts +69 -0
  140. package/src/utils/__tests__/auth-utils.unit.test.ts +70 -0
  141. package/src/utils/__tests__/bundleAnalysis.unit.test.ts +317 -0
  142. package/src/utils/__tests__/cn.unit.test.ts +34 -0
  143. package/src/utils/__tests__/deviceFingerprint.unit.test.ts +503 -0
  144. package/src/utils/__tests__/dynamicUtils.unit.test.ts +322 -0
  145. package/src/utils/__tests__/formatDate.unit.test.ts +109 -0
  146. package/src/utils/__tests__/formatting.unit.test.ts +66 -0
  147. package/src/utils/__tests__/index.unit.test.ts +251 -0
  148. package/src/utils/__tests__/lazyLoad.unit.test.tsx +309 -0
  149. package/src/utils/__tests__/organisationContext.unit.test.ts +192 -0
  150. package/src/utils/__tests__/performanceBudgets.unit.test.ts +259 -0
  151. package/src/utils/__tests__/permissionTypes.unit.test.ts +250 -0
  152. package/src/utils/__tests__/permissionUtils.unit.test.ts +362 -0
  153. package/src/utils/__tests__/sanitization.unit.test.ts +346 -0
  154. package/src/utils/__tests__/schemaUtils.unit.test.ts +441 -0
  155. package/src/utils/__tests__/secureDataAccess.unit.test.ts +334 -0
  156. package/src/utils/__tests__/secureErrors.unit.test.ts +377 -0
  157. package/src/utils/__tests__/secureStorage.unit.test.ts +293 -0
  158. package/src/utils/__tests__/security.unit.test.ts +127 -0
  159. package/src/utils/__tests__/securityMonitor.unit.test.ts +280 -0
  160. package/src/utils/__tests__/sessionTracking.unit.test.ts +356 -0
  161. package/src/utils/__tests__/validation.unit.test.ts +84 -0
  162. package/src/utils/__tests__/validationUtils.unit.test.ts +571 -0
  163. package/src/validation/__tests__/common.unit.test.ts +101 -0
  164. package/src/validation/__tests__/csrf.unit.test.ts +302 -0
  165. package/src/validation/__tests__/passwordSchema.unit.test 2.ts +98 -0
  166. package/src/validation/__tests__/passwordSchema.unit.test.ts +98 -0
  167. package/src/validation/__tests__/sqlInjectionProtection.unit.test.ts +466 -0
  168. package/dist/chunk-M4RW7PIP.js.map +0 -1
  169. /package/dist/{DataTable-ZQDRE46Q.js.map → DataTable-3SSI644S.js.map} +0 -0
  170. /package/dist/{chunk-5H3C2SWM.js.map → chunk-RTCA5ZNK.js.map} +0 -0
@@ -0,0 +1,633 @@
1
+ import { renderHook, waitFor } from '@testing-library/react';
2
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
3
+ import { TestWrapper, renderWithProviders } from '../../__tests__/helpers/test-utils';
4
+ import { useRBAC } from '../../rbac/hooks/useRBAC';
5
+
6
+ // Mock the useRBAC hook
7
+ vi.mock('../../rbac/hooks/useRBAC');
8
+
9
+ const mockUseRBAC = vi.mocked(useRBAC);
10
+
11
+ // Import after mocking
12
+ import { usePermissionCache } from '../usePermissionCache';
13
+
14
+ describe('usePermissionCache', () => {
15
+ beforeEach(() => {
16
+ vi.clearAllMocks();
17
+
18
+ // Default mock implementation
19
+ mockUseRBAC.mockReturnValue({
20
+ hasPermission: vi.fn().mockResolvedValue(true),
21
+ user: { id: 'test-user-id' }
22
+ });
23
+ });
24
+
25
+ afterEach(() => {
26
+ vi.clearAllTimers();
27
+ });
28
+
29
+ describe('Initial Configuration', () => {
30
+ it('uses default configuration when no config is provided', () => {
31
+ const { result } = renderHook(() => usePermissionCache(), {
32
+ wrapper: TestWrapper
33
+ });
34
+
35
+ expect(result.current).toHaveProperty('checkPermission');
36
+ expect(result.current).toHaveProperty('checkMultiplePermissions');
37
+ expect(result.current).toHaveProperty('getCachedPermissions');
38
+ expect(result.current).toHaveProperty('invalidateCache');
39
+ expect(result.current).toHaveProperty('getDebugInfo');
40
+ expect(result.current).toHaveProperty('getAuditTrail');
41
+ });
42
+
43
+ it('merges custom configuration with defaults', () => {
44
+ const customConfig = {
45
+ defaultTTL: 10000,
46
+ maxCacheSize: 500,
47
+ enableLogging: true,
48
+ enableAuditTrail: false
49
+ };
50
+
51
+ const { result } = renderHook(() => usePermissionCache(customConfig), {
52
+ wrapper: TestWrapper
53
+ });
54
+
55
+ expect(result.current).toHaveProperty('checkPermission');
56
+ expect(result.current).toHaveProperty('checkMultiplePermissions');
57
+ });
58
+ });
59
+
60
+ describe('Single Permission Checking', () => {
61
+ it('checks permission and caches result', async () => {
62
+ const mockHasPermission = vi.fn().mockResolvedValue(true);
63
+ mockUseRBAC.mockReturnValue({
64
+ hasPermission: mockHasPermission,
65
+ user: { id: 'test-user-id' }
66
+ });
67
+
68
+ const { result } = renderHook(() => usePermissionCache(), {
69
+ wrapper: TestWrapper
70
+ });
71
+
72
+ const permission = await result.current.checkPermission('read', 'dashboard');
73
+
74
+ expect(permission).toBe(true);
75
+ expect(mockHasPermission).toHaveBeenCalledWith('read', 'dashboard');
76
+ });
77
+
78
+ it('returns cached result for subsequent calls', async () => {
79
+ const mockHasPermission = vi.fn().mockResolvedValue(true);
80
+ mockUseRBAC.mockReturnValue({
81
+ hasPermission: mockHasPermission,
82
+ user: { id: 'test-user-id' }
83
+ });
84
+
85
+ const { result } = renderHook(() => usePermissionCache(), {
86
+ wrapper: TestWrapper
87
+ });
88
+
89
+ // First call
90
+ await result.current.checkPermission('read', 'dashboard');
91
+
92
+ // Second call should use cache
93
+ await result.current.checkPermission('read', 'dashboard');
94
+
95
+ expect(mockHasPermission).toHaveBeenCalledTimes(1);
96
+ });
97
+
98
+ it('respects custom TTL', async () => {
99
+ const mockHasPermission = vi.fn().mockResolvedValue(true);
100
+ mockUseRBAC.mockReturnValue({
101
+ hasPermission: mockHasPermission,
102
+ user: { id: 'test-user-id' }
103
+ });
104
+
105
+ const { result } = renderHook(() => usePermissionCache({ defaultTTL: 1000 }), {
106
+ wrapper: TestWrapper
107
+ });
108
+
109
+ await result.current.checkPermission('read', 'dashboard', 2000);
110
+
111
+ expect(mockHasPermission).toHaveBeenCalledWith('read', 'dashboard');
112
+ });
113
+
114
+ it('handles permission check errors gracefully', async () => {
115
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
116
+ const mockHasPermission = vi.fn().mockRejectedValue(new Error('Database error'));
117
+
118
+ mockUseRBAC.mockReturnValue({
119
+ hasPermission: mockHasPermission,
120
+ user: { id: 'test-user-id' }
121
+ });
122
+
123
+ const { result } = renderHook(() => usePermissionCache(), {
124
+ wrapper: TestWrapper
125
+ });
126
+
127
+ const permission = await result.current.checkPermission('read', 'dashboard');
128
+
129
+ expect(permission).toBe(false);
130
+ expect(consoleSpy).toHaveBeenCalled();
131
+
132
+ consoleSpy.mockRestore();
133
+ });
134
+ });
135
+
136
+ describe('Multiple Permission Checking', () => {
137
+ it('checks multiple permissions efficiently', async () => {
138
+ const mockHasPermission = vi.fn()
139
+ .mockResolvedValueOnce(true)
140
+ .mockResolvedValueOnce(false)
141
+ .mockResolvedValueOnce(true)
142
+ .mockResolvedValueOnce(false);
143
+
144
+ mockUseRBAC.mockReturnValue({
145
+ hasPermission: mockHasPermission,
146
+ user: { id: 'test-user-id' }
147
+ });
148
+
149
+
150
+ const { result } = renderHook(() => usePermissionCache(), {
151
+ wrapper: TestWrapper
152
+ });
153
+
154
+ const permissions = await result.current.checkMultiplePermissions([
155
+ ['read', 'dashboard'],
156
+ ['create', 'dashboard'],
157
+ ['update', 'dashboard'],
158
+ ['delete', 'dashboard']
159
+ ]);
160
+
161
+ expect(permissions).toHaveLength(4);
162
+ expect(permissions[0]).toEqual({
163
+ operation: 'read',
164
+ pageId: 'dashboard',
165
+ hasPermission: true,
166
+ cached: false,
167
+ timestamp: expect.any(Number)
168
+ });
169
+ expect(permissions[1]).toEqual({
170
+ operation: 'create',
171
+ pageId: 'dashboard',
172
+ hasPermission: false,
173
+ cached: false,
174
+ timestamp: expect.any(Number)
175
+ });
176
+ });
177
+
178
+ it('uses cached results for multiple permission checks', async () => {
179
+ const mockHasPermission = vi.fn()
180
+ .mockResolvedValueOnce(true)
181
+ .mockResolvedValueOnce(false);
182
+
183
+ mockUseRBAC.mockReturnValue({
184
+ hasPermission: mockHasPermission,
185
+ user: { id: 'test-user-id' }
186
+ });
187
+
188
+
189
+ const { result } = renderHook(() => usePermissionCache(), {
190
+ wrapper: TestWrapper
191
+ });
192
+
193
+ // First call
194
+ await result.current.checkMultiplePermissions([
195
+ ['read', 'dashboard'],
196
+ ['create', 'dashboard']
197
+ ]);
198
+
199
+ // Second call should use cache
200
+ await result.current.checkMultiplePermissions([
201
+ ['read', 'dashboard'],
202
+ ['create', 'dashboard']
203
+ ]);
204
+
205
+ expect(mockHasPermission).toHaveBeenCalledTimes(2);
206
+ });
207
+
208
+ it('handles mixed cached and uncached permissions', async () => {
209
+ const mockHasPermission = vi.fn()
210
+ .mockResolvedValueOnce(true)
211
+ .mockResolvedValueOnce(false)
212
+ .mockResolvedValueOnce(true);
213
+
214
+ mockUseRBAC.mockReturnValue({
215
+ hasPermission: mockHasPermission,
216
+ user: { id: 'test-user-id' }
217
+ });
218
+
219
+
220
+ const { result } = renderHook(() => usePermissionCache(), {
221
+ wrapper: TestWrapper
222
+ });
223
+
224
+ // Cache first two permissions
225
+ await result.current.checkMultiplePermissions([
226
+ ['read', 'dashboard'],
227
+ ['create', 'dashboard']
228
+ ]);
229
+
230
+ // Check all four permissions (first two should be cached)
231
+ const permissions = await result.current.checkMultiplePermissions([
232
+ ['read', 'dashboard'],
233
+ ['create', 'dashboard'],
234
+ ['update', 'dashboard'],
235
+ ['delete', 'dashboard']
236
+ ]);
237
+
238
+ expect(permissions).toHaveLength(4);
239
+ expect(permissions[0].cached).toBe(true);
240
+ expect(permissions[1].cached).toBe(true);
241
+ expect(permissions[2].cached).toBe(false);
242
+ expect(permissions[3].cached).toBe(false);
243
+ });
244
+ });
245
+
246
+ describe('Cache Management', () => {
247
+ it('enforces maximum cache size', async () => {
248
+ const mockHasPermission = vi.fn().mockResolvedValue(true);
249
+ mockUseRBAC.mockReturnValue({
250
+ hasPermission: mockHasPermission,
251
+ user: { id: 'test-user-id' }
252
+ });
253
+
254
+ const { result } = renderHook(() => usePermissionCache({ maxCacheSize: 3 }), {
255
+ wrapper: TestWrapper
256
+ });
257
+
258
+ // Add more entries than max cache size
259
+ await result.current.checkPermission('read', 'page1');
260
+ await result.current.checkPermission('read', 'page2');
261
+ await result.current.checkPermission('read', 'page3');
262
+ await result.current.checkPermission('read', 'page4');
263
+ await result.current.checkPermission('read', 'page5');
264
+
265
+ // Check that cache size is maintained
266
+ const debugInfo = result.current.getDebugInfo();
267
+ expect(debugInfo.cacheSize).toBeLessThanOrEqual(3);
268
+ });
269
+
270
+ it('invalidates cache entries', async () => {
271
+ const mockHasPermission = vi.fn().mockResolvedValue(true);
272
+ mockUseRBAC.mockReturnValue({
273
+ hasPermission: mockHasPermission,
274
+ user: { id: 'test-user-id' }
275
+ });
276
+
277
+ const { result } = renderHook(() => usePermissionCache(), {
278
+ wrapper: TestWrapper
279
+ });
280
+
281
+ // Cache some permissions
282
+ await result.current.checkPermission('read', 'dashboard');
283
+ await result.current.checkPermission('create', 'dashboard');
284
+
285
+ // Invalidate all cache
286
+ result.current.invalidateCache();
287
+
288
+ // Check permissions again (should not use cache)
289
+ await result.current.checkPermission('read', 'dashboard');
290
+ await result.current.checkPermission('create', 'dashboard');
291
+
292
+ expect(mockHasPermission).toHaveBeenCalledTimes(4);
293
+ });
294
+
295
+ it('invalidates cache entries by pattern', async () => {
296
+ const mockHasPermission = vi.fn().mockResolvedValue(true);
297
+ mockUseRBAC.mockReturnValue({
298
+ hasPermission: mockHasPermission,
299
+ user: { id: 'test-user-id' }
300
+ });
301
+
302
+ const { result } = renderHook(() => usePermissionCache(), {
303
+ wrapper: TestWrapper
304
+ });
305
+
306
+ // Cache permissions for different pages
307
+ await result.current.checkPermission('read', 'dashboard');
308
+ await result.current.checkPermission('read', 'admin');
309
+ await result.current.checkPermission('read', 'users');
310
+
311
+ // Invalidate only dashboard permissions
312
+ result.current.invalidateCache('dashboard');
313
+
314
+ // Check permissions again
315
+ await result.current.checkPermission('read', 'dashboard');
316
+ await result.current.checkPermission('read', 'admin');
317
+
318
+ // dashboard should be called again, admin should use cache
319
+ expect(mockHasPermission).toHaveBeenCalledTimes(4);
320
+ });
321
+
322
+ it('cleans up expired cache entries', async () => {
323
+ vi.useFakeTimers();
324
+
325
+ const mockHasPermission = vi.fn().mockResolvedValue(true);
326
+ mockUseRBAC.mockReturnValue({
327
+ hasPermission: mockHasPermission,
328
+ user: { id: 'test-user-id' }
329
+ });
330
+
331
+ const { result } = renderHook(() => usePermissionCache({ defaultTTL: 1000 }), {
332
+ wrapper: TestWrapper
333
+ });
334
+
335
+ // Cache a permission
336
+ await result.current.checkPermission('read', 'dashboard');
337
+
338
+ // Advance time past TTL
339
+ vi.advanceTimersByTime(2000);
340
+
341
+ // Trigger cleanup by checking another permission
342
+ await result.current.checkPermission('read', 'admin');
343
+
344
+ const debugInfo = result.current.getDebugInfo();
345
+ expect(debugInfo.cacheSize).toBe(1); // Only the new permission should remain
346
+ });
347
+ });
348
+
349
+ describe('Debug Information', () => {
350
+ it('provides accurate debug information', async () => {
351
+ const mockHasPermission = vi.fn()
352
+ .mockResolvedValueOnce(true)
353
+ .mockResolvedValueOnce(false);
354
+
355
+ mockUseRBAC.mockReturnValue({
356
+ hasPermission: mockHasPermission,
357
+ user: { id: 'test-user-id' }
358
+ });
359
+
360
+
361
+ const { result } = renderHook(() => usePermissionCache(), {
362
+ wrapper: TestWrapper
363
+ });
364
+
365
+ // Make some permission checks
366
+ await result.current.checkPermission('read', 'dashboard');
367
+ await result.current.checkPermission('create', 'dashboard');
368
+ await result.current.checkPermission('read', 'dashboard'); // This should be cached
369
+
370
+ const debugInfo = result.current.getDebugInfo();
371
+
372
+ expect(debugInfo.cacheSize).toBe(2);
373
+ expect(debugInfo.cacheHits).toBe(1);
374
+ expect(debugInfo.cacheMisses).toBe(2);
375
+ expect(debugInfo.totalChecks).toBe(3);
376
+ expect(debugInfo.averageResponseTime).toBeGreaterThan(0);
377
+ expect(debugInfo.lastInvalidation).toBeGreaterThan(0);
378
+ });
379
+
380
+ it('tracks response times accurately', async () => {
381
+ const mockHasPermission = vi.fn().mockResolvedValue(true);
382
+ mockUseRBAC.mockReturnValue({
383
+ hasPermission: mockHasPermission,
384
+ user: { id: 'test-user-id' }
385
+ });
386
+
387
+
388
+ const { result } = renderHook(() => usePermissionCache(), {
389
+ wrapper: TestWrapper
390
+ });
391
+
392
+ await result.current.checkPermission('read', 'dashboard');
393
+
394
+ const debugInfo = result.current.getDebugInfo();
395
+ // Just verify that response time is tracked (greater than 0)
396
+ expect(debugInfo.averageResponseTime).toBeGreaterThan(0);
397
+ expect(debugInfo.totalChecks).toBe(1);
398
+ });
399
+ });
400
+
401
+ describe('Audit Trail', () => {
402
+ it('records audit trail when enabled', async () => {
403
+ const mockHasPermission = vi.fn().mockResolvedValue(true);
404
+ mockUseRBAC.mockReturnValue({
405
+ hasPermission: mockHasPermission,
406
+ user: { id: 'test-user-id' }
407
+ });
408
+
409
+ const { result } = renderHook(() => usePermissionCache({ enableAuditTrail: true }), {
410
+ wrapper: TestWrapper
411
+ });
412
+
413
+ await result.current.checkPermission('read', 'dashboard');
414
+ await result.current.checkPermission('create', 'admin');
415
+
416
+ const auditTrail = result.current.getAuditTrail();
417
+
418
+ expect(auditTrail).toHaveLength(2);
419
+ expect(auditTrail[0]).toEqual({
420
+ timestamp: expect.any(Number),
421
+ operation: 'read',
422
+ pageId: 'dashboard',
423
+ result: true,
424
+ cached: false,
425
+ userId: 'test-user-id'
426
+ });
427
+ expect(auditTrail[1]).toEqual({
428
+ timestamp: expect.any(Number),
429
+ operation: 'create',
430
+ pageId: 'admin',
431
+ result: true,
432
+ cached: false,
433
+ userId: 'test-user-id'
434
+ });
435
+ });
436
+
437
+ it('does not record audit trail when disabled', async () => {
438
+ const mockHasPermission = vi.fn().mockResolvedValue(true);
439
+ mockUseRBAC.mockReturnValue({
440
+ hasPermission: mockHasPermission,
441
+ user: { id: 'test-user-id' }
442
+ });
443
+
444
+ const { result } = renderHook(() => usePermissionCache({ enableAuditTrail: false }), {
445
+ wrapper: TestWrapper
446
+ });
447
+
448
+ await result.current.checkPermission('read', 'dashboard');
449
+
450
+ const auditTrail = result.current.getAuditTrail();
451
+ expect(auditTrail).toHaveLength(0);
452
+ });
453
+
454
+ it('limits audit trail size', async () => {
455
+ const mockHasPermission = vi.fn().mockResolvedValue(true);
456
+ mockUseRBAC.mockReturnValue({
457
+ hasPermission: mockHasPermission,
458
+ user: { id: 'test-user-id' }
459
+ });
460
+
461
+ const { result } = renderHook(() => usePermissionCache({ enableAuditTrail: true }), {
462
+ wrapper: TestWrapper
463
+ });
464
+
465
+ // Make more than 1000 permission checks
466
+ for (let i = 0; i < 1100; i++) {
467
+ await result.current.checkPermission('read', `page${i}`);
468
+ }
469
+
470
+ const auditTrail = result.current.getAuditTrail();
471
+ expect(auditTrail.length).toBeLessThanOrEqual(500);
472
+ });
473
+ });
474
+
475
+ describe('Logging', () => {
476
+ it('logs permission checks when enabled', async () => {
477
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
478
+
479
+ const mockHasPermission = vi.fn().mockResolvedValue(true);
480
+ mockUseRBAC.mockReturnValue({
481
+ hasPermission: mockHasPermission,
482
+ user: { id: 'test-user-id' }
483
+ });
484
+
485
+ const { result } = renderHook(() => usePermissionCache({ enableLogging: true }), {
486
+ wrapper: TestWrapper
487
+ });
488
+
489
+ await result.current.checkPermission('read', 'dashboard');
490
+
491
+ expect(consoleSpy).toHaveBeenCalledWith(
492
+ expect.stringContaining('[PermissionCache] read:dashboard = true (fresh)')
493
+ );
494
+
495
+ consoleSpy.mockRestore();
496
+ });
497
+
498
+ it('does not log when disabled', async () => {
499
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
500
+
501
+ const mockHasPermission = vi.fn().mockResolvedValue(true);
502
+ mockUseRBAC.mockReturnValue({
503
+ hasPermission: mockHasPermission,
504
+ user: { id: 'test-user-id' }
505
+ });
506
+
507
+ const { result } = renderHook(() => usePermissionCache({ enableLogging: false }), {
508
+ wrapper: TestWrapper
509
+ });
510
+
511
+ await result.current.checkPermission('read', 'dashboard');
512
+
513
+ expect(consoleSpy).not.toHaveBeenCalledWith(
514
+ expect.stringContaining('[PermissionCache]')
515
+ );
516
+
517
+ consoleSpy.mockRestore();
518
+ });
519
+ });
520
+
521
+ describe('Cached Permissions', () => {
522
+ it('returns cached permissions for a page', async () => {
523
+ const mockHasPermission = vi.fn()
524
+ .mockResolvedValueOnce(true)
525
+ .mockResolvedValueOnce(false)
526
+ .mockResolvedValueOnce(true)
527
+ .mockResolvedValueOnce(false);
528
+
529
+ mockUseRBAC.mockReturnValue({
530
+ hasPermission: mockHasPermission,
531
+ user: { id: 'test-user-id' }
532
+ });
533
+
534
+
535
+ const { result } = renderHook(() => usePermissionCache(), {
536
+ wrapper: TestWrapper
537
+ });
538
+
539
+ // Cache all permissions for dashboard
540
+ await result.current.checkPermission('read', 'dashboard');
541
+ await result.current.checkPermission('create', 'dashboard');
542
+ await result.current.checkPermission('update', 'dashboard');
543
+ await result.current.checkPermission('delete', 'dashboard');
544
+
545
+ const cachedPermissions = result.current.getCachedPermissions('dashboard');
546
+
547
+ expect(cachedPermissions).toHaveLength(4);
548
+ expect(cachedPermissions).toEqual([
549
+ { operation: 'read', hasPermission: true },
550
+ { operation: 'create', hasPermission: false },
551
+ { operation: 'update', hasPermission: true },
552
+ { operation: 'delete', hasPermission: false }
553
+ ]);
554
+ });
555
+
556
+ it('returns empty array for uncached page', async () => {
557
+ const mockHasPermission = vi.fn().mockResolvedValue(true);
558
+ mockUseRBAC.mockReturnValue({
559
+ hasPermission: mockHasPermission,
560
+ user: { id: 'test-user-id' }
561
+ });
562
+
563
+ const { result } = renderHook(() => usePermissionCache(), {
564
+ wrapper: TestWrapper
565
+ });
566
+
567
+ const cachedPermissions = result.current.getCachedPermissions('uncached-page');
568
+
569
+ expect(cachedPermissions).toHaveLength(0);
570
+ });
571
+ });
572
+
573
+ describe('Edge Cases', () => {
574
+ it('handles concurrent permission checks', async () => {
575
+ const mockHasPermission = vi.fn().mockResolvedValue(true);
576
+ mockUseRBAC.mockReturnValue({
577
+ hasPermission: mockHasPermission,
578
+ user: { id: 'test-user-id' }
579
+ });
580
+
581
+ const { result } = renderHook(() => usePermissionCache(), {
582
+ wrapper: TestWrapper
583
+ });
584
+
585
+ // Make concurrent permission checks
586
+ const promises = [
587
+ result.current.checkPermission('read', 'dashboard'),
588
+ result.current.checkPermission('read', 'dashboard'),
589
+ result.current.checkPermission('read', 'dashboard')
590
+ ];
591
+
592
+ const results = await Promise.all(promises);
593
+
594
+ expect(results).toEqual([true, true, true]);
595
+ expect(mockHasPermission).toHaveBeenCalledTimes(1); // Should only call once due to caching
596
+ });
597
+
598
+ it('handles rapid cache invalidation', async () => {
599
+ const mockHasPermission = vi.fn().mockResolvedValue(true);
600
+ mockUseRBAC.mockReturnValue({
601
+ hasPermission: mockHasPermission,
602
+ user: { id: 'test-user-id' }
603
+ });
604
+
605
+ const { result } = renderHook(() => usePermissionCache(), {
606
+ wrapper: TestWrapper
607
+ });
608
+
609
+ await result.current.checkPermission('read', 'dashboard');
610
+ result.current.invalidateCache();
611
+ await result.current.checkPermission('read', 'dashboard');
612
+
613
+ expect(mockHasPermission).toHaveBeenCalledTimes(2);
614
+ });
615
+
616
+ it('handles empty permission arrays', async () => {
617
+ const mockHasPermission = vi.fn().mockResolvedValue(true);
618
+ mockUseRBAC.mockReturnValue({
619
+ hasPermission: mockHasPermission,
620
+ user: { id: 'test-user-id' }
621
+ });
622
+
623
+ const { result } = renderHook(() => usePermissionCache(), {
624
+ wrapper: TestWrapper
625
+ });
626
+
627
+ const permissions = await result.current.checkMultiplePermissions([]);
628
+
629
+ expect(permissions).toHaveLength(0);
630
+ expect(mockHasPermission).not.toHaveBeenCalled();
631
+ });
632
+ });
633
+ });