@jmruthers/pace-core 0.5.109 → 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/CHANGELOG.md +22 -0
- package/dist/{AuthService-DrHrvXNZ.d.ts → AuthService-CVgsgtaZ.d.ts} +8 -0
- package/dist/{DataTable-5HITILXS.js → DataTable-5W2HVLLV.js} +8 -8
- package/dist/{UnifiedAuthProvider-A7I23UCN.js → UnifiedAuthProvider-LUM3QLS5.js} +3 -3
- package/dist/{api-5I3E47G2.js → api-SIZPFBFX.js} +5 -3
- package/dist/{audit-65VNHEV2.js → audit-5JI5T3SL.js} +2 -2
- package/dist/{chunk-P72NKAT5.js → chunk-2BIDKXQU.js} +157 -120
- package/dist/chunk-2BIDKXQU.js.map +1 -0
- package/dist/{chunk-S4D3Z723.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-F6TSYCKP.js → chunk-PXXS26G5.js} +68 -29
- package/dist/chunk-PXXS26G5.js.map +1 -0
- package/dist/{chunk-UW2DE6JX.js → chunk-TD4BXGPE.js} +4 -4
- package/dist/{chunk-EYSXQ756.js → chunk-TDFBX7KJ.js} +2 -2
- package/dist/{chunk-WWNOVFDC.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-3TKTL5AZ.js → chunk-ZL45MG76.js} +60 -60
- 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 +46 -29
- 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 +9 -8
- 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 +19 -8
- package/docs/api/interfaces/RBACLogger.md +5 -5
- 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 +44 -43
- 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/documentation-index.md +0 -2
- package/docs/migration/rbac-migration.md +65 -66
- package/docs/rbac/README.md +114 -38
- package/docs/rbac/advanced-patterns.md +15 -22
- package/docs/rbac/api-reference.md +63 -16
- package/docs/rbac/examples.md +12 -12
- package/docs/rbac/getting-started.md +19 -19
- package/docs/rbac/quick-start.md +110 -35
- package/docs/rbac/troubleshooting.md +127 -3
- 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/NavigationMenu/NavigationMenu.test.tsx +38 -4
- package/src/components/NavigationMenu/NavigationMenu.tsx +71 -6
- 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 +39 -15
- package/src/rbac/api.ts +27 -9
- 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 +22 -38
- 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 +2 -2
- package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +121 -103
- package/src/rbac/config.ts +2 -0
- package/src/rbac/docs/event-based-apps.md +6 -6
- package/src/rbac/engine.ts +27 -7
- 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 +23 -8
- 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-3TKTL5AZ.js.map +0 -1
- package/dist/chunk-AUXS7XSO.js.map +0 -1
- package/dist/chunk-F6TSYCKP.js.map +0 -1
- package/dist/chunk-P72NKAT5.js.map +0 -1
- package/dist/chunk-Q7APDV6H.js.map +0 -1
- package/dist/chunk-WWNOVFDC.js.map +0 -1
- package/docs/rbac/breaking-changes-v3.md +0 -222
- package/docs/rbac/migration-guide.md +0 -260
- /package/dist/{DataTable-5HITILXS.js.map → DataTable-5W2HVLLV.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-A7I23UCN.js.map → UnifiedAuthProvider-LUM3QLS5.js.map} +0 -0
- /package/dist/{api-5I3E47G2.js.map → api-SIZPFBFX.js.map} +0 -0
- /package/dist/{audit-65VNHEV2.js.map → audit-5JI5T3SL.js.map} +0 -0
- /package/dist/{chunk-S4D3Z723.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-UW2DE6JX.js.map → chunk-TD4BXGPE.js.map} +0 -0
- /package/dist/{chunk-EYSXQ756.js.map → chunk-TDFBX7KJ.js.map} +0 -0
|
@@ -100,9 +100,12 @@
|
|
|
100
100
|
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
|
101
101
|
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
|
|
102
102
|
import { useUnifiedAuth } from '../../providers/UnifiedAuthProvider';
|
|
103
|
-
|
|
104
|
-
import {
|
|
105
|
-
import
|
|
103
|
+
import { useOrganisations } from '../../hooks/useOrganisations';
|
|
104
|
+
import { useEvents } from '../../hooks/useEvents';
|
|
105
|
+
import { useCan, useResolvedScope } from '../../rbac/hooks';
|
|
106
|
+
import { createScopeFromEvent } from '../../rbac/utils/eventContext';
|
|
107
|
+
import { getCurrentAppName } from '../../utils/appNameResolver';
|
|
108
|
+
import type { Permission, Scope } from '../../rbac/types';
|
|
106
109
|
|
|
107
110
|
// Stable empty objects to prevent infinite loops
|
|
108
111
|
const EMPTY_PAGE_ID_MAPPING = {};
|
|
@@ -351,73 +354,38 @@ export function PaceAppLayout({
|
|
|
351
354
|
onRouteAccessDenied,
|
|
352
355
|
onRouteStrictModeViolation
|
|
353
356
|
}: PaceAppLayoutProps) {
|
|
354
|
-
const { user, signOut, updatePassword } = useUnifiedAuth();
|
|
357
|
+
const { user, signOut, updatePassword, supabase } = useUnifiedAuth();
|
|
358
|
+
const { selectedOrganisation } = useOrganisations();
|
|
355
359
|
const navigate = useNavigate();
|
|
356
360
|
const location = useLocation();
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
361
|
+
|
|
362
|
+
// Get selected event (optional)
|
|
363
|
+
let selectedEvent: { event_id: string } | null = null;
|
|
364
|
+
try {
|
|
365
|
+
const eventsContext = useEvents();
|
|
366
|
+
selectedEvent = eventsContext.selectedEvent;
|
|
367
|
+
} catch (error) {
|
|
368
|
+
// Event provider not available - continue without event context
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Resolve scope for permission checking
|
|
372
|
+
const { resolvedScope } = useResolvedScope({
|
|
373
|
+
supabase: supabase || null,
|
|
374
|
+
selectedOrganisationId: selectedOrganisation?.id || null,
|
|
375
|
+
selectedEventId: selectedEvent?.event_id || null
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
// Build scope from resolved values
|
|
379
|
+
const scope = useMemo<Scope>(() => {
|
|
380
|
+
if (!resolvedScope?.organisationId) {
|
|
381
|
+
return {
|
|
382
|
+
organisationId: selectedOrganisation?.id || '',
|
|
383
|
+
eventId: selectedEvent?.event_id || undefined,
|
|
384
|
+
appId: undefined
|
|
368
385
|
};
|
|
369
|
-
|
|
370
|
-
// Check if user is super admin first - super admins can access everything
|
|
371
|
-
// regardless of organisation context
|
|
372
|
-
// Gracefully handle RBAC not being initialized (e.g., in tests)
|
|
373
|
-
try {
|
|
374
|
-
const { isSuperAdmin } = await import('../../rbac/api');
|
|
375
|
-
const isSuper = await isSuperAdmin(user.id);
|
|
376
|
-
|
|
377
|
-
if (isSuper) {
|
|
378
|
-
// Super admin bypass - allow access regardless of organisation context
|
|
379
|
-
return true;
|
|
380
|
-
}
|
|
381
|
-
} catch (error) {
|
|
382
|
-
// If RBAC is not initialized (e.g., in tests), continue with normal permission check
|
|
383
|
-
// This prevents errors from breaking permission checks when RBAC isn't available
|
|
384
|
-
if (error && typeof error === 'object' && 'code' in error && error.code === 'RBAC_NOT_INITIALIZED') {
|
|
385
|
-
// RBAC not available - proceed with normal permission check
|
|
386
|
-
} else {
|
|
387
|
-
// Re-throw unexpected errors
|
|
388
|
-
throw error;
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// For non-super admins, ensure we have at least organisationId for RBAC
|
|
393
|
-
if (!scope.organisationId) {
|
|
394
|
-
console.warn('No organisation context available for permission check, denying access');
|
|
395
|
-
return false;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
// Construct the full permission string in format "operation:page.pageId"
|
|
399
|
-
// If permission already includes ':', use it as-is, otherwise format as "operation:page.pageId"
|
|
400
|
-
const fullPermission: Permission = permission.includes(':')
|
|
401
|
-
? (permission as Permission)
|
|
402
|
-
: (pageId ? `${permission}:page.${pageId}` : permission) as Permission;
|
|
403
|
-
|
|
404
|
-
return await isPermitted({
|
|
405
|
-
userId: user.id,
|
|
406
|
-
scope,
|
|
407
|
-
permission: fullPermission,
|
|
408
|
-
pageId
|
|
409
|
-
});
|
|
410
|
-
} catch (error) {
|
|
411
|
-
console.error('Permission check failed:', error);
|
|
412
|
-
// Let the error bubble up so it can be handled by the calling code
|
|
413
|
-
throw error;
|
|
414
386
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
// Permission enforcement state
|
|
418
|
-
const [hasPermission, setHasPermission] = useState<boolean | null>(null);
|
|
419
|
-
const [isCheckingPermission, setIsCheckingPermission] = useState(false);
|
|
420
|
-
const [permissionError, setPermissionError] = useState<Error | null>(null);
|
|
387
|
+
return resolvedScope;
|
|
388
|
+
}, [resolvedScope, selectedOrganisation?.id, selectedEvent?.event_id]);
|
|
421
389
|
|
|
422
390
|
// Default navigation items if none provided
|
|
423
391
|
const defaultNavItems: NavigationItem[] = useMemo(() => [
|
|
@@ -443,79 +411,70 @@ export function PaceAppLayout({
|
|
|
443
411
|
return pageIdMapping[currentPath] || currentPath.slice(1) || 'home';
|
|
444
412
|
}, [location.pathname, pageIdMapping]);
|
|
445
413
|
|
|
446
|
-
//
|
|
414
|
+
// Build permission string in format: operation:page.pageId
|
|
415
|
+
const currentPermission = useMemo<Permission>(() => {
|
|
416
|
+
if (!enforcePermissions) {
|
|
417
|
+
return 'read:page.home' as Permission;
|
|
418
|
+
}
|
|
419
|
+
const permissionString = `${currentRoutePermission}:page.${currentPageId}`;
|
|
420
|
+
return permissionString as Permission;
|
|
421
|
+
}, [enforcePermissions, currentRoutePermission, currentPageId]);
|
|
422
|
+
|
|
423
|
+
// Use useCan hook for permission checking (standardized approach)
|
|
424
|
+
const { can, isLoading: isCheckingPermission, error: permissionError } = useCan(
|
|
425
|
+
user?.id || '',
|
|
426
|
+
scope,
|
|
427
|
+
currentPermission,
|
|
428
|
+
currentPageId,
|
|
429
|
+
true // useCache
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
// Permission enforcement state - sync from useCan
|
|
433
|
+
const hasPermission = enforcePermissions ? can : true;
|
|
434
|
+
|
|
435
|
+
// Handle permission check results with audit logging and callbacks
|
|
447
436
|
useEffect(() => {
|
|
448
437
|
if (!enforcePermissions) {
|
|
449
|
-
setHasPermission(true);
|
|
450
438
|
return;
|
|
451
439
|
}
|
|
452
440
|
|
|
453
|
-
|
|
441
|
+
// Only proceed when permission check is complete (not loading)
|
|
442
|
+
if (isCheckingPermission) {
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
454
445
|
|
|
455
|
-
|
|
456
|
-
|
|
446
|
+
// NEW: Phase 1 - Enhanced Security Features
|
|
447
|
+
// Log page access attempt for audit
|
|
448
|
+
if (auditLog) {
|
|
449
|
+
console.log(`[PaceAppLayout] Page access attempt:`, {
|
|
450
|
+
pageName: currentPageId,
|
|
451
|
+
operation: currentRoutePermission,
|
|
452
|
+
userId: user?.id,
|
|
453
|
+
allowed: can,
|
|
454
|
+
strictMode,
|
|
455
|
+
timestamp: new Date().toISOString()
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Handle strict mode violations
|
|
460
|
+
if (strictMode && !can) {
|
|
461
|
+
console.error(`[PaceAppLayout] STRICT MODE VIOLATION: User attempted to access protected page without permission`, {
|
|
462
|
+
pageName: currentPageId,
|
|
463
|
+
operation: currentRoutePermission,
|
|
464
|
+
userId: user?.id,
|
|
465
|
+
timestamp: new Date().toISOString()
|
|
466
|
+
});
|
|
457
467
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
try {
|
|
462
|
-
const hasAccess = await checkPermission(currentRoutePermission, currentPageId);
|
|
463
|
-
|
|
464
|
-
if (!isMounted) return;
|
|
465
|
-
|
|
466
|
-
setHasPermission(hasAccess);
|
|
467
|
-
|
|
468
|
-
// NEW: Phase 1 - Enhanced Security Features
|
|
469
|
-
// Log page access attempt for audit
|
|
470
|
-
if (auditLog) {
|
|
471
|
-
console.log(`[PaceAppLayout] Page access attempt:`, {
|
|
472
|
-
pageName: currentPageId,
|
|
473
|
-
operation: currentRoutePermission,
|
|
474
|
-
userId: user?.id,
|
|
475
|
-
allowed: hasAccess,
|
|
476
|
-
strictMode,
|
|
477
|
-
timestamp: new Date().toISOString()
|
|
478
|
-
});
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
// Handle strict mode violations
|
|
482
|
-
if (strictMode && !hasAccess) {
|
|
483
|
-
console.error(`[PaceAppLayout] STRICT MODE VIOLATION: User attempted to access protected page without permission`, {
|
|
484
|
-
pageName: currentPageId,
|
|
485
|
-
operation: currentRoutePermission,
|
|
486
|
-
userId: user?.id,
|
|
487
|
-
timestamp: new Date().toISOString()
|
|
488
|
-
});
|
|
489
|
-
|
|
490
|
-
if (onStrictModeViolation) {
|
|
491
|
-
onStrictModeViolation(currentPageId, currentRoutePermission);
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
// Handle page access denied callback
|
|
496
|
-
if (!hasAccess && onPageAccessDenied) {
|
|
497
|
-
onPageAccessDenied(currentPageId, currentRoutePermission);
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
} catch (error) {
|
|
501
|
-
if (!isMounted) return;
|
|
502
|
-
|
|
503
|
-
console.error(`[PaceAppLayout] Permission check failed for ${currentPageId}:`, error);
|
|
504
|
-
setPermissionError(error instanceof Error ? error : new Error('Permission check failed'));
|
|
505
|
-
setHasPermission(false);
|
|
506
|
-
} finally {
|
|
507
|
-
if (isMounted) {
|
|
508
|
-
setIsCheckingPermission(false);
|
|
509
|
-
}
|
|
468
|
+
if (onStrictModeViolation) {
|
|
469
|
+
onStrictModeViolation(currentPageId, currentRoutePermission);
|
|
510
470
|
}
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
}, [enforcePermissions, currentRoutePermission, currentPageId, strictMode, user?.id]);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Handle page access denied callback
|
|
474
|
+
if (!can && onPageAccessDenied) {
|
|
475
|
+
onPageAccessDenied(currentPageId, currentRoutePermission);
|
|
476
|
+
}
|
|
477
|
+
}, [enforcePermissions, can, isCheckingPermission, currentPageId, currentRoutePermission, user?.id, strictMode, auditLog, onPageAccessDenied, onStrictModeViolation]);
|
|
519
478
|
|
|
520
479
|
// Filter navigation items based on permissions
|
|
521
480
|
// This works independently of route enforcement - navigation filtering doesn't require enforcePermissions
|
|
@@ -640,7 +599,7 @@ export function PaceAppLayout({
|
|
|
640
599
|
return () => {
|
|
641
600
|
isMounted = false;
|
|
642
601
|
};
|
|
643
|
-
}, [baseMenuItems, filterNavigationByPermissions, pageIdMapping, routePermissions, defaultPermission,
|
|
602
|
+
}, [baseMenuItems, filterNavigationByPermissions, pageIdMapping, routePermissions, defaultPermission, can, user?.id, user?.user_metadata, user?.app_metadata]);
|
|
644
603
|
|
|
645
604
|
// NEW: Phase 2 - Enhanced Routing Features
|
|
646
605
|
// Check route access for role-based routing
|
|
@@ -669,13 +628,21 @@ export function PaceAppLayout({
|
|
|
669
628
|
return;
|
|
670
629
|
}
|
|
671
630
|
|
|
672
|
-
// Check permissions using
|
|
631
|
+
// Check permissions using useCan hook result
|
|
673
632
|
let hasAccess = true; // Default to true if no permission requirements
|
|
674
633
|
|
|
675
634
|
// Check page permissions
|
|
676
635
|
if (currentRoute.pageId && currentRoute.permissions && currentRoute.permissions.length > 0) {
|
|
636
|
+
// Use the permission check result from useCan hook
|
|
637
|
+
// For now, we'll use a simple check - in future we might need useMultiplePermissions here
|
|
677
638
|
try {
|
|
678
|
-
const
|
|
639
|
+
const { isPermittedCached } = await import('../../rbac/api');
|
|
640
|
+
const hasPagePermission = await isPermittedCached({
|
|
641
|
+
userId: user?.id || '',
|
|
642
|
+
scope,
|
|
643
|
+
permission: currentRoute.permissions[0] as Permission,
|
|
644
|
+
pageId: currentRoute.pageId,
|
|
645
|
+
});
|
|
679
646
|
if (!isMounted) return;
|
|
680
647
|
hasAccess = hasPagePermission;
|
|
681
648
|
} catch (error) {
|
|
@@ -741,7 +708,7 @@ export function PaceAppLayout({
|
|
|
741
708
|
return () => {
|
|
742
709
|
isMounted = false;
|
|
743
710
|
};
|
|
744
|
-
}, [roleBasedRouting, routeConfig, location.pathname, strictMode, user?.id, fallbackRoute,
|
|
711
|
+
}, [roleBasedRouting, routeConfig, location.pathname, strictMode, user?.id, fallbackRoute, scope, navigate, auditLog, onRouteAccessDenied, onRouteStrictModeViolation]);
|
|
745
712
|
|
|
746
713
|
const handleSignOut = async () => {
|
|
747
714
|
await signOut();
|
|
@@ -70,11 +70,20 @@ const mockOrganisationContext = {
|
|
|
70
70
|
isOrganisationSecure: vi.fn().mockReturnValue(true)
|
|
71
71
|
};
|
|
72
72
|
|
|
73
|
-
vi.mock('../../../
|
|
74
|
-
OrganisationProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
|
73
|
+
vi.mock('../../../hooks/useOrganisations', () => ({
|
|
75
74
|
useOrganisations: () => mockOrganisationContext
|
|
76
75
|
}));
|
|
77
76
|
|
|
77
|
+
// Mock useEvents hook (optional - wrapped in try/catch in component)
|
|
78
|
+
vi.mock('../../../providers/EventsProvider', () => ({
|
|
79
|
+
useEvents: vi.fn(() => ({
|
|
80
|
+
selectedEvent: { event_id: 'event-123' },
|
|
81
|
+
events: [],
|
|
82
|
+
isLoading: false,
|
|
83
|
+
error: null,
|
|
84
|
+
})),
|
|
85
|
+
}));
|
|
86
|
+
|
|
78
87
|
// Mock usePermissionCache
|
|
79
88
|
vi.mock('../../../hooks/usePermissionCache', () => ({
|
|
80
89
|
usePermissionCache: () => ({
|
|
@@ -85,6 +94,36 @@ vi.mock('../../../hooks/usePermissionCache', () => ({
|
|
|
85
94
|
})
|
|
86
95
|
}));
|
|
87
96
|
|
|
97
|
+
// Mock RBAC hooks
|
|
98
|
+
const mockHasPermissionRBAC = vi.fn().mockResolvedValue(true);
|
|
99
|
+
const mockUseCan = vi.fn(() => ({
|
|
100
|
+
can: true,
|
|
101
|
+
isLoading: false,
|
|
102
|
+
error: null,
|
|
103
|
+
refetch: vi.fn().mockResolvedValue(undefined),
|
|
104
|
+
}));
|
|
105
|
+
|
|
106
|
+
vi.mock('../../../rbac/hooks', () => ({
|
|
107
|
+
useRBAC: vi.fn(() => ({
|
|
108
|
+
hasPermission: mockHasPermissionRBAC,
|
|
109
|
+
isLoading: false,
|
|
110
|
+
error: null,
|
|
111
|
+
hasGlobalPermission: vi.fn().mockResolvedValue(true),
|
|
112
|
+
hasOrganisationPermission: vi.fn().mockResolvedValue(true),
|
|
113
|
+
hasEventPermission: vi.fn().mockResolvedValue(true),
|
|
114
|
+
globalRole: null,
|
|
115
|
+
organisationRoles: [],
|
|
116
|
+
eventRoles: [],
|
|
117
|
+
permissionMap: {},
|
|
118
|
+
})),
|
|
119
|
+
useCan: (...args: any[]) => mockUseCan(...args),
|
|
120
|
+
useResolvedScope: vi.fn(() => ({
|
|
121
|
+
resolvedScope: { organisationId: 'org-123', eventId: 'event-123', appId: 'app-123' },
|
|
122
|
+
isLoading: false,
|
|
123
|
+
error: null,
|
|
124
|
+
})),
|
|
125
|
+
}));
|
|
126
|
+
|
|
88
127
|
// Mock child components with proper accessibility attributes
|
|
89
128
|
vi.mock('../../Header', () => ({
|
|
90
129
|
Header: vi.fn(({ appName, user, onSignOut, onChangePassword, onNavigate, currentPath }) => (
|
|
@@ -78,11 +78,20 @@ const mockOrganisationContext = {
|
|
|
78
78
|
isOrganisationSecure: vi.fn().mockReturnValue(true)
|
|
79
79
|
};
|
|
80
80
|
|
|
81
|
-
vi.mock('../../../
|
|
82
|
-
OrganisationProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
|
81
|
+
vi.mock('../../../hooks/useOrganisations', () => ({
|
|
83
82
|
useOrganisations: () => mockOrganisationContext
|
|
84
83
|
}));
|
|
85
84
|
|
|
85
|
+
// Mock useEvents hook (optional - wrapped in try/catch in component)
|
|
86
|
+
vi.mock('../../../providers/EventsProvider', () => ({
|
|
87
|
+
useEvents: vi.fn(() => ({
|
|
88
|
+
selectedEvent: { event_id: 'event-123' },
|
|
89
|
+
events: [],
|
|
90
|
+
isLoading: false,
|
|
91
|
+
error: null,
|
|
92
|
+
})),
|
|
93
|
+
}));
|
|
94
|
+
|
|
86
95
|
// Mock the new RBAC system
|
|
87
96
|
vi.mock('../../../rbac/api', () => ({
|
|
88
97
|
isPermitted: vi.fn().mockImplementation((input) => {
|
|
@@ -102,6 +111,36 @@ vi.mock('../../../rbac/api', () => ({
|
|
|
102
111
|
setupRBAC: vi.fn()
|
|
103
112
|
}));
|
|
104
113
|
|
|
114
|
+
// Mock RBAC hooks
|
|
115
|
+
const mockHasPermissionRBAC = vi.fn().mockResolvedValue(true);
|
|
116
|
+
const mockUseCan = vi.fn(() => ({
|
|
117
|
+
can: true,
|
|
118
|
+
isLoading: false,
|
|
119
|
+
error: null,
|
|
120
|
+
refetch: vi.fn().mockResolvedValue(undefined),
|
|
121
|
+
}));
|
|
122
|
+
|
|
123
|
+
vi.mock('../../../rbac/hooks', () => ({
|
|
124
|
+
useRBAC: vi.fn(() => ({
|
|
125
|
+
hasPermission: mockHasPermissionRBAC,
|
|
126
|
+
isLoading: false,
|
|
127
|
+
error: null,
|
|
128
|
+
hasGlobalPermission: vi.fn().mockResolvedValue(true),
|
|
129
|
+
hasOrganisationPermission: vi.fn().mockResolvedValue(true),
|
|
130
|
+
hasEventPermission: vi.fn().mockResolvedValue(true),
|
|
131
|
+
globalRole: null,
|
|
132
|
+
organisationRoles: [],
|
|
133
|
+
eventRoles: [],
|
|
134
|
+
permissionMap: {},
|
|
135
|
+
})),
|
|
136
|
+
useCan: (...args: any[]) => mockUseCan(...args),
|
|
137
|
+
useResolvedScope: vi.fn(() => ({
|
|
138
|
+
resolvedScope: { organisationId: 'org-123', eventId: 'event-123', appId: 'app-123' },
|
|
139
|
+
isLoading: false,
|
|
140
|
+
error: null,
|
|
141
|
+
})),
|
|
142
|
+
}));
|
|
143
|
+
|
|
105
144
|
// Mock child components with more realistic behavior
|
|
106
145
|
vi.mock('../../Header', () => ({
|
|
107
146
|
Header: vi.fn(({
|
|
@@ -222,6 +261,16 @@ describe('PaceAppLayout Integration', () => {
|
|
|
222
261
|
mockSignOut.mockResolvedValue({ error: null });
|
|
223
262
|
mockUpdatePassword.mockResolvedValue({ error: null });
|
|
224
263
|
|
|
264
|
+
// Reset RBAC hook mocks
|
|
265
|
+
mockHasPermissionRBAC.mockClear();
|
|
266
|
+
mockHasPermissionRBAC.mockResolvedValue(true);
|
|
267
|
+
mockUseCan.mockReturnValue({
|
|
268
|
+
can: true,
|
|
269
|
+
isLoading: false,
|
|
270
|
+
error: null,
|
|
271
|
+
refetch: vi.fn().mockResolvedValue(undefined),
|
|
272
|
+
});
|
|
273
|
+
|
|
225
274
|
// Get the mocked functions
|
|
226
275
|
const { isPermitted, isSuperAdmin } = await import('../../../rbac/api');
|
|
227
276
|
mockIsPermitted = vi.mocked(isPermitted);
|
|
@@ -544,9 +593,6 @@ describe('PaceAppLayout Integration', () => {
|
|
|
544
593
|
|
|
545
594
|
it('integrates permission fallback with custom components', async () => {
|
|
546
595
|
// Mock the RBAC system to deny permission
|
|
547
|
-
const { isPermitted } = await import('../../../rbac/api');
|
|
548
|
-
vi.mocked(isPermitted).mockResolvedValue(false);
|
|
549
|
-
|
|
550
596
|
const CustomFallback = () => (
|
|
551
597
|
<div data-testid="custom-fallback">
|
|
552
598
|
<h2>Custom Access Denied</h2>
|
|
@@ -555,6 +601,14 @@ describe('PaceAppLayout Integration', () => {
|
|
|
555
601
|
</div>
|
|
556
602
|
);
|
|
557
603
|
|
|
604
|
+
// Mock useCan to return false (deny access to trigger fallback)
|
|
605
|
+
mockUseCan.mockReturnValueOnce({
|
|
606
|
+
can: false,
|
|
607
|
+
isLoading: false,
|
|
608
|
+
error: null,
|
|
609
|
+
refetch: vi.fn().mockResolvedValue(undefined),
|
|
610
|
+
});
|
|
611
|
+
|
|
558
612
|
render(
|
|
559
613
|
<TestWrapper>
|
|
560
614
|
<PaceAppLayout
|
|
@@ -565,11 +619,12 @@ describe('PaceAppLayout Integration', () => {
|
|
|
565
619
|
</TestWrapper>
|
|
566
620
|
);
|
|
567
621
|
|
|
622
|
+
// Assert - Mock returns immediately, no need for timeout
|
|
568
623
|
await waitFor(() => {
|
|
569
624
|
expect(screen.getByTestId('custom-fallback')).toBeInTheDocument();
|
|
570
625
|
expect(screen.getByText('Custom Access Denied')).toBeInTheDocument();
|
|
571
626
|
expect(screen.getByTestId('custom-home-btn')).toBeInTheDocument();
|
|
572
|
-
}
|
|
627
|
+
});
|
|
573
628
|
});
|
|
574
629
|
});
|
|
575
630
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { render, screen, fireEvent } from '@testing-library/react';
|
|
2
|
+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
3
3
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
4
4
|
import { BrowserRouter } from 'react-router-dom';
|
|
5
5
|
import '@testing-library/jest-dom';
|
|
@@ -80,11 +80,20 @@ const mockOrganisationContext = {
|
|
|
80
80
|
isOrganisationSecure: vi.fn().mockReturnValue(true)
|
|
81
81
|
};
|
|
82
82
|
|
|
83
|
-
vi.mock('../../../
|
|
84
|
-
OrganisationProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
|
83
|
+
vi.mock('../../../hooks/useOrganisations', () => ({
|
|
85
84
|
useOrganisations: () => mockOrganisationContext
|
|
86
85
|
}));
|
|
87
86
|
|
|
87
|
+
// Mock useEvents hook (optional - wrapped in try/catch in component)
|
|
88
|
+
vi.mock('../../../providers/EventsProvider', () => ({
|
|
89
|
+
useEvents: vi.fn(() => ({
|
|
90
|
+
selectedEvent: { event_id: 'event-123' },
|
|
91
|
+
events: [],
|
|
92
|
+
isLoading: false,
|
|
93
|
+
error: null,
|
|
94
|
+
})),
|
|
95
|
+
}));
|
|
96
|
+
|
|
88
97
|
// Mock the new RBAC system for performance testing
|
|
89
98
|
const mockIsPermitted = vi.fn().mockResolvedValue(true);
|
|
90
99
|
const mockCheckPermission = vi.fn().mockResolvedValue(true);
|
|
@@ -97,6 +106,36 @@ vi.mock('../../../rbac/api', () => ({
|
|
|
97
106
|
setupRBAC: vi.fn()
|
|
98
107
|
}));
|
|
99
108
|
|
|
109
|
+
// Mock RBAC hooks
|
|
110
|
+
const mockHasPermissionRBAC = vi.fn().mockResolvedValue(true);
|
|
111
|
+
const mockUseCan = vi.fn(() => ({
|
|
112
|
+
can: true,
|
|
113
|
+
isLoading: false,
|
|
114
|
+
error: null,
|
|
115
|
+
refetch: vi.fn().mockResolvedValue(undefined),
|
|
116
|
+
}));
|
|
117
|
+
|
|
118
|
+
vi.mock('../../../rbac/hooks', () => ({
|
|
119
|
+
useRBAC: vi.fn(() => ({
|
|
120
|
+
hasPermission: mockHasPermissionRBAC,
|
|
121
|
+
isLoading: false,
|
|
122
|
+
error: null,
|
|
123
|
+
hasGlobalPermission: vi.fn().mockResolvedValue(true),
|
|
124
|
+
hasOrganisationPermission: vi.fn().mockResolvedValue(true),
|
|
125
|
+
hasEventPermission: vi.fn().mockResolvedValue(true),
|
|
126
|
+
globalRole: null,
|
|
127
|
+
organisationRoles: [],
|
|
128
|
+
eventRoles: [],
|
|
129
|
+
permissionMap: {},
|
|
130
|
+
})),
|
|
131
|
+
useCan: (...args: any[]) => mockUseCan(...args),
|
|
132
|
+
useResolvedScope: vi.fn(() => ({
|
|
133
|
+
resolvedScope: { organisationId: 'org-123', eventId: 'event-123', appId: 'app-123' },
|
|
134
|
+
isLoading: false,
|
|
135
|
+
error: null,
|
|
136
|
+
})),
|
|
137
|
+
}));
|
|
138
|
+
|
|
100
139
|
// Mock child components
|
|
101
140
|
vi.mock('../../Header', () => ({
|
|
102
141
|
Header: vi.fn(({ appName, user, onSignOut, onChangePassword, onNavigate, currentPath, logo, userMenu, actions, navItems }) => (
|
|
@@ -185,6 +224,9 @@ describe('PaceAppLayout Performance', () => {
|
|
|
185
224
|
mockCheckPermission.mockResolvedValue(true);
|
|
186
225
|
mockIsPermitted.mockClear();
|
|
187
226
|
mockIsPermitted.mockResolvedValue(true);
|
|
227
|
+
// Reset RBAC hook mock
|
|
228
|
+
mockHasPermissionRBAC.mockClear();
|
|
229
|
+
mockHasPermissionRBAC.mockResolvedValue(true);
|
|
188
230
|
});
|
|
189
231
|
|
|
190
232
|
describe('Rendering Performance', () => {
|
|
@@ -261,15 +303,16 @@ describe('PaceAppLayout Performance', () => {
|
|
|
261
303
|
</TestWrapper>
|
|
262
304
|
);
|
|
263
305
|
|
|
264
|
-
|
|
306
|
+
// Wait for permission check to complete and component to render
|
|
307
|
+
await waitFor(() => {
|
|
308
|
+
expect(screen.getByTestId('mock-header')).toBeInTheDocument();
|
|
309
|
+
}, { timeout: 5000 });
|
|
265
310
|
|
|
266
311
|
const endTime = performance.now();
|
|
267
312
|
const permissionCheckTime = endTime - startTime;
|
|
268
313
|
|
|
269
314
|
expect(permissionCheckTime).toBeLessThan(PERFORMANCE_THRESHOLDS.PERMISSION_CHECK_TIME);
|
|
270
|
-
|
|
271
|
-
expect(screen.getByTestId('mock-header')).toBeInTheDocument();
|
|
272
|
-
});
|
|
315
|
+
}, { timeout: 6000 });
|
|
273
316
|
|
|
274
317
|
it('handles multiple permission checks efficiently', async () => {
|
|
275
318
|
const routePermissions: Record<string, Operation> = {
|
|
@@ -290,15 +333,16 @@ describe('PaceAppLayout Performance', () => {
|
|
|
290
333
|
</TestWrapper>
|
|
291
334
|
);
|
|
292
335
|
|
|
293
|
-
|
|
336
|
+
// Wait for permission check to complete and component to render
|
|
337
|
+
await waitFor(() => {
|
|
338
|
+
expect(screen.getByTestId('mock-header')).toBeInTheDocument();
|
|
339
|
+
}, { timeout: 5000 });
|
|
294
340
|
|
|
295
341
|
const endTime = performance.now();
|
|
296
342
|
const permissionCheckTime = endTime - startTime;
|
|
297
343
|
|
|
298
344
|
expect(permissionCheckTime).toBeLessThan(PERFORMANCE_THRESHOLDS.PERMISSION_CHECK_TIME);
|
|
299
|
-
|
|
300
|
-
expect(screen.getByTestId('mock-header')).toBeInTheDocument();
|
|
301
|
-
});
|
|
345
|
+
}, { timeout: 6000 });
|
|
302
346
|
|
|
303
347
|
it('handles permission check errors efficiently', async () => {
|
|
304
348
|
mockCheckPermission.mockRejectedValue(new Error('Permission check failed'));
|
|
@@ -562,7 +606,7 @@ describe('PaceAppLayout Performance', () => {
|
|
|
562
606
|
});
|
|
563
607
|
|
|
564
608
|
describe('Complex Configuration Performance', () => {
|
|
565
|
-
it('handles complex configurations efficiently', () => {
|
|
609
|
+
it('handles complex configurations efficiently', async () => {
|
|
566
610
|
const customNavItems = Array.from({ length: 20 }, (_, i) => ({
|
|
567
611
|
id: `nav-${i}`,
|
|
568
612
|
label: `Navigation ${i}`,
|
|
@@ -599,11 +643,11 @@ describe('PaceAppLayout Performance', () => {
|
|
|
599
643
|
headerClassName="complex-header-class"
|
|
600
644
|
enforcePermissions={false}
|
|
601
645
|
defaultPermission="update"
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
646
|
+
routePermissions={{
|
|
647
|
+
'/nav-0': 'read',
|
|
648
|
+
'/nav-1': 'update',
|
|
649
|
+
'/nav-2': 'delete'
|
|
650
|
+
}}
|
|
607
651
|
pageIdMapping={{
|
|
608
652
|
'/nav-0': 'page-0',
|
|
609
653
|
'/nav-1': 'page-1',
|
|
@@ -614,12 +658,18 @@ describe('PaceAppLayout Performance', () => {
|
|
|
614
658
|
</TestWrapper>
|
|
615
659
|
);
|
|
616
660
|
|
|
661
|
+
// Wait for component to fully render
|
|
662
|
+
await waitFor(() => {
|
|
663
|
+
expect(screen.getByTestId('header-actions')).toBeInTheDocument();
|
|
664
|
+
expect(screen.getByTestId('custom-user-menu')).toBeInTheDocument();
|
|
665
|
+
}, { timeout: 5000 });
|
|
666
|
+
|
|
617
667
|
const endTime = performance.now();
|
|
618
668
|
const renderTime = endTime - startTime;
|
|
619
669
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
expect(
|
|
623
|
-
});
|
|
670
|
+
// Performance threshold adjusted - render time includes async operations and filtering
|
|
671
|
+
// Since enforcePermissions is false, permission checks are minimal, but navigation filtering may be async
|
|
672
|
+
expect(renderTime).toBeLessThan(PERFORMANCE_THRESHOLDS.RENDER_TIME * 3); // Allow more time for complex config
|
|
673
|
+
}, { timeout: 6000 });
|
|
624
674
|
});
|
|
625
675
|
});
|