@jmruthers/pace-core 0.5.3 → 0.5.5
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-ZQDRE46Q.js → DataTable-3SSI644S.js} +2 -2
- package/dist/{chunk-M4RW7PIP.js → chunk-2BJFM2JC.js} +105 -81
- package/dist/chunk-2BJFM2JC.js.map +1 -0
- package/dist/{chunk-5H3C2SWM.js → chunk-RTCA5ZNK.js} +2 -2
- package/dist/components.js +2 -2
- package/dist/index.js +2 -2
- package/dist/styles/core.css +3 -0
- package/dist/utils.js +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/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/DataTableAction.md +1 -1
- package/docs/api/interfaces/DataTableColumn.md +1 -1
- package/docs/api/interfaces/DataTableProps.md +34 -34
- 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/EventContextType.md +1 -1
- package/docs/api/interfaces/EventLogoProps.md +1 -1
- package/docs/api/interfaces/EventProviderProps.md +1 -1
- package/docs/api/interfaces/FileSizeLimits.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 +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 +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/RBACContextType.md +1 -1
- 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/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/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 +1 -1
- package/docs/api/interfaces/UserMenuProps.md +1 -1
- package/docs/api/interfaces/UserProfile.md +1 -1
- package/docs/api/modules.md +3 -3
- package/docs/implementation-guides/data-tables.md +20 -0
- package/docs/quick-reference.md +9 -0
- package/docs/rbac/examples.md +4 -0
- package/package.json +1 -1
- package/src/__tests__/helpers/test-utils.tsx +147 -1
- package/src/components/DataTable/DataTable.tsx +20 -0
- package/src/components/DataTable/__tests__/DataTable.hooks.test 2.tsx +191 -0
- package/src/components/DataTable/__tests__/DataTable.hooks.test.tsx +191 -0
- package/src/components/DataTable/components/DataTableCore.tsx +167 -138
- package/src/components/Header/Header.test.tsx +1 -1
- package/src/components/OrganisationSelector/OrganisationSelector.test.tsx +1 -1
- package/src/hooks/__tests__/hooks.integration.test.tsx +575 -0
- package/src/hooks/__tests__/useApiFetch.unit.test.ts +115 -0
- package/src/hooks/__tests__/useComponentPerformance.unit.test.tsx +133 -0
- package/src/hooks/__tests__/useDebounce.unit.test.ts +82 -0
- package/src/hooks/__tests__/useFocusTrap.unit.test.tsx +293 -0
- package/src/hooks/__tests__/useInactivityTracker.unit.test.ts +385 -0
- package/src/hooks/__tests__/useOrganisationPermissions.unit.test.tsx +286 -0
- package/src/hooks/__tests__/useOrganisationSecurity.unit.test.tsx +838 -0
- package/src/hooks/__tests__/usePermissionCache.simple.test.ts +104 -0
- package/src/hooks/__tests__/usePermissionCache.unit.test.ts +633 -0
- package/src/hooks/__tests__/useRBAC.unit.test.ts +856 -0
- package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +537 -0
- package/src/hooks/__tests__/useToast.unit.test.tsx +62 -0
- package/src/hooks/__tests__/useZodForm.unit.test.tsx +37 -0
- package/src/rbac/api.test.ts +511 -0
- package/src/rbac/components/__tests__/NavigationGuard.test.tsx +843 -0
- package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +1007 -0
- package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +806 -0
- package/src/rbac/components/__tests__/RoleBasedRouter.test.tsx +741 -0
- package/src/rbac/hooks/useCan.test.ts +1 -1
- package/src/rbac/hooks/usePermissions.test.ts +10 -5
- package/src/rbac/hooks/useRBAC.test.ts +141 -93
- package/src/rbac/utils/__tests__/eventContext.test.ts +428 -0
- package/src/rbac/utils/__tests__/eventContext.unit.test.ts +428 -0
- package/src/styles/core.css +3 -0
- package/src/utils/__tests__/appConfig.unit.test.ts +55 -0
- package/src/utils/__tests__/audit.unit.test.ts +69 -0
- package/src/utils/__tests__/auth-utils.unit.test.ts +70 -0
- package/src/utils/__tests__/bundleAnalysis.unit.test.ts +317 -0
- package/src/utils/__tests__/cn.unit.test.ts +34 -0
- package/src/utils/__tests__/deviceFingerprint.unit.test.ts +503 -0
- package/src/utils/__tests__/dynamicUtils.unit.test.ts +322 -0
- package/src/utils/__tests__/formatDate.unit.test.ts +109 -0
- package/src/utils/__tests__/formatting.unit.test.ts +66 -0
- package/src/utils/__tests__/index.unit.test.ts +251 -0
- package/src/utils/__tests__/lazyLoad.unit.test.tsx +309 -0
- package/src/utils/__tests__/organisationContext.unit.test.ts +192 -0
- package/src/utils/__tests__/performanceBudgets.unit.test.ts +259 -0
- package/src/utils/__tests__/permissionTypes.unit.test.ts +250 -0
- package/src/utils/__tests__/permissionUtils.unit.test.ts +362 -0
- package/src/utils/__tests__/sanitization.unit.test.ts +346 -0
- package/src/utils/__tests__/schemaUtils.unit.test.ts +441 -0
- package/src/utils/__tests__/secureDataAccess.unit.test.ts +334 -0
- package/src/utils/__tests__/secureErrors.unit.test.ts +377 -0
- package/src/utils/__tests__/secureStorage.unit.test.ts +293 -0
- package/src/utils/__tests__/security.unit.test.ts +127 -0
- package/src/utils/__tests__/securityMonitor.unit.test.ts +280 -0
- package/src/utils/__tests__/sessionTracking.unit.test.ts +356 -0
- package/src/utils/__tests__/validation.unit.test.ts +84 -0
- package/src/utils/__tests__/validationUtils.unit.test.ts +571 -0
- package/src/validation/__tests__/common.unit.test.ts +101 -0
- package/src/validation/__tests__/csrf.unit.test.ts +302 -0
- package/src/validation/__tests__/passwordSchema.unit.test 2.ts +98 -0
- package/src/validation/__tests__/passwordSchema.unit.test.ts +98 -0
- package/src/validation/__tests__/sqlInjectionProtection.unit.test.ts +466 -0
- package/dist/chunk-M4RW7PIP.js.map +0 -1
- /package/dist/{DataTable-ZQDRE46Q.js.map → DataTable-3SSI644S.js.map} +0 -0
- /package/dist/{chunk-5H3C2SWM.js.map → chunk-RTCA5ZNK.js.map} +0 -0
package/src/rbac/api.test.ts
CHANGED
|
@@ -30,6 +30,18 @@ vi.mock('./cache', () => ({
|
|
|
30
30
|
invalidate: vi.fn(),
|
|
31
31
|
get: vi.fn(),
|
|
32
32
|
set: vi.fn()
|
|
33
|
+
},
|
|
34
|
+
RBACCache: {
|
|
35
|
+
generatePermissionKey: vi.fn(({ userId, organisationId, eventId, appId, permission }) =>
|
|
36
|
+
`permission:${userId}:${organisationId}:${eventId || 'null'}:${appId || 'null'}:${permission}`
|
|
37
|
+
)
|
|
38
|
+
},
|
|
39
|
+
CACHE_PATTERNS: {
|
|
40
|
+
PERMISSION: vi.fn((userId, organisationId) => `permission:${userId}:${organisationId}:*`),
|
|
41
|
+
USER: vi.fn((userId) => `permission:${userId}:*`),
|
|
42
|
+
ORGANISATION: vi.fn((organisationId) => `permission:*:${organisationId}:*`),
|
|
43
|
+
EVENT: vi.fn((eventId) => `permission:*:*:${eventId}:*`),
|
|
44
|
+
APP: vi.fn((appId) => `permission:*:*:*:${appId}`)
|
|
33
45
|
}
|
|
34
46
|
}));
|
|
35
47
|
|
|
@@ -438,4 +450,503 @@ describe('RBAC API', () => {
|
|
|
438
450
|
}).not.toThrow();
|
|
439
451
|
});
|
|
440
452
|
});
|
|
453
|
+
|
|
454
|
+
describe('Permission Functions', () => {
|
|
455
|
+
let mockEngine: any;
|
|
456
|
+
|
|
457
|
+
beforeEach(() => {
|
|
458
|
+
mockEngine = {
|
|
459
|
+
getAccessLevel: vi.fn(),
|
|
460
|
+
getPermissionMap: vi.fn(),
|
|
461
|
+
isPermitted: vi.fn(),
|
|
462
|
+
checkSuperAdmin: vi.fn(),
|
|
463
|
+
getAppConfig: vi.fn()
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
mockCreateRBACEngine.mockReturnValue(mockEngine);
|
|
467
|
+
mockCreateAuditManager.mockReturnValue({} as any);
|
|
468
|
+
mockGetRBACLogger.mockReturnValue({
|
|
469
|
+
info: vi.fn(),
|
|
470
|
+
warn: vi.fn(),
|
|
471
|
+
error: vi.fn(),
|
|
472
|
+
debug: vi.fn()
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
setupRBAC(mockSupabase as any);
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
describe('getAccessLevel', () => {
|
|
479
|
+
it('returns access level for user', async () => {
|
|
480
|
+
const { getAccessLevel } = await import('./api');
|
|
481
|
+
|
|
482
|
+
const mockAccessLevel = 'admin';
|
|
483
|
+
mockEngine.getAccessLevel.mockResolvedValue(mockAccessLevel);
|
|
484
|
+
|
|
485
|
+
const result = await getAccessLevel({
|
|
486
|
+
userId: 'user-123',
|
|
487
|
+
scope: { organisationId: 'org-456' }
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
expect(result).toBe(mockAccessLevel);
|
|
491
|
+
expect(mockEngine.getAccessLevel).toHaveBeenCalledWith({
|
|
492
|
+
userId: 'user-123',
|
|
493
|
+
scope: { organisationId: 'org-456' }
|
|
494
|
+
});
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
it('handles engine errors', async () => {
|
|
498
|
+
const { getAccessLevel } = await import('./api');
|
|
499
|
+
|
|
500
|
+
const error = new Error('Engine error');
|
|
501
|
+
mockEngine.getAccessLevel.mockRejectedValue(error);
|
|
502
|
+
|
|
503
|
+
await expect(getAccessLevel({
|
|
504
|
+
userId: 'user-123',
|
|
505
|
+
scope: { organisationId: 'org-456' }
|
|
506
|
+
})).rejects.toThrow('Engine error');
|
|
507
|
+
});
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
describe('getPermissionMap', () => {
|
|
511
|
+
it('returns permission map for user', async () => {
|
|
512
|
+
const { getPermissionMap } = await import('./api');
|
|
513
|
+
|
|
514
|
+
const mockPermissionMap = {
|
|
515
|
+
'read:users': true,
|
|
516
|
+
'write:users': false
|
|
517
|
+
};
|
|
518
|
+
mockEngine.getPermissionMap.mockResolvedValue(mockPermissionMap);
|
|
519
|
+
|
|
520
|
+
const result = await getPermissionMap({
|
|
521
|
+
userId: 'user-123',
|
|
522
|
+
scope: { organisationId: 'org-456' }
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
expect(result).toEqual(mockPermissionMap);
|
|
526
|
+
expect(mockEngine.getPermissionMap).toHaveBeenCalledWith({
|
|
527
|
+
userId: 'user-123',
|
|
528
|
+
scope: { organisationId: 'org-456' }
|
|
529
|
+
});
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
it('handles engine errors', async () => {
|
|
533
|
+
const { getPermissionMap } = await import('./api');
|
|
534
|
+
|
|
535
|
+
const error = new Error('Engine error');
|
|
536
|
+
mockEngine.getPermissionMap.mockRejectedValue(error);
|
|
537
|
+
|
|
538
|
+
await expect(getPermissionMap({
|
|
539
|
+
userId: 'user-123',
|
|
540
|
+
scope: { organisationId: 'org-456' }
|
|
541
|
+
})).rejects.toThrow('Engine error');
|
|
542
|
+
});
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
describe('isPermitted', () => {
|
|
546
|
+
it('returns permission result', async () => {
|
|
547
|
+
const { isPermitted } = await import('./api');
|
|
548
|
+
|
|
549
|
+
mockEngine.isPermitted.mockResolvedValue(true);
|
|
550
|
+
|
|
551
|
+
const result = await isPermitted({
|
|
552
|
+
userId: 'user-123',
|
|
553
|
+
scope: { organisationId: 'org-456' },
|
|
554
|
+
permission: 'read:users',
|
|
555
|
+
pageId: 'page-789'
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
expect(result).toBe(true);
|
|
559
|
+
expect(mockEngine.isPermitted).toHaveBeenCalledWith({
|
|
560
|
+
userId: 'user-123',
|
|
561
|
+
scope: { organisationId: 'org-456' },
|
|
562
|
+
permission: 'read:users',
|
|
563
|
+
pageId: 'page-789'
|
|
564
|
+
});
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
it('handles engine errors', async () => {
|
|
568
|
+
const { isPermitted } = await import('./api');
|
|
569
|
+
|
|
570
|
+
const error = new Error('Engine error');
|
|
571
|
+
mockEngine.isPermitted.mockRejectedValue(error);
|
|
572
|
+
|
|
573
|
+
await expect(isPermitted({
|
|
574
|
+
userId: 'user-123',
|
|
575
|
+
scope: { organisationId: 'org-456' },
|
|
576
|
+
permission: 'read:users'
|
|
577
|
+
})).rejects.toThrow('Engine error');
|
|
578
|
+
});
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
describe('isPermittedCached', () => {
|
|
582
|
+
it('returns cached result when available', async () => {
|
|
583
|
+
const { isPermittedCached } = await import('./api');
|
|
584
|
+
|
|
585
|
+
const cacheKey = 'permission:user-123:org-456:null:null:read:users';
|
|
586
|
+
rbacCache.get.mockReturnValue(true);
|
|
587
|
+
|
|
588
|
+
const result = await isPermittedCached({
|
|
589
|
+
userId: 'user-123',
|
|
590
|
+
scope: { organisationId: 'org-456' },
|
|
591
|
+
permission: 'read:users',
|
|
592
|
+
pageId: 'page-789'
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
expect(result).toBe(true);
|
|
596
|
+
expect(rbacCache.get).toHaveBeenCalledWith(cacheKey);
|
|
597
|
+
expect(mockEngine.isPermitted).not.toHaveBeenCalled();
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
it('checks permission and caches result when not cached', async () => {
|
|
601
|
+
const { isPermittedCached } = await import('./api');
|
|
602
|
+
|
|
603
|
+
rbacCache.get.mockReturnValue(null);
|
|
604
|
+
mockEngine.isPermitted.mockResolvedValue(true);
|
|
605
|
+
|
|
606
|
+
const result = await isPermittedCached({
|
|
607
|
+
userId: 'user-123',
|
|
608
|
+
scope: { organisationId: 'org-456' },
|
|
609
|
+
permission: 'read:users',
|
|
610
|
+
pageId: 'page-789'
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
expect(result).toBe(true);
|
|
614
|
+
expect(mockEngine.isPermitted).toHaveBeenCalledWith({
|
|
615
|
+
userId: 'user-123',
|
|
616
|
+
scope: { organisationId: 'org-456' },
|
|
617
|
+
permission: 'read:users',
|
|
618
|
+
pageId: 'page-789'
|
|
619
|
+
});
|
|
620
|
+
expect(rbacCache.set).toHaveBeenCalledWith(
|
|
621
|
+
expect.any(String),
|
|
622
|
+
true
|
|
623
|
+
);
|
|
624
|
+
});
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
describe('hasPermission', () => {
|
|
628
|
+
it('calls isPermitted', async () => {
|
|
629
|
+
const { hasPermission } = await import('./api');
|
|
630
|
+
|
|
631
|
+
mockEngine.isPermitted.mockResolvedValue(true);
|
|
632
|
+
|
|
633
|
+
const result = await hasPermission({
|
|
634
|
+
userId: 'user-123',
|
|
635
|
+
scope: { organisationId: 'org-456' },
|
|
636
|
+
permission: 'read:users'
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
expect(result).toBe(true);
|
|
640
|
+
expect(mockEngine.isPermitted).toHaveBeenCalledWith({
|
|
641
|
+
userId: 'user-123',
|
|
642
|
+
scope: { organisationId: 'org-456' },
|
|
643
|
+
permission: 'read:users'
|
|
644
|
+
});
|
|
645
|
+
});
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
describe('hasAnyPermission', () => {
|
|
649
|
+
it('returns true if user has any permission', async () => {
|
|
650
|
+
const { hasAnyPermission } = await import('./api');
|
|
651
|
+
|
|
652
|
+
mockEngine.isPermitted
|
|
653
|
+
.mockResolvedValueOnce(false) // First permission denied
|
|
654
|
+
.mockResolvedValueOnce(true); // Second permission granted
|
|
655
|
+
|
|
656
|
+
const result = await hasAnyPermission({
|
|
657
|
+
userId: 'user-123',
|
|
658
|
+
scope: { organisationId: 'org-456' },
|
|
659
|
+
permissions: ['read:users', 'write:users']
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
expect(result).toBe(true);
|
|
663
|
+
expect(mockEngine.isPermitted).toHaveBeenCalledTimes(2);
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
it('returns false if user has no permissions', async () => {
|
|
667
|
+
const { hasAnyPermission } = await import('./api');
|
|
668
|
+
|
|
669
|
+
mockEngine.isPermitted.mockResolvedValue(false);
|
|
670
|
+
|
|
671
|
+
const result = await hasAnyPermission({
|
|
672
|
+
userId: 'user-123',
|
|
673
|
+
scope: { organisationId: 'org-456' },
|
|
674
|
+
permissions: ['read:users', 'write:users']
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
expect(result).toBe(false);
|
|
678
|
+
expect(mockEngine.isPermitted).toHaveBeenCalledTimes(2);
|
|
679
|
+
});
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
describe('hasAllPermissions', () => {
|
|
683
|
+
it('returns true if user has all permissions', async () => {
|
|
684
|
+
const { hasAllPermissions } = await import('./api');
|
|
685
|
+
|
|
686
|
+
mockEngine.isPermitted.mockResolvedValue(true);
|
|
687
|
+
|
|
688
|
+
const result = await hasAllPermissions({
|
|
689
|
+
userId: 'user-123',
|
|
690
|
+
scope: { organisationId: 'org-456' },
|
|
691
|
+
permissions: ['read:users', 'write:users']
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
expect(result).toBe(true);
|
|
695
|
+
expect(mockEngine.isPermitted).toHaveBeenCalledTimes(2);
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
it('returns false if user lacks any permission', async () => {
|
|
699
|
+
const { hasAllPermissions } = await import('./api');
|
|
700
|
+
|
|
701
|
+
mockEngine.isPermitted
|
|
702
|
+
.mockResolvedValueOnce(true) // First permission granted
|
|
703
|
+
.mockResolvedValueOnce(false); // Second permission denied
|
|
704
|
+
|
|
705
|
+
const result = await hasAllPermissions({
|
|
706
|
+
userId: 'user-123',
|
|
707
|
+
scope: { organisationId: 'org-456' },
|
|
708
|
+
permissions: ['read:users', 'write:users']
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
expect(result).toBe(false);
|
|
712
|
+
expect(mockEngine.isPermitted).toHaveBeenCalledTimes(2);
|
|
713
|
+
});
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
describe('isSuperAdmin', () => {
|
|
717
|
+
it('returns super admin status', async () => {
|
|
718
|
+
const { isSuperAdmin } = await import('./api');
|
|
719
|
+
|
|
720
|
+
mockEngine.checkSuperAdmin.mockResolvedValue(true);
|
|
721
|
+
|
|
722
|
+
const result = await isSuperAdmin('user-123');
|
|
723
|
+
|
|
724
|
+
expect(result).toBe(true);
|
|
725
|
+
expect(mockEngine.checkSuperAdmin).toHaveBeenCalledWith('user-123');
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
it('handles engine errors', async () => {
|
|
729
|
+
const { isSuperAdmin } = await import('./api');
|
|
730
|
+
|
|
731
|
+
const error = new Error('Engine error');
|
|
732
|
+
mockEngine.checkSuperAdmin.mockRejectedValue(error);
|
|
733
|
+
|
|
734
|
+
await expect(isSuperAdmin('user-123')).rejects.toThrow('Engine error');
|
|
735
|
+
});
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
describe('getAppConfig', () => {
|
|
739
|
+
it('returns app configuration', async () => {
|
|
740
|
+
const { getAppConfig } = await import('./api');
|
|
741
|
+
|
|
742
|
+
const mockConfig = { requires_event: true };
|
|
743
|
+
mockEngine.getAppConfig.mockResolvedValue(mockConfig);
|
|
744
|
+
|
|
745
|
+
const result = await getAppConfig('app-123');
|
|
746
|
+
|
|
747
|
+
expect(result).toEqual(mockConfig);
|
|
748
|
+
expect(mockEngine.getAppConfig).toHaveBeenCalledWith('app-123');
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
it('handles engine errors', async () => {
|
|
752
|
+
const { getAppConfig } = await import('./api');
|
|
753
|
+
|
|
754
|
+
const error = new Error('Engine error');
|
|
755
|
+
mockEngine.getAppConfig.mockRejectedValue(error);
|
|
756
|
+
|
|
757
|
+
await expect(getAppConfig('app-123')).rejects.toThrow('Engine error');
|
|
758
|
+
});
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
describe('isOrganisationAdmin', () => {
|
|
762
|
+
it('returns true for admin access level', async () => {
|
|
763
|
+
const { isOrganisationAdmin } = await import('./api');
|
|
764
|
+
|
|
765
|
+
mockEngine.getAccessLevel.mockResolvedValue('admin');
|
|
766
|
+
|
|
767
|
+
const result = await isOrganisationAdmin('user-123', 'org-456');
|
|
768
|
+
|
|
769
|
+
expect(result).toBe(true);
|
|
770
|
+
expect(mockEngine.getAccessLevel).toHaveBeenCalledWith({
|
|
771
|
+
userId: 'user-123',
|
|
772
|
+
scope: { organisationId: 'org-456' }
|
|
773
|
+
});
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
it('returns true for super access level', async () => {
|
|
777
|
+
const { isOrganisationAdmin } = await import('./api');
|
|
778
|
+
|
|
779
|
+
mockEngine.getAccessLevel.mockResolvedValue('super');
|
|
780
|
+
|
|
781
|
+
const result = await isOrganisationAdmin('user-123', 'org-456');
|
|
782
|
+
|
|
783
|
+
expect(result).toBe(true);
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
it('returns false for other access levels', async () => {
|
|
787
|
+
const { isOrganisationAdmin } = await import('./api');
|
|
788
|
+
|
|
789
|
+
mockEngine.getAccessLevel.mockResolvedValue('user');
|
|
790
|
+
|
|
791
|
+
const result = await isOrganisationAdmin('user-123', 'org-456');
|
|
792
|
+
|
|
793
|
+
expect(result).toBe(false);
|
|
794
|
+
});
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
describe('isEventAdmin', () => {
|
|
798
|
+
it('returns true for admin access level', async () => {
|
|
799
|
+
const { isEventAdmin } = await import('./api');
|
|
800
|
+
|
|
801
|
+
mockEngine.getAccessLevel.mockResolvedValue('admin');
|
|
802
|
+
|
|
803
|
+
const result = await isEventAdmin('user-123', {
|
|
804
|
+
organisationId: 'org-456',
|
|
805
|
+
eventId: 'event-789',
|
|
806
|
+
appId: 'app-101'
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
expect(result).toBe(true);
|
|
810
|
+
expect(mockEngine.getAccessLevel).toHaveBeenCalledWith({
|
|
811
|
+
userId: 'user-123',
|
|
812
|
+
scope: {
|
|
813
|
+
organisationId: 'org-456',
|
|
814
|
+
eventId: 'event-789',
|
|
815
|
+
appId: 'app-101'
|
|
816
|
+
}
|
|
817
|
+
});
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
it('returns false when eventId is missing', async () => {
|
|
821
|
+
const { isEventAdmin } = await import('./api');
|
|
822
|
+
|
|
823
|
+
const result = await isEventAdmin('user-123', {
|
|
824
|
+
organisationId: 'org-456',
|
|
825
|
+
eventId: undefined,
|
|
826
|
+
appId: 'app-101'
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
expect(result).toBe(false);
|
|
830
|
+
expect(mockEngine.getAccessLevel).not.toHaveBeenCalled();
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
it('returns false when appId is missing', async () => {
|
|
834
|
+
const { isEventAdmin } = await import('./api');
|
|
835
|
+
|
|
836
|
+
const result = await isEventAdmin('user-123', {
|
|
837
|
+
organisationId: 'org-456',
|
|
838
|
+
eventId: 'event-789',
|
|
839
|
+
appId: undefined
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
expect(result).toBe(false);
|
|
843
|
+
expect(mockEngine.getAccessLevel).not.toHaveBeenCalled();
|
|
844
|
+
});
|
|
845
|
+
});
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
describe('Cache Management', () => {
|
|
849
|
+
beforeEach(() => {
|
|
850
|
+
const mockEngine = {
|
|
851
|
+
getAccessLevel: vi.fn(),
|
|
852
|
+
getPermissionMap: vi.fn(),
|
|
853
|
+
isPermitted: vi.fn(),
|
|
854
|
+
checkSuperAdmin: vi.fn(),
|
|
855
|
+
getAppConfig: vi.fn()
|
|
856
|
+
};
|
|
857
|
+
|
|
858
|
+
mockCreateRBACEngine.mockReturnValue(mockEngine);
|
|
859
|
+
mockCreateAuditManager.mockReturnValue({} as any);
|
|
860
|
+
mockGetRBACLogger.mockReturnValue({
|
|
861
|
+
info: vi.fn(),
|
|
862
|
+
warn: vi.fn(),
|
|
863
|
+
error: vi.fn(),
|
|
864
|
+
debug: vi.fn()
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
setupRBAC(mockSupabase as any);
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
describe('invalidateUserCache', () => {
|
|
871
|
+
it('invalidates user cache with organisation', async () => {
|
|
872
|
+
const { invalidateUserCache } = await import('./api');
|
|
873
|
+
|
|
874
|
+
invalidateUserCache('user-123', 'org-456');
|
|
875
|
+
|
|
876
|
+
expect(rbacCache.invalidate).toHaveBeenCalledWith(
|
|
877
|
+
expect.stringContaining('user-123')
|
|
878
|
+
);
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
it('invalidates user cache without organisation', async () => {
|
|
882
|
+
const { invalidateUserCache } = await import('./api');
|
|
883
|
+
|
|
884
|
+
invalidateUserCache('user-123');
|
|
885
|
+
|
|
886
|
+
expect(rbacCache.invalidate).toHaveBeenCalledWith(
|
|
887
|
+
expect.stringContaining('user-123')
|
|
888
|
+
);
|
|
889
|
+
});
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
describe('invalidateOrganisationCache', () => {
|
|
893
|
+
it('invalidates organisation cache', async () => {
|
|
894
|
+
const { invalidateOrganisationCache } = await import('./api');
|
|
895
|
+
|
|
896
|
+
invalidateOrganisationCache('org-456');
|
|
897
|
+
|
|
898
|
+
expect(rbacCache.invalidate).toHaveBeenCalledWith(
|
|
899
|
+
expect.stringContaining('org-456')
|
|
900
|
+
);
|
|
901
|
+
});
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
describe('invalidateEventCache', () => {
|
|
905
|
+
it('invalidates event cache', async () => {
|
|
906
|
+
const { invalidateEventCache } = await import('./api');
|
|
907
|
+
|
|
908
|
+
invalidateEventCache('event-789');
|
|
909
|
+
|
|
910
|
+
expect(rbacCache.invalidate).toHaveBeenCalledWith(
|
|
911
|
+
expect.stringContaining('event-789')
|
|
912
|
+
);
|
|
913
|
+
});
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
describe('invalidateAppCache', () => {
|
|
917
|
+
it('invalidates app cache', async () => {
|
|
918
|
+
const { invalidateAppCache } = await import('./api');
|
|
919
|
+
|
|
920
|
+
invalidateAppCache('app-101');
|
|
921
|
+
|
|
922
|
+
expect(rbacCache.invalidate).toHaveBeenCalledWith(
|
|
923
|
+
expect.stringContaining('app-101')
|
|
924
|
+
);
|
|
925
|
+
});
|
|
926
|
+
});
|
|
927
|
+
|
|
928
|
+
describe('clearCache', () => {
|
|
929
|
+
it('clears all cache', async () => {
|
|
930
|
+
const { clearCache } = await import('./api');
|
|
931
|
+
|
|
932
|
+
clearCache();
|
|
933
|
+
|
|
934
|
+
expect(rbacCache.clear).toHaveBeenCalled();
|
|
935
|
+
});
|
|
936
|
+
});
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
describe('Error Handling', () => {
|
|
940
|
+
it('throws RBACNotInitializedError when engine not available', async () => {
|
|
941
|
+
// Reset global engine by clearing the module cache
|
|
942
|
+
vi.resetModules();
|
|
943
|
+
|
|
944
|
+
const { getAccessLevel } = await import('./api');
|
|
945
|
+
|
|
946
|
+
await expect(getAccessLevel({
|
|
947
|
+
userId: 'user-123',
|
|
948
|
+
scope: { organisationId: 'org-456' }
|
|
949
|
+
})).rejects.toThrow('RBAC system not initialized');
|
|
950
|
+
});
|
|
951
|
+
});
|
|
441
952
|
});
|