@jmruthers/pace-core 0.5.110 → 0.5.111
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/{AuthService-DrHrvXNZ.d.ts → AuthService-CVgsgtaZ.d.ts} +8 -0
- package/dist/{DataTable-D3BK2FCN.js → DataTable-5W2HVLLV.js} +8 -8
- package/dist/{UnifiedAuthProvider-A7I23UCN.js → UnifiedAuthProvider-LUM3QLS5.js} +3 -3
- package/dist/{api-PIE4JRFS.js → api-SIZPFBFX.js} +5 -3
- package/dist/{audit-65VNHEV2.js → audit-5JI5T3SL.js} +2 -2
- package/dist/{chunk-3J5N2T2N.js → chunk-2BIDKXQU.js} +113 -116
- package/dist/chunk-2BIDKXQU.js.map +1 -0
- package/dist/{chunk-AWK2FAUN.js → chunk-ACYQNYHB.js} +7 -7
- package/dist/{chunk-D6MEKC27.js → chunk-EFVQBYFN.js} +2 -2
- package/dist/{chunk-EZ64QG2I.js → chunk-I5YM5BGS.js} +2 -2
- package/dist/{chunk-Q7APDV6H.js → chunk-IWJYNWXN.js} +13 -5
- package/dist/chunk-IWJYNWXN.js.map +1 -0
- package/dist/{chunk-YFMENCR4.js → chunk-JE2GFA3O.js} +3 -3
- package/dist/{chunk-AUXS7XSO.js → chunk-MW73E7SP.js} +35 -11
- package/dist/chunk-MW73E7SP.js.map +1 -0
- package/dist/{chunk-XRSP3H52.js → chunk-PXXS26G5.js} +57 -23
- package/dist/chunk-PXXS26G5.js.map +1 -0
- package/dist/{chunk-HGZSO43Y.js → chunk-TD4BXGPE.js} +4 -4
- package/dist/{chunk-EYSXQ756.js → chunk-TDFBX7KJ.js} +2 -2
- package/dist/{chunk-HADXAZT3.js → chunk-UGVU7L7N.js} +52 -90
- package/dist/chunk-UGVU7L7N.js.map +1 -0
- package/dist/{chunk-2W4WKJVF.js → chunk-X7SPKHYZ.js} +290 -255
- package/dist/chunk-X7SPKHYZ.js.map +1 -0
- package/dist/{chunk-7GBEBJLR.js → chunk-ZL45MG76.js} +45 -37
- package/dist/chunk-ZL45MG76.js.map +1 -0
- package/dist/components.js +10 -10
- package/dist/hooks.d.ts +11 -1
- package/dist/hooks.js +9 -7
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +13 -13
- package/dist/providers.d.ts +2 -2
- package/dist/providers.js +2 -2
- package/dist/rbac/index.d.ts +13 -8
- package/dist/rbac/index.js +9 -9
- package/dist/utils.js +1 -1
- package/docs/api/classes/ColumnFactory.md +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +4 -4
- package/docs/api/classes/MissingUserContextError.md +4 -4
- package/docs/api/classes/OrganisationContextRequiredError.md +4 -4
- package/docs/api/classes/PermissionDeniedError.md +4 -4
- package/docs/api/classes/PublicErrorBoundary.md +1 -1
- package/docs/api/classes/RBACAuditManager.md +8 -8
- package/docs/api/classes/RBACCache.md +8 -8
- package/docs/api/classes/RBACEngine.md +4 -4
- package/docs/api/classes/RBACError.md +4 -4
- package/docs/api/classes/RBACNotInitializedError.md +4 -4
- package/docs/api/classes/SecureSupabaseClient.md +1 -1
- package/docs/api/classes/StorageUtils.md +1 -1
- package/docs/api/enums/FileCategory.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/DataRecord.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 +1 -1
- 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/FileDisplayProps.md +1 -1
- package/docs/api/interfaces/FileMetadata.md +1 -1
- package/docs/api/interfaces/FileReference.md +1 -1
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadOptions.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 +27 -27
- package/docs/api/interfaces/PaceLoginPageProps.md +4 -4
- 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/ProtectedRouteProps.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/RBACLogger.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterContextType.md +8 -8
- package/docs/api/interfaces/RoleBasedRouterProps.md +10 -10
- package/docs/api/interfaces/RouteAccessRecord.md +10 -10
- package/docs/api/interfaces/RouteConfig.md +19 -6
- 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/SwitchProps.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/UsePublicEventOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeReturn.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 +36 -36
- package/docs/api-reference/hooks.md +8 -4
- package/docs/architecture/rpc-function-standards.md +3 -1
- package/docs/best-practices/common-patterns.md +3 -3
- package/docs/best-practices/deployment.md +10 -4
- package/docs/best-practices/performance.md +11 -3
- package/docs/core-concepts/organisations.md +8 -8
- package/docs/core-concepts/permissions.md +133 -72
- package/docs/migration/rbac-migration.md +65 -66
- package/docs/rbac/advanced-patterns.md +15 -22
- package/docs/rbac/examples.md +12 -12
- package/docs/rbac/getting-started.md +3 -3
- package/docs/rbac/troubleshooting.md +2 -1
- package/package.json +1 -1
- package/src/components/DataTable/components/__tests__/ActionButtons.test.tsx +913 -0
- package/src/components/DataTable/components/__tests__/ColumnFilter.test.tsx +609 -0
- package/src/components/DataTable/components/__tests__/EmptyState.test.tsx +434 -0
- package/src/components/DataTable/components/__tests__/LoadingState.test.tsx +120 -0
- package/src/components/DataTable/components/__tests__/PaginationControls.test.tsx +519 -0
- package/src/components/DataTable/examples/__tests__/HierarchicalActionsExample.test.tsx +316 -0
- package/src/components/DataTable/examples/__tests__/InitialPageSizeExample.test.tsx +211 -0
- package/src/components/FileUpload/FileUpload.tsx +2 -8
- package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +193 -63
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +102 -135
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.accessibility.test.tsx +41 -2
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.integration.test.tsx +61 -6
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.performance.test.tsx +71 -21
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.security.test.tsx +113 -41
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.unit.test.tsx +155 -45
- package/src/components/PaceLoginPage/PaceLoginPage.tsx +30 -1
- package/src/hooks/__tests__/usePermissionCache.simple.test.ts +63 -5
- package/src/hooks/__tests__/usePermissionCache.unit.test.ts +156 -72
- package/src/hooks/__tests__/useRBAC.unit.test.ts +4 -38
- package/src/hooks/index.ts +1 -1
- package/src/hooks/useFileDisplay.ts +51 -0
- package/src/hooks/usePermissionCache.test.ts +112 -68
- package/src/hooks/usePermissionCache.ts +55 -15
- package/src/rbac/README.md +81 -39
- package/src/rbac/__tests__/adapters.comprehensive.test.tsx +3 -3
- package/src/rbac/__tests__/engine.comprehensive.test.ts +15 -6
- package/src/rbac/__tests__/rbac-core.test.tsx +1 -1
- package/src/rbac/__tests__/rbac-engine-core-logic.test.ts +57 -4
- package/src/rbac/__tests__/rbac-engine-simplified.test.ts +3 -2
- package/src/rbac/adapters.tsx +4 -4
- package/src/rbac/api.test.ts +37 -13
- package/src/rbac/api.ts +25 -8
- package/src/rbac/audit.test.ts +2 -2
- package/src/rbac/audit.ts +14 -5
- package/src/rbac/cache.test.ts +12 -0
- package/src/rbac/cache.ts +29 -9
- package/src/rbac/components/EnhancedNavigationMenu.test.tsx +1 -1
- package/src/rbac/components/NavigationGuard.tsx +14 -14
- package/src/rbac/components/NavigationProvider.test.tsx +1 -1
- package/src/rbac/components/PagePermissionGuard.tsx +4 -3
- package/src/rbac/components/PagePermissionProvider.test.tsx +1 -1
- package/src/rbac/components/PermissionEnforcer.tsx +19 -15
- package/src/rbac/components/RoleBasedRouter.tsx +16 -9
- package/src/rbac/components/__tests__/NavigationGuard.test.tsx +123 -107
- package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +1 -1
- package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +121 -103
- package/src/rbac/docs/event-based-apps.md +6 -6
- package/src/rbac/engine.ts +12 -2
- package/src/rbac/hooks/useCan.test.ts +29 -2
- package/src/rbac/hooks/usePermissions.test.ts +25 -25
- package/src/rbac/hooks/usePermissions.ts +47 -23
- package/src/rbac/hooks/useRBAC.simple.test.ts +1 -8
- package/src/rbac/hooks/useRBAC.test.ts +3 -40
- package/src/rbac/hooks/useRBAC.ts +0 -55
- package/src/rbac/hooks/useResolvedScope.ts +23 -31
- package/src/rbac/permissions.test.ts +11 -7
- package/src/rbac/security.test.ts +2 -2
- package/src/rbac/security.ts +22 -7
- package/src/rbac/types.test.ts +2 -2
- package/src/rbac/types.ts +1 -2
- package/src/services/EventService.ts +41 -13
- package/src/services/__tests__/EventService.test.ts +25 -4
- package/src/services/interfaces/IEventService.ts +1 -0
- package/src/utils/file-reference.ts +9 -0
- package/dist/chunk-2W4WKJVF.js.map +0 -1
- package/dist/chunk-3J5N2T2N.js.map +0 -1
- package/dist/chunk-7GBEBJLR.js.map +0 -1
- package/dist/chunk-AUXS7XSO.js.map +0 -1
- package/dist/chunk-HADXAZT3.js.map +0 -1
- package/dist/chunk-Q7APDV6H.js.map +0 -1
- package/dist/chunk-XRSP3H52.js.map +0 -1
- /package/dist/{DataTable-D3BK2FCN.js.map → DataTable-5W2HVLLV.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-A7I23UCN.js.map → UnifiedAuthProvider-LUM3QLS5.js.map} +0 -0
- /package/dist/{api-PIE4JRFS.js.map → api-SIZPFBFX.js.map} +0 -0
- /package/dist/{audit-65VNHEV2.js.map → audit-5JI5T3SL.js.map} +0 -0
- /package/dist/{chunk-AWK2FAUN.js.map → chunk-ACYQNYHB.js.map} +0 -0
- /package/dist/{chunk-D6MEKC27.js.map → chunk-EFVQBYFN.js.map} +0 -0
- /package/dist/{chunk-EZ64QG2I.js.map → chunk-I5YM5BGS.js.map} +0 -0
- /package/dist/{chunk-YFMENCR4.js.map → chunk-JE2GFA3O.js.map} +0 -0
- /package/dist/{chunk-HGZSO43Y.js.map → chunk-TD4BXGPE.js.map} +0 -0
- /package/dist/{chunk-EYSXQ756.js.map → chunk-TDFBX7KJ.js.map} +0 -0
|
@@ -11,12 +11,12 @@ import { render, screen, waitFor } from '@testing-library/react';
|
|
|
11
11
|
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
12
12
|
import { ReactNode } from 'react';
|
|
13
13
|
import { PermissionEnforcer } from '../PermissionEnforcer';
|
|
14
|
-
import {
|
|
14
|
+
import { useMultiplePermissions } from '../../hooks/usePermissions';
|
|
15
15
|
import { useUnifiedAuth } from '../../../providers/UnifiedAuthProvider';
|
|
16
16
|
|
|
17
17
|
// Mock the RBAC hooks
|
|
18
|
-
vi.mock('../../hooks', () => ({
|
|
19
|
-
|
|
18
|
+
vi.mock('../../hooks/usePermissions', () => ({
|
|
19
|
+
useMultiplePermissions: vi.fn()
|
|
20
20
|
}));
|
|
21
21
|
|
|
22
22
|
// Mock the auth provider
|
|
@@ -43,7 +43,7 @@ const mockScope = {
|
|
|
43
43
|
appId: 'app-123'
|
|
44
44
|
};
|
|
45
45
|
|
|
46
|
-
const mockPermissions = ['read:events', '
|
|
46
|
+
const mockPermissions = ['read:events', 'update:events'] as const;
|
|
47
47
|
const mockOperation = 'event-management';
|
|
48
48
|
|
|
49
49
|
// Test component
|
|
@@ -60,7 +60,7 @@ const TestLoading = () => (
|
|
|
60
60
|
);
|
|
61
61
|
|
|
62
62
|
describe('PermissionEnforcer Component', () => {
|
|
63
|
-
const
|
|
63
|
+
const mockUseMultiplePermissions = vi.mocked(useMultiplePermissions);
|
|
64
64
|
const mockUseUnifiedAuth = vi.mocked(useUnifiedAuth);
|
|
65
65
|
const mockCreateScopeFromEvent = vi.mocked(createScopeFromEvent);
|
|
66
66
|
|
|
@@ -75,19 +75,24 @@ describe('PermissionEnforcer Component', () => {
|
|
|
75
75
|
supabase: {} as any
|
|
76
76
|
});
|
|
77
77
|
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
mockUseMultiplePermissions.mockReturnValue({
|
|
79
|
+
results: {
|
|
80
|
+
'read:events': true,
|
|
81
|
+
'update:events': true
|
|
82
|
+
} as Record<string, boolean>,
|
|
80
83
|
isLoading: false,
|
|
81
|
-
error: null
|
|
84
|
+
error: null,
|
|
85
|
+
refetch: vi.fn()
|
|
82
86
|
});
|
|
83
87
|
});
|
|
84
88
|
|
|
85
89
|
describe('Rendering', () => {
|
|
86
90
|
it('renders children when permission is granted', async () => {
|
|
87
|
-
|
|
88
|
-
|
|
91
|
+
mockUseMultiplePermissions.mockReturnValue({
|
|
92
|
+
results: { 'read:events': true, 'update:events': true } as Record<string, boolean>,
|
|
89
93
|
isLoading: false,
|
|
90
|
-
error: null
|
|
94
|
+
error: null,
|
|
95
|
+
refetch: vi.fn()
|
|
91
96
|
});
|
|
92
97
|
|
|
93
98
|
render(
|
|
@@ -106,10 +111,11 @@ describe('PermissionEnforcer Component', () => {
|
|
|
106
111
|
});
|
|
107
112
|
|
|
108
113
|
it('renders fallback when permission is denied', async () => {
|
|
109
|
-
|
|
110
|
-
|
|
114
|
+
mockUseMultiplePermissions.mockReturnValue({
|
|
115
|
+
results: { 'read:events': false, 'update:events': false } as Record<string, boolean>,
|
|
111
116
|
isLoading: false,
|
|
112
|
-
error: null
|
|
117
|
+
error: null,
|
|
118
|
+
refetch: vi.fn()
|
|
113
119
|
});
|
|
114
120
|
|
|
115
121
|
render(
|
|
@@ -128,11 +134,12 @@ describe('PermissionEnforcer Component', () => {
|
|
|
128
134
|
}, { interval: 10 });
|
|
129
135
|
});
|
|
130
136
|
|
|
131
|
-
it('shows loading state during permission check', () => {
|
|
132
|
-
|
|
133
|
-
|
|
137
|
+
it('shows loading state during permission check', async () => {
|
|
138
|
+
mockUseMultiplePermissions.mockReturnValue({
|
|
139
|
+
results: {} as Record<string, boolean>,
|
|
134
140
|
isLoading: true,
|
|
135
|
-
error: null
|
|
141
|
+
error: null,
|
|
142
|
+
refetch: vi.fn()
|
|
136
143
|
});
|
|
137
144
|
|
|
138
145
|
render(
|
|
@@ -150,10 +157,11 @@ describe('PermissionEnforcer Component', () => {
|
|
|
150
157
|
});
|
|
151
158
|
|
|
152
159
|
it('uses default fallback when none provided', async () => {
|
|
153
|
-
|
|
154
|
-
|
|
160
|
+
mockUseMultiplePermissions.mockReturnValue({
|
|
161
|
+
results: { 'read:events': false, 'update:events': false } as Record<string, boolean>,
|
|
155
162
|
isLoading: false,
|
|
156
|
-
error: null
|
|
163
|
+
error: null,
|
|
164
|
+
refetch: vi.fn()
|
|
157
165
|
});
|
|
158
166
|
|
|
159
167
|
render(
|
|
@@ -172,10 +180,11 @@ describe('PermissionEnforcer Component', () => {
|
|
|
172
180
|
});
|
|
173
181
|
|
|
174
182
|
it('uses default loading when none provided', () => {
|
|
175
|
-
|
|
176
|
-
|
|
183
|
+
mockUseMultiplePermissions.mockReturnValue({
|
|
184
|
+
results: {} as Record<string, boolean>,
|
|
177
185
|
isLoading: true,
|
|
178
|
-
error: null
|
|
186
|
+
error: null,
|
|
187
|
+
refetch: vi.fn()
|
|
179
188
|
});
|
|
180
189
|
|
|
181
190
|
render(
|
|
@@ -195,10 +204,11 @@ describe('PermissionEnforcer Component', () => {
|
|
|
195
204
|
it('enforces single permission correctly', async () => {
|
|
196
205
|
const singlePermission = ['read:events'] as const;
|
|
197
206
|
|
|
198
|
-
|
|
199
|
-
|
|
207
|
+
mockUseMultiplePermissions.mockReturnValue({
|
|
208
|
+
results: { 'read:events': true } as Record<string, boolean>,
|
|
200
209
|
isLoading: false,
|
|
201
|
-
error: null
|
|
210
|
+
error: null,
|
|
211
|
+
refetch: vi.fn()
|
|
202
212
|
});
|
|
203
213
|
|
|
204
214
|
render(
|
|
@@ -214,23 +224,23 @@ describe('PermissionEnforcer Component', () => {
|
|
|
214
224
|
expect(screen.getByTestId('test-component')).toBeInTheDocument();
|
|
215
225
|
}, { interval: 10 });
|
|
216
226
|
|
|
217
|
-
expect(
|
|
227
|
+
expect(mockUseMultiplePermissions).toHaveBeenCalledWith(
|
|
218
228
|
'user-123',
|
|
219
229
|
expect.objectContaining({
|
|
220
230
|
organisationId: 'org-123',
|
|
221
231
|
eventId: 'event-123'
|
|
222
232
|
}),
|
|
223
|
-
'read:events',
|
|
224
|
-
undefined,
|
|
233
|
+
['read:events'], // singlePermission only includes read:events
|
|
225
234
|
true
|
|
226
235
|
);
|
|
227
236
|
});
|
|
228
237
|
|
|
229
|
-
it('enforces multiple permissions with AND logic', async () => {
|
|
230
|
-
|
|
231
|
-
|
|
238
|
+
it('enforces multiple permissions with AND logic (requireAll=true)', async () => {
|
|
239
|
+
mockUseMultiplePermissions.mockReturnValue({
|
|
240
|
+
results: { 'read:events': true, 'update:events': true } as Record<string, boolean>,
|
|
232
241
|
isLoading: false,
|
|
233
|
-
error: null
|
|
242
|
+
error: null,
|
|
243
|
+
refetch: vi.fn()
|
|
234
244
|
});
|
|
235
245
|
|
|
236
246
|
render(
|
|
@@ -247,25 +257,25 @@ describe('PermissionEnforcer Component', () => {
|
|
|
247
257
|
expect(screen.getByTestId('test-component')).toBeInTheDocument();
|
|
248
258
|
}, { interval: 10 });
|
|
249
259
|
|
|
250
|
-
// Should check
|
|
251
|
-
expect(
|
|
260
|
+
// Should check all permissions when requireAll=true
|
|
261
|
+
expect(mockUseMultiplePermissions).toHaveBeenCalledWith(
|
|
252
262
|
'user-123',
|
|
253
263
|
expect.objectContaining({
|
|
254
264
|
organisationId: 'org-123',
|
|
255
265
|
eventId: 'event-123'
|
|
256
266
|
}),
|
|
257
|
-
'read:events',
|
|
258
|
-
undefined,
|
|
267
|
+
['read:events', 'update:events'],
|
|
259
268
|
true
|
|
260
269
|
);
|
|
261
270
|
});
|
|
262
271
|
|
|
263
272
|
it('handles permission checking errors gracefully', async () => {
|
|
264
273
|
const error = new Error('Permission check failed');
|
|
265
|
-
|
|
266
|
-
|
|
274
|
+
mockUseMultiplePermissions.mockReturnValue({
|
|
275
|
+
results: {} as Record<string, boolean>,
|
|
267
276
|
isLoading: false,
|
|
268
|
-
error
|
|
277
|
+
error,
|
|
278
|
+
refetch: vi.fn()
|
|
269
279
|
});
|
|
270
280
|
|
|
271
281
|
render(
|
|
@@ -284,10 +294,11 @@ describe('PermissionEnforcer Component', () => {
|
|
|
284
294
|
});
|
|
285
295
|
|
|
286
296
|
it('handles empty permissions array', async () => {
|
|
287
|
-
|
|
288
|
-
|
|
297
|
+
mockUseMultiplePermissions.mockReturnValue({
|
|
298
|
+
results: { 'read:events': true } as Record<string, boolean>,
|
|
289
299
|
isLoading: false,
|
|
290
|
-
error: null
|
|
300
|
+
error: null,
|
|
301
|
+
refetch: vi.fn()
|
|
291
302
|
});
|
|
292
303
|
|
|
293
304
|
render(
|
|
@@ -313,10 +324,11 @@ describe('PermissionEnforcer Component', () => {
|
|
|
313
324
|
appId: 'custom-app'
|
|
314
325
|
};
|
|
315
326
|
|
|
316
|
-
|
|
317
|
-
|
|
327
|
+
mockUseMultiplePermissions.mockReturnValue({
|
|
328
|
+
results: { 'read:events': true } as Record<string, boolean>,
|
|
318
329
|
isLoading: false,
|
|
319
|
-
error: null
|
|
330
|
+
error: null,
|
|
331
|
+
refetch: vi.fn()
|
|
320
332
|
});
|
|
321
333
|
|
|
322
334
|
render(
|
|
@@ -333,20 +345,20 @@ describe('PermissionEnforcer Component', () => {
|
|
|
333
345
|
expect(screen.getByTestId('test-component')).toBeInTheDocument();
|
|
334
346
|
}, { interval: 10 });
|
|
335
347
|
|
|
336
|
-
expect(
|
|
348
|
+
expect(mockUseMultiplePermissions).toHaveBeenCalledWith(
|
|
337
349
|
'user-123',
|
|
338
350
|
customScope,
|
|
339
|
-
'read:events',
|
|
340
|
-
undefined,
|
|
351
|
+
['read:events', 'update:events'], // mockPermissions includes both
|
|
341
352
|
true
|
|
342
353
|
);
|
|
343
354
|
});
|
|
344
355
|
|
|
345
356
|
it('resolves scope from organisation and event context', async () => {
|
|
346
|
-
|
|
347
|
-
|
|
357
|
+
mockUseMultiplePermissions.mockReturnValue({
|
|
358
|
+
results: { 'read:events': true } as Record<string, boolean>,
|
|
348
359
|
isLoading: false,
|
|
349
|
-
error: null
|
|
360
|
+
error: null,
|
|
361
|
+
refetch: vi.fn()
|
|
350
362
|
});
|
|
351
363
|
|
|
352
364
|
render(
|
|
@@ -362,14 +374,13 @@ describe('PermissionEnforcer Component', () => {
|
|
|
362
374
|
expect(screen.getByTestId('test-component')).toBeInTheDocument();
|
|
363
375
|
}, { interval: 10 });
|
|
364
376
|
|
|
365
|
-
expect(
|
|
377
|
+
expect(mockUseMultiplePermissions).toHaveBeenCalledWith(
|
|
366
378
|
'user-123',
|
|
367
379
|
expect.objectContaining({
|
|
368
380
|
organisationId: 'org-123',
|
|
369
381
|
eventId: 'event-123'
|
|
370
382
|
}),
|
|
371
|
-
'read:events',
|
|
372
|
-
undefined,
|
|
383
|
+
['read:events', 'update:events'], // mockPermissions includes both
|
|
373
384
|
true
|
|
374
385
|
);
|
|
375
386
|
});
|
|
@@ -382,10 +393,11 @@ describe('PermissionEnforcer Component', () => {
|
|
|
382
393
|
supabase: {} as any
|
|
383
394
|
});
|
|
384
395
|
|
|
385
|
-
|
|
386
|
-
|
|
396
|
+
mockUseMultiplePermissions.mockReturnValue({
|
|
397
|
+
results: { 'read:events': true } as Record<string, boolean>,
|
|
387
398
|
isLoading: false,
|
|
388
|
-
error: null
|
|
399
|
+
error: null,
|
|
400
|
+
refetch: vi.fn()
|
|
389
401
|
});
|
|
390
402
|
|
|
391
403
|
render(
|
|
@@ -401,14 +413,13 @@ describe('PermissionEnforcer Component', () => {
|
|
|
401
413
|
expect(screen.getByTestId('test-component')).toBeInTheDocument();
|
|
402
414
|
}, { interval: 10 });
|
|
403
415
|
|
|
404
|
-
expect(
|
|
416
|
+
expect(mockUseMultiplePermissions).toHaveBeenCalledWith(
|
|
405
417
|
'user-123',
|
|
406
418
|
expect.objectContaining({
|
|
407
419
|
organisationId: 'org-123',
|
|
408
420
|
eventId: undefined
|
|
409
421
|
}),
|
|
410
|
-
'read:events',
|
|
411
|
-
undefined,
|
|
422
|
+
['read:events', 'update:events'], // mockPermissions includes both
|
|
412
423
|
true
|
|
413
424
|
);
|
|
414
425
|
});
|
|
@@ -427,10 +438,11 @@ describe('PermissionEnforcer Component', () => {
|
|
|
427
438
|
appId: 'resolved-app'
|
|
428
439
|
});
|
|
429
440
|
|
|
430
|
-
|
|
431
|
-
|
|
441
|
+
mockUseMultiplePermissions.mockReturnValue({
|
|
442
|
+
results: { 'read:events': true } as Record<string, boolean>,
|
|
432
443
|
isLoading: false,
|
|
433
|
-
error: null
|
|
444
|
+
error: null,
|
|
445
|
+
refetch: vi.fn()
|
|
434
446
|
});
|
|
435
447
|
|
|
436
448
|
render(
|
|
@@ -447,14 +459,13 @@ describe('PermissionEnforcer Component', () => {
|
|
|
447
459
|
}, { interval: 10 });
|
|
448
460
|
|
|
449
461
|
expect(mockCreateScopeFromEvent).toHaveBeenCalledWith({}, 'event-123');
|
|
450
|
-
expect(
|
|
462
|
+
expect(mockUseMultiplePermissions).toHaveBeenCalledWith(
|
|
451
463
|
'user-123',
|
|
452
464
|
expect.objectContaining({
|
|
453
465
|
organisationId: 'resolved-org',
|
|
454
466
|
eventId: 'event-123'
|
|
455
467
|
}),
|
|
456
|
-
'read:events',
|
|
457
|
-
undefined,
|
|
468
|
+
['read:events', 'update:events'], // mockPermissions includes both
|
|
458
469
|
true
|
|
459
470
|
);
|
|
460
471
|
});
|
|
@@ -513,10 +524,11 @@ describe('PermissionEnforcer Component', () => {
|
|
|
513
524
|
it('prevents bypassing in strict mode', async () => {
|
|
514
525
|
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
515
526
|
|
|
516
|
-
|
|
517
|
-
|
|
527
|
+
mockUseMultiplePermissions.mockReturnValue({
|
|
528
|
+
results: { 'read:events': false, 'update:events': false } as Record<string, boolean>,
|
|
518
529
|
isLoading: false,
|
|
519
|
-
error: null
|
|
530
|
+
error: null,
|
|
531
|
+
refetch: vi.fn()
|
|
520
532
|
});
|
|
521
533
|
|
|
522
534
|
render(
|
|
@@ -549,10 +561,11 @@ describe('PermissionEnforcer Component', () => {
|
|
|
549
561
|
it('logs security violations for audit', async () => {
|
|
550
562
|
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
551
563
|
|
|
552
|
-
|
|
553
|
-
|
|
564
|
+
mockUseMultiplePermissions.mockReturnValue({
|
|
565
|
+
results: { 'read:events': false, 'update:events': false } as Record<string, boolean>,
|
|
554
566
|
isLoading: false,
|
|
555
|
-
error: null
|
|
567
|
+
error: null,
|
|
568
|
+
refetch: vi.fn()
|
|
556
569
|
});
|
|
557
570
|
|
|
558
571
|
render(
|
|
@@ -586,10 +599,11 @@ describe('PermissionEnforcer Component', () => {
|
|
|
586
599
|
it('calls onDenied callback when access is denied', async () => {
|
|
587
600
|
const onDeniedSpy = vi.fn();
|
|
588
601
|
|
|
589
|
-
|
|
590
|
-
|
|
602
|
+
mockUseMultiplePermissions.mockReturnValue({
|
|
603
|
+
results: { 'read:events': false, 'update:events': false } as Record<string, boolean>,
|
|
591
604
|
isLoading: false,
|
|
592
|
-
error: null
|
|
605
|
+
error: null,
|
|
606
|
+
refetch: vi.fn()
|
|
593
607
|
});
|
|
594
608
|
|
|
595
609
|
render(
|
|
@@ -613,10 +627,11 @@ describe('PermissionEnforcer Component', () => {
|
|
|
613
627
|
it('does not call onDenied when access is granted', async () => {
|
|
614
628
|
const onDeniedSpy = vi.fn();
|
|
615
629
|
|
|
616
|
-
|
|
617
|
-
|
|
630
|
+
mockUseMultiplePermissions.mockReturnValue({
|
|
631
|
+
results: { 'read:events': true } as Record<string, boolean>,
|
|
618
632
|
isLoading: false,
|
|
619
|
-
error: null
|
|
633
|
+
error: null,
|
|
634
|
+
refetch: vi.fn()
|
|
620
635
|
});
|
|
621
636
|
|
|
622
637
|
render(
|
|
@@ -641,10 +656,11 @@ describe('PermissionEnforcer Component', () => {
|
|
|
641
656
|
it('respects strictMode setting', async () => {
|
|
642
657
|
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
643
658
|
|
|
644
|
-
|
|
645
|
-
|
|
659
|
+
mockUseMultiplePermissions.mockReturnValue({
|
|
660
|
+
results: { 'read:events': false, 'update:events': false } as Record<string, boolean>,
|
|
646
661
|
isLoading: false,
|
|
647
|
-
error: null
|
|
662
|
+
error: null,
|
|
663
|
+
refetch: vi.fn()
|
|
648
664
|
});
|
|
649
665
|
|
|
650
666
|
render(
|
|
@@ -672,10 +688,11 @@ describe('PermissionEnforcer Component', () => {
|
|
|
672
688
|
it('respects auditLog setting', async () => {
|
|
673
689
|
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
674
690
|
|
|
675
|
-
|
|
676
|
-
|
|
691
|
+
mockUseMultiplePermissions.mockReturnValue({
|
|
692
|
+
results: { 'read:events': false, 'update:events': false } as Record<string, boolean>,
|
|
677
693
|
isLoading: false,
|
|
678
|
-
error: null
|
|
694
|
+
error: null,
|
|
695
|
+
refetch: vi.fn()
|
|
679
696
|
});
|
|
680
697
|
|
|
681
698
|
render(
|
|
@@ -700,11 +717,12 @@ describe('PermissionEnforcer Component', () => {
|
|
|
700
717
|
consoleSpy.mockRestore();
|
|
701
718
|
});
|
|
702
719
|
|
|
703
|
-
it('respects requireAll
|
|
704
|
-
|
|
705
|
-
|
|
720
|
+
it('respects requireAll=false (any permission granted)', async () => {
|
|
721
|
+
mockUseMultiplePermissions.mockReturnValue({
|
|
722
|
+
results: { 'read:events': true, 'update:events': false } as Record<string, boolean>,
|
|
706
723
|
isLoading: false,
|
|
707
|
-
error: null
|
|
724
|
+
error: null,
|
|
725
|
+
refetch: vi.fn()
|
|
708
726
|
});
|
|
709
727
|
|
|
710
728
|
render(
|
|
@@ -721,15 +739,14 @@ describe('PermissionEnforcer Component', () => {
|
|
|
721
739
|
expect(screen.getByTestId('test-component')).toBeInTheDocument();
|
|
722
740
|
}, { interval: 10 });
|
|
723
741
|
|
|
724
|
-
// Should
|
|
725
|
-
expect(
|
|
742
|
+
// Should check all permissions and allow access if any is granted
|
|
743
|
+
expect(mockUseMultiplePermissions).toHaveBeenCalledWith(
|
|
726
744
|
'user-123',
|
|
727
745
|
expect.objectContaining({
|
|
728
746
|
organisationId: 'org-123',
|
|
729
747
|
eventId: 'event-123'
|
|
730
748
|
}),
|
|
731
|
-
'read:events',
|
|
732
|
-
undefined,
|
|
749
|
+
['read:events', 'update:events'],
|
|
733
750
|
true
|
|
734
751
|
);
|
|
735
752
|
});
|
|
@@ -744,10 +761,11 @@ describe('PermissionEnforcer Component', () => {
|
|
|
744
761
|
supabase: {} as any
|
|
745
762
|
});
|
|
746
763
|
|
|
747
|
-
|
|
748
|
-
|
|
764
|
+
mockUseMultiplePermissions.mockReturnValue({
|
|
765
|
+
results: { 'read:events': false, 'update:events': false } as Record<string, boolean>,
|
|
749
766
|
isLoading: false,
|
|
750
|
-
error: null
|
|
767
|
+
error: null,
|
|
768
|
+
refetch: vi.fn()
|
|
751
769
|
});
|
|
752
770
|
|
|
753
771
|
render(
|
|
@@ -764,24 +782,24 @@ describe('PermissionEnforcer Component', () => {
|
|
|
764
782
|
expect(screen.getByTestId('test-fallback')).toBeInTheDocument();
|
|
765
783
|
}, { interval: 10 });
|
|
766
784
|
|
|
767
|
-
expect(
|
|
785
|
+
expect(mockUseMultiplePermissions).toHaveBeenCalledWith(
|
|
768
786
|
'',
|
|
769
787
|
expect.objectContaining({
|
|
770
788
|
organisationId: 'org-123',
|
|
771
789
|
eventId: 'event-123'
|
|
772
790
|
}),
|
|
773
|
-
'read:events',
|
|
774
|
-
undefined,
|
|
791
|
+
['read:events', 'update:events'], // mockPermissions includes both
|
|
775
792
|
true
|
|
776
793
|
);
|
|
777
794
|
});
|
|
778
795
|
|
|
779
796
|
it('handles permission check errors', async () => {
|
|
780
797
|
const error = new Error('Database connection failed');
|
|
781
|
-
|
|
782
|
-
|
|
798
|
+
mockUseMultiplePermissions.mockReturnValue({
|
|
799
|
+
results: {} as Record<string, boolean>,
|
|
783
800
|
isLoading: false,
|
|
784
|
-
error
|
|
801
|
+
error,
|
|
802
|
+
refetch: vi.fn()
|
|
785
803
|
});
|
|
786
804
|
|
|
787
805
|
render(
|
|
@@ -40,7 +40,7 @@ import { PermissionEnforcer } from '@jmruthers/pace-core/rbac';
|
|
|
40
40
|
function EventDashboard() {
|
|
41
41
|
return (
|
|
42
42
|
<PermissionEnforcer
|
|
43
|
-
permissions={['read:events', '
|
|
43
|
+
permissions={['read:events', 'update:participants']}
|
|
44
44
|
operation="dashboard"
|
|
45
45
|
>
|
|
46
46
|
<div>Event Dashboard Content</div>
|
|
@@ -84,7 +84,7 @@ const navigationItems = [
|
|
|
84
84
|
id: 'settings',
|
|
85
85
|
name: 'Settings',
|
|
86
86
|
path: '/event/settings',
|
|
87
|
-
permissions: ['
|
|
87
|
+
permissions: ['update:events']
|
|
88
88
|
}
|
|
89
89
|
];
|
|
90
90
|
|
|
@@ -118,7 +118,7 @@ function EventComponent() {
|
|
|
118
118
|
const { can: canManageEvents } = useCan(
|
|
119
119
|
userId,
|
|
120
120
|
{ eventId: selectedEventId },
|
|
121
|
-
'
|
|
121
|
+
'update:events'
|
|
122
122
|
);
|
|
123
123
|
|
|
124
124
|
return (
|
|
@@ -178,19 +178,19 @@ These permissions are specific to an event and app combination:
|
|
|
178
178
|
```typescript
|
|
179
179
|
const EVENT_APP_PERMISSIONS = {
|
|
180
180
|
// Event management
|
|
181
|
-
MANAGE_EVENT: '
|
|
181
|
+
MANAGE_EVENT: 'update:event',
|
|
182
182
|
READ_EVENT: 'read:event',
|
|
183
183
|
UPDATE_EVENT: 'update:event',
|
|
184
184
|
|
|
185
185
|
// Participant management
|
|
186
|
-
MANAGE_PARTICIPANTS: '
|
|
186
|
+
MANAGE_PARTICIPANTS: 'update:participants',
|
|
187
187
|
READ_PARTICIPANTS: 'read:participants',
|
|
188
188
|
CREATE_PARTICIPANTS: 'create:participants',
|
|
189
189
|
UPDATE_PARTICIPANTS: 'update:participants',
|
|
190
190
|
DELETE_PARTICIPANTS: 'delete:participants',
|
|
191
191
|
|
|
192
192
|
// App-specific permissions
|
|
193
|
-
MANAGE_APP: '
|
|
193
|
+
MANAGE_APP: 'update:app',
|
|
194
194
|
READ_APP: 'read:app',
|
|
195
195
|
UPDATE_APP: 'update:app',
|
|
196
196
|
};
|
package/src/rbac/engine.ts
CHANGED
|
@@ -301,6 +301,8 @@ export class RBACEngine {
|
|
|
301
301
|
return cached;
|
|
302
302
|
}
|
|
303
303
|
|
|
304
|
+
const now = new Date().toISOString();
|
|
305
|
+
|
|
304
306
|
// Check super admin
|
|
305
307
|
const isSuperAdmin = await this.checkSuperAdmin(userId);
|
|
306
308
|
if (isSuperAdmin) {
|
|
@@ -317,6 +319,8 @@ export class RBACEngine {
|
|
|
317
319
|
.eq('organisation_id', scope.organisationId)
|
|
318
320
|
.eq('status', 'active')
|
|
319
321
|
.is('revoked_at', null)
|
|
322
|
+
.lte('valid_from', now)
|
|
323
|
+
.or(`valid_to.is.null,valid_to.gte.${now}`)
|
|
320
324
|
.single() as { data: { role: string } | null; error: any };
|
|
321
325
|
|
|
322
326
|
if (orgRole?.role === 'org_admin') {
|
|
@@ -327,7 +331,6 @@ export class RBACEngine {
|
|
|
327
331
|
|
|
328
332
|
// Check event-app role
|
|
329
333
|
if (scope.eventId && scope.appId) {
|
|
330
|
-
const now = new Date().toISOString();
|
|
331
334
|
const { data: eventRole } = await this.supabase
|
|
332
335
|
.from('rbac_event_app_roles')
|
|
333
336
|
.select('role')
|
|
@@ -408,10 +411,17 @@ export class RBACEngine {
|
|
|
408
411
|
.eq('app_id', scope.appId) as { data: Array<{ id: string; page_name: string }> | null };
|
|
409
412
|
|
|
410
413
|
if (pages) {
|
|
414
|
+
// OrganisationId is required for permission checks
|
|
415
|
+
if (!scope.organisationId) {
|
|
416
|
+
// Return empty permission map if no organisation context
|
|
417
|
+
rbacCache.set(cacheKey, permissionMap, 60000);
|
|
418
|
+
return permissionMap;
|
|
419
|
+
}
|
|
420
|
+
|
|
411
421
|
// Create a security context for permission checks
|
|
412
422
|
const securityContext: SecurityContext = {
|
|
413
423
|
userId,
|
|
414
|
-
organisationId: scope.organisationId,
|
|
424
|
+
organisationId: scope.organisationId, // Required
|
|
415
425
|
timestamp: new Date(),
|
|
416
426
|
};
|
|
417
427
|
|
|
@@ -350,7 +350,7 @@ describe('useCan Hook', () => {
|
|
|
350
350
|
mockIsPermitted.mockResolvedValue(true);
|
|
351
351
|
|
|
352
352
|
const { result } = renderHook(() =>
|
|
353
|
-
useCan(mockUserId, mockScope, '
|
|
353
|
+
useCan(mockUserId, mockScope, 'update:organisations', undefined, false)
|
|
354
354
|
);
|
|
355
355
|
|
|
356
356
|
await waitFor(() => {
|
|
@@ -362,7 +362,7 @@ describe('useCan Hook', () => {
|
|
|
362
362
|
expect(mockIsPermitted).toHaveBeenCalledWith({
|
|
363
363
|
userId: mockUserId,
|
|
364
364
|
scope: mockScope,
|
|
365
|
-
permission: '
|
|
365
|
+
permission: 'update:organisations',
|
|
366
366
|
pageId: undefined
|
|
367
367
|
});
|
|
368
368
|
});
|
|
@@ -601,6 +601,33 @@ describe('useCan Hook', () => {
|
|
|
601
601
|
}, { timeout: 20000 });
|
|
602
602
|
});
|
|
603
603
|
|
|
604
|
+
it('times out after 3 seconds when organisationId is missing', async () => {
|
|
605
|
+
vi.useFakeTimers({ shouldAdvanceTime: true });
|
|
606
|
+
|
|
607
|
+
const emptyScope = {} as any;
|
|
608
|
+
const { result } = renderHook(() =>
|
|
609
|
+
useCan(mockUserId, emptyScope, mockPermission, undefined, false)
|
|
610
|
+
);
|
|
611
|
+
|
|
612
|
+
// Initially loading
|
|
613
|
+
expect(result.current.isLoading).toBe(true);
|
|
614
|
+
expect(result.current.can).toBe(false);
|
|
615
|
+
expect(result.current.error).toBeNull();
|
|
616
|
+
|
|
617
|
+
// Fast-forward 3 seconds - advance time and run pending timers
|
|
618
|
+
await vi.advanceTimersByTimeAsync(3000);
|
|
619
|
+
await vi.runOnlyPendingTimersAsync();
|
|
620
|
+
|
|
621
|
+
// Flush React updates - waitFor with real timers to handle React state updates
|
|
622
|
+
vi.useRealTimers();
|
|
623
|
+
await waitFor(() => {
|
|
624
|
+
expect(result.current.isLoading).toBe(false);
|
|
625
|
+
expect(result.current.can).toBe(false);
|
|
626
|
+
expect(result.current.error).not.toBeNull();
|
|
627
|
+
expect(result.current.error?.message).toBe('Organisation context is required for permission checks');
|
|
628
|
+
}, { timeout: 2000 });
|
|
629
|
+
}, { timeout: 10000 });
|
|
630
|
+
|
|
604
631
|
it('handles null userId', async () => {
|
|
605
632
|
const { result } = renderHook(() =>
|
|
606
633
|
useCan(null as any, mockScope, mockPermission, undefined, false)
|