@jmruthers/pace-core 0.5.114 → 0.5.116

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 (236) hide show
  1. package/dist/{AuthService-CVgsgtaZ.d.ts → AuthService-D4646R4b.d.ts} +9 -4
  2. package/dist/{DataTable-3JRLZXER.js → DataTable-ZOAKQ3SU.js} +10 -9
  3. package/dist/{UnifiedAuthProvider-KZZUO27W.js → UnifiedAuthProvider-YFN7YGVN.js} +4 -3
  4. package/dist/{api-PKU4PUBO.js → api-TNIBJWLM.js} +3 -3
  5. package/dist/{audit-H4YJJF7R.js → audit-T36HM7IM.js} +2 -2
  6. package/dist/{chunk-4OX5PXHX.js → chunk-2GJ5GL77.js} +4 -5
  7. package/dist/chunk-2GJ5GL77.js.map +1 -0
  8. package/dist/{chunk-5YIZFEUQ.js → chunk-2LM4QQGH.js} +31 -35
  9. package/dist/chunk-2LM4QQGH.js.map +1 -0
  10. package/dist/{chunk-3OGQLOJM.js → chunk-3DBFLLLU.js} +30 -1
  11. package/dist/chunk-3DBFLLLU.js.map +1 -0
  12. package/dist/{chunk-KTHLNIMA.js → chunk-ECOVPXYS.js} +13 -62
  13. package/dist/chunk-ECOVPXYS.js.map +1 -0
  14. package/dist/{chunk-OO3V7W4H.js → chunk-KA3PSVNV.js} +87 -40
  15. package/dist/chunk-KA3PSVNV.js.map +1 -0
  16. package/dist/{chunk-HKWQN44G.js → chunk-KMPWND3F.js} +15 -15
  17. package/dist/{chunk-L36JW4KV.js → chunk-LFS45U62.js} +2 -2
  18. package/dist/{chunk-NEONKMTU.js → chunk-LZYHAL7Y.js} +9 -4
  19. package/dist/{chunk-NEONKMTU.js.map → chunk-LZYHAL7Y.js.map} +1 -1
  20. package/dist/{chunk-BUN7NMV7.js → chunk-O3FTRYEU.js} +2 -2
  21. package/dist/{chunk-F6QB26OS.js → chunk-P3PUOL6B.js} +80 -8
  22. package/dist/chunk-P3PUOL6B.js.map +1 -0
  23. package/dist/{chunk-ZPXWJA4H.js → chunk-PHDAXDHB.js} +131 -5
  24. package/dist/chunk-PHDAXDHB.js.map +1 -0
  25. package/dist/chunk-UJI6WSMD.js +201 -0
  26. package/dist/{chunk-5CDJCTOO.js.map → chunk-UJI6WSMD.js.map} +1 -1
  27. package/dist/{chunk-JHWQNJP3.js → chunk-UKZWNQMB.js} +65 -19
  28. package/dist/{chunk-JHWQNJP3.js.map → chunk-UKZWNQMB.js.map} +1 -1
  29. package/dist/{chunk-7H75SHXZ.js → chunk-VN3OOE35.js} +2 -2
  30. package/dist/{chunk-QKIVSZ2O.js → chunk-WP5I5GLN.js} +2 -2
  31. package/dist/components.d.ts +1 -1
  32. package/dist/components.js +12 -11
  33. package/dist/components.js.map +1 -1
  34. package/dist/hooks.d.ts +1 -1
  35. package/dist/hooks.js +10 -9
  36. package/dist/hooks.js.map +1 -1
  37. package/dist/index.d.ts +4 -4
  38. package/dist/index.js +19 -16
  39. package/dist/index.js.map +1 -1
  40. package/dist/providers.d.ts +2 -2
  41. package/dist/providers.js +3 -2
  42. package/dist/rbac/index.d.ts +82 -1
  43. package/dist/rbac/index.js +13 -10
  44. package/dist/{useToast-DRah6K-g.d.ts → useToast-Cs_g32bg.d.ts} +8 -6
  45. package/dist/utils.js +6 -4
  46. package/dist/utils.js.map +1 -1
  47. package/dist/validation.js +3 -1
  48. package/dist/validation.js.map +1 -1
  49. package/docs/README.md +4 -0
  50. package/docs/api/classes/ColumnFactory.md +1 -1
  51. package/docs/api/classes/ErrorBoundary.md +1 -1
  52. package/docs/api/classes/InvalidScopeError.md +1 -1
  53. package/docs/api/classes/MissingUserContextError.md +1 -1
  54. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  55. package/docs/api/classes/PermissionDeniedError.md +1 -1
  56. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  57. package/docs/api/classes/RBACAuditManager.md +35 -12
  58. package/docs/api/classes/RBACCache.md +1 -1
  59. package/docs/api/classes/RBACEngine.md +1 -1
  60. package/docs/api/classes/RBACError.md +1 -1
  61. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  62. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  63. package/docs/api/classes/StorageUtils.md +1 -1
  64. package/docs/api/enums/FileCategory.md +1 -1
  65. package/docs/api/interfaces/AggregateConfig.md +1 -1
  66. package/docs/api/interfaces/ButtonProps.md +1 -1
  67. package/docs/api/interfaces/CardProps.md +1 -1
  68. package/docs/api/interfaces/ColorPalette.md +1 -1
  69. package/docs/api/interfaces/ColorShade.md +1 -1
  70. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  71. package/docs/api/interfaces/DataRecord.md +1 -1
  72. package/docs/api/interfaces/DataTableAction.md +1 -1
  73. package/docs/api/interfaces/DataTableColumn.md +1 -1
  74. package/docs/api/interfaces/DataTableProps.md +1 -1
  75. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  76. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  77. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  78. package/docs/api/interfaces/EventAppRoleData.md +71 -0
  79. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  80. package/docs/api/interfaces/FileMetadata.md +1 -1
  81. package/docs/api/interfaces/FileReference.md +1 -1
  82. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  83. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  84. package/docs/api/interfaces/FileUploadProps.md +1 -1
  85. package/docs/api/interfaces/FooterProps.md +1 -1
  86. package/docs/api/interfaces/GrantEventAppRoleParams.md +122 -0
  87. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  88. package/docs/api/interfaces/InputProps.md +1 -1
  89. package/docs/api/interfaces/LabelProps.md +1 -1
  90. package/docs/api/interfaces/LoginFormProps.md +1 -1
  91. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  92. package/docs/api/interfaces/NavigationContextType.md +1 -1
  93. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  94. package/docs/api/interfaces/NavigationItem.md +1 -1
  95. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  96. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  97. package/docs/api/interfaces/Organisation.md +1 -1
  98. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  99. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  100. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  101. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  102. package/docs/api/interfaces/PaceAppLayoutProps.md +27 -27
  103. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  104. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  105. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  106. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  107. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  108. package/docs/api/interfaces/PaletteData.md +1 -1
  109. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  110. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  111. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  112. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  113. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  114. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  115. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  116. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  117. package/docs/api/interfaces/RBACConfig.md +1 -1
  118. package/docs/api/interfaces/RBACLogger.md +1 -1
  119. package/docs/api/interfaces/RevokeEventAppRoleParams.md +100 -0
  120. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  121. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  122. package/docs/api/interfaces/RoleManagementResult.md +52 -0
  123. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  124. package/docs/api/interfaces/RouteConfig.md +1 -1
  125. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  126. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  127. package/docs/api/interfaces/StorageConfig.md +1 -1
  128. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  129. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  130. package/docs/api/interfaces/StorageListOptions.md +1 -1
  131. package/docs/api/interfaces/StorageListResult.md +1 -1
  132. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  133. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  134. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  135. package/docs/api/interfaces/StyleImport.md +1 -1
  136. package/docs/api/interfaces/SwitchProps.md +1 -1
  137. package/docs/api/interfaces/ToastActionElement.md +1 -1
  138. package/docs/api/interfaces/ToastProps.md +1 -1
  139. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  140. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  141. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  142. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  143. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  144. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  145. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
  146. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  147. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  148. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  149. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  150. package/docs/api/interfaces/UserEventAccess.md +1 -1
  151. package/docs/api/interfaces/UserMenuProps.md +1 -1
  152. package/docs/api/interfaces/UserProfile.md +1 -1
  153. package/docs/api/modules.md +43 -16
  154. package/docs/architecture/rpc-function-standards.md +193 -0
  155. package/package.json +1 -1
  156. package/src/__tests__/TEST_STANDARD.md +244 -2
  157. package/src/components/DataTable/__tests__/a11y.basic.test.tsx +46 -16
  158. package/src/components/DataTable/__tests__/keyboard.test.tsx +276 -217
  159. package/src/components/DataTable/components/DataTableCore.tsx +32 -17
  160. package/src/components/DataTable/components/DataTableToolbar.tsx +3 -2
  161. package/src/components/DataTable/components/EditableRow.tsx +18 -1
  162. package/src/components/DataTable/components/ImportModal.tsx +25 -2
  163. package/src/components/DataTable/components/ViewRowModal.tsx +1 -1
  164. package/src/components/DataTable/components/__tests__/AccessDeniedPage.test.tsx +735 -0
  165. package/src/components/DataTable/components/__tests__/BulkOperationsDropdown.test.tsx +572 -0
  166. package/src/components/DataTable/components/__tests__/ColumnVisibilityDropdown.test.tsx +708 -0
  167. package/src/components/DataTable/components/__tests__/DataTableErrorBoundary.test.tsx +451 -0
  168. package/src/components/DataTable/components/__tests__/DataTableModals.test.tsx +456 -0
  169. package/src/components/DataTable/components/__tests__/EditableRow.test.tsx +454 -0
  170. package/src/components/DataTable/components/__tests__/ExpandButton.test.tsx +462 -0
  171. package/src/components/DataTable/components/__tests__/FilterRow.test.tsx +423 -0
  172. package/src/components/DataTable/components/__tests__/GroupHeader.test.tsx +393 -0
  173. package/src/components/DataTable/components/__tests__/GroupingDropdown.test.tsx +617 -0
  174. package/src/components/DataTable/components/__tests__/ImportModal.test.tsx +734 -0
  175. package/src/components/DataTable/components/__tests__/ViewRowModal.test.tsx +412 -0
  176. package/src/components/DataTable/hooks/useTableHandlers.ts +4 -0
  177. package/src/components/EventSelector/EventSelector.tsx +5 -25
  178. package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +12 -7
  179. package/src/components/PaceAppLayout/PaceAppLayout.tsx +4 -0
  180. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.accessibility.test.tsx +7 -2
  181. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.integration.test.tsx +13 -8
  182. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.performance.test.tsx +109 -100
  183. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.security.test.tsx +18 -13
  184. package/src/components/PaceAppLayout/__tests__/PaceAppLayout.unit.test.tsx +17 -12
  185. package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +2 -0
  186. package/src/components/PaceLoginPage/PaceLoginPage.tsx +11 -1
  187. package/src/components/PasswordReset/PasswordChangeForm.test.tsx +2 -2
  188. package/src/components/ProtectedRoute/ProtectedRoute.test.tsx +648 -0
  189. package/src/components/ProtectedRoute/ProtectedRoute.tsx +10 -7
  190. package/src/components/PublicLayout/__tests__/PublicErrorBoundary.test.tsx +4 -12
  191. package/src/components/Select/Select.tsx +8 -0
  192. package/src/components/Toast/Toast.test.tsx +8 -7
  193. package/src/components/Toast/Toast.tsx +4 -4
  194. package/src/hooks/__tests__/usePublicEvent.simple.test.ts +367 -3
  195. package/src/hooks/__tests__/usePublicFileDisplay.test.ts +916 -0
  196. package/src/hooks/useEventTheme.ts +49 -18
  197. package/src/hooks/usePermissionCache.ts +5 -3
  198. package/src/hooks/useSecureDataAccess.ts +11 -1
  199. package/src/hooks/useToast.ts +11 -12
  200. package/src/providers/services/EventServiceProvider.tsx +15 -8
  201. package/src/rbac/__tests__/cache-invalidation.test.ts +385 -0
  202. package/src/rbac/audit.test.ts +206 -0
  203. package/src/rbac/audit.ts +37 -2
  204. package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +26 -23
  205. package/src/rbac/errors.test.ts +340 -0
  206. package/src/rbac/hooks/index.ts +9 -0
  207. package/src/rbac/hooks/useResolvedScope.test.ts +1063 -0
  208. package/src/rbac/hooks/useRoleManagement.test.ts +908 -0
  209. package/src/rbac/hooks/useRoleManagement.ts +255 -0
  210. package/src/services/AuthService.ts +10 -0
  211. package/src/services/EventService.ts +111 -50
  212. package/src/services/__tests__/AuthService.test.ts +1 -1
  213. package/src/services/__tests__/EventService.test.ts +60 -45
  214. package/src/services/interfaces/IEventService.ts +1 -1
  215. package/src/utils/__tests__/deviceFingerprint.unit.test.ts +320 -0
  216. package/src/utils/__tests__/logger.unit.test.ts +398 -0
  217. package/src/utils/__tests__/validation.unit.test.ts +225 -1
  218. package/src/utils/file-reference.test.ts +214 -0
  219. package/dist/chunk-3OGQLOJM.js.map +0 -1
  220. package/dist/chunk-4OX5PXHX.js.map +0 -1
  221. package/dist/chunk-5CDJCTOO.js +0 -190
  222. package/dist/chunk-5YIZFEUQ.js.map +0 -1
  223. package/dist/chunk-F6QB26OS.js.map +0 -1
  224. package/dist/chunk-KTHLNIMA.js.map +0 -1
  225. package/dist/chunk-OO3V7W4H.js.map +0 -1
  226. package/dist/chunk-ZPXWJA4H.js.map +0 -1
  227. package/src/rbac/audit-enhanced.ts +0 -351
  228. /package/dist/{DataTable-3JRLZXER.js.map → DataTable-ZOAKQ3SU.js.map} +0 -0
  229. /package/dist/{UnifiedAuthProvider-KZZUO27W.js.map → UnifiedAuthProvider-YFN7YGVN.js.map} +0 -0
  230. /package/dist/{api-PKU4PUBO.js.map → api-TNIBJWLM.js.map} +0 -0
  231. /package/dist/{audit-H4YJJF7R.js.map → audit-T36HM7IM.js.map} +0 -0
  232. /package/dist/{chunk-HKWQN44G.js.map → chunk-KMPWND3F.js.map} +0 -0
  233. /package/dist/{chunk-L36JW4KV.js.map → chunk-LFS45U62.js.map} +0 -0
  234. /package/dist/{chunk-BUN7NMV7.js.map → chunk-O3FTRYEU.js.map} +0 -0
  235. /package/dist/{chunk-7H75SHXZ.js.map → chunk-VN3OOE35.js.map} +0 -0
  236. /package/dist/{chunk-QKIVSZ2O.js.map → chunk-WP5I5GLN.js.map} +0 -0
@@ -0,0 +1,908 @@
1
+ /**
2
+ * @file useRoleManagement Hook Tests
3
+ * @package @jmruthers/pace-core
4
+ * @module RBAC/Hooks
5
+ * @since 2.1.0
6
+ *
7
+ * Comprehensive tests for the useRoleManagement hook following TEST_STANDARD.md.
8
+ * Tests focus on behavior: role granting, revoking, loading states, and error handling.
9
+ */
10
+
11
+ import { renderHook, waitFor } from '@testing-library/react';
12
+ import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
13
+ import { useRoleManagement } from './useRoleManagement';
14
+ import type { SupabaseClient } from '@supabase/supabase-js';
15
+ import type { Database } from '../../types/database';
16
+
17
+ // Mock UnifiedAuthProvider
18
+ vi.mock('../../providers/services/UnifiedAuthProvider', () => ({
19
+ useUnifiedAuth: vi.fn(),
20
+ }));
21
+
22
+ import { useUnifiedAuth } from '../../providers/services/UnifiedAuthProvider';
23
+
24
+ // Mock Supabase client
25
+ const createMockSupabaseClient = () => {
26
+ return {
27
+ rpc: vi.fn(),
28
+ from: vi.fn(),
29
+ } as unknown as SupabaseClient<Database>;
30
+ };
31
+
32
+ describe('useRoleManagement Hook', () => {
33
+ const mockUseUnifiedAuth = vi.mocked(useUnifiedAuth);
34
+
35
+ let mockSupabase: SupabaseClient<Database>;
36
+ const mockUser = { id: 'user-123', email: 'test@example.com' };
37
+
38
+ const mockRoleParams = {
39
+ user_id: 'user-456',
40
+ organisation_id: 'org-123',
41
+ event_id: 'event-123',
42
+ app_id: 'app-123',
43
+ role: 'viewer' as const,
44
+ };
45
+
46
+ beforeEach(() => {
47
+ vi.clearAllMocks();
48
+ mockSupabase = createMockSupabaseClient();
49
+
50
+ mockUseUnifiedAuth.mockReturnValue({
51
+ user: mockUser,
52
+ supabase: mockSupabase,
53
+ } as any);
54
+ });
55
+
56
+ // Helper function to set up the from().select().eq().single() chain mock
57
+ const setupFromChain = (roleData: any) => {
58
+ const mockSingle = vi.fn().mockResolvedValue({
59
+ data: roleData,
60
+ error: null,
61
+ });
62
+ const mockEq = vi.fn().mockReturnValue({ single: mockSingle });
63
+ const mockSelect = vi.fn().mockReturnValue({ eq: mockEq });
64
+ const mockFrom = (mockSupabase as any).from;
65
+ mockFrom.mockReturnValue({ select: mockSelect });
66
+ return { mockFrom, mockSelect, mockEq, mockSingle };
67
+ };
68
+
69
+ afterEach(() => {
70
+ vi.clearAllMocks();
71
+ });
72
+
73
+ describe('Initialization', () => {
74
+ it('throws error when supabase client is not available', () => {
75
+ mockUseUnifiedAuth.mockReturnValue({
76
+ user: mockUser,
77
+ supabase: null,
78
+ } as any);
79
+
80
+ // Wrap in try-catch to catch the error during render
81
+ let error: Error | null = null;
82
+ try {
83
+ renderHook(() => useRoleManagement());
84
+ } catch (e) {
85
+ error = e as Error;
86
+ }
87
+
88
+ expect(error).toBeInstanceOf(Error);
89
+ expect(error?.message).toBe(
90
+ 'useRoleManagement requires a Supabase client. Ensure UnifiedAuthProvider is configured.'
91
+ );
92
+ });
93
+
94
+ it('initializes with correct state', () => {
95
+ const { result } = renderHook(() => useRoleManagement());
96
+
97
+ expect(result.current.isLoading).toBe(false);
98
+ expect(result.current.error).toBeNull();
99
+ expect(typeof result.current.revokeEventAppRole).toBe('function');
100
+ expect(typeof result.current.grantEventAppRole).toBe('function');
101
+ expect(typeof result.current.revokeRoleById).toBe('function');
102
+ });
103
+ });
104
+
105
+ describe('revokeEventAppRole', () => {
106
+ it('revokes role successfully', async () => {
107
+ (mockSupabase.rpc as any).mockResolvedValue({
108
+ data: true,
109
+ error: null,
110
+ });
111
+
112
+ const { result } = renderHook(() => useRoleManagement());
113
+
114
+ const revokeResult = await result.current.revokeEventAppRole(mockRoleParams);
115
+
116
+ expect(revokeResult.success).toBe(true);
117
+ expect(revokeResult.message).toBe('Role revoked successfully');
118
+ expect(revokeResult.error).toBeUndefined();
119
+ expect(result.current.error).toBeNull();
120
+ expect(mockSupabase.rpc).toHaveBeenCalledWith('revoke_event_app_role', {
121
+ p_user_id: 'user-456',
122
+ p_organisation_id: 'org-123',
123
+ p_event_id: 'event-123',
124
+ p_app_id: 'app-123',
125
+ p_role: 'viewer',
126
+ p_revoked_by: 'user-123',
127
+ });
128
+ });
129
+
130
+ it('handles case when role not found', async () => {
131
+ (mockSupabase.rpc as any).mockResolvedValue({
132
+ data: false,
133
+ error: null,
134
+ });
135
+
136
+ const { result } = renderHook(() => useRoleManagement());
137
+
138
+ const revokeResult = await result.current.revokeEventAppRole(mockRoleParams);
139
+
140
+ expect(revokeResult.success).toBe(false);
141
+ expect(revokeResult.message).toBe('No role found to revoke');
142
+ expect(revokeResult.error).toBe('No matching role found');
143
+ });
144
+
145
+ it('uses provided revoked_by parameter', async () => {
146
+ (mockSupabase.rpc as any).mockResolvedValue({
147
+ data: true,
148
+ error: null,
149
+ });
150
+
151
+ const { result } = renderHook(() => useRoleManagement());
152
+
153
+ await result.current.revokeEventAppRole({
154
+ ...mockRoleParams,
155
+ revoked_by: 'admin-789',
156
+ });
157
+
158
+ expect(mockSupabase.rpc).toHaveBeenCalledWith(
159
+ 'revoke_event_app_role',
160
+ expect.objectContaining({
161
+ p_revoked_by: 'admin-789',
162
+ })
163
+ );
164
+ });
165
+
166
+ it('uses user ID as revoked_by when not provided', async () => {
167
+ (mockSupabase.rpc as any).mockResolvedValue({
168
+ data: true,
169
+ error: null,
170
+ });
171
+
172
+ const { result } = renderHook(() => useRoleManagement());
173
+
174
+ await result.current.revokeEventAppRole(mockRoleParams);
175
+
176
+ expect(mockSupabase.rpc).toHaveBeenCalledWith(
177
+ 'revoke_event_app_role',
178
+ expect.objectContaining({
179
+ p_revoked_by: 'user-123',
180
+ })
181
+ );
182
+ });
183
+
184
+ it('handles RPC errors', async () => {
185
+ const rpcError = { message: 'Permission denied', code: 'PERMISSION_DENIED' };
186
+ (mockSupabase.rpc as any).mockResolvedValue({
187
+ data: null,
188
+ error: rpcError,
189
+ });
190
+
191
+ const { result } = renderHook(() => useRoleManagement());
192
+
193
+ const revokeResult = await result.current.revokeEventAppRole(mockRoleParams);
194
+
195
+ expect(revokeResult.success).toBe(false);
196
+ expect(revokeResult.error).toBe('Permission denied');
197
+ // Error is set during operation but cleared on next operation
198
+ await waitFor(
199
+ () => {
200
+ expect(result.current.error).toBeInstanceOf(Error);
201
+ expect(result.current.error?.message).toBe('Permission denied');
202
+ },
203
+ { timeout: 1000 }
204
+ );
205
+ });
206
+
207
+ it('handles exceptions during revocation', async () => {
208
+ const error = new Error('Network error');
209
+ (mockSupabase.rpc as any).mockRejectedValue(error);
210
+
211
+ const { result } = renderHook(() => useRoleManagement());
212
+
213
+ const revokeResult = await result.current.revokeEventAppRole(mockRoleParams);
214
+
215
+ expect(revokeResult.success).toBe(false);
216
+ expect(revokeResult.error).toBe('Network error');
217
+ // Error is set during operation
218
+ await waitFor(
219
+ () => {
220
+ expect(result.current.error).toEqual(error);
221
+ },
222
+ { timeout: 1000 }
223
+ );
224
+ });
225
+
226
+ it('handles non-Error exceptions', async () => {
227
+ (mockSupabase.rpc as any).mockRejectedValue('String error');
228
+
229
+ const { result } = renderHook(() => useRoleManagement());
230
+
231
+ const revokeResult = await result.current.revokeEventAppRole(mockRoleParams);
232
+
233
+ expect(revokeResult.success).toBe(false);
234
+ expect(revokeResult.error).toBe('Unknown error occurred');
235
+ // Error is set during operation
236
+ await waitFor(
237
+ () => {
238
+ expect(result.current.error).toBeInstanceOf(Error);
239
+ },
240
+ { timeout: 1000 }
241
+ );
242
+ });
243
+
244
+ it('manages loading state correctly', async () => {
245
+ let resolvePromise: (value: any) => void;
246
+ const promise = new Promise((resolve) => {
247
+ resolvePromise = resolve;
248
+ });
249
+
250
+ (mockSupabase.rpc as any).mockReturnValue(promise);
251
+
252
+ const { result } = renderHook(() => useRoleManagement());
253
+
254
+ const revokePromise = result.current.revokeEventAppRole(mockRoleParams);
255
+
256
+ // Loading state is set synchronously
257
+ await waitFor(
258
+ () => {
259
+ expect(result.current.isLoading).toBe(true);
260
+ },
261
+ { timeout: 100 }
262
+ );
263
+
264
+ resolvePromise!({
265
+ data: true,
266
+ error: null,
267
+ });
268
+
269
+ await revokePromise;
270
+
271
+ await waitFor(
272
+ () => {
273
+ expect(result.current.isLoading).toBe(false);
274
+ },
275
+ { timeout: 2000 }
276
+ );
277
+ });
278
+ });
279
+
280
+ describe('grantEventAppRole', () => {
281
+ it('grants role successfully', async () => {
282
+ const roleId = 'role-789';
283
+ (mockSupabase.rpc as any).mockResolvedValue({
284
+ data: roleId,
285
+ error: null,
286
+ });
287
+
288
+ const { result } = renderHook(() => useRoleManagement());
289
+
290
+ const grantResult = await result.current.grantEventAppRole(mockRoleParams);
291
+
292
+ expect(grantResult.success).toBe(true);
293
+ expect(grantResult.message).toBe('Role granted successfully');
294
+ expect(grantResult.roleId).toBe(roleId);
295
+ expect(grantResult.error).toBeUndefined();
296
+ expect(result.current.error).toBeNull();
297
+ expect(mockSupabase.rpc).toHaveBeenCalledWith('grant_event_app_role', {
298
+ p_user_id: 'user-456',
299
+ p_organisation_id: 'org-123',
300
+ p_event_id: 'event-123',
301
+ p_app_id: 'app-123',
302
+ p_role: 'viewer',
303
+ p_granted_by: 'user-123',
304
+ p_valid_from: undefined,
305
+ p_valid_to: undefined,
306
+ });
307
+ });
308
+
309
+ it('handles case when no role ID returned', async () => {
310
+ (mockSupabase.rpc as any).mockResolvedValue({
311
+ data: null,
312
+ error: null,
313
+ });
314
+
315
+ const { result } = renderHook(() => useRoleManagement());
316
+
317
+ const grantResult = await result.current.grantEventAppRole(mockRoleParams);
318
+
319
+ expect(grantResult.success).toBe(false);
320
+ expect(grantResult.error).toBe('Failed to grant role - no role ID returned');
321
+ });
322
+
323
+ it('uses provided granted_by parameter', async () => {
324
+ const roleId = 'role-789';
325
+ (mockSupabase.rpc as any).mockResolvedValue({
326
+ data: roleId,
327
+ error: null,
328
+ });
329
+
330
+ const { result } = renderHook(() => useRoleManagement());
331
+
332
+ await result.current.grantEventAppRole({
333
+ ...mockRoleParams,
334
+ granted_by: 'admin-789',
335
+ });
336
+
337
+ expect(mockSupabase.rpc).toHaveBeenCalledWith(
338
+ 'grant_event_app_role',
339
+ expect.objectContaining({
340
+ p_granted_by: 'admin-789',
341
+ })
342
+ );
343
+ });
344
+
345
+ it('uses user ID as granted_by when not provided', async () => {
346
+ const roleId = 'role-789';
347
+ (mockSupabase.rpc as any).mockResolvedValue({
348
+ data: roleId,
349
+ error: null,
350
+ });
351
+
352
+ const { result } = renderHook(() => useRoleManagement());
353
+
354
+ await result.current.grantEventAppRole(mockRoleParams);
355
+
356
+ expect(mockSupabase.rpc).toHaveBeenCalledWith(
357
+ 'grant_event_app_role',
358
+ expect.objectContaining({
359
+ p_granted_by: 'user-123',
360
+ })
361
+ );
362
+ });
363
+
364
+ it('passes valid_from and valid_to parameters', async () => {
365
+ const roleId = 'role-789';
366
+ (mockSupabase.rpc as any).mockResolvedValue({
367
+ data: roleId,
368
+ error: null,
369
+ });
370
+
371
+ const { result } = renderHook(() => useRoleManagement());
372
+
373
+ await result.current.grantEventAppRole({
374
+ ...mockRoleParams,
375
+ valid_from: '2024-01-01',
376
+ valid_to: '2024-12-31',
377
+ });
378
+
379
+ expect(mockSupabase.rpc).toHaveBeenCalledWith(
380
+ 'grant_event_app_role',
381
+ expect.objectContaining({
382
+ p_valid_from: '2024-01-01',
383
+ p_valid_to: '2024-12-31',
384
+ })
385
+ );
386
+ });
387
+
388
+ it('handles null valid_to parameter', async () => {
389
+ const roleId = 'role-789';
390
+ (mockSupabase.rpc as any).mockResolvedValue({
391
+ data: roleId,
392
+ error: null,
393
+ });
394
+
395
+ const { result } = renderHook(() => useRoleManagement());
396
+
397
+ await result.current.grantEventAppRole({
398
+ ...mockRoleParams,
399
+ valid_from: '2024-01-01',
400
+ valid_to: null,
401
+ });
402
+
403
+ expect(mockSupabase.rpc).toHaveBeenCalledWith(
404
+ 'grant_event_app_role',
405
+ expect.objectContaining({
406
+ p_valid_from: '2024-01-01',
407
+ p_valid_to: null,
408
+ })
409
+ );
410
+ });
411
+
412
+ it('handles RPC errors', async () => {
413
+ const rpcError = { message: 'User not found', code: 'USER_NOT_FOUND' };
414
+ (mockSupabase.rpc as any).mockResolvedValue({
415
+ data: null,
416
+ error: rpcError,
417
+ });
418
+
419
+ const { result } = renderHook(() => useRoleManagement());
420
+
421
+ const grantResult = await result.current.grantEventAppRole(mockRoleParams);
422
+
423
+ expect(grantResult.success).toBe(false);
424
+ expect(grantResult.error).toBe('User not found');
425
+ // Error is set during operation
426
+ await waitFor(
427
+ () => {
428
+ expect(result.current.error).toBeInstanceOf(Error);
429
+ expect(result.current.error?.message).toBe('User not found');
430
+ },
431
+ { timeout: 1000 }
432
+ );
433
+ });
434
+
435
+ it('handles exceptions during grant', async () => {
436
+ const error = new Error('Database connection failed');
437
+ (mockSupabase.rpc as any).mockRejectedValue(error);
438
+
439
+ const { result } = renderHook(() => useRoleManagement());
440
+
441
+ const grantResult = await result.current.grantEventAppRole(mockRoleParams);
442
+
443
+ expect(grantResult.success).toBe(false);
444
+ expect(grantResult.error).toBe('Database connection failed');
445
+ // Error is set during operation
446
+ await waitFor(
447
+ () => {
448
+ expect(result.current.error).toEqual(error);
449
+ },
450
+ { timeout: 1000 }
451
+ );
452
+ });
453
+
454
+ it('manages loading state correctly', async () => {
455
+ let resolvePromise: (value: any) => void;
456
+ const promise = new Promise((resolve) => {
457
+ resolvePromise = resolve;
458
+ });
459
+
460
+ (mockSupabase.rpc as any).mockReturnValue(promise);
461
+
462
+ const { result } = renderHook(() => useRoleManagement());
463
+
464
+ const grantPromise = result.current.grantEventAppRole(mockRoleParams);
465
+
466
+ // Loading state is set synchronously, but React state updates are async
467
+ await waitFor(
468
+ () => {
469
+ expect(result.current.isLoading).toBe(true);
470
+ },
471
+ { timeout: 100 }
472
+ );
473
+
474
+ resolvePromise!({
475
+ data: 'role-789',
476
+ error: null,
477
+ });
478
+
479
+ await grantPromise;
480
+
481
+ await waitFor(
482
+ () => {
483
+ expect(result.current.isLoading).toBe(false);
484
+ },
485
+ { timeout: 2000 }
486
+ );
487
+ });
488
+ });
489
+
490
+ describe('revokeRoleById', () => {
491
+ it('revokes role by ID successfully', async () => {
492
+ const roleId = 'role-789';
493
+ const mockRoleData = {
494
+ user_id: 'user-456',
495
+ role: 'viewer',
496
+ event_id: 'event-123',
497
+ app_id: 'app-123',
498
+ };
499
+
500
+ // Mock the from().select().eq().single() chain
501
+ const { mockFrom, mockSelect, mockEq, mockSingle } = setupFromChain(mockRoleData);
502
+
503
+ // Mock the rpc call
504
+ (mockSupabase.rpc as any).mockResolvedValue({
505
+ data: [{ success: true, message: 'Role revoked', revoked_count: 1 }],
506
+ error: null,
507
+ });
508
+
509
+ const { result } = renderHook(() => useRoleManagement());
510
+
511
+ const revokeResult = await result.current.revokeRoleById(roleId);
512
+
513
+ expect(revokeResult.success).toBe(true);
514
+ expect(revokeResult.message).toBe('Role revoked');
515
+ expect(revokeResult.error).toBeUndefined();
516
+ expect(result.current.error).toBeNull();
517
+
518
+ // Verify the role was fetched first
519
+ expect(mockFrom).toHaveBeenCalledWith('rbac_event_app_roles');
520
+ expect(mockSelect).toHaveBeenCalledWith('user_id, role, event_id, app_id');
521
+ expect(mockEq).toHaveBeenCalledWith('id', roleId);
522
+ expect(mockSingle).toHaveBeenCalled();
523
+
524
+ // Verify rpc was called with correct parameters from fetched role
525
+ expect(mockSupabase.rpc).toHaveBeenCalledWith('rbac_role_revoke', {
526
+ p_user_id: 'user-456',
527
+ p_role_type: 'event_app',
528
+ p_role_name: 'viewer',
529
+ p_context_id: 'event-123:app-123',
530
+ p_revoked_by: 'user-123',
531
+ });
532
+ });
533
+
534
+ it('handles case when role not found by ID', async () => {
535
+ // Mock the from().select().eq().single() chain to return error
536
+ const mockSingle = vi.fn().mockResolvedValue({
537
+ data: null,
538
+ error: { message: 'Role not found', code: 'PGRST116' },
539
+ });
540
+ const mockEq = vi.fn().mockReturnValue({ single: mockSingle });
541
+ const mockSelect = vi.fn().mockReturnValue({ eq: mockEq });
542
+ const mockFrom = (mockSupabase as any).from;
543
+ mockFrom.mockReturnValue({ select: mockSelect });
544
+
545
+ const { result } = renderHook(() => useRoleManagement());
546
+
547
+ const revokeResult = await result.current.revokeRoleById('role-789');
548
+
549
+ expect(revokeResult.success).toBe(false);
550
+ expect(revokeResult.error).toBe('Role not found');
551
+ // rpc should not be called when role fetch fails
552
+ expect(mockSupabase.rpc).not.toHaveBeenCalled();
553
+ });
554
+
555
+ it('handles empty result array', async () => {
556
+ const mockRoleData = {
557
+ user_id: 'user-456',
558
+ role: 'viewer',
559
+ event_id: 'event-123',
560
+ app_id: 'app-123',
561
+ };
562
+
563
+ // Mock the from().select().eq().single() chain
564
+ setupFromChain(mockRoleData);
565
+
566
+ // Mock rpc to return empty array
567
+ (mockSupabase.rpc as any).mockResolvedValue({
568
+ data: [],
569
+ error: null,
570
+ });
571
+
572
+ const { result } = renderHook(() => useRoleManagement());
573
+
574
+ const revokeResult = await result.current.revokeRoleById('role-789');
575
+
576
+ // When data is empty array, result is null, so error is undefined
577
+ expect(revokeResult.success).toBe(false);
578
+ expect(revokeResult.error).toBeUndefined();
579
+ });
580
+
581
+ it('handles null result', async () => {
582
+ const mockRoleData = {
583
+ user_id: 'user-456',
584
+ role: 'viewer',
585
+ event_id: 'event-123',
586
+ app_id: 'app-123',
587
+ };
588
+
589
+ // Mock the from().select().eq().single() chain
590
+ setupFromChain(mockRoleData);
591
+
592
+ // Mock rpc to return null
593
+ (mockSupabase.rpc as any).mockResolvedValue({
594
+ data: null,
595
+ error: null,
596
+ });
597
+
598
+ const { result } = renderHook(() => useRoleManagement());
599
+
600
+ const revokeResult = await result.current.revokeRoleById('role-789');
601
+
602
+ // When data is null, result is null, so error is undefined
603
+ expect(revokeResult.success).toBe(false);
604
+ expect(revokeResult.error).toBeUndefined();
605
+ expect(result.current.error).toBeNull(); // Error is cleared on each operation
606
+ });
607
+
608
+ it('handles RPC errors', async () => {
609
+ const mockRoleData = {
610
+ user_id: 'user-456',
611
+ role: 'viewer',
612
+ event_id: 'event-123',
613
+ app_id: 'app-123',
614
+ };
615
+
616
+ // Mock the from().select().eq().single() chain
617
+ setupFromChain(mockRoleData);
618
+
619
+ // Mock rpc to return error
620
+ const rpcError = { message: 'Invalid role ID', code: 'INVALID_ID' };
621
+ (mockSupabase.rpc as any).mockResolvedValue({
622
+ data: null,
623
+ error: rpcError,
624
+ });
625
+
626
+ const { result } = renderHook(() => useRoleManagement());
627
+
628
+ const revokeResult = await result.current.revokeRoleById('role-789');
629
+
630
+ expect(revokeResult.success).toBe(false);
631
+ expect(revokeResult.error).toBe('Invalid role ID');
632
+ // Error is set during operation but cleared on next operation
633
+ await waitFor(
634
+ () => {
635
+ expect(result.current.error).toBeInstanceOf(Error);
636
+ },
637
+ { timeout: 1000 }
638
+ );
639
+ });
640
+
641
+ it('handles exceptions during revocation by ID', async () => {
642
+ const mockRoleData = {
643
+ user_id: 'user-456',
644
+ role: 'viewer',
645
+ event_id: 'event-123',
646
+ app_id: 'app-123',
647
+ };
648
+
649
+ // Mock the from().select().eq().single() chain
650
+ setupFromChain(mockRoleData);
651
+
652
+ // Mock rpc to throw error
653
+ const error = new Error('Network timeout');
654
+ (mockSupabase.rpc as any).mockRejectedValue(error);
655
+
656
+ const { result } = renderHook(() => useRoleManagement());
657
+
658
+ const revokeResult = await result.current.revokeRoleById('role-789');
659
+
660
+ expect(revokeResult.success).toBe(false);
661
+ expect(revokeResult.error).toBe('Network timeout');
662
+ // Error is set during operation
663
+ await waitFor(
664
+ () => {
665
+ expect(result.current.error).toBeInstanceOf(Error);
666
+ expect(result.current.error?.message).toBe('Network timeout');
667
+ },
668
+ { timeout: 1000 }
669
+ );
670
+ });
671
+
672
+ it('uses user ID when available', async () => {
673
+ const mockRoleData = {
674
+ user_id: 'user-456',
675
+ role: 'viewer',
676
+ event_id: 'event-123',
677
+ app_id: 'app-123',
678
+ };
679
+
680
+ // Mock the from().select().eq().single() chain
681
+ setupFromChain(mockRoleData);
682
+
683
+ (mockSupabase.rpc as any).mockResolvedValue({
684
+ data: [{ success: true, message: 'Role revoked', revoked_count: 1 }],
685
+ error: null,
686
+ });
687
+
688
+ const { result } = renderHook(() => useRoleManagement());
689
+
690
+ await result.current.revokeRoleById('role-789');
691
+
692
+ expect(mockSupabase.rpc).toHaveBeenCalledWith(
693
+ 'rbac_role_revoke',
694
+ expect.objectContaining({
695
+ p_user_id: 'user-456', // From fetched role data
696
+ p_revoked_by: 'user-123', // From current user
697
+ })
698
+ );
699
+ });
700
+
701
+ it('handles missing user ID', async () => {
702
+ const mockRoleData = {
703
+ user_id: 'user-456',
704
+ role: 'viewer',
705
+ event_id: 'event-123',
706
+ app_id: 'app-123',
707
+ };
708
+
709
+ // Mock the from().select().eq().single() chain
710
+ setupFromChain(mockRoleData);
711
+
712
+ mockUseUnifiedAuth.mockReturnValue({
713
+ user: null,
714
+ supabase: mockSupabase,
715
+ } as any);
716
+
717
+ (mockSupabase.rpc as any).mockResolvedValue({
718
+ data: [{ success: true, message: 'Role revoked', revoked_count: 1 }],
719
+ error: null,
720
+ });
721
+
722
+ const { result } = renderHook(() => useRoleManagement());
723
+
724
+ await result.current.revokeRoleById('role-789');
725
+
726
+ expect(mockSupabase.rpc).toHaveBeenCalledWith(
727
+ 'rbac_role_revoke',
728
+ expect.objectContaining({
729
+ p_user_id: 'user-456', // From fetched role data
730
+ p_revoked_by: undefined, // No current user
731
+ })
732
+ );
733
+ });
734
+
735
+ it('manages loading state correctly', async () => {
736
+ const mockRoleData = {
737
+ user_id: 'user-456',
738
+ role: 'viewer',
739
+ event_id: 'event-123',
740
+ app_id: 'app-123',
741
+ };
742
+
743
+ // Mock the from().select().eq().single() chain to resolve immediately
744
+ setupFromChain(mockRoleData);
745
+
746
+ // Mock rpc to return a promise that we can control
747
+ let resolvePromise: (value: any) => void;
748
+ const promise = new Promise((resolve) => {
749
+ resolvePromise = resolve;
750
+ });
751
+
752
+ (mockSupabase.rpc as any).mockReturnValue(promise);
753
+
754
+ const { result } = renderHook(() => useRoleManagement());
755
+
756
+ const revokePromise = result.current.revokeRoleById('role-789');
757
+
758
+ // Loading state may be set asynchronously, wait for it
759
+ await waitFor(
760
+ () => {
761
+ expect(result.current.isLoading).toBe(true);
762
+ },
763
+ { timeout: 1000 }
764
+ );
765
+
766
+ resolvePromise!({
767
+ data: [{ success: true, message: 'Role revoked', revoked_count: 1 }],
768
+ error: null,
769
+ });
770
+
771
+ await revokePromise;
772
+
773
+ await waitFor(
774
+ () => {
775
+ expect(result.current.isLoading).toBe(false);
776
+ },
777
+ { timeout: 2000 }
778
+ );
779
+ });
780
+ });
781
+
782
+ describe('Role Types', () => {
783
+ it('handles all valid role types', async () => {
784
+ const roleTypes = ['viewer', 'participant', 'planner', 'event_admin'] as const;
785
+
786
+ for (const role of roleTypes) {
787
+ (mockSupabase.rpc as any).mockResolvedValue({
788
+ data: true,
789
+ error: null,
790
+ });
791
+
792
+ const { result } = renderHook(() => useRoleManagement());
793
+
794
+ await result.current.revokeEventAppRole({
795
+ ...mockRoleParams,
796
+ role,
797
+ });
798
+
799
+ expect(mockSupabase.rpc).toHaveBeenCalledWith(
800
+ 'revoke_event_app_role',
801
+ expect.objectContaining({
802
+ p_role: role,
803
+ })
804
+ );
805
+ }
806
+ });
807
+ });
808
+
809
+ describe('Error Recovery', () => {
810
+ it('clears error on successful operation after error', async () => {
811
+ // First call fails
812
+ (mockSupabase.rpc as any).mockRejectedValueOnce(new Error('Network error'));
813
+
814
+ const { result } = renderHook(() => useRoleManagement());
815
+
816
+ await result.current.revokeEventAppRole(mockRoleParams);
817
+
818
+ // Error should be set after failed operation
819
+ await waitFor(
820
+ () => {
821
+ expect(result.current.error).toBeInstanceOf(Error);
822
+ },
823
+ { timeout: 1000 }
824
+ );
825
+
826
+ // Second call succeeds
827
+ (mockSupabase.rpc as any).mockResolvedValueOnce({
828
+ data: true,
829
+ error: null,
830
+ });
831
+
832
+ await result.current.revokeEventAppRole(mockRoleParams);
833
+
834
+ // Error should be cleared after successful operation
835
+ await waitFor(
836
+ () => {
837
+ expect(result.current.error).toBeNull();
838
+ },
839
+ { timeout: 1000 }
840
+ );
841
+ });
842
+
843
+ it('maintains stability after error recovery', async () => {
844
+ // First call fails
845
+ (mockSupabase.rpc as any).mockRejectedValueOnce(new Error('Error'));
846
+
847
+ const { result } = renderHook(() => useRoleManagement());
848
+
849
+ await result.current.revokeEventAppRole(mockRoleParams);
850
+
851
+ // Second call succeeds
852
+ (mockSupabase.rpc as any).mockResolvedValueOnce({
853
+ data: true,
854
+ error: null,
855
+ });
856
+
857
+ const revokeResult = await result.current.revokeEventAppRole(mockRoleParams);
858
+
859
+ expect(revokeResult.success).toBe(true);
860
+ expect(result.current.error).toBeNull();
861
+ });
862
+ });
863
+
864
+ describe('Concurrent Operations', () => {
865
+ it('handles concurrent role operations', async () => {
866
+ (mockSupabase.rpc as any).mockResolvedValue({
867
+ data: true,
868
+ error: null,
869
+ });
870
+
871
+ const { result } = renderHook(() => useRoleManagement());
872
+
873
+ const operations = [
874
+ result.current.revokeEventAppRole(mockRoleParams),
875
+ result.current.revokeEventAppRole({
876
+ ...mockRoleParams,
877
+ user_id: 'user-789',
878
+ }),
879
+ ];
880
+
881
+ const results = await Promise.all(operations);
882
+
883
+ expect(results.every((r) => r.success)).toBe(true);
884
+ });
885
+
886
+ it('handles rapid sequential operations', async () => {
887
+ (mockSupabase.rpc as any).mockResolvedValue({
888
+ data: true,
889
+ error: null,
890
+ });
891
+
892
+ const { result } = renderHook(() => useRoleManagement());
893
+
894
+ await result.current.revokeEventAppRole(mockRoleParams);
895
+ await result.current.revokeEventAppRole({
896
+ ...mockRoleParams,
897
+ user_id: 'user-789',
898
+ });
899
+ await result.current.revokeEventAppRole({
900
+ ...mockRoleParams,
901
+ user_id: 'user-101',
902
+ });
903
+
904
+ expect(mockSupabase.rpc).toHaveBeenCalledTimes(3);
905
+ });
906
+ });
907
+ });
908
+