@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.
Files changed (210) hide show
  1. package/dist/{DataTable-GX3XERFJ.js → DataTable-ZQDRE46Q.js} +7 -6
  2. package/dist/{PublicLoadingSpinner-DztrzuJr.d.ts → PublicLoadingSpinner-Bq_-BeK-.d.ts} +1 -1
  3. package/dist/RBACProvider-BO4ilsQB.d.ts +63 -0
  4. package/dist/{UnifiedAuthProvider-w66zSCUf.d.ts → UnifiedAuthProvider-DGQsy-vY.d.ts} +2 -59
  5. package/dist/{api-ETQ6YJ3C.js → api-H5A3H4IR.js} +2 -2
  6. package/dist/{chunk-T3XIA4AJ.js → chunk-5H3C2SWM.js} +14 -16
  7. package/dist/chunk-5H3C2SWM.js.map +1 -0
  8. package/dist/chunk-5SIXIV7R.js +1925 -0
  9. package/dist/chunk-5SIXIV7R.js.map +1 -0
  10. package/dist/chunk-GNTALZV3.js +17 -0
  11. package/dist/chunk-GNTALZV3.js.map +1 -0
  12. package/dist/{chunk-C5G2A4PO.js → chunk-GWSBHC4J.js} +6 -6
  13. package/dist/{chunk-XJK2J4N6.js → chunk-HD7PYDUV.js} +4 -6
  14. package/dist/{chunk-XJK2J4N6.js.map → chunk-HD7PYDUV.js.map} +1 -1
  15. package/dist/{chunk-TGDCLPP2.js → chunk-HXX35Q2M.js} +6 -21
  16. package/dist/chunk-HXX35Q2M.js.map +1 -0
  17. package/dist/{chunk-5EL3KHOQ.js → chunk-K6B7BLSE.js} +2 -2
  18. package/dist/{chunk-GSNM5D6H.js → chunk-M4RW7PIP.js} +4 -4
  19. package/dist/{chunk-U6JDHVC2.js → chunk-PVMYVQSM.js} +6 -8
  20. package/dist/{chunk-U6JDHVC2.js.map → chunk-PVMYVQSM.js.map} +1 -1
  21. package/dist/{chunk-6CR3MRZN.js → chunk-QKHFMQ5R.js} +372 -11
  22. package/dist/{chunk-6CR3MRZN.js.map → chunk-QKHFMQ5R.js.map} +1 -1
  23. package/dist/chunk-QVYBYGT2.js +428 -0
  24. package/dist/chunk-QVYBYGT2.js.map +1 -0
  25. package/dist/{chunk-OEGRKULD.js → chunk-WJARTBCT.js} +56 -1
  26. package/dist/chunk-WJARTBCT.js.map +1 -0
  27. package/dist/components.d.ts +4 -3
  28. package/dist/components.js +16 -162
  29. package/dist/components.js.map +1 -1
  30. package/dist/hooks.d.ts +2 -2
  31. package/dist/hooks.js +7 -9
  32. package/dist/hooks.js.map +1 -1
  33. package/dist/index.d.ts +8 -6
  34. package/dist/index.js +152 -17
  35. package/dist/index.js.map +1 -1
  36. package/dist/providers.d.ts +3 -2
  37. package/dist/providers.js +6 -12
  38. package/dist/rbac/index.d.ts +167 -98
  39. package/dist/rbac/index.js +48 -1881
  40. package/dist/rbac/index.js.map +1 -1
  41. package/dist/styles/core.css +0 -55
  42. package/dist/types.d.ts +2 -2
  43. package/dist/{unified-CM7T0aTK.d.ts → unified-CMPjE_fv.d.ts} +1 -1
  44. package/dist/{usePublicRouteParams-B6i0KtXW.d.ts → usePublicRouteParams-B2OcAsur.d.ts} +1 -1
  45. package/dist/utils.js +12 -14
  46. package/dist/utils.js.map +1 -1
  47. package/docs/api/classes/ErrorBoundary.md +1 -1
  48. package/docs/api/classes/InvalidScopeError.md +73 -0
  49. package/docs/api/classes/MissingUserContextError.md +66 -0
  50. package/docs/api/classes/OrganisationContextRequiredError.md +66 -0
  51. package/docs/api/classes/PermissionDeniedError.md +73 -0
  52. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  53. package/docs/api/classes/RBACAuditManager.md +270 -0
  54. package/docs/api/classes/RBACCache.md +284 -0
  55. package/docs/api/classes/RBACEngine.md +141 -0
  56. package/docs/api/classes/RBACError.md +76 -0
  57. package/docs/api/classes/RBACNotInitializedError.md +66 -0
  58. package/docs/api/classes/SecureSupabaseClient.md +135 -0
  59. package/docs/api/interfaces/AggregateConfig.md +1 -1
  60. package/docs/api/interfaces/ButtonProps.md +1 -1
  61. package/docs/api/interfaces/CardProps.md +1 -1
  62. package/docs/api/interfaces/ColorPalette.md +1 -1
  63. package/docs/api/interfaces/ColorShade.md +1 -1
  64. package/docs/api/interfaces/DataAccessRecord.md +96 -0
  65. package/docs/api/interfaces/DataTableAction.md +1 -1
  66. package/docs/api/interfaces/DataTableColumn.md +1 -1
  67. package/docs/api/interfaces/DataTableProps.md +1 -1
  68. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  69. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  70. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +235 -0
  71. package/docs/api/interfaces/EventContextType.md +1 -1
  72. package/docs/api/interfaces/EventLogoProps.md +1 -1
  73. package/docs/api/interfaces/EventProviderProps.md +1 -1
  74. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  75. package/docs/api/interfaces/FileUploadProps.md +1 -1
  76. package/docs/api/interfaces/FooterProps.md +1 -1
  77. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  78. package/docs/api/interfaces/InputProps.md +1 -1
  79. package/docs/api/interfaces/LabelProps.md +1 -1
  80. package/docs/api/interfaces/LoginFormProps.md +1 -1
  81. package/docs/api/interfaces/NavigationAccessRecord.md +107 -0
  82. package/docs/api/interfaces/NavigationContextType.md +164 -0
  83. package/docs/api/interfaces/NavigationGuardProps.md +139 -0
  84. package/docs/api/interfaces/NavigationItem.md +1 -1
  85. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  86. package/docs/api/interfaces/NavigationProviderProps.md +117 -0
  87. package/docs/api/interfaces/Organisation.md +1 -1
  88. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  89. package/docs/api/interfaces/OrganisationMembership.md +2 -2
  90. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  91. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  92. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  93. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  94. package/docs/api/interfaces/PageAccessRecord.md +85 -0
  95. package/docs/api/interfaces/PagePermissionContextType.md +140 -0
  96. package/docs/api/interfaces/PagePermissionGuardProps.md +153 -0
  97. package/docs/api/interfaces/PagePermissionProviderProps.md +119 -0
  98. package/docs/api/interfaces/PaletteData.md +1 -1
  99. package/docs/api/interfaces/PermissionEnforcerProps.md +153 -0
  100. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  101. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  102. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  103. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  104. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  105. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  106. package/docs/api/interfaces/RBACConfig.md +99 -0
  107. package/docs/api/interfaces/RBACContextType.md +474 -0
  108. package/docs/api/interfaces/RBACLogger.md +112 -0
  109. package/docs/api/interfaces/RBACProviderProps.md +107 -0
  110. package/docs/api/interfaces/RoleBasedRouterContextType.md +151 -0
  111. package/docs/api/interfaces/RoleBasedRouterProps.md +156 -0
  112. package/docs/api/interfaces/RouteAccessRecord.md +107 -0
  113. package/docs/api/interfaces/RouteConfig.md +121 -0
  114. package/docs/api/interfaces/SecureDataContextType.md +168 -0
  115. package/docs/api/interfaces/SecureDataProviderProps.md +132 -0
  116. package/docs/api/interfaces/StorageConfig.md +1 -1
  117. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  118. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  119. package/docs/api/interfaces/StorageListOptions.md +1 -1
  120. package/docs/api/interfaces/StorageListResult.md +1 -1
  121. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  122. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  123. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  124. package/docs/api/interfaces/StyleImport.md +1 -1
  125. package/docs/api/interfaces/ToastActionElement.md +1 -1
  126. package/docs/api/interfaces/ToastProps.md +1 -1
  127. package/docs/api/interfaces/UnifiedAuthContextType.md +85 -85
  128. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  129. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  130. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  131. package/docs/api/interfaces/UsePublicEventLogoOptions.md +1 -1
  132. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  133. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  134. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  135. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  136. package/docs/api/interfaces/UserEventAccess.md +11 -11
  137. package/docs/api/interfaces/UserMenuProps.md +1 -1
  138. package/docs/api/interfaces/UserProfile.md +1 -1
  139. package/docs/api/modules.md +2244 -3
  140. package/docs/migration-guide.md +43 -18
  141. package/docs/styles/README.md +187 -98
  142. package/docs/usage.md +32 -7
  143. package/package.json +2 -2
  144. package/src/components/Footer/Footer.test.tsx +482 -0
  145. package/src/components/Form/Form.test.tsx +1158 -0
  146. package/src/components/Header/Header.test.tsx +582 -0
  147. package/src/components/Header/Header.tsx +1 -1
  148. package/src/components/InactivityWarningModal/InactivityWarningModal.test.tsx +489 -0
  149. package/src/components/Input/Input.test.tsx +466 -0
  150. package/src/components/LoadingSpinner/LoadingSpinner.test.tsx +450 -0
  151. package/src/components/LoginForm/LoginForm.test.tsx +816 -0
  152. package/src/components/NavigationMenu/NavigationMenu.test.tsx +883 -0
  153. package/src/components/OrganisationSelector/OrganisationSelector.test.tsx +748 -0
  154. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +891 -0
  155. package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +475 -0
  156. package/src/components/PasswordReset/PasswordChangeForm.test.tsx +621 -0
  157. package/src/components/PasswordReset/PasswordResetForm.test.tsx +605 -0
  158. package/src/components/Select/Select.test.tsx +948 -0
  159. package/src/components/SuperAdminGuard.tsx +1 -1
  160. package/src/components/Toast/Toast.test.tsx +586 -0
  161. package/src/components/Tooltip/Tooltip.test.tsx +852 -0
  162. package/src/components/UserMenu/UserMenu.test.tsx +702 -0
  163. package/src/components/UserMenu/UserMenu.tsx +2 -2
  164. package/src/hooks/useDebounce.test.ts +375 -0
  165. package/src/hooks/useOrganisationPermissions.test.ts +528 -0
  166. package/src/hooks/useOrganisationSecurity.test.ts +734 -0
  167. package/src/hooks/usePermissionCache.test.ts +542 -0
  168. package/src/hooks/usePermissionCache.ts +1 -1
  169. package/src/index.ts +2 -3
  170. package/src/providers/UnifiedAuthProvider.tsx +2 -2
  171. package/src/providers/index.ts +3 -1
  172. package/src/rbac/__tests__/integration.test.tsx +218 -0
  173. package/src/rbac/api.test.ts +952 -0
  174. package/src/rbac/components/__tests__/NavigationGuard.test.tsx +843 -0
  175. package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +1007 -0
  176. package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +806 -0
  177. package/src/rbac/components/__tests__/RoleBasedRouter.test.tsx +741 -0
  178. package/src/rbac/hooks/index.ts +21 -0
  179. package/src/rbac/hooks/useCan.test.ts +461 -0
  180. package/src/rbac/hooks/usePermissions.test.ts +364 -0
  181. package/src/rbac/hooks/usePermissions.ts +567 -0
  182. package/src/rbac/hooks/useRBAC.simple.test.ts +90 -0
  183. package/src/rbac/hooks/useRBAC.test.ts +551 -0
  184. package/src/{hooks → rbac/hooks}/useRBAC.ts +7 -7
  185. package/src/rbac/index.ts +5 -10
  186. package/src/{providers → rbac/providers}/RBACProvider.tsx +6 -6
  187. package/src/rbac/providers/__tests__/RBACProvider.test.tsx +687 -0
  188. package/src/rbac/providers/index.ts +11 -0
  189. package/src/styles/core.css +0 -55
  190. package/src/utils/formatDate.test.ts +241 -0
  191. package/dist/chunk-AUE24LVR.js +0 -268
  192. package/dist/chunk-AUE24LVR.js.map +0 -1
  193. package/dist/chunk-COBPIXXQ.js +0 -379
  194. package/dist/chunk-COBPIXXQ.js.map +0 -1
  195. package/dist/chunk-OEGRKULD.js.map +0 -1
  196. package/dist/chunk-OYRY44Q2.js +0 -62
  197. package/dist/chunk-OYRY44Q2.js.map +0 -1
  198. package/dist/chunk-T3XIA4AJ.js.map +0 -1
  199. package/dist/chunk-TGDCLPP2.js.map +0 -1
  200. package/src/components/RBAC/PagePermissionGuard.tsx +0 -287
  201. package/src/components/RBAC/RBACGuard.tsx +0 -143
  202. package/src/components/RBAC/RBACProvider.tsx +0 -186
  203. package/src/components/RBAC/RoleBasedContent.tsx +0 -129
  204. package/src/components/RBAC/index.ts +0 -23
  205. package/src/rbac/hooks.ts +0 -570
  206. /package/dist/{DataTable-GX3XERFJ.js.map → DataTable-ZQDRE46Q.js.map} +0 -0
  207. /package/dist/{api-ETQ6YJ3C.js.map → api-H5A3H4IR.js.map} +0 -0
  208. /package/dist/{chunk-C5G2A4PO.js.map → chunk-GWSBHC4J.js.map} +0 -0
  209. /package/dist/{chunk-5EL3KHOQ.js.map → chunk-K6B7BLSE.js.map} +0 -0
  210. /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
+ });