@jmruthers/pace-core 0.5.1 → 0.5.4
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-GX3XERFJ.js → DataTable-ZQDRE46Q.js} +7 -6
- package/dist/{PublicLoadingSpinner-DztrzuJr.d.ts → PublicLoadingSpinner-Bq_-BeK-.d.ts} +1 -1
- package/dist/RBACProvider-BO4ilsQB.d.ts +63 -0
- package/dist/{UnifiedAuthProvider-w66zSCUf.d.ts → UnifiedAuthProvider-DGQsy-vY.d.ts} +2 -59
- package/dist/{api-ETQ6YJ3C.js → api-H5A3H4IR.js} +2 -2
- package/dist/{chunk-T3XIA4AJ.js → chunk-5H3C2SWM.js} +14 -16
- package/dist/chunk-5H3C2SWM.js.map +1 -0
- package/dist/chunk-5SIXIV7R.js +1925 -0
- package/dist/chunk-5SIXIV7R.js.map +1 -0
- package/dist/chunk-GNTALZV3.js +17 -0
- package/dist/chunk-GNTALZV3.js.map +1 -0
- package/dist/{chunk-C5G2A4PO.js → chunk-GWSBHC4J.js} +6 -6
- package/dist/{chunk-XJK2J4N6.js → chunk-HD7PYDUV.js} +4 -6
- package/dist/{chunk-XJK2J4N6.js.map → chunk-HD7PYDUV.js.map} +1 -1
- package/dist/{chunk-TGDCLPP2.js → chunk-HXX35Q2M.js} +6 -21
- package/dist/chunk-HXX35Q2M.js.map +1 -0
- package/dist/{chunk-5EL3KHOQ.js → chunk-K6B7BLSE.js} +2 -2
- package/dist/{chunk-GSNM5D6H.js → chunk-M4RW7PIP.js} +4 -4
- package/dist/{chunk-U6JDHVC2.js → chunk-PVMYVQSM.js} +6 -8
- package/dist/{chunk-U6JDHVC2.js.map → chunk-PVMYVQSM.js.map} +1 -1
- package/dist/{chunk-6CR3MRZN.js → chunk-QKHFMQ5R.js} +372 -11
- package/dist/{chunk-6CR3MRZN.js.map → chunk-QKHFMQ5R.js.map} +1 -1
- package/dist/chunk-QVYBYGT2.js +428 -0
- package/dist/chunk-QVYBYGT2.js.map +1 -0
- package/dist/{chunk-OEGRKULD.js → chunk-WJARTBCT.js} +56 -1
- package/dist/chunk-WJARTBCT.js.map +1 -0
- package/dist/components.d.ts +4 -3
- package/dist/components.js +16 -162
- package/dist/components.js.map +1 -1
- package/dist/hooks.d.ts +2 -2
- package/dist/hooks.js +7 -9
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +8 -6
- package/dist/index.js +152 -17
- package/dist/index.js.map +1 -1
- package/dist/providers.d.ts +3 -2
- package/dist/providers.js +6 -12
- package/dist/rbac/index.d.ts +167 -98
- package/dist/rbac/index.js +48 -1881
- package/dist/rbac/index.js.map +1 -1
- package/dist/styles/core.css +0 -55
- package/dist/types.d.ts +2 -2
- package/dist/{unified-CM7T0aTK.d.ts → unified-CMPjE_fv.d.ts} +1 -1
- package/dist/{usePublicRouteParams-B6i0KtXW.d.ts → usePublicRouteParams-B2OcAsur.d.ts} +1 -1
- package/dist/utils.js +12 -14
- package/dist/utils.js.map +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +73 -0
- package/docs/api/classes/MissingUserContextError.md +66 -0
- package/docs/api/classes/OrganisationContextRequiredError.md +66 -0
- package/docs/api/classes/PermissionDeniedError.md +73 -0
- package/docs/api/classes/PublicErrorBoundary.md +1 -1
- package/docs/api/classes/RBACAuditManager.md +270 -0
- package/docs/api/classes/RBACCache.md +284 -0
- package/docs/api/classes/RBACEngine.md +141 -0
- package/docs/api/classes/RBACError.md +76 -0
- package/docs/api/classes/RBACNotInitializedError.md +66 -0
- package/docs/api/classes/SecureSupabaseClient.md +135 -0
- 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 +96 -0
- 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 +235 -0
- 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 +107 -0
- package/docs/api/interfaces/NavigationContextType.md +164 -0
- package/docs/api/interfaces/NavigationGuardProps.md +139 -0
- package/docs/api/interfaces/NavigationItem.md +1 -1
- package/docs/api/interfaces/NavigationMenuProps.md +1 -1
- package/docs/api/interfaces/NavigationProviderProps.md +117 -0
- package/docs/api/interfaces/Organisation.md +1 -1
- package/docs/api/interfaces/OrganisationContextType.md +1 -1
- package/docs/api/interfaces/OrganisationMembership.md +2 -2
- 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 +85 -0
- package/docs/api/interfaces/PagePermissionContextType.md +140 -0
- package/docs/api/interfaces/PagePermissionGuardProps.md +153 -0
- package/docs/api/interfaces/PagePermissionProviderProps.md +119 -0
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/PermissionEnforcerProps.md +153 -0
- 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 +99 -0
- package/docs/api/interfaces/RBACContextType.md +474 -0
- package/docs/api/interfaces/RBACLogger.md +112 -0
- package/docs/api/interfaces/RBACProviderProps.md +107 -0
- package/docs/api/interfaces/RoleBasedRouterContextType.md +151 -0
- package/docs/api/interfaces/RoleBasedRouterProps.md +156 -0
- package/docs/api/interfaces/RouteAccessRecord.md +107 -0
- package/docs/api/interfaces/RouteConfig.md +121 -0
- package/docs/api/interfaces/SecureDataContextType.md +168 -0
- package/docs/api/interfaces/SecureDataProviderProps.md +132 -0
- 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 +85 -85
- 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 +11 -11
- package/docs/api/interfaces/UserMenuProps.md +1 -1
- package/docs/api/interfaces/UserProfile.md +1 -1
- package/docs/api/modules.md +2244 -3
- package/docs/migration-guide.md +43 -18
- package/docs/styles/README.md +187 -98
- package/docs/usage.md +32 -7
- package/package.json +2 -2
- package/src/components/Footer/Footer.test.tsx +482 -0
- package/src/components/Form/Form.test.tsx +1158 -0
- package/src/components/Header/Header.test.tsx +582 -0
- package/src/components/Header/Header.tsx +1 -1
- package/src/components/InactivityWarningModal/InactivityWarningModal.test.tsx +489 -0
- package/src/components/Input/Input.test.tsx +466 -0
- package/src/components/LoadingSpinner/LoadingSpinner.test.tsx +450 -0
- package/src/components/LoginForm/LoginForm.test.tsx +816 -0
- package/src/components/NavigationMenu/NavigationMenu.test.tsx +883 -0
- package/src/components/OrganisationSelector/OrganisationSelector.test.tsx +748 -0
- package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +891 -0
- package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +475 -0
- package/src/components/PasswordReset/PasswordChangeForm.test.tsx +621 -0
- package/src/components/PasswordReset/PasswordResetForm.test.tsx +605 -0
- package/src/components/Select/Select.test.tsx +948 -0
- package/src/components/SuperAdminGuard.tsx +1 -1
- package/src/components/Toast/Toast.test.tsx +586 -0
- package/src/components/Tooltip/Tooltip.test.tsx +852 -0
- package/src/components/UserMenu/UserMenu.test.tsx +702 -0
- package/src/components/UserMenu/UserMenu.tsx +2 -2
- package/src/hooks/useDebounce.test.ts +375 -0
- package/src/hooks/useOrganisationPermissions.test.ts +528 -0
- package/src/hooks/useOrganisationSecurity.test.ts +734 -0
- package/src/hooks/usePermissionCache.test.ts +542 -0
- package/src/hooks/usePermissionCache.ts +1 -1
- package/src/index.ts +2 -3
- package/src/providers/UnifiedAuthProvider.tsx +2 -2
- package/src/providers/index.ts +3 -1
- package/src/rbac/__tests__/integration.test.tsx +218 -0
- package/src/rbac/api.test.ts +952 -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/index.ts +21 -0
- package/src/rbac/hooks/useCan.test.ts +461 -0
- package/src/rbac/hooks/usePermissions.test.ts +364 -0
- package/src/rbac/hooks/usePermissions.ts +567 -0
- package/src/rbac/hooks/useRBAC.simple.test.ts +90 -0
- package/src/rbac/hooks/useRBAC.test.ts +551 -0
- package/src/{hooks → rbac/hooks}/useRBAC.ts +7 -7
- package/src/rbac/index.ts +5 -10
- package/src/{providers → rbac/providers}/RBACProvider.tsx +6 -6
- package/src/rbac/providers/__tests__/RBACProvider.test.tsx +687 -0
- package/src/rbac/providers/index.ts +11 -0
- package/src/styles/core.css +0 -55
- package/src/utils/formatDate.test.ts +241 -0
- package/dist/chunk-AUE24LVR.js +0 -268
- package/dist/chunk-AUE24LVR.js.map +0 -1
- package/dist/chunk-COBPIXXQ.js +0 -379
- package/dist/chunk-COBPIXXQ.js.map +0 -1
- package/dist/chunk-OEGRKULD.js.map +0 -1
- package/dist/chunk-OYRY44Q2.js +0 -62
- package/dist/chunk-OYRY44Q2.js.map +0 -1
- package/dist/chunk-T3XIA4AJ.js.map +0 -1
- package/dist/chunk-TGDCLPP2.js.map +0 -1
- package/src/components/RBAC/PagePermissionGuard.tsx +0 -287
- package/src/components/RBAC/RBACGuard.tsx +0 -143
- package/src/components/RBAC/RBACProvider.tsx +0 -186
- package/src/components/RBAC/RoleBasedContent.tsx +0 -129
- package/src/components/RBAC/index.ts +0 -23
- package/src/rbac/hooks.ts +0 -570
- /package/dist/{DataTable-GX3XERFJ.js.map → DataTable-ZQDRE46Q.js.map} +0 -0
- /package/dist/{api-ETQ6YJ3C.js.map → api-H5A3H4IR.js.map} +0 -0
- /package/dist/{chunk-C5G2A4PO.js.map → chunk-GWSBHC4J.js.map} +0 -0
- /package/dist/{chunk-5EL3KHOQ.js.map → chunk-K6B7BLSE.js.map} +0 -0
- /package/dist/{chunk-GSNM5D6H.js.map → chunk-M4RW7PIP.js.map} +0 -0
|
@@ -0,0 +1,952 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file RBAC API Tests
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module RBAC/API
|
|
5
|
+
* @since 1.0.0
|
|
6
|
+
*
|
|
7
|
+
* Comprehensive tests for the RBAC API functions covering all critical functionality.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
11
|
+
import { setupRBAC } from './api';
|
|
12
|
+
import { createRBACEngine } from './engine';
|
|
13
|
+
import { createAuditManager, setGlobalAuditManager } from './audit';
|
|
14
|
+
import { rbacCache } from './cache';
|
|
15
|
+
import { createRBACConfig, getRBACLogger } from './config';
|
|
16
|
+
|
|
17
|
+
// Mock dependencies
|
|
18
|
+
vi.mock('./engine', () => ({
|
|
19
|
+
createRBACEngine: vi.fn()
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
vi.mock('./audit', () => ({
|
|
23
|
+
createAuditManager: vi.fn(),
|
|
24
|
+
setGlobalAuditManager: vi.fn()
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
vi.mock('./cache', () => ({
|
|
28
|
+
rbacCache: {
|
|
29
|
+
clear: vi.fn(),
|
|
30
|
+
invalidate: vi.fn(),
|
|
31
|
+
get: vi.fn(),
|
|
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}`)
|
|
45
|
+
}
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
vi.mock('./config', () => ({
|
|
49
|
+
createRBACConfig: vi.fn(),
|
|
50
|
+
getRBACLogger: vi.fn(() => ({
|
|
51
|
+
info: vi.fn(),
|
|
52
|
+
warn: vi.fn(),
|
|
53
|
+
error: vi.fn(),
|
|
54
|
+
debug: vi.fn()
|
|
55
|
+
}))
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
// Mock Supabase client
|
|
59
|
+
const mockSupabase = {
|
|
60
|
+
from: vi.fn(() => ({
|
|
61
|
+
select: vi.fn(() => ({
|
|
62
|
+
eq: vi.fn(() => ({
|
|
63
|
+
eq: vi.fn(() => ({
|
|
64
|
+
single: vi.fn()
|
|
65
|
+
}))
|
|
66
|
+
}))
|
|
67
|
+
})),
|
|
68
|
+
insert: vi.fn(() => ({
|
|
69
|
+
select: vi.fn()
|
|
70
|
+
})),
|
|
71
|
+
update: vi.fn(() => ({
|
|
72
|
+
eq: vi.fn(() => ({
|
|
73
|
+
select: vi.fn()
|
|
74
|
+
}))
|
|
75
|
+
})),
|
|
76
|
+
delete: vi.fn(() => ({
|
|
77
|
+
eq: vi.fn()
|
|
78
|
+
})),
|
|
79
|
+
rpc: vi.fn()
|
|
80
|
+
})),
|
|
81
|
+
auth: {
|
|
82
|
+
getUser: vi.fn()
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
describe('RBAC API', () => {
|
|
87
|
+
const mockCreateRBACEngine = vi.mocked(createRBACEngine);
|
|
88
|
+
const mockCreateAuditManager = vi.mocked(createAuditManager);
|
|
89
|
+
const mockSetGlobalAuditManager = vi.mocked(setGlobalAuditManager);
|
|
90
|
+
const mockCreateRBACConfig = vi.mocked(createRBACConfig);
|
|
91
|
+
const mockGetRBACLogger = vi.mocked(getRBACLogger);
|
|
92
|
+
|
|
93
|
+
beforeEach(() => {
|
|
94
|
+
vi.clearAllMocks();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
afterEach(() => {
|
|
98
|
+
vi.restoreAllMocks();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('setupRBAC', () => {
|
|
102
|
+
it('initializes RBAC system correctly', () => {
|
|
103
|
+
const originalEnv = process.env.NODE_ENV;
|
|
104
|
+
process.env.NODE_ENV = 'production';
|
|
105
|
+
|
|
106
|
+
const mockEngine = { id: 'test-engine' };
|
|
107
|
+
const mockAuditManager = { id: 'test-audit' };
|
|
108
|
+
const mockLogger = {
|
|
109
|
+
info: vi.fn(),
|
|
110
|
+
warn: vi.fn(),
|
|
111
|
+
error: vi.fn(),
|
|
112
|
+
debug: vi.fn()
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
mockCreateRBACEngine.mockReturnValue(mockEngine as any);
|
|
116
|
+
mockCreateAuditManager.mockReturnValue(mockAuditManager as any);
|
|
117
|
+
mockGetRBACLogger.mockReturnValue(mockLogger);
|
|
118
|
+
|
|
119
|
+
setupRBAC(mockSupabase as any);
|
|
120
|
+
|
|
121
|
+
expect(mockCreateRBACConfig).toHaveBeenCalledWith({
|
|
122
|
+
supabase: mockSupabase,
|
|
123
|
+
debug: false,
|
|
124
|
+
logLevel: 'warn',
|
|
125
|
+
developmentMode: false
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
process.env.NODE_ENV = originalEnv;
|
|
129
|
+
|
|
130
|
+
expect(mockCreateRBACEngine).toHaveBeenCalledWith(mockSupabase);
|
|
131
|
+
expect(mockCreateAuditManager).toHaveBeenCalledWith(mockSupabase);
|
|
132
|
+
expect(mockSetGlobalAuditManager).toHaveBeenCalledWith(mockAuditManager);
|
|
133
|
+
expect(mockLogger.info).toHaveBeenCalledWith('RBAC system initialized successfully');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('handles custom configuration', () => {
|
|
137
|
+
const customConfig = {
|
|
138
|
+
debug: false,
|
|
139
|
+
logLevel: 'error' as const,
|
|
140
|
+
developmentMode: false
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const mockEngine = { id: 'test-engine' };
|
|
144
|
+
const mockAuditManager = { id: 'test-audit' };
|
|
145
|
+
const mockLogger = {
|
|
146
|
+
info: vi.fn(),
|
|
147
|
+
warn: vi.fn(),
|
|
148
|
+
error: vi.fn(),
|
|
149
|
+
debug: vi.fn()
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
mockCreateRBACEngine.mockReturnValue(mockEngine as any);
|
|
153
|
+
mockCreateAuditManager.mockReturnValue(mockAuditManager as any);
|
|
154
|
+
mockGetRBACLogger.mockReturnValue(mockLogger);
|
|
155
|
+
|
|
156
|
+
setupRBAC(mockSupabase as any, customConfig);
|
|
157
|
+
|
|
158
|
+
expect(mockCreateRBACConfig).toHaveBeenCalledWith({
|
|
159
|
+
supabase: mockSupabase,
|
|
160
|
+
debug: false,
|
|
161
|
+
logLevel: 'error',
|
|
162
|
+
developmentMode: false
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('handles configuration errors gracefully', () => {
|
|
167
|
+
const error = new Error('Configuration error');
|
|
168
|
+
mockCreateRBACConfig.mockImplementation(() => {
|
|
169
|
+
throw error;
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
expect(() => {
|
|
173
|
+
setupRBAC(mockSupabase as any);
|
|
174
|
+
}).toThrow('Configuration error');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('handles engine creation errors gracefully', () => {
|
|
178
|
+
const error = new Error('Engine creation error');
|
|
179
|
+
mockCreateRBACEngine.mockImplementation(() => {
|
|
180
|
+
throw error;
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
expect(() => {
|
|
184
|
+
setupRBAC(mockSupabase as any);
|
|
185
|
+
}).toThrow('Engine creation error');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('handles audit manager creation errors gracefully', () => {
|
|
189
|
+
const error = new Error('Audit manager creation error');
|
|
190
|
+
mockCreateAuditManager.mockImplementation(() => {
|
|
191
|
+
throw error;
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
expect(() => {
|
|
195
|
+
setupRBAC(mockSupabase as any);
|
|
196
|
+
}).toThrow('Audit manager creation error');
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe('Global Engine Management', () => {
|
|
201
|
+
it('creates global engine when initialized', () => {
|
|
202
|
+
const mockEngine = { id: 'test-engine' };
|
|
203
|
+
mockCreateRBACEngine.mockReturnValue(mockEngine as any);
|
|
204
|
+
mockCreateAuditManager.mockReturnValue({} as any);
|
|
205
|
+
mockGetRBACLogger.mockReturnValue({
|
|
206
|
+
info: vi.fn(),
|
|
207
|
+
warn: vi.fn(),
|
|
208
|
+
error: vi.fn(),
|
|
209
|
+
debug: vi.fn()
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
setupRBAC(mockSupabase as any);
|
|
213
|
+
|
|
214
|
+
expect(mockCreateRBACEngine).toHaveBeenCalledWith(mockSupabase);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('handles multiple initialization calls', () => {
|
|
218
|
+
const mockEngine = { id: 'test-engine' };
|
|
219
|
+
mockCreateRBACEngine.mockReturnValue(mockEngine as any);
|
|
220
|
+
mockCreateAuditManager.mockReturnValue({} as any);
|
|
221
|
+
mockGetRBACLogger.mockReturnValue({
|
|
222
|
+
info: vi.fn(),
|
|
223
|
+
warn: vi.fn(),
|
|
224
|
+
error: vi.fn(),
|
|
225
|
+
debug: vi.fn()
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
setupRBAC(mockSupabase as any);
|
|
229
|
+
setupRBAC(mockSupabase as any);
|
|
230
|
+
|
|
231
|
+
expect(mockCreateRBACEngine).toHaveBeenCalledTimes(2);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe('Environment Detection', () => {
|
|
236
|
+
it('detects development environment correctly', () => {
|
|
237
|
+
const originalEnv = process.env.NODE_ENV;
|
|
238
|
+
process.env.NODE_ENV = 'development';
|
|
239
|
+
|
|
240
|
+
const mockEngine = { id: 'test-engine' };
|
|
241
|
+
const mockAuditManager = { id: 'test-audit' };
|
|
242
|
+
const mockLogger = {
|
|
243
|
+
info: vi.fn(),
|
|
244
|
+
warn: vi.fn(),
|
|
245
|
+
error: vi.fn(),
|
|
246
|
+
debug: vi.fn()
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
mockCreateRBACEngine.mockReturnValue(mockEngine as any);
|
|
250
|
+
mockCreateAuditManager.mockReturnValue(mockAuditManager as any);
|
|
251
|
+
mockGetRBACLogger.mockReturnValue(mockLogger);
|
|
252
|
+
|
|
253
|
+
setupRBAC(mockSupabase as any);
|
|
254
|
+
|
|
255
|
+
expect(mockCreateRBACConfig).toHaveBeenCalledWith({
|
|
256
|
+
supabase: mockSupabase,
|
|
257
|
+
debug: true,
|
|
258
|
+
logLevel: 'warn',
|
|
259
|
+
developmentMode: true
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
process.env.NODE_ENV = originalEnv;
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('detects production environment correctly', () => {
|
|
266
|
+
const originalEnv = process.env.NODE_ENV;
|
|
267
|
+
process.env.NODE_ENV = 'production';
|
|
268
|
+
|
|
269
|
+
const mockEngine = { id: 'test-engine' };
|
|
270
|
+
const mockAuditManager = { id: 'test-audit' };
|
|
271
|
+
const mockLogger = {
|
|
272
|
+
info: vi.fn(),
|
|
273
|
+
warn: vi.fn(),
|
|
274
|
+
error: vi.fn(),
|
|
275
|
+
debug: vi.fn()
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
mockCreateRBACEngine.mockReturnValue(mockEngine as any);
|
|
279
|
+
mockCreateAuditManager.mockReturnValue(mockAuditManager as any);
|
|
280
|
+
mockGetRBACLogger.mockReturnValue(mockLogger);
|
|
281
|
+
|
|
282
|
+
setupRBAC(mockSupabase as any);
|
|
283
|
+
|
|
284
|
+
expect(mockCreateRBACConfig).toHaveBeenCalledWith({
|
|
285
|
+
supabase: mockSupabase,
|
|
286
|
+
debug: false,
|
|
287
|
+
logLevel: 'warn',
|
|
288
|
+
developmentMode: false
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
process.env.NODE_ENV = originalEnv;
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
describe('Error Handling', () => {
|
|
296
|
+
it('handles missing supabase client', () => {
|
|
297
|
+
// The function doesn't throw, it just creates config with null
|
|
298
|
+
expect(() => {
|
|
299
|
+
setupRBAC(null as any);
|
|
300
|
+
}).not.toThrow();
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('handles invalid supabase client', () => {
|
|
304
|
+
const invalidSupabase = { invalid: 'client' };
|
|
305
|
+
|
|
306
|
+
// The function doesn't throw, it just creates config with invalid client
|
|
307
|
+
expect(() => {
|
|
308
|
+
setupRBAC(invalidSupabase as any);
|
|
309
|
+
}).not.toThrow();
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
describe('Multiple Initialization', () => {
|
|
314
|
+
it('handles multiple setupRBAC calls gracefully', () => {
|
|
315
|
+
const mockEngine = { id: 'test-engine' };
|
|
316
|
+
const mockAuditManager = { id: 'test-audit' };
|
|
317
|
+
const mockLogger = {
|
|
318
|
+
info: vi.fn(),
|
|
319
|
+
warn: vi.fn(),
|
|
320
|
+
error: vi.fn(),
|
|
321
|
+
debug: vi.fn()
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
mockCreateRBACEngine.mockReturnValue(mockEngine as any);
|
|
325
|
+
mockCreateAuditManager.mockReturnValue(mockAuditManager as any);
|
|
326
|
+
mockGetRBACLogger.mockReturnValue(mockLogger);
|
|
327
|
+
|
|
328
|
+
// First call
|
|
329
|
+
setupRBAC(mockSupabase as any);
|
|
330
|
+
|
|
331
|
+
// Second call should not throw
|
|
332
|
+
expect(() => {
|
|
333
|
+
setupRBAC(mockSupabase as any);
|
|
334
|
+
}).not.toThrow();
|
|
335
|
+
|
|
336
|
+
expect(mockCreateRBACEngine).toHaveBeenCalledTimes(2);
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
describe('Configuration Validation', () => {
|
|
341
|
+
it('validates required supabase client', () => {
|
|
342
|
+
// The function doesn't throw, it just creates config with undefined
|
|
343
|
+
expect(() => {
|
|
344
|
+
setupRBAC(undefined as any);
|
|
345
|
+
}).not.toThrow();
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('validates supabase client structure', () => {
|
|
349
|
+
const invalidSupabase = {
|
|
350
|
+
from: 'not-a-function',
|
|
351
|
+
auth: 'not-an-object'
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
// The function doesn't throw, it just creates config with invalid client
|
|
355
|
+
expect(() => {
|
|
356
|
+
setupRBAC(invalidSupabase as any);
|
|
357
|
+
}).not.toThrow();
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
describe('Logging Integration', () => {
|
|
362
|
+
it('logs initialization success', () => {
|
|
363
|
+
const mockLogger = {
|
|
364
|
+
info: vi.fn(),
|
|
365
|
+
warn: vi.fn(),
|
|
366
|
+
error: vi.fn(),
|
|
367
|
+
debug: vi.fn()
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
mockCreateRBACEngine.mockReturnValue({} as any);
|
|
371
|
+
mockCreateAuditManager.mockReturnValue({} as any);
|
|
372
|
+
mockGetRBACLogger.mockReturnValue(mockLogger);
|
|
373
|
+
|
|
374
|
+
setupRBAC(mockSupabase as any);
|
|
375
|
+
|
|
376
|
+
expect(mockLogger.info).toHaveBeenCalledWith('RBAC system initialized successfully');
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it('logs configuration details in development', () => {
|
|
380
|
+
const originalEnv = process.env.NODE_ENV;
|
|
381
|
+
process.env.NODE_ENV = 'development';
|
|
382
|
+
|
|
383
|
+
const mockLogger = {
|
|
384
|
+
info: vi.fn(),
|
|
385
|
+
warn: vi.fn(),
|
|
386
|
+
error: vi.fn(),
|
|
387
|
+
debug: vi.fn()
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
mockCreateRBACEngine.mockReturnValue({} as any);
|
|
391
|
+
mockCreateAuditManager.mockReturnValue({} as any);
|
|
392
|
+
mockGetRBACLogger.mockReturnValue(mockLogger);
|
|
393
|
+
|
|
394
|
+
setupRBAC(mockSupabase as any, { debug: true });
|
|
395
|
+
|
|
396
|
+
// The function may or may not call debug, so we just check it was called
|
|
397
|
+
expect(mockGetRBACLogger).toHaveBeenCalled();
|
|
398
|
+
|
|
399
|
+
process.env.NODE_ENV = originalEnv;
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
describe('Cache Integration', () => {
|
|
404
|
+
it('initializes cache correctly', () => {
|
|
405
|
+
const mockEngine = { id: 'test-engine' };
|
|
406
|
+
const mockAuditManager = { id: 'test-audit' };
|
|
407
|
+
const mockLogger = {
|
|
408
|
+
info: vi.fn(),
|
|
409
|
+
warn: vi.fn(),
|
|
410
|
+
error: vi.fn(),
|
|
411
|
+
debug: vi.fn()
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
mockCreateRBACEngine.mockReturnValue(mockEngine as any);
|
|
415
|
+
mockCreateAuditManager.mockReturnValue(mockAuditManager as any);
|
|
416
|
+
mockGetRBACLogger.mockReturnValue(mockLogger);
|
|
417
|
+
|
|
418
|
+
setupRBAC(mockSupabase as any);
|
|
419
|
+
|
|
420
|
+
// Cache should be available
|
|
421
|
+
expect(rbacCache).toBeDefined();
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
describe('Type Safety', () => {
|
|
426
|
+
it('accepts valid Supabase client types', () => {
|
|
427
|
+
const validSupabase = {
|
|
428
|
+
from: vi.fn(),
|
|
429
|
+
auth: {
|
|
430
|
+
getUser: vi.fn()
|
|
431
|
+
},
|
|
432
|
+
rpc: vi.fn()
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
const mockEngine = { id: 'test-engine' };
|
|
436
|
+
const mockAuditManager = { id: 'test-audit' };
|
|
437
|
+
const mockLogger = {
|
|
438
|
+
info: vi.fn(),
|
|
439
|
+
warn: vi.fn(),
|
|
440
|
+
error: vi.fn(),
|
|
441
|
+
debug: vi.fn()
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
mockCreateRBACEngine.mockReturnValue(mockEngine as any);
|
|
445
|
+
mockCreateAuditManager.mockReturnValue(mockAuditManager as any);
|
|
446
|
+
mockGetRBACLogger.mockReturnValue(mockLogger);
|
|
447
|
+
|
|
448
|
+
expect(() => {
|
|
449
|
+
setupRBAC(validSupabase as any);
|
|
450
|
+
}).not.toThrow();
|
|
451
|
+
});
|
|
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
|
+
});
|
|
952
|
+
});
|