@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.
- package/dist/{AuthService-CVgsgtaZ.d.ts → AuthService-D4646R4b.d.ts} +9 -4
- package/dist/{DataTable-3JRLZXER.js → DataTable-ZOAKQ3SU.js} +10 -9
- package/dist/{UnifiedAuthProvider-KZZUO27W.js → UnifiedAuthProvider-YFN7YGVN.js} +4 -3
- package/dist/{api-PKU4PUBO.js → api-TNIBJWLM.js} +3 -3
- package/dist/{audit-H4YJJF7R.js → audit-T36HM7IM.js} +2 -2
- package/dist/{chunk-4OX5PXHX.js → chunk-2GJ5GL77.js} +4 -5
- package/dist/chunk-2GJ5GL77.js.map +1 -0
- package/dist/{chunk-5YIZFEUQ.js → chunk-2LM4QQGH.js} +31 -35
- package/dist/chunk-2LM4QQGH.js.map +1 -0
- package/dist/{chunk-3OGQLOJM.js → chunk-3DBFLLLU.js} +30 -1
- package/dist/chunk-3DBFLLLU.js.map +1 -0
- package/dist/{chunk-KTHLNIMA.js → chunk-ECOVPXYS.js} +13 -62
- package/dist/chunk-ECOVPXYS.js.map +1 -0
- package/dist/{chunk-OO3V7W4H.js → chunk-KA3PSVNV.js} +87 -40
- package/dist/chunk-KA3PSVNV.js.map +1 -0
- package/dist/{chunk-HKWQN44G.js → chunk-KMPWND3F.js} +15 -15
- package/dist/{chunk-L36JW4KV.js → chunk-LFS45U62.js} +2 -2
- package/dist/{chunk-NEONKMTU.js → chunk-LZYHAL7Y.js} +9 -4
- package/dist/{chunk-NEONKMTU.js.map → chunk-LZYHAL7Y.js.map} +1 -1
- package/dist/{chunk-BUN7NMV7.js → chunk-O3FTRYEU.js} +2 -2
- package/dist/{chunk-F6QB26OS.js → chunk-P3PUOL6B.js} +80 -8
- package/dist/chunk-P3PUOL6B.js.map +1 -0
- package/dist/{chunk-ZPXWJA4H.js → chunk-PHDAXDHB.js} +131 -5
- package/dist/chunk-PHDAXDHB.js.map +1 -0
- package/dist/chunk-UJI6WSMD.js +201 -0
- package/dist/{chunk-5CDJCTOO.js.map → chunk-UJI6WSMD.js.map} +1 -1
- package/dist/{chunk-JHWQNJP3.js → chunk-UKZWNQMB.js} +65 -19
- package/dist/{chunk-JHWQNJP3.js.map → chunk-UKZWNQMB.js.map} +1 -1
- package/dist/{chunk-7H75SHXZ.js → chunk-VN3OOE35.js} +2 -2
- package/dist/{chunk-QKIVSZ2O.js → chunk-WP5I5GLN.js} +2 -2
- package/dist/components.d.ts +1 -1
- package/dist/components.js +12 -11
- package/dist/components.js.map +1 -1
- package/dist/hooks.d.ts +1 -1
- package/dist/hooks.js +10 -9
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +19 -16
- package/dist/index.js.map +1 -1
- package/dist/providers.d.ts +2 -2
- package/dist/providers.js +3 -2
- package/dist/rbac/index.d.ts +82 -1
- package/dist/rbac/index.js +13 -10
- package/dist/{useToast-DRah6K-g.d.ts → useToast-Cs_g32bg.d.ts} +8 -6
- package/dist/utils.js +6 -4
- package/dist/utils.js.map +1 -1
- package/dist/validation.js +3 -1
- package/dist/validation.js.map +1 -1
- package/docs/README.md +4 -0
- package/docs/api/classes/ColumnFactory.md +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +1 -1
- package/docs/api/classes/MissingUserContextError.md +1 -1
- package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
- package/docs/api/classes/PermissionDeniedError.md +1 -1
- package/docs/api/classes/PublicErrorBoundary.md +1 -1
- package/docs/api/classes/RBACAuditManager.md +35 -12
- package/docs/api/classes/RBACCache.md +1 -1
- package/docs/api/classes/RBACEngine.md +1 -1
- package/docs/api/classes/RBACError.md +1 -1
- package/docs/api/classes/RBACNotInitializedError.md +1 -1
- package/docs/api/classes/SecureSupabaseClient.md +1 -1
- package/docs/api/classes/StorageUtils.md +1 -1
- package/docs/api/enums/FileCategory.md +1 -1
- package/docs/api/interfaces/AggregateConfig.md +1 -1
- package/docs/api/interfaces/ButtonProps.md +1 -1
- package/docs/api/interfaces/CardProps.md +1 -1
- package/docs/api/interfaces/ColorPalette.md +1 -1
- package/docs/api/interfaces/ColorShade.md +1 -1
- package/docs/api/interfaces/DataAccessRecord.md +1 -1
- package/docs/api/interfaces/DataRecord.md +1 -1
- package/docs/api/interfaces/DataTableAction.md +1 -1
- package/docs/api/interfaces/DataTableColumn.md +1 -1
- package/docs/api/interfaces/DataTableProps.md +1 -1
- package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
- package/docs/api/interfaces/EmptyStateConfig.md +1 -1
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
- package/docs/api/interfaces/EventAppRoleData.md +71 -0
- package/docs/api/interfaces/FileDisplayProps.md +1 -1
- package/docs/api/interfaces/FileMetadata.md +1 -1
- package/docs/api/interfaces/FileReference.md +1 -1
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadOptions.md +1 -1
- package/docs/api/interfaces/FileUploadProps.md +1 -1
- package/docs/api/interfaces/FooterProps.md +1 -1
- package/docs/api/interfaces/GrantEventAppRoleParams.md +122 -0
- package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
- package/docs/api/interfaces/InputProps.md +1 -1
- package/docs/api/interfaces/LabelProps.md +1 -1
- package/docs/api/interfaces/LoginFormProps.md +1 -1
- package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
- package/docs/api/interfaces/NavigationContextType.md +1 -1
- package/docs/api/interfaces/NavigationGuardProps.md +1 -1
- package/docs/api/interfaces/NavigationItem.md +1 -1
- package/docs/api/interfaces/NavigationMenuProps.md +1 -1
- package/docs/api/interfaces/NavigationProviderProps.md +1 -1
- package/docs/api/interfaces/Organisation.md +1 -1
- package/docs/api/interfaces/OrganisationContextType.md +1 -1
- package/docs/api/interfaces/OrganisationMembership.md +1 -1
- package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
- package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
- package/docs/api/interfaces/PaceAppLayoutProps.md +27 -27
- package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
- package/docs/api/interfaces/PageAccessRecord.md +1 -1
- package/docs/api/interfaces/PagePermissionContextType.md +1 -1
- package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
- package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
- package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
- package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
- package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
- package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
- package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
- package/docs/api/interfaces/RBACConfig.md +1 -1
- package/docs/api/interfaces/RBACLogger.md +1 -1
- package/docs/api/interfaces/RevokeEventAppRoleParams.md +100 -0
- package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
- package/docs/api/interfaces/RoleManagementResult.md +52 -0
- package/docs/api/interfaces/RouteAccessRecord.md +1 -1
- package/docs/api/interfaces/RouteConfig.md +1 -1
- package/docs/api/interfaces/SecureDataContextType.md +1 -1
- package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
- package/docs/api/interfaces/StorageConfig.md +1 -1
- package/docs/api/interfaces/StorageFileInfo.md +1 -1
- package/docs/api/interfaces/StorageFileMetadata.md +1 -1
- package/docs/api/interfaces/StorageListOptions.md +1 -1
- package/docs/api/interfaces/StorageListResult.md +1 -1
- package/docs/api/interfaces/StorageUploadOptions.md +1 -1
- package/docs/api/interfaces/StorageUploadResult.md +1 -1
- package/docs/api/interfaces/StorageUrlOptions.md +1 -1
- package/docs/api/interfaces/StyleImport.md +1 -1
- package/docs/api/interfaces/SwitchProps.md +1 -1
- package/docs/api/interfaces/ToastActionElement.md +1 -1
- package/docs/api/interfaces/ToastProps.md +1 -1
- package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
- package/docs/api/interfaces/UserEventAccess.md +1 -1
- package/docs/api/interfaces/UserMenuProps.md +1 -1
- package/docs/api/interfaces/UserProfile.md +1 -1
- package/docs/api/modules.md +43 -16
- package/docs/architecture/rpc-function-standards.md +193 -0
- package/package.json +1 -1
- package/src/__tests__/TEST_STANDARD.md +244 -2
- package/src/components/DataTable/__tests__/a11y.basic.test.tsx +46 -16
- package/src/components/DataTable/__tests__/keyboard.test.tsx +276 -217
- package/src/components/DataTable/components/DataTableCore.tsx +32 -17
- package/src/components/DataTable/components/DataTableToolbar.tsx +3 -2
- package/src/components/DataTable/components/EditableRow.tsx +18 -1
- package/src/components/DataTable/components/ImportModal.tsx +25 -2
- package/src/components/DataTable/components/ViewRowModal.tsx +1 -1
- package/src/components/DataTable/components/__tests__/AccessDeniedPage.test.tsx +735 -0
- package/src/components/DataTable/components/__tests__/BulkOperationsDropdown.test.tsx +572 -0
- package/src/components/DataTable/components/__tests__/ColumnVisibilityDropdown.test.tsx +708 -0
- package/src/components/DataTable/components/__tests__/DataTableErrorBoundary.test.tsx +451 -0
- package/src/components/DataTable/components/__tests__/DataTableModals.test.tsx +456 -0
- package/src/components/DataTable/components/__tests__/EditableRow.test.tsx +454 -0
- package/src/components/DataTable/components/__tests__/ExpandButton.test.tsx +462 -0
- package/src/components/DataTable/components/__tests__/FilterRow.test.tsx +423 -0
- package/src/components/DataTable/components/__tests__/GroupHeader.test.tsx +393 -0
- package/src/components/DataTable/components/__tests__/GroupingDropdown.test.tsx +617 -0
- package/src/components/DataTable/components/__tests__/ImportModal.test.tsx +734 -0
- package/src/components/DataTable/components/__tests__/ViewRowModal.test.tsx +412 -0
- package/src/components/DataTable/hooks/useTableHandlers.ts +4 -0
- package/src/components/EventSelector/EventSelector.tsx +5 -25
- package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +12 -7
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +4 -0
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.accessibility.test.tsx +7 -2
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.integration.test.tsx +13 -8
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.performance.test.tsx +109 -100
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.security.test.tsx +18 -13
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.unit.test.tsx +17 -12
- package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +2 -0
- package/src/components/PaceLoginPage/PaceLoginPage.tsx +11 -1
- package/src/components/PasswordReset/PasswordChangeForm.test.tsx +2 -2
- package/src/components/ProtectedRoute/ProtectedRoute.test.tsx +648 -0
- package/src/components/ProtectedRoute/ProtectedRoute.tsx +10 -7
- package/src/components/PublicLayout/__tests__/PublicErrorBoundary.test.tsx +4 -12
- package/src/components/Select/Select.tsx +8 -0
- package/src/components/Toast/Toast.test.tsx +8 -7
- package/src/components/Toast/Toast.tsx +4 -4
- package/src/hooks/__tests__/usePublicEvent.simple.test.ts +367 -3
- package/src/hooks/__tests__/usePublicFileDisplay.test.ts +916 -0
- package/src/hooks/useEventTheme.ts +49 -18
- package/src/hooks/usePermissionCache.ts +5 -3
- package/src/hooks/useSecureDataAccess.ts +11 -1
- package/src/hooks/useToast.ts +11 -12
- package/src/providers/services/EventServiceProvider.tsx +15 -8
- package/src/rbac/__tests__/cache-invalidation.test.ts +385 -0
- package/src/rbac/audit.test.ts +206 -0
- package/src/rbac/audit.ts +37 -2
- package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +26 -23
- package/src/rbac/errors.test.ts +340 -0
- package/src/rbac/hooks/index.ts +9 -0
- package/src/rbac/hooks/useResolvedScope.test.ts +1063 -0
- package/src/rbac/hooks/useRoleManagement.test.ts +908 -0
- package/src/rbac/hooks/useRoleManagement.ts +255 -0
- package/src/services/AuthService.ts +10 -0
- package/src/services/EventService.ts +111 -50
- package/src/services/__tests__/AuthService.test.ts +1 -1
- package/src/services/__tests__/EventService.test.ts +60 -45
- package/src/services/interfaces/IEventService.ts +1 -1
- package/src/utils/__tests__/deviceFingerprint.unit.test.ts +320 -0
- package/src/utils/__tests__/logger.unit.test.ts +398 -0
- package/src/utils/__tests__/validation.unit.test.ts +225 -1
- package/src/utils/file-reference.test.ts +214 -0
- package/dist/chunk-3OGQLOJM.js.map +0 -1
- package/dist/chunk-4OX5PXHX.js.map +0 -1
- package/dist/chunk-5CDJCTOO.js +0 -190
- package/dist/chunk-5YIZFEUQ.js.map +0 -1
- package/dist/chunk-F6QB26OS.js.map +0 -1
- package/dist/chunk-KTHLNIMA.js.map +0 -1
- package/dist/chunk-OO3V7W4H.js.map +0 -1
- package/dist/chunk-ZPXWJA4H.js.map +0 -1
- package/src/rbac/audit-enhanced.ts +0 -351
- /package/dist/{DataTable-3JRLZXER.js.map → DataTable-ZOAKQ3SU.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-KZZUO27W.js.map → UnifiedAuthProvider-YFN7YGVN.js.map} +0 -0
- /package/dist/{api-PKU4PUBO.js.map → api-TNIBJWLM.js.map} +0 -0
- /package/dist/{audit-H4YJJF7R.js.map → audit-T36HM7IM.js.map} +0 -0
- /package/dist/{chunk-HKWQN44G.js.map → chunk-KMPWND3F.js.map} +0 -0
- /package/dist/{chunk-L36JW4KV.js.map → chunk-LFS45U62.js.map} +0 -0
- /package/dist/{chunk-BUN7NMV7.js.map → chunk-O3FTRYEU.js.map} +0 -0
- /package/dist/{chunk-7H75SHXZ.js.map → chunk-VN3OOE35.js.map} +0 -0
- /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
|
+
|