@jmruthers/pace-core 0.5.115 → 0.5.117
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-H5KJCAIS.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-SYXOZQ4P.js → chunk-2GJ5GL77.js} +1 -1
- package/dist/chunk-2GJ5GL77.js.map +1 -0
- package/dist/{chunk-XYRZV7R5.js → chunk-2LM4QQGH.js} +30 -34
- 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-HKWQN44G.js → chunk-IZXS7RZK.js} +15 -15
- package/dist/{chunk-OO3V7W4H.js → chunk-KA3PSVNV.js} +87 -40
- package/dist/chunk-KA3PSVNV.js.map +1 -0
- package/dist/{chunk-L36JW4KV.js → chunk-LFS45U62.js} +2 -2
- 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-OUU3SP6I.js → chunk-UKZWNQMB.js} +50 -7
- package/dist/{chunk-OUU3SP6I.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/{chunk-NEONKMTU.js → chunk-XN2LYHDI.js} +47 -6
- package/dist/chunk-XN2LYHDI.js.map +1 -0
- 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-DVT4dMtf.d.ts → useToast-Cs_g32bg.d.ts} +1 -1
- 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 +41 -14
- 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 +29 -2
- package/src/components/DataTable/components/DataTableToolbar.tsx +3 -2
- package/src/components/DataTable/components/EditableRow.tsx +18 -1
- 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.tsx +1 -1
- 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 +56 -3
- package/src/hooks/useToast.ts +1 -1
- 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-5CDJCTOO.js +0 -190
- package/dist/chunk-F6QB26OS.js.map +0 -1
- package/dist/chunk-KTHLNIMA.js.map +0 -1
- package/dist/chunk-NEONKMTU.js.map +0 -1
- package/dist/chunk-OO3V7W4H.js.map +0 -1
- package/dist/chunk-SYXOZQ4P.js.map +0 -1
- package/dist/chunk-XYRZV7R5.js.map +0 -1
- package/dist/chunk-ZPXWJA4H.js.map +0 -1
- package/src/rbac/audit-enhanced.ts +0 -351
- /package/dist/{DataTable-H5KJCAIS.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-IZXS7RZK.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,1063 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file useResolvedScope Hook Tests
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module RBAC/Hooks
|
|
5
|
+
* @since 1.0.0
|
|
6
|
+
*
|
|
7
|
+
* Comprehensive tests for the useResolvedScope hook following TEST_STANDARD.md.
|
|
8
|
+
* Tests focus on behavior: scope resolution from various contexts, 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 { useResolvedScope } from './useResolvedScope';
|
|
14
|
+
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
15
|
+
import type { Database } from '../../types/database';
|
|
16
|
+
|
|
17
|
+
// Mock dependencies
|
|
18
|
+
vi.mock('../utils/eventContext', () => ({
|
|
19
|
+
createScopeFromEvent: vi.fn(),
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
vi.mock('../../utils/appNameResolver', () => ({
|
|
23
|
+
getCurrentAppName: vi.fn(),
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
import { createScopeFromEvent } from '../utils/eventContext';
|
|
27
|
+
import { getCurrentAppName } from '../../utils/appNameResolver';
|
|
28
|
+
import { createMockSupabaseClient } from '../../__tests__/helpers/supabaseMock';
|
|
29
|
+
|
|
30
|
+
describe('useResolvedScope Hook', () => {
|
|
31
|
+
const mockCreateScopeFromEvent = vi.mocked(createScopeFromEvent);
|
|
32
|
+
const mockGetCurrentAppName = vi.mocked(getCurrentAppName);
|
|
33
|
+
|
|
34
|
+
let mockSupabase: SupabaseClient<Database>;
|
|
35
|
+
let sharedMockQuery: any;
|
|
36
|
+
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
vi.clearAllMocks();
|
|
39
|
+
|
|
40
|
+
// Create a shared query builder that will be returned by from()
|
|
41
|
+
// Default to resolving successfully for app lookup
|
|
42
|
+
sharedMockQuery = {
|
|
43
|
+
select: vi.fn().mockReturnThis(),
|
|
44
|
+
eq: vi.fn().mockReturnThis(),
|
|
45
|
+
single: vi.fn().mockResolvedValue({
|
|
46
|
+
data: { id: 'app-123', name: 'test-app', is_active: true },
|
|
47
|
+
error: null,
|
|
48
|
+
}),
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
mockSupabase = {
|
|
52
|
+
from: vi.fn().mockReturnValue(sharedMockQuery),
|
|
53
|
+
rpc: vi.fn(),
|
|
54
|
+
} as any;
|
|
55
|
+
|
|
56
|
+
mockGetCurrentAppName.mockReturnValue('test-app');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
afterEach(() => {
|
|
60
|
+
vi.clearAllMocks();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('Scope Resolution', () => {
|
|
64
|
+
it('resolves scope when both organisation and event are provided', async () => {
|
|
65
|
+
// Set up mock BEFORE rendering hook
|
|
66
|
+
sharedMockQuery.single.mockResolvedValue({
|
|
67
|
+
data: { id: 'app-123', name: 'test-app', is_active: true },
|
|
68
|
+
error: null,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const { result, rerender } = renderHook(() =>
|
|
72
|
+
useResolvedScope({
|
|
73
|
+
supabase: mockSupabase,
|
|
74
|
+
selectedOrganisationId: 'org-123',
|
|
75
|
+
selectedEventId: 'event-123',
|
|
76
|
+
})
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
expect(result.current.isLoading).toBe(true);
|
|
80
|
+
expect(result.current.resolvedScope).toBeNull();
|
|
81
|
+
|
|
82
|
+
// Wait for async app ID resolution to complete
|
|
83
|
+
await waitFor(
|
|
84
|
+
() => {
|
|
85
|
+
expect(result.current.isLoading).toBe(false);
|
|
86
|
+
},
|
|
87
|
+
{ timeout: 2000 }
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
// The stable scope ref is updated in a useEffect that depends on resolvedScope state
|
|
91
|
+
// The return value checks stableScope.organisationId, so we need to wait for the ref update
|
|
92
|
+
// Force a re-render to pick up the ref change (refs don't trigger re-renders)
|
|
93
|
+
rerender();
|
|
94
|
+
|
|
95
|
+
await waitFor(
|
|
96
|
+
() => {
|
|
97
|
+
expect(result.current.resolvedScope).not.toBeNull();
|
|
98
|
+
expect(result.current.resolvedScope?.organisationId).toBe('org-123');
|
|
99
|
+
},
|
|
100
|
+
{ timeout: 2000, interval: 10 }
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
// Verify the mock was called
|
|
104
|
+
expect(sharedMockQuery.single).toHaveBeenCalled();
|
|
105
|
+
|
|
106
|
+
// The resolved scope should include organisation, event, and app ID
|
|
107
|
+
expect(result.current.resolvedScope).toEqual({
|
|
108
|
+
organisationId: 'org-123',
|
|
109
|
+
eventId: 'event-123',
|
|
110
|
+
appId: 'app-123',
|
|
111
|
+
});
|
|
112
|
+
expect(result.current.error).toBeNull();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('resolves scope when only organisation is provided', async () => {
|
|
116
|
+
sharedMockQuery.single.mockResolvedValue({
|
|
117
|
+
data: { id: 'app-123', name: 'test-app', is_active: true },
|
|
118
|
+
error: null,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const { result, rerender } = renderHook(() =>
|
|
122
|
+
useResolvedScope({
|
|
123
|
+
supabase: mockSupabase,
|
|
124
|
+
selectedOrganisationId: 'org-123',
|
|
125
|
+
selectedEventId: null,
|
|
126
|
+
})
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
// Wait for async app ID resolution to complete
|
|
130
|
+
await waitFor(
|
|
131
|
+
() => {
|
|
132
|
+
expect(result.current.isLoading).toBe(false);
|
|
133
|
+
},
|
|
134
|
+
{ timeout: 2000 }
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
// The stable scope ref is updated in a useEffect after resolvedScope state updates
|
|
138
|
+
// Force a rerender to pick up the ref change (refs don't trigger re-renders)
|
|
139
|
+
rerender();
|
|
140
|
+
|
|
141
|
+
// Wait for stable scope ref to update (happens in useEffect after state update)
|
|
142
|
+
await waitFor(
|
|
143
|
+
() => {
|
|
144
|
+
expect(result.current.resolvedScope).not.toBeNull();
|
|
145
|
+
},
|
|
146
|
+
{ timeout: 2000, interval: 10 }
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
expect(result.current.resolvedScope).toEqual({
|
|
150
|
+
organisationId: 'org-123',
|
|
151
|
+
eventId: undefined,
|
|
152
|
+
appId: 'app-123',
|
|
153
|
+
});
|
|
154
|
+
expect(result.current.error).toBeNull();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('resolves scope from event when only event is provided', async () => {
|
|
158
|
+
sharedMockQuery.single.mockResolvedValue({
|
|
159
|
+
data: { id: 'app-123', name: 'test-app', is_active: true },
|
|
160
|
+
error: null,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
mockCreateScopeFromEvent.mockResolvedValue({
|
|
164
|
+
organisationId: 'org-456',
|
|
165
|
+
eventId: 'event-123',
|
|
166
|
+
appId: 'app-123',
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const { result, rerender } = renderHook(() =>
|
|
170
|
+
useResolvedScope({
|
|
171
|
+
supabase: mockSupabase,
|
|
172
|
+
selectedOrganisationId: null,
|
|
173
|
+
selectedEventId: 'event-123',
|
|
174
|
+
})
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
// Wait for async app ID resolution to complete
|
|
178
|
+
await waitFor(
|
|
179
|
+
() => {
|
|
180
|
+
expect(result.current.isLoading).toBe(false);
|
|
181
|
+
},
|
|
182
|
+
{ timeout: 2000 }
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
// The stable scope ref is updated in a useEffect after resolvedScope state updates
|
|
186
|
+
// Force a rerender to pick up the ref change (refs don't trigger re-renders)
|
|
187
|
+
rerender();
|
|
188
|
+
|
|
189
|
+
// Wait for stable scope ref to update (happens in useEffect after state update)
|
|
190
|
+
await waitFor(
|
|
191
|
+
() => {
|
|
192
|
+
expect(result.current.resolvedScope).not.toBeNull();
|
|
193
|
+
},
|
|
194
|
+
{ timeout: 2000, interval: 10 }
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
expect(result.current.resolvedScope).toEqual({
|
|
198
|
+
organisationId: 'org-456',
|
|
199
|
+
eventId: 'event-123',
|
|
200
|
+
appId: 'app-123',
|
|
201
|
+
});
|
|
202
|
+
expect(result.current.error).toBeNull();
|
|
203
|
+
expect(mockCreateScopeFromEvent).toHaveBeenCalledWith(
|
|
204
|
+
mockSupabase,
|
|
205
|
+
'event-123',
|
|
206
|
+
'app-123'
|
|
207
|
+
);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('handles no context available', async () => {
|
|
211
|
+
const { result } = renderHook(() =>
|
|
212
|
+
useResolvedScope({
|
|
213
|
+
supabase: mockSupabase,
|
|
214
|
+
selectedOrganisationId: null,
|
|
215
|
+
selectedEventId: null,
|
|
216
|
+
})
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
await waitFor(
|
|
220
|
+
() => {
|
|
221
|
+
expect(result.current.isLoading).toBe(false);
|
|
222
|
+
},
|
|
223
|
+
{ timeout: 2000 }
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
expect(result.current.resolvedScope).toBeNull();
|
|
227
|
+
expect(result.current.error).toBeInstanceOf(Error);
|
|
228
|
+
expect(result.current.error?.message).toBe(
|
|
229
|
+
'No organisation or event context available'
|
|
230
|
+
);
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
describe('App ID Resolution', () => {
|
|
235
|
+
it('resolves app ID from database when app name is available', async () => {
|
|
236
|
+
sharedMockQuery.single.mockResolvedValue({
|
|
237
|
+
data: { id: 'app-123', name: 'test-app', is_active: true },
|
|
238
|
+
error: null,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const { result, rerender } = renderHook(() =>
|
|
242
|
+
useResolvedScope({
|
|
243
|
+
supabase: mockSupabase,
|
|
244
|
+
selectedOrganisationId: 'org-123',
|
|
245
|
+
selectedEventId: 'event-123',
|
|
246
|
+
})
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
// Wait for async app ID resolution to complete
|
|
250
|
+
await waitFor(
|
|
251
|
+
() => {
|
|
252
|
+
expect(result.current.isLoading).toBe(false);
|
|
253
|
+
},
|
|
254
|
+
{ timeout: 2000 }
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
// The stable scope ref is updated in a useEffect after resolvedScope state updates
|
|
258
|
+
// Force a rerender to pick up the ref change (refs don't trigger re-renders)
|
|
259
|
+
rerender();
|
|
260
|
+
|
|
261
|
+
// Wait for stable scope ref to update (happens in useEffect after state update)
|
|
262
|
+
await waitFor(
|
|
263
|
+
() => {
|
|
264
|
+
expect(result.current.resolvedScope).not.toBeNull();
|
|
265
|
+
},
|
|
266
|
+
{ timeout: 2000, interval: 10 }
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
expect(result.current.resolvedScope?.appId).toBe('app-123');
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('handles app not found in database', async () => {
|
|
273
|
+
sharedMockQuery.single.mockResolvedValue({
|
|
274
|
+
data: null,
|
|
275
|
+
error: { message: 'App not found' },
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// Mock inactive app check - second call uses same query builder
|
|
279
|
+
sharedMockQuery.single.mockResolvedValueOnce({
|
|
280
|
+
data: null,
|
|
281
|
+
error: null,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const { result, rerender } = renderHook(() =>
|
|
285
|
+
useResolvedScope({
|
|
286
|
+
supabase: mockSupabase,
|
|
287
|
+
selectedOrganisationId: 'org-123',
|
|
288
|
+
selectedEventId: 'event-123',
|
|
289
|
+
})
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
await waitFor(
|
|
293
|
+
() => {
|
|
294
|
+
expect(result.current.isLoading).toBe(false);
|
|
295
|
+
},
|
|
296
|
+
{ timeout: 2000 }
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
// Force rerender to pick up ref update - need to pass props
|
|
300
|
+
rerender({
|
|
301
|
+
supabase: mockSupabase,
|
|
302
|
+
selectedOrganisationId: 'org-123',
|
|
303
|
+
selectedEventId: 'event-123',
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
await waitFor(
|
|
307
|
+
() => {
|
|
308
|
+
expect(result.current.resolvedScope).not.toBeNull();
|
|
309
|
+
},
|
|
310
|
+
{ timeout: 2000, interval: 10 }
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
// Should still resolve scope without app ID
|
|
314
|
+
// Note: appId is set to empty string '' when not provided (not undefined)
|
|
315
|
+
expect(result.current.resolvedScope?.organisationId).toBe('org-123');
|
|
316
|
+
expect(result.current.resolvedScope?.appId).toBe('');
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('handles inactive app', async () => {
|
|
320
|
+
// First call (with is_active=true filter) returns error
|
|
321
|
+
// Second call (without is_active filter) returns inactive app
|
|
322
|
+
sharedMockQuery.single
|
|
323
|
+
.mockResolvedValueOnce({
|
|
324
|
+
data: null,
|
|
325
|
+
error: { message: 'App not found' },
|
|
326
|
+
})
|
|
327
|
+
.mockResolvedValueOnce({
|
|
328
|
+
data: { id: 'app-123', name: 'test-app', is_active: false },
|
|
329
|
+
error: null,
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
const { result, rerender } = renderHook(() =>
|
|
333
|
+
useResolvedScope({
|
|
334
|
+
supabase: mockSupabase,
|
|
335
|
+
selectedOrganisationId: 'org-123',
|
|
336
|
+
selectedEventId: 'event-123',
|
|
337
|
+
})
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
await waitFor(
|
|
341
|
+
() => {
|
|
342
|
+
expect(result.current.isLoading).toBe(false);
|
|
343
|
+
},
|
|
344
|
+
{ timeout: 2000 }
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
// Force rerender to pick up ref update - need to pass props
|
|
348
|
+
rerender({
|
|
349
|
+
supabase: mockSupabase,
|
|
350
|
+
selectedOrganisationId: 'org-123',
|
|
351
|
+
selectedEventId: 'event-123',
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
await waitFor(
|
|
355
|
+
() => {
|
|
356
|
+
expect(result.current.resolvedScope).not.toBeNull();
|
|
357
|
+
},
|
|
358
|
+
{ timeout: 2000, interval: 10 }
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
// Should still resolve scope without app ID (app is inactive)
|
|
362
|
+
// Note: appId is set to empty string '' when not provided (not undefined)
|
|
363
|
+
expect(result.current.resolvedScope?.organisationId).toBe('org-123');
|
|
364
|
+
expect(result.current.resolvedScope?.appId).toBe('');
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it('handles missing app name', async () => {
|
|
368
|
+
mockGetCurrentAppName.mockReturnValue(null);
|
|
369
|
+
|
|
370
|
+
const { result, rerender } = renderHook(() =>
|
|
371
|
+
useResolvedScope({
|
|
372
|
+
supabase: mockSupabase,
|
|
373
|
+
selectedOrganisationId: 'org-123',
|
|
374
|
+
selectedEventId: 'event-123',
|
|
375
|
+
})
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
await waitFor(
|
|
379
|
+
() => {
|
|
380
|
+
expect(result.current.isLoading).toBe(false);
|
|
381
|
+
},
|
|
382
|
+
{ timeout: 2000 }
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
// Force rerender to pick up ref update - need to pass props
|
|
386
|
+
rerender({
|
|
387
|
+
supabase: mockSupabase,
|
|
388
|
+
selectedOrganisationId: 'org-123',
|
|
389
|
+
selectedEventId: 'event-123',
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
await waitFor(
|
|
393
|
+
() => {
|
|
394
|
+
expect(result.current.resolvedScope).not.toBeNull();
|
|
395
|
+
},
|
|
396
|
+
{ timeout: 2000, interval: 10 }
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
// Note: appId is set to empty string '' when not provided (not undefined)
|
|
400
|
+
expect(result.current.resolvedScope?.organisationId).toBe('org-123');
|
|
401
|
+
expect(result.current.resolvedScope?.appId).toBe('');
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it('handles null supabase client', async () => {
|
|
405
|
+
const { result, rerender } = renderHook(() =>
|
|
406
|
+
useResolvedScope({
|
|
407
|
+
supabase: null,
|
|
408
|
+
selectedOrganisationId: 'org-123',
|
|
409
|
+
selectedEventId: 'event-123',
|
|
410
|
+
})
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
await waitFor(
|
|
414
|
+
() => {
|
|
415
|
+
expect(result.current.isLoading).toBe(false);
|
|
416
|
+
},
|
|
417
|
+
{ timeout: 2000 }
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
// Force rerender to pick up ref update - need to pass props
|
|
421
|
+
rerender({
|
|
422
|
+
supabase: mockSupabase,
|
|
423
|
+
selectedOrganisationId: 'org-123',
|
|
424
|
+
selectedEventId: 'event-123',
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
await waitFor(
|
|
428
|
+
() => {
|
|
429
|
+
expect(result.current.resolvedScope).not.toBeNull();
|
|
430
|
+
},
|
|
431
|
+
{ timeout: 2000, interval: 10 }
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
// Should resolve scope without app ID when supabase is null
|
|
435
|
+
// Note: appId is set to empty string '' when not provided (not undefined)
|
|
436
|
+
expect(result.current.resolvedScope?.organisationId).toBe('org-123');
|
|
437
|
+
expect(result.current.resolvedScope?.appId).toBe('');
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
describe('Error Handling', () => {
|
|
442
|
+
it('handles error when event scope resolution fails', async () => {
|
|
443
|
+
const error = new Error('Failed to resolve event scope');
|
|
444
|
+
mockCreateScopeFromEvent.mockResolvedValue(null);
|
|
445
|
+
|
|
446
|
+
const { result } = renderHook(() =>
|
|
447
|
+
useResolvedScope({
|
|
448
|
+
supabase: mockSupabase,
|
|
449
|
+
selectedOrganisationId: null,
|
|
450
|
+
selectedEventId: 'event-123',
|
|
451
|
+
})
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
await waitFor(
|
|
455
|
+
() => {
|
|
456
|
+
expect(result.current.isLoading).toBe(false);
|
|
457
|
+
},
|
|
458
|
+
{ timeout: 2000 }
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
expect(result.current.resolvedScope).toBeNull();
|
|
462
|
+
expect(result.current.error).toBeInstanceOf(Error);
|
|
463
|
+
expect(result.current.error?.message).toBe(
|
|
464
|
+
'Could not resolve organisation from event context'
|
|
465
|
+
);
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
it('handles error when createScopeFromEvent throws', async () => {
|
|
469
|
+
const error = new Error('Database error');
|
|
470
|
+
mockCreateScopeFromEvent.mockRejectedValue(error);
|
|
471
|
+
|
|
472
|
+
const { result } = renderHook(() =>
|
|
473
|
+
useResolvedScope({
|
|
474
|
+
supabase: mockSupabase,
|
|
475
|
+
selectedOrganisationId: null,
|
|
476
|
+
selectedEventId: 'event-123',
|
|
477
|
+
})
|
|
478
|
+
);
|
|
479
|
+
|
|
480
|
+
await waitFor(
|
|
481
|
+
() => {
|
|
482
|
+
expect(result.current.isLoading).toBe(false);
|
|
483
|
+
},
|
|
484
|
+
{ timeout: 2000 }
|
|
485
|
+
);
|
|
486
|
+
|
|
487
|
+
expect(result.current.resolvedScope).toBeNull();
|
|
488
|
+
expect(result.current.error).toEqual(error);
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it('handles database error when resolving app ID', async () => {
|
|
492
|
+
sharedMockQuery.single.mockRejectedValueOnce(
|
|
493
|
+
new Error('Database connection failed')
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
const { result, rerender } = renderHook(() =>
|
|
497
|
+
useResolvedScope({
|
|
498
|
+
supabase: mockSupabase,
|
|
499
|
+
selectedOrganisationId: 'org-123',
|
|
500
|
+
selectedEventId: 'event-123',
|
|
501
|
+
})
|
|
502
|
+
);
|
|
503
|
+
|
|
504
|
+
await waitFor(
|
|
505
|
+
() => {
|
|
506
|
+
expect(result.current.isLoading).toBe(false);
|
|
507
|
+
},
|
|
508
|
+
{ timeout: 2000 }
|
|
509
|
+
);
|
|
510
|
+
|
|
511
|
+
// Force rerender to pick up ref update - need to pass props
|
|
512
|
+
rerender({
|
|
513
|
+
supabase: mockSupabase,
|
|
514
|
+
selectedOrganisationId: 'org-123',
|
|
515
|
+
selectedEventId: 'event-123',
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
await waitFor(
|
|
519
|
+
() => {
|
|
520
|
+
expect(result.current.resolvedScope).not.toBeNull();
|
|
521
|
+
},
|
|
522
|
+
{ timeout: 2000, interval: 10 }
|
|
523
|
+
);
|
|
524
|
+
|
|
525
|
+
// Should still resolve scope without app ID
|
|
526
|
+
// Note: appId is set to empty string '' when not provided (not undefined)
|
|
527
|
+
expect(result.current.resolvedScope?.organisationId).toBe('org-123');
|
|
528
|
+
expect(result.current.resolvedScope?.appId).toBe('');
|
|
529
|
+
});
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
describe('Dependency Changes', () => {
|
|
533
|
+
it('refetches when organisation ID changes', async () => {
|
|
534
|
+
sharedMockQuery.single.mockResolvedValue({
|
|
535
|
+
data: { id: 'app-123', name: 'test-app', is_active: true },
|
|
536
|
+
error: null,
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
const { result, rerender } = renderHook(
|
|
540
|
+
({ selectedOrganisationId, selectedEventId }) =>
|
|
541
|
+
useResolvedScope({
|
|
542
|
+
supabase: mockSupabase,
|
|
543
|
+
selectedOrganisationId,
|
|
544
|
+
selectedEventId,
|
|
545
|
+
}),
|
|
546
|
+
{
|
|
547
|
+
initialProps: {
|
|
548
|
+
selectedOrganisationId: 'org-123',
|
|
549
|
+
selectedEventId: 'event-123',
|
|
550
|
+
},
|
|
551
|
+
}
|
|
552
|
+
);
|
|
553
|
+
|
|
554
|
+
await waitFor(
|
|
555
|
+
() => {
|
|
556
|
+
expect(result.current.isLoading).toBe(false);
|
|
557
|
+
},
|
|
558
|
+
{ timeout: 2000 }
|
|
559
|
+
);
|
|
560
|
+
|
|
561
|
+
// Force rerender to pick up ref update - need to pass current props
|
|
562
|
+
rerender({
|
|
563
|
+
supabase: mockSupabase,
|
|
564
|
+
selectedOrganisationId: 'org-123',
|
|
565
|
+
selectedEventId: 'event-123',
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
await waitFor(
|
|
569
|
+
() => {
|
|
570
|
+
expect(result.current.resolvedScope).not.toBeNull();
|
|
571
|
+
expect(result.current.resolvedScope?.organisationId).toBe('org-123');
|
|
572
|
+
},
|
|
573
|
+
{ timeout: 2000, interval: 10 }
|
|
574
|
+
);
|
|
575
|
+
|
|
576
|
+
// Change organisation
|
|
577
|
+
rerender({
|
|
578
|
+
supabase: mockSupabase,
|
|
579
|
+
selectedOrganisationId: 'org-456',
|
|
580
|
+
selectedEventId: 'event-123',
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
await waitFor(
|
|
584
|
+
() => {
|
|
585
|
+
expect(result.current.isLoading).toBe(false);
|
|
586
|
+
},
|
|
587
|
+
{ timeout: 2000 }
|
|
588
|
+
);
|
|
589
|
+
|
|
590
|
+
// Force rerender to pick up ref update - need to pass current props
|
|
591
|
+
rerender({
|
|
592
|
+
supabase: mockSupabase,
|
|
593
|
+
selectedOrganisationId: 'org-456',
|
|
594
|
+
selectedEventId: 'event-123',
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
await waitFor(
|
|
598
|
+
() => {
|
|
599
|
+
expect(result.current.resolvedScope?.organisationId).toBe('org-456');
|
|
600
|
+
},
|
|
601
|
+
{ timeout: 2000, interval: 10 }
|
|
602
|
+
);
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
it('refetches when event ID changes', async () => {
|
|
606
|
+
sharedMockQuery.single.mockResolvedValue({
|
|
607
|
+
data: { id: 'app-123', name: 'test-app', is_active: true },
|
|
608
|
+
error: null,
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
const { result, rerender } = renderHook(
|
|
612
|
+
({ selectedOrganisationId, selectedEventId }) =>
|
|
613
|
+
useResolvedScope({
|
|
614
|
+
supabase: mockSupabase,
|
|
615
|
+
selectedOrganisationId,
|
|
616
|
+
selectedEventId,
|
|
617
|
+
}),
|
|
618
|
+
{
|
|
619
|
+
initialProps: {
|
|
620
|
+
selectedOrganisationId: 'org-123',
|
|
621
|
+
selectedEventId: 'event-123',
|
|
622
|
+
},
|
|
623
|
+
}
|
|
624
|
+
);
|
|
625
|
+
|
|
626
|
+
await waitFor(
|
|
627
|
+
() => {
|
|
628
|
+
expect(result.current.isLoading).toBe(false);
|
|
629
|
+
},
|
|
630
|
+
{ timeout: 2000 }
|
|
631
|
+
);
|
|
632
|
+
|
|
633
|
+
// Force rerender to pick up ref update - need to pass current props
|
|
634
|
+
rerender({
|
|
635
|
+
supabase: mockSupabase,
|
|
636
|
+
selectedOrganisationId: 'org-123',
|
|
637
|
+
selectedEventId: 'event-123',
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
await waitFor(
|
|
641
|
+
() => {
|
|
642
|
+
expect(result.current.resolvedScope).not.toBeNull();
|
|
643
|
+
expect(result.current.resolvedScope?.eventId).toBe('event-123');
|
|
644
|
+
},
|
|
645
|
+
{ timeout: 2000, interval: 10 }
|
|
646
|
+
);
|
|
647
|
+
|
|
648
|
+
// Change event
|
|
649
|
+
rerender({
|
|
650
|
+
supabase: mockSupabase,
|
|
651
|
+
selectedOrganisationId: 'org-123',
|
|
652
|
+
selectedEventId: 'event-456',
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
await waitFor(
|
|
656
|
+
() => {
|
|
657
|
+
expect(result.current.isLoading).toBe(false);
|
|
658
|
+
},
|
|
659
|
+
{ timeout: 2000 }
|
|
660
|
+
);
|
|
661
|
+
|
|
662
|
+
// Force rerender to pick up ref update - need to pass current props
|
|
663
|
+
rerender({
|
|
664
|
+
supabase: mockSupabase,
|
|
665
|
+
selectedOrganisationId: 'org-123',
|
|
666
|
+
selectedEventId: 'event-456',
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
await waitFor(
|
|
670
|
+
() => {
|
|
671
|
+
expect(result.current.resolvedScope?.eventId).toBe('event-456');
|
|
672
|
+
},
|
|
673
|
+
{ timeout: 2000, interval: 10 }
|
|
674
|
+
);
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
it('refetches when supabase client changes', async () => {
|
|
678
|
+
const mockQuery1 = (mockSupabase.from as any)('rbac_apps');
|
|
679
|
+
mockQuery1.select().eq().eq().single.mockResolvedValue({
|
|
680
|
+
data: { id: 'app-123', name: 'test-app', is_active: true },
|
|
681
|
+
error: null,
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
const { result, rerender } = renderHook(
|
|
685
|
+
({ supabase, selectedOrganisationId, selectedEventId }) =>
|
|
686
|
+
useResolvedScope({
|
|
687
|
+
supabase,
|
|
688
|
+
selectedOrganisationId,
|
|
689
|
+
selectedEventId,
|
|
690
|
+
}),
|
|
691
|
+
{
|
|
692
|
+
initialProps: {
|
|
693
|
+
supabase: mockSupabase,
|
|
694
|
+
selectedOrganisationId: 'org-123',
|
|
695
|
+
selectedEventId: 'event-123',
|
|
696
|
+
},
|
|
697
|
+
}
|
|
698
|
+
);
|
|
699
|
+
|
|
700
|
+
await waitFor(
|
|
701
|
+
() => {
|
|
702
|
+
expect(result.current.isLoading).toBe(false);
|
|
703
|
+
},
|
|
704
|
+
{ timeout: 2000 }
|
|
705
|
+
);
|
|
706
|
+
|
|
707
|
+
// Force rerender to pick up ref update - need to pass current props
|
|
708
|
+
rerender({
|
|
709
|
+
supabase: mockSupabase,
|
|
710
|
+
selectedOrganisationId: 'org-123',
|
|
711
|
+
selectedEventId: 'event-123',
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
await waitFor(
|
|
715
|
+
() => {
|
|
716
|
+
expect(result.current.resolvedScope).not.toBeNull();
|
|
717
|
+
},
|
|
718
|
+
{ timeout: 2000, interval: 10 }
|
|
719
|
+
);
|
|
720
|
+
|
|
721
|
+
// Change supabase client - create new mock with shared query builder
|
|
722
|
+
const newSupabaseQuery = {
|
|
723
|
+
select: vi.fn().mockReturnThis(),
|
|
724
|
+
eq: vi.fn().mockReturnThis(),
|
|
725
|
+
single: vi.fn().mockResolvedValue({
|
|
726
|
+
data: { id: 'app-456', name: 'test-app', is_active: true },
|
|
727
|
+
error: null,
|
|
728
|
+
}),
|
|
729
|
+
};
|
|
730
|
+
const newSupabase = {
|
|
731
|
+
from: vi.fn().mockReturnValue(newSupabaseQuery),
|
|
732
|
+
rpc: vi.fn(),
|
|
733
|
+
} as any;
|
|
734
|
+
|
|
735
|
+
rerender({
|
|
736
|
+
supabase: newSupabase,
|
|
737
|
+
selectedOrganisationId: 'org-123',
|
|
738
|
+
selectedEventId: 'event-123',
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
await waitFor(
|
|
742
|
+
() => {
|
|
743
|
+
expect(result.current.isLoading).toBe(false);
|
|
744
|
+
},
|
|
745
|
+
{ timeout: 2000 }
|
|
746
|
+
);
|
|
747
|
+
|
|
748
|
+
// Force rerender to pick up ref update - need to pass current props
|
|
749
|
+
rerender({
|
|
750
|
+
supabase: newSupabase,
|
|
751
|
+
selectedOrganisationId: 'org-123',
|
|
752
|
+
selectedEventId: 'event-123',
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
await waitFor(
|
|
756
|
+
() => {
|
|
757
|
+
expect(result.current.resolvedScope?.appId).toBe('app-456');
|
|
758
|
+
},
|
|
759
|
+
{ timeout: 2000, interval: 10 }
|
|
760
|
+
);
|
|
761
|
+
});
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
describe('Edge Cases', () => {
|
|
765
|
+
it('handles empty string organisation ID', async () => {
|
|
766
|
+
const { result } = renderHook(() =>
|
|
767
|
+
useResolvedScope({
|
|
768
|
+
supabase: mockSupabase,
|
|
769
|
+
selectedOrganisationId: '',
|
|
770
|
+
selectedEventId: null,
|
|
771
|
+
})
|
|
772
|
+
);
|
|
773
|
+
|
|
774
|
+
await waitFor(
|
|
775
|
+
() => {
|
|
776
|
+
expect(result.current.isLoading).toBe(false);
|
|
777
|
+
},
|
|
778
|
+
{ timeout: 2000 }
|
|
779
|
+
);
|
|
780
|
+
|
|
781
|
+
expect(result.current.resolvedScope).toBeNull();
|
|
782
|
+
expect(result.current.error).toBeInstanceOf(Error);
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
it('handles empty string event ID', async () => {
|
|
786
|
+
sharedMockQuery.single.mockResolvedValue({
|
|
787
|
+
data: { id: 'app-123', name: 'test-app', is_active: true },
|
|
788
|
+
error: null,
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
const { result, rerender } = renderHook(() =>
|
|
792
|
+
useResolvedScope({
|
|
793
|
+
supabase: mockSupabase,
|
|
794
|
+
selectedOrganisationId: 'org-123',
|
|
795
|
+
selectedEventId: '',
|
|
796
|
+
})
|
|
797
|
+
);
|
|
798
|
+
|
|
799
|
+
await waitFor(
|
|
800
|
+
() => {
|
|
801
|
+
expect(result.current.isLoading).toBe(false);
|
|
802
|
+
},
|
|
803
|
+
{ timeout: 2000 }
|
|
804
|
+
);
|
|
805
|
+
|
|
806
|
+
// Force rerender to pick up ref update - need to pass props
|
|
807
|
+
rerender({
|
|
808
|
+
supabase: mockSupabase,
|
|
809
|
+
selectedOrganisationId: 'org-123',
|
|
810
|
+
selectedEventId: 'event-123',
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
await waitFor(
|
|
814
|
+
() => {
|
|
815
|
+
expect(result.current.resolvedScope).not.toBeNull();
|
|
816
|
+
},
|
|
817
|
+
{ timeout: 2000, interval: 10 }
|
|
818
|
+
);
|
|
819
|
+
|
|
820
|
+
// Should resolve with organisation only
|
|
821
|
+
expect(result.current.resolvedScope?.organisationId).toBe('org-123');
|
|
822
|
+
expect(result.current.resolvedScope?.eventId).toBeUndefined();
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
it('preserves app ID from event scope when resolving from event', async () => {
|
|
826
|
+
sharedMockQuery.single.mockResolvedValue({
|
|
827
|
+
data: { id: 'app-123', name: 'test-app', is_active: true },
|
|
828
|
+
error: null,
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
mockCreateScopeFromEvent.mockResolvedValue({
|
|
832
|
+
organisationId: 'org-456',
|
|
833
|
+
eventId: 'event-123',
|
|
834
|
+
appId: 'app-789', // Different app ID from event scope
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
const { result, rerender } = renderHook(() =>
|
|
838
|
+
useResolvedScope({
|
|
839
|
+
supabase: mockSupabase,
|
|
840
|
+
selectedOrganisationId: null,
|
|
841
|
+
selectedEventId: 'event-123',
|
|
842
|
+
})
|
|
843
|
+
);
|
|
844
|
+
|
|
845
|
+
await waitFor(
|
|
846
|
+
() => {
|
|
847
|
+
expect(result.current.isLoading).toBe(false);
|
|
848
|
+
},
|
|
849
|
+
{ timeout: 2000 }
|
|
850
|
+
);
|
|
851
|
+
|
|
852
|
+
// Force rerender to pick up ref update - need to pass props
|
|
853
|
+
rerender({
|
|
854
|
+
supabase: mockSupabase,
|
|
855
|
+
selectedOrganisationId: 'org-123',
|
|
856
|
+
selectedEventId: 'event-123',
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
await waitFor(
|
|
860
|
+
() => {
|
|
861
|
+
expect(result.current.resolvedScope).not.toBeNull();
|
|
862
|
+
},
|
|
863
|
+
{ timeout: 2000, interval: 10 }
|
|
864
|
+
);
|
|
865
|
+
|
|
866
|
+
// Should use resolved app ID (app-123) over event scope app ID
|
|
867
|
+
expect(result.current.resolvedScope?.appId).toBe('app-123');
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
it('uses event scope app ID when app ID not resolved from database', async () => {
|
|
871
|
+
sharedMockQuery.single.mockResolvedValue({
|
|
872
|
+
data: null,
|
|
873
|
+
error: { message: 'App not found' },
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
// Mock inactive app check - second call uses same query builder
|
|
877
|
+
sharedMockQuery.single.mockResolvedValueOnce({
|
|
878
|
+
data: null,
|
|
879
|
+
error: null,
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
mockCreateScopeFromEvent.mockResolvedValue({
|
|
883
|
+
organisationId: 'org-456',
|
|
884
|
+
eventId: 'event-123',
|
|
885
|
+
appId: 'app-789', // App ID from event scope
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
const { result, rerender } = renderHook(() =>
|
|
889
|
+
useResolvedScope({
|
|
890
|
+
supabase: mockSupabase,
|
|
891
|
+
selectedOrganisationId: null,
|
|
892
|
+
selectedEventId: 'event-123',
|
|
893
|
+
})
|
|
894
|
+
);
|
|
895
|
+
|
|
896
|
+
await waitFor(
|
|
897
|
+
() => {
|
|
898
|
+
expect(result.current.isLoading).toBe(false);
|
|
899
|
+
},
|
|
900
|
+
{ timeout: 2000 }
|
|
901
|
+
);
|
|
902
|
+
|
|
903
|
+
// Force rerender to pick up ref update - need to pass props
|
|
904
|
+
rerender({
|
|
905
|
+
supabase: mockSupabase,
|
|
906
|
+
selectedOrganisationId: 'org-123',
|
|
907
|
+
selectedEventId: 'event-123',
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
await waitFor(
|
|
911
|
+
() => {
|
|
912
|
+
expect(result.current.resolvedScope).not.toBeNull();
|
|
913
|
+
},
|
|
914
|
+
{ timeout: 2000, interval: 10 }
|
|
915
|
+
);
|
|
916
|
+
|
|
917
|
+
// Should use event scope app ID when database resolution fails
|
|
918
|
+
expect(result.current.resolvedScope?.appId).toBe('app-789');
|
|
919
|
+
});
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
describe('Loading States', () => {
|
|
923
|
+
it('shows loading state during initial resolution', () => {
|
|
924
|
+
sharedMockQuery.single.mockImplementation(
|
|
925
|
+
() => new Promise(() => {}) // Never resolves
|
|
926
|
+
);
|
|
927
|
+
|
|
928
|
+
const { result } = renderHook(() =>
|
|
929
|
+
useResolvedScope({
|
|
930
|
+
supabase: mockSupabase,
|
|
931
|
+
selectedOrganisationId: 'org-123',
|
|
932
|
+
selectedEventId: 'event-123',
|
|
933
|
+
})
|
|
934
|
+
);
|
|
935
|
+
|
|
936
|
+
expect(result.current.isLoading).toBe(true);
|
|
937
|
+
expect(result.current.resolvedScope).toBeNull();
|
|
938
|
+
expect(result.current.error).toBeNull();
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
it('shows loading state during refetch', async () => {
|
|
942
|
+
sharedMockQuery.single.mockResolvedValue({
|
|
943
|
+
data: { id: 'app-123', name: 'test-app', is_active: true },
|
|
944
|
+
error: null,
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
const { result, rerender } = renderHook(
|
|
948
|
+
({ selectedOrganisationId, selectedEventId }) =>
|
|
949
|
+
useResolvedScope({
|
|
950
|
+
supabase: mockSupabase,
|
|
951
|
+
selectedOrganisationId,
|
|
952
|
+
selectedEventId,
|
|
953
|
+
}),
|
|
954
|
+
{
|
|
955
|
+
initialProps: {
|
|
956
|
+
selectedOrganisationId: 'org-123',
|
|
957
|
+
selectedEventId: 'event-123',
|
|
958
|
+
},
|
|
959
|
+
}
|
|
960
|
+
);
|
|
961
|
+
|
|
962
|
+
await waitFor(
|
|
963
|
+
() => {
|
|
964
|
+
expect(result.current.isLoading).toBe(false);
|
|
965
|
+
},
|
|
966
|
+
{ timeout: 2000 }
|
|
967
|
+
);
|
|
968
|
+
|
|
969
|
+
// Force rerender to pick up ref update
|
|
970
|
+
rerender({
|
|
971
|
+
supabase: mockSupabase,
|
|
972
|
+
selectedOrganisationId: 'org-123',
|
|
973
|
+
selectedEventId: 'event-123',
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
await waitFor(
|
|
977
|
+
() => {
|
|
978
|
+
expect(result.current.resolvedScope).not.toBeNull();
|
|
979
|
+
},
|
|
980
|
+
{ timeout: 2000, interval: 10 }
|
|
981
|
+
);
|
|
982
|
+
|
|
983
|
+
// Mock slow refetch
|
|
984
|
+
sharedMockQuery.single.mockImplementation(
|
|
985
|
+
() => new Promise(() => {}) // Never resolves
|
|
986
|
+
);
|
|
987
|
+
|
|
988
|
+
rerender({
|
|
989
|
+
selectedOrganisationId: 'org-456',
|
|
990
|
+
selectedEventId: 'event-123',
|
|
991
|
+
});
|
|
992
|
+
|
|
993
|
+
// Should show loading during refetch
|
|
994
|
+
expect(result.current.isLoading).toBe(true);
|
|
995
|
+
});
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
describe('Cleanup', () => {
|
|
999
|
+
it('cancels in-flight requests when dependencies change', async () => {
|
|
1000
|
+
let resolveCount = 0;
|
|
1001
|
+
|
|
1002
|
+
sharedMockQuery.single.mockImplementation(() => {
|
|
1003
|
+
resolveCount++;
|
|
1004
|
+
return new Promise((resolve) => {
|
|
1005
|
+
setTimeout(() => {
|
|
1006
|
+
resolve({
|
|
1007
|
+
data: { id: 'app-123', name: 'test-app', is_active: true },
|
|
1008
|
+
error: null,
|
|
1009
|
+
});
|
|
1010
|
+
}, 100);
|
|
1011
|
+
});
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
const { result, rerender } = renderHook(
|
|
1015
|
+
({ selectedOrganisationId, selectedEventId }) =>
|
|
1016
|
+
useResolvedScope({
|
|
1017
|
+
supabase: mockSupabase,
|
|
1018
|
+
selectedOrganisationId,
|
|
1019
|
+
selectedEventId,
|
|
1020
|
+
}),
|
|
1021
|
+
{
|
|
1022
|
+
initialProps: {
|
|
1023
|
+
selectedOrganisationId: 'org-123',
|
|
1024
|
+
selectedEventId: 'event-123',
|
|
1025
|
+
},
|
|
1026
|
+
}
|
|
1027
|
+
);
|
|
1028
|
+
|
|
1029
|
+
// Rapidly change dependencies
|
|
1030
|
+
rerender({
|
|
1031
|
+
selectedOrganisationId: 'org-456',
|
|
1032
|
+
selectedEventId: 'event-123',
|
|
1033
|
+
});
|
|
1034
|
+
|
|
1035
|
+
rerender({
|
|
1036
|
+
selectedOrganisationId: 'org-789',
|
|
1037
|
+
selectedEventId: 'event-123',
|
|
1038
|
+
});
|
|
1039
|
+
|
|
1040
|
+
await waitFor(
|
|
1041
|
+
() => {
|
|
1042
|
+
expect(result.current.isLoading).toBe(false);
|
|
1043
|
+
},
|
|
1044
|
+
{ timeout: 2000 }
|
|
1045
|
+
);
|
|
1046
|
+
|
|
1047
|
+
// Force rerender to pick up ref update
|
|
1048
|
+
rerender({
|
|
1049
|
+
selectedOrganisationId: 'org-789',
|
|
1050
|
+
selectedEventId: 'event-123',
|
|
1051
|
+
});
|
|
1052
|
+
|
|
1053
|
+
await waitFor(
|
|
1054
|
+
() => {
|
|
1055
|
+
expect(result.current.resolvedScope).not.toBeNull();
|
|
1056
|
+
expect(result.current.resolvedScope?.organisationId).toBe('org-789');
|
|
1057
|
+
},
|
|
1058
|
+
{ timeout: 2000, interval: 10 }
|
|
1059
|
+
);
|
|
1060
|
+
});
|
|
1061
|
+
});
|
|
1062
|
+
});
|
|
1063
|
+
|