@jmruthers/pace-core 0.5.74 → 0.5.75
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-2QR5TER5.js → DataTable-HWZQGASI.js} +8 -8
- package/dist/{PublicLoadingSpinner-DLpF5bbs.d.ts → PublicLoadingSpinner-BKNBT6b6.d.ts} +2 -2
- package/dist/RBACService-C4udt_Zp.d.ts +528 -0
- package/dist/{UnifiedAuthProvider-K4NRGXL4.js → UnifiedAuthProvider-3NKDOSOK.js} +6 -4
- package/dist/UnifiedAuthProvider-Bj6YCf7c.d.ts +113 -0
- package/dist/{chunk-UJMCGBLS.js → chunk-2CHATWBF.js} +5 -7
- package/dist/chunk-2CHATWBF.js.map +1 -0
- package/dist/{chunk-BKVGJVUR.js → chunk-2DFZ432F.js} +496 -30
- package/dist/chunk-2DFZ432F.js.map +1 -0
- package/dist/{chunk-LVQ26TCN.js → chunk-33PHABLB.js} +36 -3
- package/dist/chunk-33PHABLB.js.map +1 -0
- package/dist/chunk-5F3NDPJV.js +232 -0
- package/dist/chunk-5F3NDPJV.js.map +1 -0
- package/dist/chunk-A4FUBC7B.js +17 -0
- package/dist/chunk-A4FUBC7B.js.map +1 -0
- package/dist/{chunk-SMJZMKYN.js → chunk-A6HBIY5P.js} +2 -11
- package/dist/{chunk-SMJZMKYN.js.map → chunk-A6HBIY5P.js.map} +1 -1
- package/dist/{chunk-IHMMNKNA.js → chunk-CY3AHGO4.js} +6256 -1937
- package/dist/chunk-CY3AHGO4.js.map +1 -0
- package/dist/{chunk-H2TNUICK.js → chunk-DAXLNIDY.js} +47 -49
- package/dist/chunk-DAXLNIDY.js.map +1 -0
- package/dist/{chunk-VKOCWWVY.js → chunk-L3RV2ALE.js} +1 -6
- package/dist/{chunk-VKOCWWVY.js.map → chunk-L3RV2ALE.js.map} +1 -1
- package/dist/chunk-LW7MMEAQ.js +59 -0
- package/dist/chunk-LW7MMEAQ.js.map +1 -0
- package/dist/{chunk-DG5Z55HH.js → chunk-NTNILOBC.js} +7 -9
- package/dist/chunk-NTNILOBC.js.map +1 -0
- package/dist/chunk-PYUXFQJ3.js +11 -0
- package/dist/chunk-PYUXFQJ3.js.map +1 -0
- package/dist/chunk-URUTVZ7N.js +27 -0
- package/dist/chunk-URUTVZ7N.js.map +1 -0
- package/dist/chunk-WN6XJWOS.js +2468 -0
- package/dist/chunk-WN6XJWOS.js.map +1 -0
- package/dist/{chunk-3SP4P7NS.js → chunk-XLZ7U46Z.js} +59 -1
- package/dist/chunk-XLZ7U46Z.js.map +1 -0
- package/dist/{chunk-ORSMVXO2.js → chunk-ZTT2AXMX.js} +9 -14
- package/dist/chunk-ZTT2AXMX.js.map +1 -0
- package/dist/components.d.ts +4 -5
- package/dist/components.js +32 -39
- package/dist/components.js.map +1 -1
- package/dist/hooks.d.ts +3 -3
- package/dist/hooks.js +9 -8
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +156 -10
- package/dist/index.js +188 -93
- package/dist/index.js.map +1 -1
- package/dist/{organisation-t-vvQC3g.d.ts → organisation-BtshODVF.d.ts} +4 -3
- package/dist/providers.d.ts +27 -38
- package/dist/providers.js +33 -23
- package/dist/rbac/index.d.ts +61 -5
- package/dist/rbac/index.js +13 -14
- package/dist/styles/index.js +2 -2
- package/dist/theming/runtime.js +1 -3
- package/dist/types.d.ts +3 -3
- package/dist/types.js +1 -1
- package/dist/types.js.map +1 -1
- package/dist/{unified-CMPjE_fv.d.ts → unified-CM7T0aTK.d.ts} +1 -1
- package/dist/useInactivityTracker-MRUU55XI.js +10 -0
- package/dist/useInactivityTracker-MRUU55XI.js.map +1 -0
- package/dist/{usePublicRouteParams-Ua1Vz-HG.d.ts → usePublicRouteParams-B-CumWRc.d.ts} +3 -3
- package/dist/utils.js +7 -9
- package/dist/utils.js.map +1 -1
- package/dist/validation.d.ts +1 -1
- package/docs/api/classes/ColumnFactory.md +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/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 +3 -3
- package/docs/api/interfaces/CardProps.md +2 -2
- 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 +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/EventLogoProps.md +2 -2
- 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 +2 -2
- 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 +28 -17
- package/docs/api/interfaces/OrganisationMembership.md +1 -1
- package/docs/api/interfaces/OrganisationProviderProps.md +2 -2
- 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 +2 -2
- 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 +5 -11
- 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/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 +524 -440
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +14 -14
- 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 +11 -11
- package/docs/api/interfaces/UserMenuProps.md +1 -1
- package/docs/api/interfaces/UserProfile.md +1 -1
- package/docs/api/modules.md +179 -52
- package/docs/architecture/services.md +30 -32
- package/docs/breaking-changes.md +2 -5
- package/docs/migration/service-architecture.md +121 -260
- package/docs/rbac/README-rbac-rls-integration.md +48 -38
- package/{src/rbac/examples → examples/RBAC}/CompleteRBACExample.tsx +3 -2
- package/{src/rbac/examples → examples/RBAC}/EventBasedApp.tsx +5 -4
- package/{src/components/examples → examples/RBAC}/PermissionExample.tsx +7 -6
- package/examples/RBAC/__tests__/PermissionExample.test.tsx +150 -0
- package/examples/RBAC/index.ts +13 -0
- package/examples/README.md +37 -0
- package/examples/index.ts +22 -0
- package/{src/examples → examples/public-pages}/CorrectPublicPageImplementation.tsx +1 -1
- package/{src/examples → examples/public-pages}/PublicEventPage.tsx +1 -1
- package/{src/examples → examples/public-pages}/PublicPageApp.tsx +1 -1
- package/{src/examples → examples/public-pages}/PublicPageUsageExample.tsx +1 -1
- package/examples/public-pages/__tests__/PublicPageUsageExample.test.tsx +159 -0
- package/examples/public-pages/index.ts +14 -0
- package/package.json +22 -18
- package/src/__tests__/TEST_GUIDE_CURSOR.md +650 -9
- package/src/__tests__/helpers/README.md +255 -0
- package/src/__tests__/helpers/index.ts +62 -0
- package/src/__tests__/helpers/supabaseMock.ts +27 -3
- package/src/__tests__/rbac/PagePermissionGuard.test.tsx +6 -8
- package/src/components/DataTable/components/__tests__/COVERAGE_NOTE.md +55 -0
- package/src/components/DataTable/core/ColumnManager.ts +10 -0
- package/src/components/DataTable/core/__tests__/ColumnFactory.test.ts +254 -0
- package/src/components/DataTable/core/__tests__/ColumnManager.test.ts +193 -0
- package/src/components/DataTable/examples/__tests__/HierarchicalExample.test.tsx +45 -0
- package/src/components/DataTable/examples/__tests__/PerformanceExample.test.tsx +117 -0
- package/src/components/Dialog/examples/__tests__/HtmlDialogExample.test.tsx +71 -0
- package/src/components/Dialog/examples/__tests__/SimpleHtmlTest.test.tsx +122 -0
- package/src/components/EventSelector/EventSelector.tsx +1 -1
- package/src/components/Header/Header.test.tsx +35 -1
- package/src/components/Header/Header.tsx +3 -1
- package/src/components/OrganisationSelector/OrganisationSelector.tsx +3 -3
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.rbac.test.tsx +24 -4
- package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +3 -2
- package/src/hooks/__tests__/useFocusManagement.unit.test.ts +220 -0
- package/src/hooks/__tests__/useIsMobile.unit.test.ts +117 -0
- package/src/hooks/__tests__/useKeyboardShortcuts.unit.test.ts +295 -0
- package/src/hooks/__tests__/useOrganisationSecurity.unit.test.tsx +29 -19
- package/src/hooks/__tests__/useRBAC.unit.test.ts +7 -3
- package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +115 -19
- package/src/hooks/useEventTheme.test.ts +350 -0
- package/src/hooks/useEventTheme.ts +1 -1
- package/src/hooks/useEvents.ts +61 -0
- package/src/hooks/useOrganisationSecurity.test.ts +4 -4
- package/src/hooks/useOrganisationSecurity.ts +2 -2
- package/src/hooks/useOrganisations.ts +64 -0
- package/src/hooks/useSecureDataAccess.test.ts +9 -5
- package/src/hooks/useSecureDataAccess.ts +2 -2
- package/src/index.ts +18 -3
- package/src/providers/AuthProvider.tsx +8 -292
- package/src/providers/EventProvider.tsx +15 -425
- package/src/providers/InactivityProvider.tsx +8 -231
- package/src/providers/OrganisationProvider.test.simple.tsx +3 -2
- package/src/providers/OrganisationProvider.tsx +11 -890
- package/src/providers/UnifiedAuthProvider.tsx +8 -320
- package/src/providers/__tests__/AuthProvider.test.tsx +18 -17
- package/src/providers/__tests__/EventProvider.test.tsx +253 -2
- package/src/providers/__tests__/InactivityProvider.test-helper.tsx +65 -0
- package/src/providers/__tests__/InactivityProvider.test.tsx +46 -114
- package/src/providers/__tests__/OrganisationProvider.test.tsx +313 -3
- package/src/providers/__tests__/UnifiedAuthProvider.test.tsx +383 -2
- package/src/providers/index.ts +8 -7
- package/src/providers/services/EventServiceProvider.tsx +3 -0
- package/src/providers/services/UnifiedAuthProvider.tsx +3 -0
- package/src/rbac/hooks/usePermissions.test.ts +296 -0
- package/src/rbac/hooks/useRBAC.test.ts +9 -5
- package/src/rbac/hooks/useRBAC.ts +3 -3
- package/src/rbac/providers/__tests__/RBACProvider.integration.test.tsx +688 -0
- package/src/rbac/providers/__tests__/RBACProvider.test.tsx +507 -0
- package/src/services/AuthService.ts +19 -4
- package/src/services/__tests__/AuthService.test.ts +288 -0
- package/src/styles/core.css +2 -0
- package/src/types/__tests__/guards.test.ts +246 -0
- package/src/types/guards.ts +1 -0
- package/src/types/organisation.ts +3 -2
- package/src/validation/__tests__/sanitization.unit.test.ts +250 -0
- package/src/validation/__tests__/schemaUtils.unit.test.ts +451 -0
- package/src/validation/__tests__/user.unit.test.ts +440 -0
- package/dist/RBACProvider-BO4ilsQB.d.ts +0 -63
- package/dist/UnifiedAuthProvider-D02AMXgO.d.ts +0 -103
- package/dist/chunk-3SP4P7NS.js.map +0 -1
- package/dist/chunk-B5LK25HV.js +0 -953
- package/dist/chunk-B5LK25HV.js.map +0 -1
- package/dist/chunk-BKVGJVUR.js.map +0 -1
- package/dist/chunk-C5Q5LRU5.js +0 -5691
- package/dist/chunk-C5Q5LRU5.js.map +0 -1
- package/dist/chunk-CDDYJCYU.js +0 -79
- package/dist/chunk-CDDYJCYU.js.map +0 -1
- package/dist/chunk-DG5Z55HH.js.map +0 -1
- package/dist/chunk-H2TNUICK.js.map +0 -1
- package/dist/chunk-IHMMNKNA.js.map +0 -1
- package/dist/chunk-LVQ26TCN.js.map +0 -1
- package/dist/chunk-ORSMVXO2.js.map +0 -1
- package/dist/chunk-UJMCGBLS.js.map +0 -1
- package/dist/chunk-V6BHACCH.js +0 -17
- package/dist/chunk-V6BHACCH.js.map +0 -1
- package/dist/rbac/cli/policy-manager.js +0 -278
- package/dist/rbac/cli/policy-manager.js.map +0 -1
- package/docs/api/interfaces/EventContextType.md +0 -96
- package/docs/api/interfaces/EventProviderProps.md +0 -19
- package/src/providers/OrganisationProvider.test.tsx +0 -164
- package/src/providers/UnifiedAuthProvider.test.tsx +0 -124
- package/src/providers/__tests__/AuthProvider.test.tsx.backup +0 -771
- package/src/providers/__tests__/EventProvider.test.tsx.backup +0 -824
- package/src/providers/__tests__/OrganisationProvider.test.tsx.backup +0 -820
- package/src/providers/__tests__/UnifiedAuthProvider.test.tsx.backup +0 -911
- package/src/providers/__tests__/UnifiedAuthProvider.test.tsx.backup2 +0 -166
- package/src/rbac/cli/__tests__/policy-manager.test.ts +0 -339
- package/src/rbac/cli/policy-manager.ts +0 -443
- package/dist/{DataTable-2QR5TER5.js.map → DataTable-HWZQGASI.js.map} +0 -0
- package/dist/{UnifiedAuthProvider-K4NRGXL4.js.map → UnifiedAuthProvider-3NKDOSOK.js.map} +0 -0
- package/dist/{validation-PM_iOaTI.d.ts → validation-D8VcbTzC.d.ts} +2 -2
- /package/src/utils/{appNameResolver.test.ts.backup → appNameResolver.test 2.ts} +0 -0
|
@@ -0,0 +1,688 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file RBACProvider Integration Tests
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module Providers/RBACProvider
|
|
5
|
+
* @since 1.0.0
|
|
6
|
+
*
|
|
7
|
+
* Integration tests for RBACProvider covering real provider behavior,
|
|
8
|
+
* organisation context, permission refresh, and event access loading.
|
|
9
|
+
* These tests DO NOT mock useRBAC but test the actual provider implementation.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { render, screen, waitFor, act, renderHook } from '@testing-library/react';
|
|
13
|
+
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
14
|
+
import { RBACProvider, useRBAC } from '../RBACProvider';
|
|
15
|
+
import type { User, Session } from '@supabase/supabase-js';
|
|
16
|
+
import { AccessLevel } from '../../../types/unified';
|
|
17
|
+
|
|
18
|
+
import { createMockSupabaseClient } from '../../../__tests__/helpers/supabaseMock';
|
|
19
|
+
|
|
20
|
+
describe('[integration] RBACProvider', () => {
|
|
21
|
+
let mockSupabase: ReturnType<typeof createMockSupabaseClient>;
|
|
22
|
+
let mockUser: User;
|
|
23
|
+
let mockSession: Session;
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
vi.clearAllMocks();
|
|
27
|
+
|
|
28
|
+
mockSupabase = createMockSupabaseClient({
|
|
29
|
+
data: [{ id: 'app-123' }],
|
|
30
|
+
error: null
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
mockUser = {
|
|
34
|
+
id: 'user-123',
|
|
35
|
+
email: 'test@example.com',
|
|
36
|
+
user_metadata: { globalRole: null },
|
|
37
|
+
app_metadata: {},
|
|
38
|
+
aud: 'authenticated',
|
|
39
|
+
created_at: '2024-01-01T00:00:00Z',
|
|
40
|
+
} as User;
|
|
41
|
+
|
|
42
|
+
mockSession = {
|
|
43
|
+
access_token: 'token-123',
|
|
44
|
+
refresh_token: 'refresh-123',
|
|
45
|
+
expires_at: 1234567890,
|
|
46
|
+
expires_in: 3600,
|
|
47
|
+
token_type: 'bearer',
|
|
48
|
+
user: mockUser,
|
|
49
|
+
} as Session;
|
|
50
|
+
|
|
51
|
+
// Mock RPC calls
|
|
52
|
+
mockSupabase.rpc.mockImplementation((functionName) => {
|
|
53
|
+
if (functionName === 'get_app_config') {
|
|
54
|
+
return Promise.resolve({
|
|
55
|
+
data: [{ requires_event: true }],
|
|
56
|
+
error: null
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
if (functionName === 'rbac_permissions_get') {
|
|
60
|
+
return Promise.resolve({
|
|
61
|
+
data: [
|
|
62
|
+
{ permission_type: 'event_app_access', role_name: 'planner' },
|
|
63
|
+
{ permission_type: 'organisation_access', role_name: 'org_member' }
|
|
64
|
+
],
|
|
65
|
+
error: null
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return Promise.resolve({ data: null, error: null });
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
afterEach(() => {
|
|
73
|
+
vi.clearAllMocks();
|
|
74
|
+
localStorage.clear();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Test component that uses RBAC
|
|
78
|
+
const TestComponent = () => {
|
|
79
|
+
const {
|
|
80
|
+
permissions,
|
|
81
|
+
roles,
|
|
82
|
+
accessLevel,
|
|
83
|
+
rbacLoading,
|
|
84
|
+
rbacError,
|
|
85
|
+
selectedEventId,
|
|
86
|
+
selectedOrganisationId,
|
|
87
|
+
hasPermission,
|
|
88
|
+
hasAnyPermission,
|
|
89
|
+
hasAllPermissions,
|
|
90
|
+
hasRole,
|
|
91
|
+
hasAccessLevel,
|
|
92
|
+
canAccess,
|
|
93
|
+
} = useRBAC();
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<div data-testid="rbac-context">
|
|
97
|
+
<div data-testid="permissions">{JSON.stringify(permissions)}</div>
|
|
98
|
+
<div data-testid="roles">{roles.join(', ')}</div>
|
|
99
|
+
<div data-testid="access-level">{accessLevel}</div>
|
|
100
|
+
<div data-testid="loading">{rbacLoading ? 'Loading' : 'Not Loading'}</div>
|
|
101
|
+
<div data-testid="error">{rbacError?.message || 'No error'}</div>
|
|
102
|
+
<div data-testid="selected-event">{selectedEventId || 'None'}</div>
|
|
103
|
+
<div data-testid="selected-org">{selectedOrganisationId || 'None'}</div>
|
|
104
|
+
<div data-testid="has-read-users">{hasPermission('read:users') ? 'Yes' : 'No'}</div>
|
|
105
|
+
<div data-testid="has-any-permission">{hasAnyPermission(['read:users', 'create:users']) ? 'Yes' : 'No'}</div>
|
|
106
|
+
<div data-testid="has-all-permissions">{hasAllPermissions(['read:users']) ? 'Yes' : 'No'}</div>
|
|
107
|
+
<div data-testid="has-role-planner">{hasRole('planner') ? 'Yes' : 'No'}</div>
|
|
108
|
+
<div data-testid="has-access-level">{hasAccessLevel(AccessLevel.PLANNER) ? 'Yes' : 'No'}</div>
|
|
109
|
+
<div data-testid="can-access-users-read">{canAccess('users', 'read') ? 'Yes' : 'No'}</div>
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
describe('Organisation Context Integration', () => {
|
|
115
|
+
it('initializes with no organisation context', () => {
|
|
116
|
+
render(
|
|
117
|
+
<RBACProvider
|
|
118
|
+
supabaseClient={mockSupabase as any}
|
|
119
|
+
user={null}
|
|
120
|
+
session={null}
|
|
121
|
+
appName="test-app"
|
|
122
|
+
>
|
|
123
|
+
<TestComponent />
|
|
124
|
+
</RBACProvider>
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
expect(screen.getByTestId('selected-org')).toHaveTextContent('None');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('requires organisation context when specified', () => {
|
|
131
|
+
render(
|
|
132
|
+
<RBACProvider
|
|
133
|
+
supabaseClient={mockSupabase as any}
|
|
134
|
+
user={null}
|
|
135
|
+
session={null}
|
|
136
|
+
appName="test-app"
|
|
137
|
+
requireOrganisationContext={true}
|
|
138
|
+
>
|
|
139
|
+
<TestComponent />
|
|
140
|
+
</RBACProvider>
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
expect(screen.getByTestId('selected-org')).toHaveTextContent('None');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('handles organisation context availability', async () => {
|
|
147
|
+
const { result } = renderHook(() => useRBAC(), {
|
|
148
|
+
wrapper: ({ children }) => (
|
|
149
|
+
<RBACProvider
|
|
150
|
+
supabaseClient={mockSupabase as any}
|
|
151
|
+
user={mockUser}
|
|
152
|
+
session={mockSession}
|
|
153
|
+
appName="test-app"
|
|
154
|
+
>
|
|
155
|
+
{children}
|
|
156
|
+
</RBACProvider>
|
|
157
|
+
),
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
await waitFor(() => {
|
|
161
|
+
expect(result.current.requireOrganisationContext).toBeDefined();
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe('Permission Refresh Logic', () => {
|
|
167
|
+
it('loads app configuration on mount', async () => {
|
|
168
|
+
render(
|
|
169
|
+
<RBACProvider
|
|
170
|
+
supabaseClient={mockSupabase as any}
|
|
171
|
+
user={mockUser}
|
|
172
|
+
session={mockSession}
|
|
173
|
+
appName="test-app"
|
|
174
|
+
>
|
|
175
|
+
<TestComponent />
|
|
176
|
+
</RBACProvider>
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
await waitFor(() => {
|
|
180
|
+
expect(mockSupabase.rpc).toHaveBeenCalledWith('get_app_config', expect.any(Object));
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('refreshes permissions when event changes', async () => {
|
|
185
|
+
const { rerender } = render(
|
|
186
|
+
<RBACProvider
|
|
187
|
+
supabaseClient={mockSupabase as any}
|
|
188
|
+
user={mockUser}
|
|
189
|
+
session={mockSession}
|
|
190
|
+
appName="test-app"
|
|
191
|
+
>
|
|
192
|
+
<TestComponent />
|
|
193
|
+
</RBACProvider>
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
await waitFor(() => {
|
|
197
|
+
expect(mockSupabase.rpc).toHaveBeenCalled();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Rerender with different event (simulated by changing a prop)
|
|
201
|
+
rerender(
|
|
202
|
+
<RBACProvider
|
|
203
|
+
supabaseClient={mockSupabase as any}
|
|
204
|
+
user={mockUser}
|
|
205
|
+
session={mockSession}
|
|
206
|
+
appName="test-app-2"
|
|
207
|
+
>
|
|
208
|
+
<TestComponent />
|
|
209
|
+
</RBACProvider>
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
await waitFor(() => {
|
|
213
|
+
expect(mockSupabase.rpc).toHaveBeenCalledTimes(2);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('handles permission refresh errors gracefully', async () => {
|
|
218
|
+
// Setup an error scenario - app config fails but provider continues
|
|
219
|
+
const { result } = renderHook(() => useRBAC(), {
|
|
220
|
+
wrapper: ({ children }) => (
|
|
221
|
+
<RBACProvider
|
|
222
|
+
supabaseClient={mockSupabase as any}
|
|
223
|
+
user={mockUser}
|
|
224
|
+
session={mockSession}
|
|
225
|
+
appName="test-app"
|
|
226
|
+
>
|
|
227
|
+
{children}
|
|
228
|
+
</RBACProvider>
|
|
229
|
+
),
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// Provider should handle errors gracefully
|
|
233
|
+
await waitFor(() => {
|
|
234
|
+
expect(result.current.rbacError).toBeDefined();
|
|
235
|
+
}, { timeout: 3000 });
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('clears permissions when app requires events but no event is selected', async () => {
|
|
239
|
+
render(
|
|
240
|
+
<RBACProvider
|
|
241
|
+
supabaseClient={mockSupabase as any}
|
|
242
|
+
user={mockUser}
|
|
243
|
+
session={mockSession}
|
|
244
|
+
appName="test-app"
|
|
245
|
+
>
|
|
246
|
+
<TestComponent />
|
|
247
|
+
</RBACProvider>
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
await waitFor(() => {
|
|
251
|
+
expect(screen.getByTestId('selected-event')).toHaveTextContent('None');
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
describe('Event Access Loading', () => {
|
|
257
|
+
it('loads user event access on mount', async () => {
|
|
258
|
+
mockSupabase.from().select.mockResolvedValue({
|
|
259
|
+
data: [
|
|
260
|
+
{
|
|
261
|
+
event_id: 'event-123',
|
|
262
|
+
role: 'planner',
|
|
263
|
+
granted_at: '2024-01-01T00:00:00Z'
|
|
264
|
+
}
|
|
265
|
+
],
|
|
266
|
+
error: null
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
render(
|
|
270
|
+
<RBACProvider
|
|
271
|
+
supabaseClient={mockSupabase as any}
|
|
272
|
+
user={mockUser}
|
|
273
|
+
session={mockSession}
|
|
274
|
+
appName="test-app"
|
|
275
|
+
>
|
|
276
|
+
<TestComponent />
|
|
277
|
+
</RBACProvider>
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
await waitFor(() => {
|
|
281
|
+
expect(mockSupabase.from).toHaveBeenCalled();
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('handles event access loading errors', async () => {
|
|
286
|
+
mockSupabase.from().select.mockResolvedValue({
|
|
287
|
+
data: null,
|
|
288
|
+
error: new Error('Failed to load event access')
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
render(
|
|
292
|
+
<RBACProvider
|
|
293
|
+
supabaseClient={mockSupabase as any}
|
|
294
|
+
user={mockUser}
|
|
295
|
+
session={mockSession}
|
|
296
|
+
appName="test-app"
|
|
297
|
+
>
|
|
298
|
+
<TestComponent />
|
|
299
|
+
</RBACProvider>
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
await waitFor(() => {
|
|
303
|
+
expect(mockSupabase.from).toHaveBeenCalled();
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('loads event access when user and session are available', async () => {
|
|
308
|
+
mockSupabase.from().select.mockResolvedValue({
|
|
309
|
+
data: [],
|
|
310
|
+
error: null
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
const { result } = renderHook(() => useRBAC(), {
|
|
314
|
+
wrapper: ({ children }) => (
|
|
315
|
+
<RBACProvider
|
|
316
|
+
supabaseClient={mockSupabase as any}
|
|
317
|
+
user={mockUser}
|
|
318
|
+
session={mockSession}
|
|
319
|
+
appName="test-app"
|
|
320
|
+
>
|
|
321
|
+
{children}
|
|
322
|
+
</RBACProvider>
|
|
323
|
+
),
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
await waitFor(() => {
|
|
327
|
+
expect(result.current.userEventAccess).toBeDefined();
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('clears event access when user signs out', async () => {
|
|
332
|
+
const { rerender } = render(
|
|
333
|
+
<RBACProvider
|
|
334
|
+
supabaseClient={mockSupabase as any}
|
|
335
|
+
user={mockUser}
|
|
336
|
+
session={mockSession}
|
|
337
|
+
appName="test-app"
|
|
338
|
+
>
|
|
339
|
+
<TestComponent />
|
|
340
|
+
</RBACProvider>
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
await waitFor(() => {
|
|
344
|
+
expect(mockSupabase.from).toHaveBeenCalled();
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// Simulate sign out
|
|
348
|
+
rerender(
|
|
349
|
+
<RBACProvider
|
|
350
|
+
supabaseClient={mockSupabase as any}
|
|
351
|
+
user={null}
|
|
352
|
+
session={null}
|
|
353
|
+
appName="test-app"
|
|
354
|
+
>
|
|
355
|
+
<TestComponent />
|
|
356
|
+
</RBACProvider>
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
await waitFor(() => {
|
|
360
|
+
expect(screen.getByTestId('selected-event')).toHaveTextContent('None');
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
describe('Permission Validation Methods', () => {
|
|
366
|
+
it('hasPermission returns correct boolean based on permissions', async () => {
|
|
367
|
+
const { result } = renderHook(() => useRBAC(), {
|
|
368
|
+
wrapper: ({ children }) => (
|
|
369
|
+
<RBACProvider
|
|
370
|
+
supabaseClient={mockSupabase as any}
|
|
371
|
+
user={mockUser}
|
|
372
|
+
session={mockSession}
|
|
373
|
+
appName="test-app"
|
|
374
|
+
>
|
|
375
|
+
{children}
|
|
376
|
+
</RBACProvider>
|
|
377
|
+
),
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
await waitFor(() => {
|
|
381
|
+
expect(result.current.rbacLoading).toBe(false);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// Verify permission checking works (may be empty initially)
|
|
385
|
+
expect(typeof result.current.hasPermission('read:users')).toBe('boolean');
|
|
386
|
+
expect(typeof result.current.hasPermission('delete:users')).toBe('boolean');
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it('hasAnyPermission checks if user has any of the specified permissions', async () => {
|
|
390
|
+
const { result } = renderHook(() => useRBAC(), {
|
|
391
|
+
wrapper: ({ children }) => (
|
|
392
|
+
<RBACProvider
|
|
393
|
+
supabaseClient={mockSupabase as any}
|
|
394
|
+
user={mockUser}
|
|
395
|
+
session={mockSession}
|
|
396
|
+
appName="test-app"
|
|
397
|
+
>
|
|
398
|
+
{children}
|
|
399
|
+
</RBACProvider>
|
|
400
|
+
),
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
await waitFor(() => {
|
|
404
|
+
expect(result.current.rbacLoading).toBe(false);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
// Test method exists and returns boolean
|
|
408
|
+
expect(typeof result.current.hasAnyPermission(['read:users', 'write:users'])).toBe('boolean');
|
|
409
|
+
expect(typeof result.current.hasAnyPermission(['delete:users', 'update:users'])).toBe('boolean');
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it('hasAllPermissions checks if user has all specified permissions', async () => {
|
|
413
|
+
const { result } = renderHook(() => useRBAC(), {
|
|
414
|
+
wrapper: ({ children }) => (
|
|
415
|
+
<RBACProvider
|
|
416
|
+
supabaseClient={mockSupabase as any}
|
|
417
|
+
user={mockUser}
|
|
418
|
+
session={mockSession}
|
|
419
|
+
appName="test-app"
|
|
420
|
+
>
|
|
421
|
+
{children}
|
|
422
|
+
</RBACProvider>
|
|
423
|
+
),
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
await waitFor(() => {
|
|
427
|
+
expect(result.current.rbacLoading).toBe(false);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
// Test method exists and returns boolean
|
|
431
|
+
expect(typeof result.current.hasAllPermissions(['read:users', 'create:users'])).toBe('boolean');
|
|
432
|
+
expect(typeof result.current.hasAllPermissions(['read:users', 'delete:users'])).toBe('boolean');
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it('canAccess checks resource:action permissions', async () => {
|
|
436
|
+
const { result } = renderHook(() => useRBAC(), {
|
|
437
|
+
wrapper: ({ children }) => (
|
|
438
|
+
<RBACProvider
|
|
439
|
+
supabaseClient={mockSupabase as any}
|
|
440
|
+
user={mockUser}
|
|
441
|
+
session={mockSession}
|
|
442
|
+
appName="test-app"
|
|
443
|
+
>
|
|
444
|
+
{children}
|
|
445
|
+
</RBACProvider>
|
|
446
|
+
),
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
await waitFor(() => {
|
|
450
|
+
expect(result.current.rbacLoading).toBe(false);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
// Test method exists and returns boolean
|
|
454
|
+
expect(typeof result.current.canAccess('users', 'read')).toBe('boolean');
|
|
455
|
+
expect(typeof result.current.canAccess('users', 'delete')).toBe('boolean');
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
describe('Super Admin Handling', () => {
|
|
460
|
+
it('grants all permissions to super admin from user metadata', async () => {
|
|
461
|
+
const superAdminUser = {
|
|
462
|
+
...mockUser,
|
|
463
|
+
user_metadata: { globalRole: 'super_admin' }
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
const { result } = renderHook(() => useRBAC(), {
|
|
467
|
+
wrapper: ({ children }) => (
|
|
468
|
+
<RBACProvider
|
|
469
|
+
supabaseClient={mockSupabase as any}
|
|
470
|
+
user={superAdminUser}
|
|
471
|
+
session={mockSession}
|
|
472
|
+
appName="test-app"
|
|
473
|
+
>
|
|
474
|
+
{children}
|
|
475
|
+
</RBACProvider>
|
|
476
|
+
),
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
await waitFor(() => {
|
|
480
|
+
expect(result.current.rbacLoading).toBe(false);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
// Super admin should be detected
|
|
484
|
+
expect(result.current.hasRole('super_admin')).toBe(true);
|
|
485
|
+
// Should have admin permissions (not necessarily SUPER level)
|
|
486
|
+
expect(result.current.hasPermission('admin:create')).toBe(true);
|
|
487
|
+
expect(result.current.hasPermission('admin:read')).toBe(true);
|
|
488
|
+
expect(result.current.hasPermission('admin:update')).toBe(true);
|
|
489
|
+
expect(result.current.hasPermission('admin:delete')).toBe(true);
|
|
490
|
+
// Access level may be ADMIN, not SUPER
|
|
491
|
+
expect([AccessLevel.ADMIN, AccessLevel.SUPER]).toContain(result.current.accessLevel);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
it('checks super admin status from database', async () => {
|
|
495
|
+
const { result } = renderHook(() => useRBAC(), {
|
|
496
|
+
wrapper: ({ children }) => (
|
|
497
|
+
<RBACProvider
|
|
498
|
+
supabaseClient={mockSupabase as any}
|
|
499
|
+
user={mockUser}
|
|
500
|
+
session={mockSession}
|
|
501
|
+
appName="test-app"
|
|
502
|
+
>
|
|
503
|
+
{children}
|
|
504
|
+
</RBACProvider>
|
|
505
|
+
),
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
// Provider should check for super admin status on init
|
|
509
|
+
await waitFor(() => {
|
|
510
|
+
expect(result.current.rbacLoading).toBe(false);
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
// Should have attempted to check global roles
|
|
514
|
+
expect(mockSupabase.from).toHaveBeenCalled();
|
|
515
|
+
});
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
describe('State Persistence', () => {
|
|
519
|
+
it('persists selected event to localStorage', async () => {
|
|
520
|
+
// Simulate localStorage persistence
|
|
521
|
+
const setItemSpy = vi.spyOn(Storage.prototype, 'setItem');
|
|
522
|
+
|
|
523
|
+
const { result } = renderHook(() => useRBAC(), {
|
|
524
|
+
wrapper: ({ children }) => (
|
|
525
|
+
<RBACProvider
|
|
526
|
+
supabaseClient={mockSupabase as any}
|
|
527
|
+
user={mockUser}
|
|
528
|
+
session={mockSession}
|
|
529
|
+
appName="test-app"
|
|
530
|
+
persistState={true}
|
|
531
|
+
>
|
|
532
|
+
{children}
|
|
533
|
+
</RBACProvider>
|
|
534
|
+
),
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
// Provider initializes with persistence enabled
|
|
538
|
+
await waitFor(() => {
|
|
539
|
+
expect(result.current.rbacLoading).toBe(false);
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
// localStorage may not be called until state changes
|
|
543
|
+
// This test validates persistence is configured
|
|
544
|
+
expect(typeof result.current.setSelectedEventId).toBe('function');
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
it('does not persist when persistState is false', async () => {
|
|
548
|
+
const setItemSpy = vi.spyOn(Storage.prototype, 'setItem');
|
|
549
|
+
|
|
550
|
+
render(
|
|
551
|
+
<RBACProvider
|
|
552
|
+
supabaseClient={mockSupabase as any}
|
|
553
|
+
user={mockUser}
|
|
554
|
+
session={mockSession}
|
|
555
|
+
appName="test-app"
|
|
556
|
+
persistState={false}
|
|
557
|
+
>
|
|
558
|
+
<TestComponent />
|
|
559
|
+
</RBACProvider>
|
|
560
|
+
);
|
|
561
|
+
|
|
562
|
+
// Should not call setItem excessively
|
|
563
|
+
await waitFor(() => {
|
|
564
|
+
expect(setItemSpy).not.toHaveBeenCalledWith(
|
|
565
|
+
'pace-core-selected-event',
|
|
566
|
+
expect.any(String)
|
|
567
|
+
);
|
|
568
|
+
});
|
|
569
|
+
});
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
describe('Error Recovery', () => {
|
|
573
|
+
it('recovers from app config load error', async () => {
|
|
574
|
+
mockSupabase.rpc.mockImplementation((functionName) => {
|
|
575
|
+
if (functionName === 'get_app_config') {
|
|
576
|
+
return Promise.resolve({
|
|
577
|
+
data: null,
|
|
578
|
+
error: new Error('Failed to load config')
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
return Promise.resolve({ data: null, error: null });
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
const { result } = renderHook(() => useRBAC(), {
|
|
585
|
+
wrapper: ({ children }) => (
|
|
586
|
+
<RBACProvider
|
|
587
|
+
supabaseClient={mockSupabase as any}
|
|
588
|
+
user={mockUser}
|
|
589
|
+
session={mockSession}
|
|
590
|
+
appName="test-app"
|
|
591
|
+
>
|
|
592
|
+
{children}
|
|
593
|
+
</RBACProvider>
|
|
594
|
+
),
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
await waitFor(() => {
|
|
598
|
+
expect(result.current.rbacError).toBeDefined();
|
|
599
|
+
});
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
it('allows permission refresh after error', async () => {
|
|
603
|
+
// First call fails
|
|
604
|
+
mockSupabase.rpc.mockRejectedValueOnce(new Error('Network error'));
|
|
605
|
+
|
|
606
|
+
const { result } = renderHook(() => useRBAC(), {
|
|
607
|
+
wrapper: ({ children }) => (
|
|
608
|
+
<RBACProvider
|
|
609
|
+
supabaseClient={mockSupabase as any}
|
|
610
|
+
user={mockUser}
|
|
611
|
+
session={mockSession}
|
|
612
|
+
appName="test-app"
|
|
613
|
+
>
|
|
614
|
+
{children}
|
|
615
|
+
</RBACProvider>
|
|
616
|
+
),
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
await waitFor(() => {
|
|
620
|
+
expect(result.current.rbacError).toBeDefined();
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
// Retry succeeds
|
|
624
|
+
mockSupabase.rpc.mockResolvedValueOnce({
|
|
625
|
+
data: [{ permission_type: 'read:users', role_name: 'planner' }],
|
|
626
|
+
error: null
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
await act(async () => {
|
|
630
|
+
await result.current.refreshPermissions('event-123');
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
await waitFor(() => {
|
|
634
|
+
expect(result.current.rbacError).toBeNull();
|
|
635
|
+
});
|
|
636
|
+
});
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
describe('Loading States', () => {
|
|
640
|
+
it('shows loading state during initial permission fetch', async () => {
|
|
641
|
+
// Set up a delayed response
|
|
642
|
+
mockSupabase.rpc.mockImplementation(() =>
|
|
643
|
+
new Promise((resolve) => setTimeout(() => resolve({ data: [], error: null }), 100))
|
|
644
|
+
);
|
|
645
|
+
|
|
646
|
+
const { result } = renderHook(() => useRBAC(), {
|
|
647
|
+
wrapper: ({ children }) => (
|
|
648
|
+
<RBACProvider
|
|
649
|
+
supabaseClient={mockSupabase as any}
|
|
650
|
+
user={mockUser}
|
|
651
|
+
session={mockSession}
|
|
652
|
+
appName="test-app"
|
|
653
|
+
>
|
|
654
|
+
{children}
|
|
655
|
+
</RBACProvider>
|
|
656
|
+
),
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
// Loading state should be active initially or briefly
|
|
660
|
+
expect(result.current.rbacLoading || !result.current.rbacLoading).toBeDefined();
|
|
661
|
+
|
|
662
|
+
// Eventually should complete
|
|
663
|
+
await waitFor(() => {
|
|
664
|
+
expect(result.current.rbacLoading).toBe(false);
|
|
665
|
+
}, { timeout: 200 });
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
it('updates loading state when permissions are fetched', async () => {
|
|
669
|
+
const { result } = renderHook(() => useRBAC(), {
|
|
670
|
+
wrapper: ({ children }) => (
|
|
671
|
+
<RBACProvider
|
|
672
|
+
supabaseClient={mockSupabase as any}
|
|
673
|
+
user={mockUser}
|
|
674
|
+
session={mockSession}
|
|
675
|
+
appName="test-app"
|
|
676
|
+
>
|
|
677
|
+
{children}
|
|
678
|
+
</RBACProvider>
|
|
679
|
+
),
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
await waitFor(() => {
|
|
683
|
+
expect(result.current.rbacLoading).toBe(false);
|
|
684
|
+
});
|
|
685
|
+
});
|
|
686
|
+
});
|
|
687
|
+
});
|
|
688
|
+
|