@jmruthers/pace-core 0.5.110 → 0.5.112
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-3D3BUZDV.js} +8 -8
- package/dist/{UnifiedAuthProvider-A7I23UCN.js → UnifiedAuthProvider-KZZUO27W.js} +3 -3
- package/dist/{api-PIE4JRFS.js → api-QPMBZZUZ.js} +5 -3
- package/dist/{audit-65VNHEV2.js → audit-H4YJJF7R.js} +2 -2
- package/dist/{chunk-Q7APDV6H.js → chunk-3OGQLOJM.js} +23 -7
- package/dist/chunk-3OGQLOJM.js.map +1 -0
- package/dist/{chunk-EYSXQ756.js → chunk-7H75SHXZ.js} +2 -2
- package/dist/{chunk-D6MEKC27.js → chunk-BUN7NMV7.js} +2 -2
- package/dist/{chunk-AWK2FAUN.js → chunk-C5RN4TE5.js} +7 -7
- package/dist/{chunk-3J5N2T2N.js → chunk-EKVVTPIF.js} +183 -127
- package/dist/chunk-EKVVTPIF.js.map +1 -0
- package/dist/{chunk-2W4WKJVF.js → chunk-F6QB26OS.js} +290 -255
- package/dist/chunk-F6QB26OS.js.map +1 -0
- package/dist/{chunk-HADXAZT3.js → chunk-I7JC7PTJ.js} +54 -92
- package/dist/chunk-I7JC7PTJ.js.map +1 -0
- package/dist/{chunk-EZ64QG2I.js → chunk-L36JW4KV.js} +2 -2
- package/dist/{chunk-7GBEBJLR.js → chunk-MNSGWRPB.js} +45 -37
- package/dist/chunk-MNSGWRPB.js.map +1 -0
- package/dist/{chunk-YFMENCR4.js → chunk-NEONKMTU.js} +3 -3
- package/dist/{chunk-AUXS7XSO.js → chunk-OO3V7W4H.js} +35 -11
- package/dist/chunk-OO3V7W4H.js.map +1 -0
- package/dist/{chunk-XRSP3H52.js → chunk-TAJRS6YB.js} +57 -23
- package/dist/chunk-TAJRS6YB.js.map +1 -0
- package/dist/{chunk-HGZSO43Y.js → chunk-WMPZY26G.js} +8 -4
- package/dist/{chunk-HGZSO43Y.js.map → chunk-WMPZY26G.js.map} +1 -1
- 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/DataTable.test.tsx +405 -154
- package/src/components/DataTable/components/DataTableCore.tsx +6 -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/EventSelector/EventSelector.tsx +32 -2
- package/src/components/FileUpload/FileUpload.tsx +2 -8
- package/src/components/NavigationMenu/NavigationMenu.test.tsx +56 -8
- package/src/components/NavigationMenu/NavigationMenu.tsx +75 -12
- 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-enhanced.ts +14 -2
- package/src/rbac/audit.test.ts +18 -8
- package/src/rbac/audit.ts +25 -6
- 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 +65 -25
- 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 +42 -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-3D3BUZDV.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-A7I23UCN.js.map → UnifiedAuthProvider-KZZUO27W.js.map} +0 -0
- /package/dist/{api-PIE4JRFS.js.map → api-QPMBZZUZ.js.map} +0 -0
- /package/dist/{audit-65VNHEV2.js.map → audit-H4YJJF7R.js.map} +0 -0
- /package/dist/{chunk-EYSXQ756.js.map → chunk-7H75SHXZ.js.map} +0 -0
- /package/dist/{chunk-D6MEKC27.js.map → chunk-BUN7NMV7.js.map} +0 -0
- /package/dist/{chunk-AWK2FAUN.js.map → chunk-C5RN4TE5.js.map} +0 -0
- /package/dist/{chunk-EZ64QG2I.js.map → chunk-L36JW4KV.js.map} +0 -0
- /package/dist/{chunk-YFMENCR4.js.map → chunk-NEONKMTU.js.map} +0 -0
|
@@ -487,11 +487,25 @@ describe('NavigationMenu Component', () => {
|
|
|
487
487
|
mockUsePermissions.mockReturnValue({
|
|
488
488
|
permissions: {
|
|
489
489
|
'dashboard:read': true,
|
|
490
|
+
// Add page permissions for items with hrefs (NavigationMenu checks these)
|
|
491
|
+
'read:page.dashboard': true,
|
|
492
|
+
'read:page.users': true,
|
|
493
|
+
'read:page.settings': true,
|
|
490
494
|
} as any,
|
|
491
495
|
isLoading: false,
|
|
492
496
|
error: null,
|
|
493
|
-
hasPermission: vi.fn((p: any) =>
|
|
494
|
-
|
|
497
|
+
hasPermission: vi.fn((p: any) => {
|
|
498
|
+
return p === 'dashboard:read' ||
|
|
499
|
+
p === 'read:page.dashboard' ||
|
|
500
|
+
p === 'read:page.users' ||
|
|
501
|
+
p === 'read:page.settings';
|
|
502
|
+
}),
|
|
503
|
+
hasAnyPermission: vi.fn((ps: any[]) => ps.some((p: any) => {
|
|
504
|
+
return p === 'dashboard:read' ||
|
|
505
|
+
p === 'read:page.dashboard' ||
|
|
506
|
+
p === 'read:page.users' ||
|
|
507
|
+
p === 'read:page.settings';
|
|
508
|
+
})),
|
|
495
509
|
hasAllPermissions: vi.fn(() => true),
|
|
496
510
|
refetch: vi.fn(),
|
|
497
511
|
});
|
|
@@ -636,11 +650,25 @@ describe('NavigationMenu Component', () => {
|
|
|
636
650
|
mockUsePermissions.mockReturnValue({
|
|
637
651
|
permissions: {
|
|
638
652
|
'dashboard:read': true,
|
|
653
|
+
// Add page permissions for items with hrefs (NavigationMenu checks these)
|
|
654
|
+
'read:page.dashboard': true,
|
|
655
|
+
'read:page.users': true,
|
|
656
|
+
'read:page.settings': true,
|
|
639
657
|
} as any,
|
|
640
658
|
isLoading: false,
|
|
641
659
|
error: null,
|
|
642
|
-
hasPermission: vi.fn((p: any) =>
|
|
643
|
-
|
|
660
|
+
hasPermission: vi.fn((p: any) => {
|
|
661
|
+
return p === 'dashboard:read' ||
|
|
662
|
+
p === 'read:page.dashboard' ||
|
|
663
|
+
p === 'read:page.users' ||
|
|
664
|
+
p === 'read:page.settings';
|
|
665
|
+
}),
|
|
666
|
+
hasAnyPermission: vi.fn((ps: any[]) => ps.some((p: any) => {
|
|
667
|
+
return p === 'dashboard:read' ||
|
|
668
|
+
p === 'read:page.dashboard' ||
|
|
669
|
+
p === 'read:page.users' ||
|
|
670
|
+
p === 'read:page.settings';
|
|
671
|
+
})),
|
|
644
672
|
hasAllPermissions: vi.fn(() => true),
|
|
645
673
|
refetch: vi.fn(),
|
|
646
674
|
});
|
|
@@ -700,11 +728,25 @@ describe('NavigationMenu Component', () => {
|
|
|
700
728
|
mockUsePermissions.mockReturnValue({
|
|
701
729
|
permissions: {
|
|
702
730
|
'dashboard:read': true,
|
|
731
|
+
// Add page permissions for items with hrefs (NavigationMenu checks these)
|
|
732
|
+
'read:page.dashboard': true,
|
|
733
|
+
'read:page.users': true,
|
|
734
|
+
'read:page.settings': true,
|
|
703
735
|
} as any,
|
|
704
736
|
isLoading: false,
|
|
705
737
|
error: null,
|
|
706
|
-
hasPermission: vi.fn((p: any) =>
|
|
707
|
-
|
|
738
|
+
hasPermission: vi.fn((p: any) => {
|
|
739
|
+
return p === 'dashboard:read' ||
|
|
740
|
+
p === 'read:page.dashboard' ||
|
|
741
|
+
p === 'read:page.users' ||
|
|
742
|
+
p === 'read:page.settings';
|
|
743
|
+
}),
|
|
744
|
+
hasAnyPermission: vi.fn((ps: any[]) => ps.some((p: any) => {
|
|
745
|
+
return p === 'dashboard:read' ||
|
|
746
|
+
p === 'read:page.dashboard' ||
|
|
747
|
+
p === 'read:page.users' ||
|
|
748
|
+
p === 'read:page.settings';
|
|
749
|
+
})),
|
|
708
750
|
hasAllPermissions: vi.fn(() => true),
|
|
709
751
|
refetch: vi.fn(),
|
|
710
752
|
});
|
|
@@ -894,11 +936,17 @@ describe('NavigationMenu Component', () => {
|
|
|
894
936
|
mockUsePermissions.mockReturnValue({
|
|
895
937
|
permissions: {
|
|
896
938
|
'valid-permission': true,
|
|
939
|
+
// Add page permission for /test href (NavigationMenu checks this)
|
|
940
|
+
'read:page.test': true,
|
|
897
941
|
} as any,
|
|
898
942
|
isLoading: false,
|
|
899
943
|
error: null,
|
|
900
|
-
hasPermission: vi.fn((p: any) =>
|
|
901
|
-
|
|
944
|
+
hasPermission: vi.fn((p: any) => {
|
|
945
|
+
return p === 'valid-permission' || p === 'read:page.test';
|
|
946
|
+
}),
|
|
947
|
+
hasAnyPermission: vi.fn((ps: any[]) => ps.some((p: any) => {
|
|
948
|
+
return (typeof p === 'string' && p === 'valid-permission') || p === 'read:page.test';
|
|
949
|
+
})),
|
|
902
950
|
hasAllPermissions: vi.fn(() => true),
|
|
903
951
|
refetch: vi.fn(),
|
|
904
952
|
});
|
|
@@ -446,19 +446,71 @@ export const NavigationMenu = React.forwardRef<
|
|
|
446
446
|
selectedEventId: selectedEvent?.event_id || null
|
|
447
447
|
});
|
|
448
448
|
|
|
449
|
+
// Stabilize scope object to prevent unnecessary permission refetches
|
|
450
|
+
// This prevents the permission map from being cleared when scope object reference changes
|
|
451
|
+
const stableScopeRef = React.useRef<{ organisationId: string; eventId?: string; appId?: string }>({
|
|
452
|
+
organisationId: '',
|
|
453
|
+
eventId: undefined,
|
|
454
|
+
appId: undefined
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
// Only update stable scope if values actually changed
|
|
458
|
+
if (resolvedScope?.organisationId) {
|
|
459
|
+
const newOrgId = resolvedScope.organisationId;
|
|
460
|
+
const newEventId = resolvedScope.eventId;
|
|
461
|
+
const newAppId = resolvedScope.appId;
|
|
462
|
+
|
|
463
|
+
if (stableScopeRef.current.organisationId !== newOrgId ||
|
|
464
|
+
stableScopeRef.current.eventId !== newEventId ||
|
|
465
|
+
stableScopeRef.current.appId !== newAppId) {
|
|
466
|
+
stableScopeRef.current = {
|
|
467
|
+
organisationId: newOrgId,
|
|
468
|
+
eventId: newEventId,
|
|
469
|
+
appId: newAppId
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
} else if (!resolvedScope) {
|
|
473
|
+
// Only reset if we had a previous value - don't clear on initial render
|
|
474
|
+
if (stableScopeRef.current.organisationId !== '') {
|
|
475
|
+
stableScopeRef.current = {
|
|
476
|
+
organisationId: '',
|
|
477
|
+
eventId: undefined,
|
|
478
|
+
appId: undefined
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const stableScope = stableScopeRef.current;
|
|
484
|
+
|
|
449
485
|
// Get permissions map for synchronous permission checks
|
|
450
486
|
const userId = authContext?.user?.id || '';
|
|
451
|
-
const
|
|
452
|
-
const { permissions: permissionMap, hasAnyPermission, isLoading: permissionsLoading } = usePermissions(
|
|
487
|
+
const { permissions: permissionMap, hasAnyPermission, isLoading: permissionsLoading, error: permissionsError } = usePermissions(
|
|
453
488
|
userId as any,
|
|
454
|
-
|
|
489
|
+
stableScope as any
|
|
455
490
|
);
|
|
456
491
|
|
|
457
492
|
// NEW: Phase 2 - Enhanced Security Features
|
|
458
493
|
// Filter navigation items based on permissions using RBAC hooks
|
|
459
494
|
const filteredItems = React.useMemo(() => {
|
|
460
|
-
// If filtering
|
|
461
|
-
|
|
495
|
+
// Security: If filtering is enabled but we're missing required context or still loading, show NO items
|
|
496
|
+
// This prevents security risk of showing items before permissions are verified
|
|
497
|
+
if (filterByPermissions) {
|
|
498
|
+
if (!authContext || !rbacContext || scopeLoading || permissionsLoading || !resolvedScope?.organisationId) {
|
|
499
|
+
// Still loading - show nothing to prevent security risk
|
|
500
|
+
return [];
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// If there's an error or empty permission map after loading, show nothing
|
|
504
|
+
// Security: Better to show nothing than risk showing unauthorized items
|
|
505
|
+
if (permissionsError || !permissionMap || Object.keys(permissionMap).length === 0) {
|
|
506
|
+
console.warn('[NavigationMenu] Permission map is empty or has error - showing no items for security', {
|
|
507
|
+
permissionsError: permissionsError?.message,
|
|
508
|
+
permissionMapSize: permissionMap ? Object.keys(permissionMap).length : 0
|
|
509
|
+
});
|
|
510
|
+
return [];
|
|
511
|
+
}
|
|
512
|
+
} else {
|
|
513
|
+
// Filtering disabled - only filter out hidden items
|
|
462
514
|
return (items || []).filter(item => !item.meta?.hidden);
|
|
463
515
|
}
|
|
464
516
|
|
|
@@ -553,25 +605,32 @@ export const NavigationMenu = React.forwardRef<
|
|
|
553
605
|
}
|
|
554
606
|
}
|
|
555
607
|
|
|
556
|
-
// NEW: Auto-check page permissions for items with href
|
|
557
|
-
//
|
|
558
|
-
if
|
|
608
|
+
// NEW: Auto-check page permissions for items with href
|
|
609
|
+
// Always check page permissions for items with href, even if they have explicit permissions/roles/accessLevel
|
|
610
|
+
// This ensures that items are filtered out if the user doesn't have access to the page itself
|
|
611
|
+
if (item.href) {
|
|
559
612
|
const pageId = item.pageId || getPageIdFromHref(item.href);
|
|
560
613
|
if (pageId) {
|
|
561
614
|
// Check for read permission on the page
|
|
562
615
|
const pagePermission: Permission = `read:page.${pageId}` as Permission;
|
|
563
616
|
|
|
564
617
|
// Check permission map (super admin has access to everything via '*' key)
|
|
565
|
-
|
|
618
|
+
// Only allow if permission is explicitly true (undefined/false means no access)
|
|
619
|
+
const isSuperAdmin = permissionMap['*'] === true;
|
|
620
|
+
const hasPagePermission = permissionMap[pagePermission] === true;
|
|
621
|
+
const finalHasPermission = isSuperAdmin || hasPagePermission;
|
|
566
622
|
|
|
567
|
-
if (!
|
|
623
|
+
if (!finalHasPermission) {
|
|
568
624
|
if (auditLog) {
|
|
569
625
|
console.log(`[NavigationMenu] Filtering out navigation item "${item.label}" - no page permission:`, {
|
|
570
626
|
itemId: item.id,
|
|
571
627
|
href: item.href,
|
|
572
628
|
pageId,
|
|
573
629
|
permission: pagePermission,
|
|
574
|
-
hasPermission:
|
|
630
|
+
hasPermission: finalHasPermission,
|
|
631
|
+
isSuperAdmin,
|
|
632
|
+
permissionMapValue: permissionMap[pagePermission],
|
|
633
|
+
permissionMapKeys: Object.keys(permissionMap).slice(0, 10) // Show first 10 keys for debugging
|
|
575
634
|
});
|
|
576
635
|
}
|
|
577
636
|
return false;
|
|
@@ -610,9 +669,13 @@ export const NavigationMenu = React.forwardRef<
|
|
|
610
669
|
};
|
|
611
670
|
};
|
|
612
671
|
|
|
613
|
-
|
|
672
|
+
// Filter items based on permissions - only show items with explicit permission
|
|
673
|
+
// Security: No fallback - if items don't have permission, they are hidden
|
|
674
|
+
const filtered = (items || [])
|
|
614
675
|
.map(item => filterItem(item))
|
|
615
676
|
.filter((item): item is NavigationItem => item !== null);
|
|
677
|
+
|
|
678
|
+
return filtered;
|
|
616
679
|
}, [
|
|
617
680
|
items,
|
|
618
681
|
filterByPermissions,
|
|
@@ -54,13 +54,91 @@ vi.mock('../../providers/UnifiedAuthProvider', () => ({
|
|
|
54
54
|
useUnifiedAuth: vi.fn(() => mockUnifiedAuth),
|
|
55
55
|
}));
|
|
56
56
|
|
|
57
|
+
// Mock useOrganisations hook
|
|
58
|
+
const mockSelectedOrganisation = {
|
|
59
|
+
id: 'org-123',
|
|
60
|
+
name: 'Test Organisation',
|
|
61
|
+
display_name: 'Test Organisation',
|
|
62
|
+
slug: 'test-org',
|
|
63
|
+
description: 'Test organisation',
|
|
64
|
+
subscription_tier: 'basic',
|
|
65
|
+
settings: {},
|
|
66
|
+
is_active: true,
|
|
67
|
+
created_at: '2023-01-01T00:00:00Z',
|
|
68
|
+
updated_at: '2023-01-01T00:00:00Z',
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
vi.mock('../../hooks/useOrganisations', () => ({
|
|
72
|
+
useOrganisations: vi.fn(() => ({
|
|
73
|
+
selectedOrganisation: mockSelectedOrganisation,
|
|
74
|
+
organisations: [mockSelectedOrganisation],
|
|
75
|
+
userMemberships: [],
|
|
76
|
+
isLoading: false,
|
|
77
|
+
error: null,
|
|
78
|
+
hasValidOrganisationContext: true,
|
|
79
|
+
setSelectedOrganisation: vi.fn(),
|
|
80
|
+
switchOrganisation: vi.fn().mockResolvedValue(undefined),
|
|
81
|
+
getUserRole: vi.fn().mockReturnValue('member'),
|
|
82
|
+
validateOrganisationAccess: vi.fn().mockReturnValue(true),
|
|
83
|
+
ensureOrganisationContext: vi.fn().mockReturnValue(mockSelectedOrganisation),
|
|
84
|
+
refreshOrganisations: vi.fn().mockResolvedValue(undefined),
|
|
85
|
+
getPrimaryOrganisation: vi.fn().mockReturnValue(mockSelectedOrganisation),
|
|
86
|
+
isOrganisationSecure: vi.fn().mockReturnValue(true),
|
|
87
|
+
})),
|
|
88
|
+
}));
|
|
89
|
+
|
|
90
|
+
// Mock useEvents hook (optional - wrapped in try/catch in component)
|
|
91
|
+
vi.mock('../../providers/EventsProvider', () => ({
|
|
92
|
+
useEvents: vi.fn(() => ({
|
|
93
|
+
selectedEvent: { event_id: 'event-123' },
|
|
94
|
+
events: [],
|
|
95
|
+
isLoading: false,
|
|
96
|
+
error: null,
|
|
97
|
+
})),
|
|
98
|
+
}));
|
|
99
|
+
|
|
57
100
|
// Mock RBAC functions
|
|
101
|
+
const mockIsPermitted = vi.fn().mockResolvedValue(true);
|
|
102
|
+
const mockIsPermittedCached = vi.fn().mockResolvedValue(true);
|
|
103
|
+
|
|
58
104
|
vi.mock('../../rbac/api', () => ({
|
|
59
|
-
isPermitted: vi.fn()
|
|
105
|
+
isPermitted: vi.fn(),
|
|
106
|
+
isPermittedCached: vi.fn(),
|
|
60
107
|
isSuperAdmin: vi.fn().mockResolvedValue(false),
|
|
61
108
|
setupRBAC: vi.fn(),
|
|
62
109
|
}));
|
|
63
110
|
|
|
111
|
+
// Mock useCan hook - this is what PaceAppLayout actually uses
|
|
112
|
+
const mockUseCan = vi.fn(() => ({
|
|
113
|
+
can: true,
|
|
114
|
+
isLoading: false,
|
|
115
|
+
error: null,
|
|
116
|
+
refetch: vi.fn().mockResolvedValue(undefined),
|
|
117
|
+
}));
|
|
118
|
+
|
|
119
|
+
// Mock RBAC hooks
|
|
120
|
+
const mockHasPermissionFn = vi.fn().mockResolvedValue(true);
|
|
121
|
+
vi.mock('../../rbac/hooks', () => ({
|
|
122
|
+
useRBAC: vi.fn(() => ({
|
|
123
|
+
hasPermission: mockHasPermissionFn,
|
|
124
|
+
isLoading: false,
|
|
125
|
+
error: null,
|
|
126
|
+
hasGlobalPermission: vi.fn().mockResolvedValue(true),
|
|
127
|
+
hasOrganisationPermission: vi.fn().mockResolvedValue(true),
|
|
128
|
+
hasEventPermission: vi.fn().mockResolvedValue(true),
|
|
129
|
+
globalRole: null,
|
|
130
|
+
organisationRoles: [],
|
|
131
|
+
eventRoles: [],
|
|
132
|
+
permissionMap: {},
|
|
133
|
+
})),
|
|
134
|
+
useCan: (...args: any[]) => mockUseCan(...args),
|
|
135
|
+
useResolvedScope: vi.fn(() => ({
|
|
136
|
+
resolvedScope: { organisationId: 'org-123', eventId: 'event-123', appId: 'app-123' },
|
|
137
|
+
isLoading: false,
|
|
138
|
+
error: null,
|
|
139
|
+
})),
|
|
140
|
+
}));
|
|
141
|
+
|
|
64
142
|
// Mock Header component
|
|
65
143
|
vi.mock('../Header', () => ({
|
|
66
144
|
Header: ({
|
|
@@ -151,10 +229,21 @@ describe('PaceAppLayout Component', () => {
|
|
|
151
229
|
vi.clearAllMocks();
|
|
152
230
|
// Reset location mock
|
|
153
231
|
mockLocation.pathname = '/dashboard';
|
|
154
|
-
// Reset RBAC mocks
|
|
155
|
-
|
|
232
|
+
// Reset RBAC hook mocks
|
|
233
|
+
mockHasPermissionFn.mockClear();
|
|
234
|
+
mockHasPermissionFn.mockResolvedValue(true);
|
|
235
|
+
mockUseCan.mockReturnValue({
|
|
236
|
+
can: true,
|
|
237
|
+
isLoading: false,
|
|
238
|
+
error: null,
|
|
239
|
+
refetch: vi.fn().mockResolvedValue(undefined),
|
|
240
|
+
});
|
|
241
|
+
// Reset RBAC API mocks
|
|
242
|
+
const { isPermitted, isPermittedCached, isSuperAdmin } = await import('../../rbac/api');
|
|
156
243
|
vi.mocked(isPermitted).mockReset();
|
|
157
244
|
vi.mocked(isPermitted).mockResolvedValue(true);
|
|
245
|
+
vi.mocked(isPermittedCached).mockReset();
|
|
246
|
+
vi.mocked(isPermittedCached).mockResolvedValue(true);
|
|
158
247
|
vi.mocked(isSuperAdmin).mockReset();
|
|
159
248
|
vi.mocked(isSuperAdmin).mockResolvedValue(false);
|
|
160
249
|
});
|
|
@@ -360,8 +449,13 @@ describe('PaceAppLayout Component', () => {
|
|
|
360
449
|
});
|
|
361
450
|
|
|
362
451
|
it('shows loading state when checking permissions', async () => {
|
|
363
|
-
|
|
364
|
-
|
|
452
|
+
// Mock useCan to return loading state
|
|
453
|
+
mockUseCan.mockReturnValueOnce({
|
|
454
|
+
can: false,
|
|
455
|
+
isLoading: true,
|
|
456
|
+
error: null,
|
|
457
|
+
refetch: vi.fn().mockResolvedValue(undefined),
|
|
458
|
+
});
|
|
365
459
|
|
|
366
460
|
renderWithProviders(
|
|
367
461
|
<TestWrapper>
|
|
@@ -374,8 +468,13 @@ describe('PaceAppLayout Component', () => {
|
|
|
374
468
|
});
|
|
375
469
|
|
|
376
470
|
it('shows permission error when check fails', async () => {
|
|
377
|
-
|
|
378
|
-
|
|
471
|
+
// Mock useCan to return an error state
|
|
472
|
+
mockUseCan.mockReturnValueOnce({
|
|
473
|
+
can: false,
|
|
474
|
+
isLoading: false,
|
|
475
|
+
error: new Error('Permission check failed'),
|
|
476
|
+
refetch: vi.fn().mockResolvedValue(undefined),
|
|
477
|
+
});
|
|
379
478
|
|
|
380
479
|
renderWithProviders(
|
|
381
480
|
<TestWrapper>
|
|
@@ -390,8 +489,13 @@ describe('PaceAppLayout Component', () => {
|
|
|
390
489
|
});
|
|
391
490
|
|
|
392
491
|
it('shows access denied when user lacks permission', async () => {
|
|
393
|
-
|
|
394
|
-
|
|
492
|
+
// Mock useCan to return false (no permission)
|
|
493
|
+
mockUseCan.mockReturnValueOnce({
|
|
494
|
+
can: false,
|
|
495
|
+
isLoading: false,
|
|
496
|
+
error: null,
|
|
497
|
+
refetch: vi.fn().mockResolvedValue(undefined),
|
|
498
|
+
});
|
|
395
499
|
|
|
396
500
|
renderWithProviders(
|
|
397
501
|
<TestWrapper>
|
|
@@ -406,11 +510,16 @@ describe('PaceAppLayout Component', () => {
|
|
|
406
510
|
});
|
|
407
511
|
|
|
408
512
|
it('shows custom permission fallback when provided', async () => {
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
513
|
+
// Arrange
|
|
514
|
+
mockUseCan.mockReturnValueOnce({
|
|
515
|
+
can: false,
|
|
516
|
+
isLoading: false,
|
|
517
|
+
error: null,
|
|
518
|
+
refetch: vi.fn().mockResolvedValue(undefined),
|
|
519
|
+
});
|
|
412
520
|
const customFallback = <div data-testid="custom-fallback">Custom Access Denied</div>;
|
|
413
521
|
|
|
522
|
+
// Act
|
|
414
523
|
renderWithProviders(
|
|
415
524
|
<TestWrapper>
|
|
416
525
|
<PaceAppLayout
|
|
@@ -421,17 +530,23 @@ describe('PaceAppLayout Component', () => {
|
|
|
421
530
|
</TestWrapper>
|
|
422
531
|
);
|
|
423
532
|
|
|
533
|
+
// Assert - No need to wait for loading since mock returns immediately
|
|
424
534
|
await waitFor(() => {
|
|
425
535
|
expect(screen.getByTestId('custom-fallback')).toBeInTheDocument();
|
|
426
|
-
}
|
|
536
|
+
});
|
|
427
537
|
});
|
|
428
538
|
|
|
429
539
|
it('shows page permission fallback when enforcePagePermissions is true', async () => {
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
540
|
+
// Arrange
|
|
541
|
+
mockUseCan.mockReturnValueOnce({
|
|
542
|
+
can: false,
|
|
543
|
+
isLoading: false,
|
|
544
|
+
error: null,
|
|
545
|
+
refetch: vi.fn().mockResolvedValue(undefined),
|
|
546
|
+
});
|
|
433
547
|
const pageFallback = <div data-testid="page-fallback">Page Access Denied</div>;
|
|
434
548
|
|
|
549
|
+
// Act
|
|
435
550
|
renderWithProviders(
|
|
436
551
|
<TestWrapper>
|
|
437
552
|
<PaceAppLayout
|
|
@@ -443,9 +558,10 @@ describe('PaceAppLayout Component', () => {
|
|
|
443
558
|
</TestWrapper>
|
|
444
559
|
);
|
|
445
560
|
|
|
561
|
+
// Assert - No need to wait for loading since mock returns immediately
|
|
446
562
|
await waitFor(() => {
|
|
447
563
|
expect(screen.getByTestId('page-fallback')).toBeInTheDocument();
|
|
448
|
-
}
|
|
564
|
+
});
|
|
449
565
|
});
|
|
450
566
|
});
|
|
451
567
|
|
|
@@ -514,9 +630,6 @@ describe('PaceAppLayout Component', () => {
|
|
|
514
630
|
|
|
515
631
|
describe('Route-Specific Permissions', () => {
|
|
516
632
|
it('uses route-specific permissions when provided', async () => {
|
|
517
|
-
const { isPermitted } = await import('../../rbac/api');
|
|
518
|
-
vi.mocked(isPermitted).mockResolvedValue(true);
|
|
519
|
-
|
|
520
633
|
renderWithProviders(
|
|
521
634
|
<TestWrapper>
|
|
522
635
|
<PaceAppLayout
|
|
@@ -533,23 +646,18 @@ describe('PaceAppLayout Component', () => {
|
|
|
533
646
|
);
|
|
534
647
|
|
|
535
648
|
await waitFor(() => {
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
}, { timeout: 3000 });
|
|
547
|
-
});
|
|
649
|
+
// useCan is called with userId, scope, permission, pageId, useCache
|
|
650
|
+
expect(mockUseCan).toHaveBeenCalledWith(
|
|
651
|
+
'user-123',
|
|
652
|
+
expect.objectContaining({ organisationId: 'org-123' }),
|
|
653
|
+
'update:page.dashboard-page',
|
|
654
|
+
'dashboard-page',
|
|
655
|
+
true
|
|
656
|
+
);
|
|
657
|
+
}, { timeout: 5000 });
|
|
658
|
+
}, { timeout: 6000 });
|
|
548
659
|
|
|
549
660
|
it('uses default permission when route not in routePermissions', async () => {
|
|
550
|
-
const { isPermitted } = await import('../../rbac/api');
|
|
551
|
-
vi.mocked(isPermitted).mockResolvedValue(true);
|
|
552
|
-
|
|
553
661
|
renderWithProviders(
|
|
554
662
|
<TestWrapper>
|
|
555
663
|
<PaceAppLayout
|
|
@@ -564,18 +672,17 @@ describe('PaceAppLayout Component', () => {
|
|
|
564
672
|
);
|
|
565
673
|
|
|
566
674
|
await waitFor(() => {
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
});
|
|
675
|
+
// useCan is called with userId, scope, permission, pageId, useCache
|
|
676
|
+
// Uses defaultPermission "create" since /dashboard is not in routePermissions
|
|
677
|
+
expect(mockUseCan).toHaveBeenCalledWith(
|
|
678
|
+
'user-123',
|
|
679
|
+
expect.objectContaining({ organisationId: 'org-123' }),
|
|
680
|
+
'create:page.dashboard',
|
|
681
|
+
'dashboard',
|
|
682
|
+
true
|
|
683
|
+
);
|
|
684
|
+
}, { timeout: 5000 });
|
|
685
|
+
}, { timeout: 6000 });
|
|
579
686
|
});
|
|
580
687
|
|
|
581
688
|
describe('Super Admin Bypass', () => {
|
|
@@ -601,11 +708,16 @@ describe('PaceAppLayout Component', () => {
|
|
|
601
708
|
|
|
602
709
|
describe('Callbacks and Event Handling', () => {
|
|
603
710
|
it('calls onPageAccessDenied when access is denied', async () => {
|
|
604
|
-
|
|
605
|
-
vi.mocked(isPermitted).mockResolvedValue(false);
|
|
606
|
-
|
|
711
|
+
// Arrange
|
|
607
712
|
const onPageAccessDenied = vi.fn();
|
|
713
|
+
mockUseCan.mockReturnValueOnce({
|
|
714
|
+
can: false,
|
|
715
|
+
isLoading: false,
|
|
716
|
+
error: null,
|
|
717
|
+
refetch: vi.fn().mockResolvedValue(undefined),
|
|
718
|
+
});
|
|
608
719
|
|
|
720
|
+
// Act
|
|
609
721
|
renderWithProviders(
|
|
610
722
|
<TestWrapper>
|
|
611
723
|
<PaceAppLayout
|
|
@@ -616,17 +728,23 @@ describe('PaceAppLayout Component', () => {
|
|
|
616
728
|
</TestWrapper>
|
|
617
729
|
);
|
|
618
730
|
|
|
731
|
+
// Assert - Callback should be called immediately when can is false
|
|
619
732
|
await waitFor(() => {
|
|
620
733
|
expect(onPageAccessDenied).toHaveBeenCalledWith('dashboard', 'read');
|
|
621
|
-
}
|
|
734
|
+
});
|
|
622
735
|
});
|
|
623
736
|
|
|
624
737
|
it('calls onStrictModeViolation when strict mode is violated', async () => {
|
|
625
|
-
|
|
626
|
-
vi.mocked(isPermitted).mockResolvedValue(false);
|
|
627
|
-
|
|
738
|
+
// Arrange
|
|
628
739
|
const onStrictModeViolation = vi.fn();
|
|
740
|
+
mockUseCan.mockReturnValueOnce({
|
|
741
|
+
can: false,
|
|
742
|
+
isLoading: false,
|
|
743
|
+
error: null,
|
|
744
|
+
refetch: vi.fn().mockResolvedValue(undefined),
|
|
745
|
+
});
|
|
629
746
|
|
|
747
|
+
// Act
|
|
630
748
|
renderWithProviders(
|
|
631
749
|
<TestWrapper>
|
|
632
750
|
<PaceAppLayout
|
|
@@ -638,9 +756,10 @@ describe('PaceAppLayout Component', () => {
|
|
|
638
756
|
</TestWrapper>
|
|
639
757
|
);
|
|
640
758
|
|
|
759
|
+
// Assert - Callback should be called immediately when can is false
|
|
641
760
|
await waitFor(() => {
|
|
642
761
|
expect(onStrictModeViolation).toHaveBeenCalledWith('dashboard', 'read');
|
|
643
|
-
}
|
|
762
|
+
});
|
|
644
763
|
});
|
|
645
764
|
});
|
|
646
765
|
|
|
@@ -714,8 +833,14 @@ describe('PaceAppLayout Component', () => {
|
|
|
714
833
|
// Mock the useUnifiedAuth hook to return null user
|
|
715
834
|
vi.mocked(useUnifiedAuth).mockReturnValue(mockAuthWithoutUser);
|
|
716
835
|
|
|
717
|
-
|
|
718
|
-
|
|
836
|
+
// When there's no user, useCan is called with empty string userId
|
|
837
|
+
// and will return false (no permission)
|
|
838
|
+
mockUseCan.mockReturnValueOnce({
|
|
839
|
+
can: false,
|
|
840
|
+
isLoading: false,
|
|
841
|
+
error: null,
|
|
842
|
+
refetch: vi.fn().mockResolvedValue(undefined),
|
|
843
|
+
});
|
|
719
844
|
|
|
720
845
|
renderWithProviders(
|
|
721
846
|
<TestWrapper>
|
|
@@ -732,6 +857,7 @@ describe('PaceAppLayout Component', () => {
|
|
|
732
857
|
});
|
|
733
858
|
|
|
734
859
|
it('handles missing organisation context', async () => {
|
|
860
|
+
// Arrange
|
|
735
861
|
const mockUserWithoutOrg = {
|
|
736
862
|
...mockUser,
|
|
737
863
|
user_metadata: {},
|
|
@@ -743,12 +869,15 @@ describe('PaceAppLayout Component', () => {
|
|
|
743
869
|
user: mockUserWithoutOrg,
|
|
744
870
|
};
|
|
745
871
|
|
|
746
|
-
// Mock the useUnifiedAuth hook to return user without org context
|
|
747
872
|
vi.mocked(useUnifiedAuth).mockReturnValue(mockAuthWithoutOrg);
|
|
873
|
+
mockUseCan.mockReturnValueOnce({
|
|
874
|
+
can: false,
|
|
875
|
+
isLoading: false,
|
|
876
|
+
error: null,
|
|
877
|
+
refetch: vi.fn().mockResolvedValue(undefined),
|
|
878
|
+
});
|
|
748
879
|
|
|
749
|
-
|
|
750
|
-
vi.mocked(isPermitted).mockResolvedValue(false);
|
|
751
|
-
|
|
880
|
+
// Act
|
|
752
881
|
renderWithProviders(
|
|
753
882
|
<TestWrapper>
|
|
754
883
|
<PaceAppLayout
|
|
@@ -758,9 +887,10 @@ describe('PaceAppLayout Component', () => {
|
|
|
758
887
|
</TestWrapper>
|
|
759
888
|
);
|
|
760
889
|
|
|
890
|
+
// Assert - Component should show access denied
|
|
761
891
|
await waitFor(() => {
|
|
762
|
-
expect(screen.
|
|
763
|
-
}
|
|
892
|
+
expect(screen.getByRole('heading', { name: 'Access Denied' })).toBeInTheDocument();
|
|
893
|
+
});
|
|
764
894
|
});
|
|
765
895
|
});
|
|
766
896
|
|