@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.
- package/dist/{DataTable-ZQDRE46Q.js → DataTable-3SSI644S.js} +2 -2
- package/dist/{chunk-M4RW7PIP.js → chunk-2BJFM2JC.js} +105 -81
- package/dist/chunk-2BJFM2JC.js.map +1 -0
- package/dist/{chunk-5H3C2SWM.js → chunk-RTCA5ZNK.js} +2 -2
- package/dist/components.js +2 -2
- package/dist/index.js +2 -2
- package/dist/styles/core.css +3 -0
- 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 +167 -138
- package/src/components/Header/Header.test.tsx +1 -1
- package/src/components/OrganisationSelector/OrganisationSelector.test.tsx +1 -1
- 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/api.test.ts +511 -0
- package/src/rbac/components/__tests__/NavigationGuard.test.tsx +843 -0
- package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +1007 -0
- package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +806 -0
- package/src/rbac/components/__tests__/RoleBasedRouter.test.tsx +741 -0
- package/src/rbac/hooks/useCan.test.ts +1 -1
- package/src/rbac/hooks/usePermissions.test.ts +10 -5
- package/src/rbac/hooks/useRBAC.test.ts +141 -93
- package/src/rbac/utils/__tests__/eventContext.test.ts +428 -0
- package/src/rbac/utils/__tests__/eventContext.unit.test.ts +428 -0
- package/src/styles/core.css +3 -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-3SSI644S.js.map} +0 -0
- /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
|
+
});
|