@jmruthers/pace-core 0.5.4 → 0.5.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/dist/{DataTable-ZQDRE46Q.js → DataTable-BEMN72L5.js} +2 -2
  2. package/dist/{chunk-5H3C2SWM.js → chunk-4EIBJ6DF.js} +2 -2
  3. package/dist/{chunk-M4RW7PIP.js → chunk-SFGUMWEE.js} +105 -81
  4. package/dist/chunk-SFGUMWEE.js.map +1 -0
  5. package/dist/components.js +2 -2
  6. package/dist/index.js +2 -2
  7. package/dist/utils.js +1 -1
  8. package/docs/api/classes/ErrorBoundary.md +1 -1
  9. package/docs/api/classes/InvalidScopeError.md +1 -1
  10. package/docs/api/classes/MissingUserContextError.md +1 -1
  11. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  12. package/docs/api/classes/PermissionDeniedError.md +1 -1
  13. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  14. package/docs/api/classes/RBACAuditManager.md +1 -1
  15. package/docs/api/classes/RBACCache.md +1 -1
  16. package/docs/api/classes/RBACEngine.md +1 -1
  17. package/docs/api/classes/RBACError.md +1 -1
  18. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  19. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  20. package/docs/api/interfaces/AggregateConfig.md +1 -1
  21. package/docs/api/interfaces/ButtonProps.md +1 -1
  22. package/docs/api/interfaces/CardProps.md +1 -1
  23. package/docs/api/interfaces/ColorPalette.md +1 -1
  24. package/docs/api/interfaces/ColorShade.md +1 -1
  25. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  26. package/docs/api/interfaces/DataTableAction.md +1 -1
  27. package/docs/api/interfaces/DataTableColumn.md +1 -1
  28. package/docs/api/interfaces/DataTableProps.md +34 -34
  29. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  30. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  31. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  32. package/docs/api/interfaces/EventContextType.md +1 -1
  33. package/docs/api/interfaces/EventLogoProps.md +1 -1
  34. package/docs/api/interfaces/EventProviderProps.md +1 -1
  35. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  36. package/docs/api/interfaces/FileUploadProps.md +1 -1
  37. package/docs/api/interfaces/FooterProps.md +1 -1
  38. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  39. package/docs/api/interfaces/InputProps.md +1 -1
  40. package/docs/api/interfaces/LabelProps.md +1 -1
  41. package/docs/api/interfaces/LoginFormProps.md +1 -1
  42. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  43. package/docs/api/interfaces/NavigationContextType.md +1 -1
  44. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  45. package/docs/api/interfaces/NavigationItem.md +1 -1
  46. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  47. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  48. package/docs/api/interfaces/Organisation.md +1 -1
  49. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  50. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  51. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  52. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  53. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  54. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  55. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  56. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  57. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  58. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  59. package/docs/api/interfaces/PaletteData.md +1 -1
  60. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  61. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  62. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  63. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  64. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  65. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  66. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  67. package/docs/api/interfaces/RBACConfig.md +1 -1
  68. package/docs/api/interfaces/RBACContextType.md +1 -1
  69. package/docs/api/interfaces/RBACLogger.md +1 -1
  70. package/docs/api/interfaces/RBACProviderProps.md +1 -1
  71. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  72. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  73. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  74. package/docs/api/interfaces/RouteConfig.md +1 -1
  75. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  76. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  77. package/docs/api/interfaces/StorageConfig.md +1 -1
  78. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  79. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  80. package/docs/api/interfaces/StorageListOptions.md +1 -1
  81. package/docs/api/interfaces/StorageListResult.md +1 -1
  82. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  83. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  84. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  85. package/docs/api/interfaces/StyleImport.md +1 -1
  86. package/docs/api/interfaces/ToastActionElement.md +1 -1
  87. package/docs/api/interfaces/ToastProps.md +1 -1
  88. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  89. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  90. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  91. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  92. package/docs/api/interfaces/UsePublicEventLogoOptions.md +1 -1
  93. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  94. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  95. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  96. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  97. package/docs/api/interfaces/UserEventAccess.md +1 -1
  98. package/docs/api/interfaces/UserMenuProps.md +1 -1
  99. package/docs/api/interfaces/UserProfile.md +1 -1
  100. package/docs/api/modules.md +3 -3
  101. package/docs/implementation-guides/data-tables.md +20 -0
  102. package/docs/quick-reference.md +9 -0
  103. package/docs/rbac/examples.md +4 -0
  104. package/package.json +1 -1
  105. package/src/__tests__/helpers/test-utils.tsx +147 -1
  106. package/src/components/DataTable/DataTable.tsx +20 -0
  107. package/src/components/DataTable/__tests__/DataTable.hooks.test 2.tsx +191 -0
  108. package/src/components/DataTable/__tests__/DataTable.hooks.test.tsx +191 -0
  109. package/src/components/DataTable/components/DataTableCore.tsx +164 -131
  110. package/src/hooks/__tests__/hooks.integration.test.tsx +575 -0
  111. package/src/hooks/__tests__/useApiFetch.unit.test.ts +115 -0
  112. package/src/hooks/__tests__/useComponentPerformance.unit.test.tsx +133 -0
  113. package/src/hooks/__tests__/useDebounce.unit.test.ts +82 -0
  114. package/src/hooks/__tests__/useFocusTrap.unit.test.tsx +293 -0
  115. package/src/hooks/__tests__/useInactivityTracker.unit.test.ts +385 -0
  116. package/src/hooks/__tests__/useOrganisationPermissions.unit.test.tsx +286 -0
  117. package/src/hooks/__tests__/useOrganisationSecurity.unit.test.tsx +838 -0
  118. package/src/hooks/__tests__/usePermissionCache.simple.test.ts +104 -0
  119. package/src/hooks/__tests__/usePermissionCache.unit.test.ts +633 -0
  120. package/src/hooks/__tests__/useRBAC.unit.test.ts +856 -0
  121. package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +537 -0
  122. package/src/hooks/__tests__/useToast.unit.test.tsx +62 -0
  123. package/src/hooks/__tests__/useZodForm.unit.test.tsx +37 -0
  124. package/src/rbac/utils/__tests__/eventContext.test.ts +428 -0
  125. package/src/rbac/utils/__tests__/eventContext.unit.test.ts +428 -0
  126. package/src/utils/__tests__/appConfig.unit.test.ts +55 -0
  127. package/src/utils/__tests__/audit.unit.test.ts +69 -0
  128. package/src/utils/__tests__/auth-utils.unit.test.ts +70 -0
  129. package/src/utils/__tests__/bundleAnalysis.unit.test.ts +317 -0
  130. package/src/utils/__tests__/cn.unit.test.ts +34 -0
  131. package/src/utils/__tests__/deviceFingerprint.unit.test.ts +503 -0
  132. package/src/utils/__tests__/dynamicUtils.unit.test.ts +322 -0
  133. package/src/utils/__tests__/formatDate.unit.test.ts +109 -0
  134. package/src/utils/__tests__/formatting.unit.test.ts +66 -0
  135. package/src/utils/__tests__/index.unit.test.ts +251 -0
  136. package/src/utils/__tests__/lazyLoad.unit.test.tsx +309 -0
  137. package/src/utils/__tests__/organisationContext.unit.test.ts +192 -0
  138. package/src/utils/__tests__/performanceBudgets.unit.test.ts +259 -0
  139. package/src/utils/__tests__/permissionTypes.unit.test.ts +250 -0
  140. package/src/utils/__tests__/permissionUtils.unit.test.ts +362 -0
  141. package/src/utils/__tests__/sanitization.unit.test.ts +346 -0
  142. package/src/utils/__tests__/schemaUtils.unit.test.ts +441 -0
  143. package/src/utils/__tests__/secureDataAccess.unit.test.ts +334 -0
  144. package/src/utils/__tests__/secureErrors.unit.test.ts +377 -0
  145. package/src/utils/__tests__/secureStorage.unit.test.ts +293 -0
  146. package/src/utils/__tests__/security.unit.test.ts +127 -0
  147. package/src/utils/__tests__/securityMonitor.unit.test.ts +280 -0
  148. package/src/utils/__tests__/sessionTracking.unit.test.ts +356 -0
  149. package/src/utils/__tests__/validation.unit.test.ts +84 -0
  150. package/src/utils/__tests__/validationUtils.unit.test.ts +571 -0
  151. package/src/validation/__tests__/common.unit.test.ts +101 -0
  152. package/src/validation/__tests__/csrf.unit.test.ts +302 -0
  153. package/src/validation/__tests__/passwordSchema.unit.test 2.ts +98 -0
  154. package/src/validation/__tests__/passwordSchema.unit.test.ts +98 -0
  155. package/src/validation/__tests__/sqlInjectionProtection.unit.test.ts +466 -0
  156. package/dist/chunk-M4RW7PIP.js.map +0 -1
  157. /package/dist/{DataTable-ZQDRE46Q.js.map → DataTable-BEMN72L5.js.map} +0 -0
  158. /package/dist/{chunk-5H3C2SWM.js.map → chunk-4EIBJ6DF.js.map} +0 -0
@@ -0,0 +1,856 @@
1
+ import { renderHook, waitFor } from '@testing-library/react';
2
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
3
+ import { useRBAC } from '../../rbac/hooks/useRBAC';
4
+ import { createMockSupabaseClient, testDataGenerators } from '../../__tests__/helpers/test-utils';
5
+
6
+ // Mock the providers
7
+ const mockUseUnifiedAuth = vi.fn();
8
+ const mockUseOrganisations = vi.fn();
9
+ const mockUseEvents = vi.fn();
10
+
11
+ vi.mock('../../providers/UnifiedAuthProvider', () => ({
12
+ useUnifiedAuth: () => mockUseUnifiedAuth()
13
+ }));
14
+
15
+ vi.mock('../../providers/OrganisationProvider', () => ({
16
+ useOrganisations: () => mockUseOrganisations()
17
+ }));
18
+
19
+ vi.mock('../../providers/EventProvider', () => ({
20
+ useEvents: () => mockUseEvents()
21
+ }));
22
+
23
+ // Mock Supabase client
24
+ const mockSupabase = createMockSupabaseClient();
25
+
26
+ // Ensure rpc is properly mocked
27
+ vi.spyOn(mockSupabase, 'rpc').mockResolvedValue({
28
+ data: { permissions: {}, roles: [], access_level: 'viewer' },
29
+ error: null
30
+ });
31
+
32
+ describe('useRBAC', () => {
33
+ beforeEach(() => {
34
+ vi.clearAllMocks();
35
+
36
+ // Default mock implementations
37
+ mockUseUnifiedAuth.mockReturnValue({
38
+ user: { id: 'test-user-id' },
39
+ session: { access_token: 'test-token' },
40
+ supabase: mockSupabase,
41
+ appName: 'test-app'
42
+ });
43
+
44
+ mockUseOrganisations.mockReturnValue({
45
+ selectedOrganisation: { id: 'test-org-id' }
46
+ });
47
+
48
+ mockUseEvents.mockReturnValue({
49
+ selectedEvent: { event_id: 'test-event-id' }
50
+ });
51
+ });
52
+
53
+ describe('Initial State', () => {
54
+ it('returns initial state when no user is provided', () => {
55
+ mockUseUnifiedAuth.mockReturnValue({
56
+ user: null,
57
+ supabase: null,
58
+ appName: null
59
+ });
60
+
61
+ const { result } = renderHook(() => useRBAC());
62
+
63
+ expect(result.current.globalRole).toBeNull();
64
+ expect(result.current.organisationRole).toBeNull();
65
+ expect(result.current.eventAppRole).toBeNull();
66
+ expect(result.current.isLoading).toBe(false);
67
+ expect(result.current.error).toBeNull();
68
+ });
69
+
70
+ it('returns initial state when no supabase client is provided', () => {
71
+ mockUseUnifiedAuth.mockReturnValue({
72
+ user: { id: 'test-user-id' },
73
+ supabase: null,
74
+ appName: 'test-app'
75
+ });
76
+
77
+ const { result } = renderHook(() => useRBAC());
78
+
79
+ expect(result.current.globalRole).toBeNull();
80
+ expect(result.current.organisationRole).toBeNull();
81
+ expect(result.current.eventAppRole).toBeNull();
82
+ expect(result.current.isLoading).toBe(false);
83
+ expect(result.current.error).toBeNull();
84
+ });
85
+
86
+ it('returns initial state when no app name is provided', () => {
87
+ mockUseUnifiedAuth.mockReturnValue({
88
+ user: { id: 'test-user-id' },
89
+ supabase: mockSupabase,
90
+ appName: null
91
+ });
92
+
93
+ const { result } = renderHook(() => useRBAC());
94
+
95
+ expect(result.current.globalRole).toBeNull();
96
+ expect(result.current.organisationRole).toBeNull();
97
+ expect(result.current.eventAppRole).toBeNull();
98
+ expect(result.current.isLoading).toBe(false);
99
+ expect(result.current.error).toBeNull();
100
+ });
101
+ });
102
+
103
+ describe('Role Detection', () => {
104
+ it('detects global role from permissions', async () => {
105
+ const mockPermissions = [
106
+ {
107
+ permission_type: 'all_permissions',
108
+ role_name: 'super_admin',
109
+ has_permission: true,
110
+ granted_at: '2023-01-01T00:00:00Z'
111
+ }
112
+ ];
113
+
114
+ mockSupabase.rpc.mockResolvedValue({
115
+ data: mockPermissions,
116
+ error: null
117
+ });
118
+
119
+ const { result } = renderHook(() => useRBAC());
120
+
121
+ await waitFor(() => {
122
+ expect(result.current.globalRole).toBe('super_admin');
123
+ expect(result.current.organisationRole).toBeNull();
124
+ expect(result.current.eventAppRole).toBeNull();
125
+ }, { timeout: 3000 });
126
+ });
127
+
128
+ it('detects organisation role from permissions', async () => {
129
+ const mockPermissions = [
130
+ {
131
+ permission_type: 'organisation_access',
132
+ role_name: 'org_admin',
133
+ has_permission: true,
134
+ granted_at: '2023-01-01T00:00:00Z'
135
+ }
136
+ ];
137
+
138
+ mockSupabase.rpc.mockResolvedValue({
139
+ data: mockPermissions,
140
+ error: null
141
+ });
142
+
143
+ const { result } = renderHook(() => useRBAC());
144
+
145
+ await waitFor(() => {
146
+ expect(result.current.globalRole).toBeNull();
147
+ expect(result.current.organisationRole).toBe('org_admin');
148
+ expect(result.current.eventAppRole).toBeNull();
149
+ }, { timeout: 3000 });
150
+ });
151
+
152
+ it('detects event app role from permissions', async () => {
153
+ const mockPermissions = [
154
+ {
155
+ permission_type: 'event_app_access',
156
+ role_name: 'event_admin',
157
+ has_permission: true,
158
+ granted_at: '2023-01-01T00:00:00Z'
159
+ }
160
+ ];
161
+
162
+ mockSupabase.rpc.mockResolvedValue({
163
+ data: mockPermissions,
164
+ error: null
165
+ });
166
+
167
+ const { result } = renderHook(() => useRBAC());
168
+
169
+ await waitFor(() => {
170
+ expect(result.current.globalRole).toBeNull();
171
+ expect(result.current.organisationRole).toBeNull();
172
+ expect(result.current.eventAppRole).toBe('event_admin');
173
+ }, { timeout: 3000 });
174
+ });
175
+
176
+ it('detects multiple roles from permissions', async () => {
177
+ const mockPermissions = [
178
+ {
179
+ permission_type: 'all_permissions',
180
+ role_name: 'super_admin',
181
+ has_permission: true,
182
+ granted_at: '2023-01-01T00:00:00Z'
183
+ },
184
+ {
185
+ permission_type: 'organisation_access',
186
+ role_name: 'org_admin',
187
+ has_permission: true,
188
+ granted_at: '2023-01-01T00:00:00Z'
189
+ },
190
+ {
191
+ permission_type: 'event_app_access',
192
+ role_name: 'event_admin',
193
+ has_permission: true,
194
+ granted_at: '2023-01-01T00:00:00Z'
195
+ }
196
+ ];
197
+
198
+ mockSupabase.rpc.mockResolvedValue({
199
+ data: mockPermissions,
200
+ error: null
201
+ });
202
+
203
+ const { result } = renderHook(() => useRBAC());
204
+
205
+ await waitFor(() => {
206
+ expect(result.current.globalRole).toBe('super_admin');
207
+ expect(result.current.organisationRole).toBe('org_admin');
208
+ expect(result.current.eventAppRole).toBe('event_admin');
209
+ }, { timeout: 3000 });
210
+ });
211
+
212
+ it('handles empty permissions array', async () => {
213
+ mockSupabase.rpc.mockResolvedValue({
214
+ data: [],
215
+ error: null
216
+ });
217
+
218
+ const { result } = renderHook(() => useRBAC());
219
+
220
+ await waitFor(() => {
221
+ expect(result.current.globalRole).toBeNull();
222
+ expect(result.current.organisationRole).toBeNull();
223
+ expect(result.current.eventAppRole).toBeNull();
224
+ });
225
+ });
226
+ });
227
+
228
+ describe('Permission Checking', () => {
229
+ it('returns true for super admin regardless of operation', async () => {
230
+ const mockPermissions = [
231
+ {
232
+ permission_type: 'all_permissions',
233
+ role_name: 'super_admin',
234
+ has_permission: true,
235
+ granted_at: '2023-01-01T00:00:00Z'
236
+ }
237
+ ];
238
+
239
+ mockSupabase.rpc.mockResolvedValue({
240
+ data: mockPermissions,
241
+ error: null
242
+ });
243
+
244
+ const { result } = renderHook(() => useRBAC());
245
+
246
+ await waitFor(() => {
247
+ expect(result.current.globalRole).toBe('super_admin');
248
+ }, { timeout: 3000 });
249
+
250
+ const hasPermission = await result.current.hasPermission('read', 'dashboard');
251
+ expect(hasPermission).toBe(true);
252
+ });
253
+
254
+ it('checks specific permission with database call', async () => {
255
+ const mockPermissions = [
256
+ {
257
+ permission_type: 'organisation_access',
258
+ role_name: 'org_admin',
259
+ has_permission: true,
260
+ granted_at: '2023-01-01T00:00:00Z'
261
+ }
262
+ ];
263
+
264
+ mockSupabase.rpc
265
+ .mockResolvedValueOnce({
266
+ data: mockPermissions,
267
+ error: null
268
+ })
269
+ .mockResolvedValueOnce({
270
+ data: true,
271
+ error: null
272
+ });
273
+
274
+ const { result } = renderHook(() => useRBAC());
275
+
276
+ await waitFor(() => {
277
+ expect(result.current.organisationRole).toBe('org_admin');
278
+ }, { timeout: 3000 });
279
+
280
+ const hasPermission = await result.current.hasPermission('read', 'dashboard');
281
+ expect(hasPermission).toBe(true);
282
+ expect(mockSupabase.rpc).toHaveBeenCalledWith('check_page_permission', {
283
+ p_user_id: 'test-user-id',
284
+ p_app_id: 'test-app-id',
285
+ p_page_id: 'dashboard',
286
+ p_operation: 'read',
287
+ p_event_id: 'test-event-id',
288
+ p_organisation_id: 'test-org-id'
289
+ });
290
+ });
291
+
292
+ it('returns false when permission check fails', async () => {
293
+ const mockPermissions = [
294
+ {
295
+ permission_type: 'organisation_access',
296
+ role_name: 'org_admin',
297
+ has_permission: true,
298
+ granted_at: '2023-01-01T00:00:00Z'
299
+ }
300
+ ];
301
+
302
+ mockSupabase.rpc
303
+ .mockResolvedValueOnce({
304
+ data: mockPermissions,
305
+ error: null
306
+ })
307
+ .mockResolvedValueOnce({
308
+ data: false,
309
+ error: null
310
+ });
311
+
312
+ const { result } = renderHook(() => useRBAC());
313
+
314
+ await waitFor(() => {
315
+ expect(result.current.organisationRole).toBe('org_admin');
316
+ }, { timeout: 3000 });
317
+
318
+ const hasPermission = await result.current.hasPermission('write', 'admin');
319
+ expect(hasPermission).toBe(false);
320
+ });
321
+
322
+ it('returns false when permission check throws error', async () => {
323
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
324
+
325
+ const mockPermissions = [
326
+ {
327
+ permission_type: 'organisation_access',
328
+ role_name: 'org_admin',
329
+ has_permission: true,
330
+ granted_at: '2023-01-01T00:00:00Z'
331
+ }
332
+ ];
333
+
334
+ mockSupabase.rpc
335
+ .mockResolvedValueOnce({
336
+ data: mockPermissions,
337
+ error: null
338
+ })
339
+ .mockRejectedValueOnce(new Error('Database error'));
340
+
341
+ const { result } = renderHook(() => useRBAC());
342
+
343
+ await waitFor(() => {
344
+ expect(result.current.organisationRole).toBe('org_admin');
345
+ }, { timeout: 3000 });
346
+
347
+ const hasPermission = await result.current.hasPermission('read', 'dashboard');
348
+ expect(hasPermission).toBe(false);
349
+
350
+ consoleSpy.mockRestore();
351
+ });
352
+
353
+ it('uses default pageId when not provided', async () => {
354
+ const mockPermissions = [
355
+ {
356
+ permission_type: 'organisation_access',
357
+ role_name: 'org_admin',
358
+ has_permission: true,
359
+ granted_at: '2023-01-01T00:00:00Z'
360
+ }
361
+ ];
362
+
363
+ mockSupabase.rpc
364
+ .mockResolvedValueOnce({
365
+ data: mockPermissions,
366
+ error: null
367
+ })
368
+ .mockResolvedValueOnce({
369
+ data: true,
370
+ error: null
371
+ });
372
+
373
+ const { result } = renderHook(() => useRBAC('default-page'));
374
+
375
+ await waitFor(() => {
376
+ expect(result.current.organisationRole).toBe('org_admin');
377
+ }, { timeout: 3000 });
378
+
379
+ await result.current.hasPermission('read');
380
+
381
+ expect(mockSupabase.rpc).toHaveBeenCalledWith('check_page_permission', {
382
+ p_user_id: 'test-user-id',
383
+ p_app_id: 'test-app-id',
384
+ p_page_id: 'default-page',
385
+ p_operation: 'read',
386
+ p_event_id: 'test-event-id',
387
+ p_organisation_id: 'test-org-id'
388
+ });
389
+ });
390
+ });
391
+
392
+ describe('Global Permission Checking', () => {
393
+ it('returns true for super admin', async () => {
394
+ const mockPermissions = [
395
+ {
396
+ permission_type: 'all_permissions',
397
+ role_name: 'super_admin',
398
+ has_permission: true,
399
+ granted_at: '2023-01-01T00:00:00Z'
400
+ }
401
+ ];
402
+
403
+ mockSupabase.rpc.mockResolvedValue({
404
+ data: mockPermissions,
405
+ error: null
406
+ });
407
+
408
+ const { result } = renderHook(() => useRBAC());
409
+
410
+ await waitFor(() => {
411
+ expect(result.current.isLoading).toBe(false);
412
+ });
413
+
414
+ expect(result.current.hasGlobalPermission('super_admin')).toBe(true);
415
+ expect(result.current.hasGlobalPermission('org_admin')).toBe(true);
416
+ expect(result.current.hasGlobalPermission('any_permission')).toBe(true);
417
+ });
418
+
419
+ it('returns true for org admin when checking org_admin permission', async () => {
420
+ const mockPermissions = [
421
+ {
422
+ permission_type: 'organisation_access',
423
+ role_name: 'org_admin',
424
+ has_permission: true,
425
+ granted_at: '2023-01-01T00:00:00Z'
426
+ }
427
+ ];
428
+
429
+ mockSupabase.rpc.mockResolvedValue({
430
+ data: mockPermissions,
431
+ error: null
432
+ });
433
+
434
+ const { result } = renderHook(() => useRBAC());
435
+
436
+ await waitFor(() => {
437
+ expect(result.current.isLoading).toBe(false);
438
+ });
439
+
440
+ expect(result.current.hasGlobalPermission('org_admin')).toBe(true);
441
+ expect(result.current.hasGlobalPermission('super_admin')).toBe(false);
442
+ expect(result.current.hasGlobalPermission('other_permission')).toBe(false);
443
+ });
444
+
445
+ it('returns false for non-admin users', () => {
446
+ const mockPermissions = [
447
+ {
448
+ permission_type: 'event_app_access',
449
+ role_name: 'viewer',
450
+ has_permission: true,
451
+ granted_at: '2023-01-01T00:00:00Z'
452
+ }
453
+ ];
454
+
455
+ mockSupabase.rpc.mockResolvedValue({
456
+ data: mockPermissions,
457
+ error: null
458
+ });
459
+
460
+ const { result } = renderHook(() => useRBAC());
461
+
462
+ expect(result.current.hasGlobalPermission('super_admin')).toBe(false);
463
+ expect(result.current.hasGlobalPermission('org_admin')).toBe(false);
464
+ expect(result.current.hasGlobalPermission('any_permission')).toBe(false);
465
+ });
466
+ });
467
+
468
+ describe('Computed Properties', () => {
469
+ it('correctly computes isSuperAdmin', async () => {
470
+ const mockPermissions = [
471
+ {
472
+ permission_type: 'all_permissions',
473
+ role_name: 'super_admin',
474
+ has_permission: true,
475
+ granted_at: '2023-01-01T00:00:00Z'
476
+ }
477
+ ];
478
+
479
+ mockSupabase.rpc.mockResolvedValue({
480
+ data: mockPermissions,
481
+ error: null
482
+ });
483
+
484
+ const { result } = renderHook(() => useRBAC());
485
+
486
+ await waitFor(() => {
487
+ expect(result.current.isSuperAdmin).toBe(true);
488
+ });
489
+ });
490
+
491
+ it('correctly computes isOrgAdmin', async () => {
492
+ const mockPermissions = [
493
+ {
494
+ permission_type: 'organisation_access',
495
+ role_name: 'org_admin',
496
+ has_permission: true,
497
+ granted_at: '2023-01-01T00:00:00Z'
498
+ }
499
+ ];
500
+
501
+ mockSupabase.rpc.mockResolvedValue({
502
+ data: mockPermissions,
503
+ error: null
504
+ });
505
+
506
+ const { result } = renderHook(() => useRBAC());
507
+
508
+ await waitFor(() => {
509
+ expect(result.current.isOrgAdmin).toBe(true);
510
+ });
511
+ });
512
+
513
+ it('correctly computes isEventAdmin', async () => {
514
+ const mockPermissions = [
515
+ {
516
+ permission_type: 'event_app_access',
517
+ role_name: 'event_admin',
518
+ has_permission: true,
519
+ granted_at: '2023-01-01T00:00:00Z'
520
+ }
521
+ ];
522
+
523
+ mockSupabase.rpc.mockResolvedValue({
524
+ data: mockPermissions,
525
+ error: null
526
+ });
527
+
528
+ const { result } = renderHook(() => useRBAC());
529
+
530
+ await waitFor(() => {
531
+ expect(result.current.isEventAdmin).toBe(true);
532
+ });
533
+ });
534
+
535
+ it('correctly computes canManageOrganisation', async () => {
536
+ const mockPermissions = [
537
+ {
538
+ permission_type: 'organisation_access',
539
+ role_name: 'org_admin',
540
+ has_permission: true,
541
+ granted_at: '2023-01-01T00:00:00Z'
542
+ }
543
+ ];
544
+
545
+ mockSupabase.rpc.mockResolvedValue({
546
+ data: mockPermissions,
547
+ error: null
548
+ });
549
+
550
+ const { result } = renderHook(() => useRBAC());
551
+
552
+ await waitFor(() => {
553
+ expect(result.current.canManageOrganisation).toBe(true);
554
+ });
555
+ });
556
+
557
+ it('correctly computes canManageEvent', async () => {
558
+ const mockPermissions = [
559
+ {
560
+ permission_type: 'event_app_access',
561
+ role_name: 'event_admin',
562
+ has_permission: true,
563
+ granted_at: '2023-01-01T00:00:00Z'
564
+ }
565
+ ];
566
+
567
+ mockSupabase.rpc.mockResolvedValue({
568
+ data: mockPermissions,
569
+ error: null
570
+ });
571
+
572
+ const { result } = renderHook(() => useRBAC());
573
+
574
+ await waitFor(() => {
575
+ expect(result.current.canManageEvent).toBe(true);
576
+ });
577
+ });
578
+
579
+ it('super admin can manage both organisation and event', async () => {
580
+ const mockPermissions = [
581
+ {
582
+ permission_type: 'all_permissions',
583
+ role_name: 'super_admin',
584
+ has_permission: true,
585
+ granted_at: '2023-01-01T00:00:00Z'
586
+ }
587
+ ];
588
+
589
+ mockSupabase.rpc.mockResolvedValue({
590
+ data: mockPermissions,
591
+ error: null
592
+ });
593
+
594
+ const { result } = renderHook(() => useRBAC());
595
+
596
+ await waitFor(() => {
597
+ expect(result.current.canManageOrganisation).toBe(true);
598
+ expect(result.current.canManageEvent).toBe(true);
599
+ });
600
+ });
601
+ });
602
+
603
+ describe('Loading States', () => {
604
+ it('sets loading state while fetching permissions', async () => {
605
+ let resolvePermissions: (value: any) => void;
606
+ const permissionsPromise = new Promise(resolve => {
607
+ resolvePermissions = resolve;
608
+ });
609
+
610
+ mockSupabase.rpc.mockReturnValue(permissionsPromise);
611
+
612
+ const { result } = renderHook(() => useRBAC());
613
+
614
+ expect(result.current.isLoading).toBe(true);
615
+
616
+ resolvePermissions!({
617
+ data: [],
618
+ error: null
619
+ });
620
+
621
+ await waitFor(() => {
622
+ expect(result.current.isLoading).toBe(false);
623
+ });
624
+ });
625
+
626
+ it('resets loading state on error', async () => {
627
+ mockSupabase.rpc.mockRejectedValue(new Error('Network error'));
628
+
629
+ const { result } = renderHook(() => useRBAC());
630
+
631
+ await waitFor(() => {
632
+ expect(result.current.isLoading).toBe(false);
633
+ expect(result.current.error).toBeInstanceOf(Error);
634
+ });
635
+ });
636
+ });
637
+
638
+ describe('Error Handling', () => {
639
+ it('handles RPC errors gracefully', async () => {
640
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
641
+
642
+ mockSupabase.rpc.mockResolvedValue({
643
+ data: null,
644
+ error: { message: 'Database connection failed' }
645
+ });
646
+
647
+ const { result } = renderHook(() => useRBAC());
648
+
649
+ await waitFor(() => {
650
+ expect(result.current.error).toBeInstanceOf(Error);
651
+ expect(result.current.error?.message).toContain('Failed to load RBAC permissions');
652
+ });
653
+
654
+ consoleSpy.mockRestore();
655
+ });
656
+
657
+ it('handles network errors gracefully', async () => {
658
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
659
+
660
+ mockSupabase.rpc.mockResolvedValue({
661
+ data: null,
662
+ error: { message: 'Network timeout' }
663
+ });
664
+
665
+ const { result } = renderHook(() => useRBAC());
666
+
667
+ await waitFor(() => {
668
+ expect(result.current.error).toBeInstanceOf(Error);
669
+ expect(result.current.error?.message).toContain('Failed to load RBAC permissions');
670
+ });
671
+
672
+ consoleSpy.mockRestore();
673
+ });
674
+
675
+ it('handles unknown errors gracefully', async () => {
676
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
677
+
678
+ mockSupabase.rpc.mockRejectedValue('Unknown error');
679
+
680
+ const { result } = renderHook(() => useRBAC());
681
+
682
+ await waitFor(() => {
683
+ expect(result.current.error).toBeInstanceOf(Error);
684
+ expect(result.current.error?.message).toBe('Unknown error loading RBAC context');
685
+ });
686
+
687
+ consoleSpy.mockRestore();
688
+ });
689
+ });
690
+
691
+ describe('Context Dependencies', () => {
692
+ it('reloads when user changes', async () => {
693
+ const mockPermissions = [
694
+ {
695
+ permission_type: 'all_permissions',
696
+ role_name: 'super_admin',
697
+ has_permission: true,
698
+ granted_at: '2023-01-01T00:00:00Z'
699
+ }
700
+ ];
701
+
702
+ mockSupabase.rpc.mockResolvedValue({
703
+ data: mockPermissions,
704
+ error: null
705
+ });
706
+
707
+ const { result, rerender } = renderHook(() => useRBAC());
708
+
709
+ await waitFor(() => {
710
+ expect(result.current.globalRole).toBe('super_admin');
711
+ });
712
+
713
+ // Change user
714
+ mockUseUnifiedAuth.mockReturnValue({
715
+ user: { id: 'different-user-id' },
716
+ session: { access_token: 'test-token' },
717
+ supabase: mockSupabase,
718
+ appName: 'test-app'
719
+ });
720
+
721
+ rerender();
722
+
723
+ await waitFor(() => {
724
+ expect(mockSupabase.rpc).toHaveBeenCalledTimes(2);
725
+ });
726
+ });
727
+
728
+ it('reloads when organisation changes', async () => {
729
+ const mockPermissions = [
730
+ {
731
+ permission_type: 'organisation_access',
732
+ role_name: 'org_admin',
733
+ has_permission: true,
734
+ granted_at: '2023-01-01T00:00:00Z'
735
+ }
736
+ ];
737
+
738
+ mockSupabase.rpc.mockResolvedValue({
739
+ data: mockPermissions,
740
+ error: null
741
+ });
742
+
743
+ const { result, rerender } = renderHook(() => useRBAC());
744
+
745
+ await waitFor(() => {
746
+ expect(result.current.organisationRole).toBe('org_admin');
747
+ }, { timeout: 3000 });
748
+
749
+ // Change organisation
750
+ mockUseOrganisations.mockReturnValue({
751
+ selectedOrganisation: { id: 'different-org-id' }
752
+ });
753
+
754
+ rerender();
755
+
756
+ await waitFor(() => {
757
+ expect(mockSupabase.rpc).toHaveBeenCalledTimes(2);
758
+ });
759
+ });
760
+
761
+ it('reloads when event changes', async () => {
762
+ const mockPermissions = [
763
+ {
764
+ permission_type: 'event_app_access',
765
+ role_name: 'event_admin',
766
+ has_permission: true,
767
+ granted_at: '2023-01-01T00:00:00Z'
768
+ }
769
+ ];
770
+
771
+ mockSupabase.rpc.mockResolvedValue({
772
+ data: mockPermissions,
773
+ error: null
774
+ });
775
+
776
+ const { result, rerender } = renderHook(() => useRBAC());
777
+
778
+ await waitFor(() => {
779
+ expect(result.current.eventAppRole).toBe('event_admin');
780
+ });
781
+
782
+ // Change event
783
+ mockUseEvents.mockReturnValue({
784
+ selectedEvent: { event_id: 'different-event-id' }
785
+ });
786
+
787
+ rerender();
788
+
789
+ await waitFor(() => {
790
+ expect(mockSupabase.rpc).toHaveBeenCalledTimes(2);
791
+ });
792
+ });
793
+
794
+ it('handles missing EventProvider gracefully', () => {
795
+ const consoleSpy = vi.spyOn(console, 'debug').mockImplementation(() => {});
796
+
797
+ // Mock EventProvider to throw error
798
+ mockUseEvents.mockImplementation(() => {
799
+ throw new Error('EventProvider not available');
800
+ });
801
+
802
+ const mockPermissions = [
803
+ {
804
+ permission_type: 'organisation_access',
805
+ role_name: 'org_admin',
806
+ has_permission: true,
807
+ granted_at: '2023-01-01T00:00:00Z'
808
+ }
809
+ ];
810
+
811
+ mockSupabase.rpc.mockResolvedValue({
812
+ data: mockPermissions,
813
+ error: null
814
+ });
815
+
816
+ const { result } = renderHook(() => useRBAC());
817
+
818
+ expect(result.current.eventAppRole).toBeNull();
819
+ expect(consoleSpy).toHaveBeenCalledWith('useRBAC: EventProvider not available, continuing without event context');
820
+
821
+ consoleSpy.mockRestore();
822
+ });
823
+ });
824
+
825
+ describe('User Context', () => {
826
+ it('includes user in returned context', async () => {
827
+ const mockUser = testDataGenerators.createUser();
828
+
829
+ mockUseUnifiedAuth.mockReturnValue({
830
+ user: mockUser,
831
+ supabase: mockSupabase,
832
+ appName: 'test-app'
833
+ });
834
+
835
+ const mockPermissions = [
836
+ {
837
+ permission_type: 'organisation_access',
838
+ role_name: 'org_admin',
839
+ has_permission: true,
840
+ granted_at: '2023-01-01T00:00:00Z'
841
+ }
842
+ ];
843
+
844
+ mockSupabase.rpc.mockResolvedValue({
845
+ data: mockPermissions,
846
+ error: null
847
+ });
848
+
849
+ const { result } = renderHook(() => useRBAC());
850
+
851
+ await waitFor(() => {
852
+ expect(result.current.user).toEqual(mockUser);
853
+ });
854
+ });
855
+ });
856
+ });