@jmruthers/pace-core 0.5.190 → 0.5.191
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{DataTable-IVYljGJ6.d.ts → DataTable-Be6dH_dR.d.ts} +1 -1
- package/dist/{DataTable-ON3IXISJ.js → DataTable-WKRZD47S.js} +6 -6
- package/dist/{PublicPageProvider-C4uxosp6.d.ts → PublicPageProvider-ULXC_u6U.d.ts} +1 -1
- package/dist/{UnifiedAuthProvider-X5NXANVI.js → UnifiedAuthProvider-FTSG5XH7.js} +3 -3
- package/dist/{api-I6UCQ5S6.js → api-IHKALJZD.js} +2 -2
- package/dist/{chunk-J2XXC7R5.js → chunk-6LTQQAT6.js} +77 -111
- package/dist/chunk-6LTQQAT6.js.map +1 -0
- package/dist/{chunk-STYK4OH2.js → chunk-6TQDD426.js} +10 -10
- package/dist/chunk-6TQDD426.js.map +1 -0
- package/dist/{chunk-DZWK57KZ.js → chunk-G37KK66H.js} +1 -1
- package/dist/{chunk-DZWK57KZ.js.map → chunk-G37KK66H.js.map} +1 -1
- package/dist/{chunk-73HSNNOQ.js → chunk-LOMZXPSN.js} +13 -13
- package/dist/{chunk-Y4BUBBHD.js → chunk-OETXORNB.js} +3 -3
- package/dist/{chunk-RUYZKXOD.js → chunk-ROXMHMY2.js} +5 -3
- package/dist/chunk-ROXMHMY2.js.map +1 -0
- package/dist/{chunk-SDMHPX3X.js → chunk-ULHIJK66.js} +56 -21
- package/dist/{chunk-SDMHPX3X.js.map → chunk-ULHIJK66.js.map} +1 -1
- package/dist/{chunk-VVBAW5A5.js → chunk-VKB2CO4Z.js} +46 -35
- package/dist/chunk-VKB2CO4Z.js.map +1 -0
- package/dist/{chunk-HQVPB5MZ.js → chunk-VRGWKHDB.js} +6 -6
- package/dist/{chunk-NIU6J6OX.js → chunk-XNYQOL3Z.js} +16 -16
- package/dist/chunk-XNYQOL3Z.js.map +1 -0
- package/dist/{chunk-4QYC5L4K.js → chunk-XYXSXPUK.js} +22 -27
- package/dist/chunk-XYXSXPUK.js.map +1 -0
- package/dist/components.d.ts +3 -3
- package/dist/components.js +8 -8
- package/dist/{database.generated-DI89OQeI.d.ts → database.generated-CzIvgcPu.d.ts} +165 -201
- package/dist/hooks.d.ts +12 -12
- package/dist/hooks.js +7 -7
- package/dist/index.d.ts +7 -7
- package/dist/index.js +18 -23
- package/dist/index.js.map +1 -1
- package/dist/providers.js +2 -2
- package/dist/rbac/index.d.ts +1 -1
- package/dist/rbac/index.js +6 -6
- package/dist/{types-Bwgl--Xo.d.ts → types-CEpcvwwF.d.ts} +1 -1
- package/dist/types.d.ts +2 -2
- package/dist/{usePublicRouteParams-DxIDS4bC.d.ts → usePublicRouteParams-TZe0gy-4.d.ts} +1 -1
- package/dist/utils.d.ts +8 -8
- package/dist/utils.js +2 -2
- 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/Logger.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/RBACAuditManager.md +2 -2
- package/docs/api/classes/RBACCache.md +1 -1
- package/docs/api/classes/RBACEngine.md +2 -2
- package/docs/api/classes/RBACError.md +1 -1
- package/docs/api/classes/RBACNotInitializedError.md +1 -1
- package/docs/api/classes/SecureSupabaseClient.md +5 -5
- package/docs/api/classes/StorageUtils.md +1 -1
- package/docs/api/enums/FileCategory.md +1 -1
- package/docs/api/enums/LogLevel.md +1 -1
- package/docs/api/enums/RBACErrorCode.md +1 -1
- package/docs/api/enums/RPCFunction.md +1 -1
- package/docs/api/interfaces/AddressFieldProps.md +1 -1
- package/docs/api/interfaces/AddressFieldRef.md +1 -1
- package/docs/api/interfaces/AggregateConfig.md +1 -1
- package/docs/api/interfaces/AutocompleteOptions.md +1 -1
- package/docs/api/interfaces/AvatarProps.md +1 -1
- package/docs/api/interfaces/BadgeProps.md +1 -1
- package/docs/api/interfaces/ButtonProps.md +1 -1
- package/docs/api/interfaces/CalendarProps.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/ComplianceResult.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/DatabaseComplianceResult.md +1 -1
- package/docs/api/interfaces/DatabaseIssue.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 +1 -1
- package/docs/api/interfaces/ExportColumn.md +1 -1
- package/docs/api/interfaces/ExportOptions.md +1 -1
- 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/FormFieldProps.md +1 -1
- package/docs/api/interfaces/FormProps.md +1 -1
- package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
- package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
- package/docs/api/interfaces/InputProps.md +1 -1
- package/docs/api/interfaces/LabelProps.md +1 -1
- package/docs/api/interfaces/LoggerConfig.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 +1 -1
- 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/ParsedAddress.md +2 -2
- package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
- package/docs/api/interfaces/ProgressProps.md +1 -1
- package/docs/api/interfaces/ProtectedRouteProps.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/QuickFix.md +1 -1
- package/docs/api/interfaces/RBACAccessValidateParams.md +1 -1
- package/docs/api/interfaces/RBACAccessValidateResult.md +1 -1
- package/docs/api/interfaces/RBACAuditLogParams.md +1 -1
- package/docs/api/interfaces/RBACAuditLogResult.md +1 -1
- package/docs/api/interfaces/RBACConfig.md +2 -2
- package/docs/api/interfaces/RBACContext.md +1 -1
- package/docs/api/interfaces/RBACLogger.md +1 -1
- package/docs/api/interfaces/RBACPageAccessCheckParams.md +1 -1
- package/docs/api/interfaces/RBACPerformanceMetrics.md +1 -1
- package/docs/api/interfaces/RBACPermissionCheckParams.md +1 -1
- package/docs/api/interfaces/RBACPermissionCheckResult.md +1 -1
- package/docs/api/interfaces/RBACPermissionsGetParams.md +1 -1
- package/docs/api/interfaces/RBACPermissionsGetResult.md +1 -1
- package/docs/api/interfaces/RBACResult.md +1 -1
- package/docs/api/interfaces/RBACRoleGrantParams.md +1 -1
- package/docs/api/interfaces/RBACRoleGrantResult.md +1 -1
- package/docs/api/interfaces/RBACRoleRevokeParams.md +1 -1
- package/docs/api/interfaces/RBACRoleRevokeResult.md +1 -1
- package/docs/api/interfaces/RBACRoleValidateParams.md +1 -1
- package/docs/api/interfaces/RBACRoleValidateResult.md +1 -1
- package/docs/api/interfaces/RBACRolesListParams.md +1 -1
- package/docs/api/interfaces/RBACRolesListResult.md +1 -1
- package/docs/api/interfaces/RBACSessionTrackParams.md +1 -1
- package/docs/api/interfaces/RBACSessionTrackResult.md +1 -1
- package/docs/api/interfaces/ResourcePermissions.md +1 -1
- package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
- package/docs/api/interfaces/RoleManagementResult.md +1 -1
- package/docs/api/interfaces/RouteAccessRecord.md +1 -1
- package/docs/api/interfaces/RouteConfig.md +1 -1
- package/docs/api/interfaces/RuntimeComplianceResult.md +1 -1
- package/docs/api/interfaces/SecureDataContextType.md +1 -1
- package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
- package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
- package/docs/api/interfaces/SetupIssue.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/TabsContentProps.md +1 -1
- package/docs/api/interfaces/TabsListProps.md +1 -1
- package/docs/api/interfaces/TabsProps.md +1 -1
- package/docs/api/interfaces/TabsTriggerProps.md +1 -1
- package/docs/api/interfaces/TextareaProps.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/UseFormDialogOptions.md +1 -1
- package/docs/api/interfaces/UseFormDialogReturn.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventLogoOptions.md +2 -2
- package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayOptions.md +2 -2
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeOptions.md +2 -2
- package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
- package/docs/api/interfaces/UseResourcePermissionsOptions.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 +16 -16
- package/docs/migration/README.md +18 -0
- package/docs/migration/database-changes-december-2025.md +767 -0
- package/docs/migration/person-scoped-profiles-migration-guide.md +472 -0
- package/package.json +1 -1
- package/src/__tests__/public-recipe-view.test.ts +10 -10
- package/src/__tests__/rls-policies.test.ts +13 -13
- package/src/components/AddressField/README.md +6 -6
- package/src/components/OrganisationSelector/OrganisationSelector.tsx +35 -15
- package/src/components/Select/Select.test.tsx +4 -1
- package/src/components/Select/Select.tsx +60 -15
- package/src/hooks/__tests__/usePermissionCache.simple.test.ts +192 -0
- package/src/hooks/__tests__/usePermissionCache.unit.test.ts +741 -0
- package/src/hooks/__tests__/usePublicEvent.simple.test.ts +703 -0
- package/src/hooks/__tests__/usePublicEvent.unit.test.ts +581 -0
- package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +9 -8
- package/src/hooks/public/usePublicEvent.ts +8 -8
- package/src/hooks/public/usePublicFileDisplay.ts +2 -2
- package/src/hooks/useFileDisplay.ts +8 -9
- package/src/hooks/useQueryCache.ts +6 -6
- package/src/hooks/useSecureDataAccess.test.ts +8 -8
- package/src/hooks/useSecureDataAccess.ts +15 -11
- package/src/providers/__tests__/OrganisationProvider.test.tsx +27 -21
- package/src/rbac/hooks/useRBAC.simple.test.ts +95 -0
- package/src/rbac/utils/__tests__/eventContext.test.ts +2 -2
- package/src/rbac/utils/__tests__/eventContext.unit.test.ts +490 -0
- package/src/rbac/utils/eventContext.ts +5 -2
- package/src/services/AuthService.ts +37 -8
- package/src/services/OrganisationService.ts +92 -139
- package/src/services/__tests__/OrganisationService.pagination.test.ts +34 -8
- package/src/services/__tests__/OrganisationService.test.ts +218 -86
- package/src/types/database.generated.ts +166 -201
- package/src/types/supabase.ts +2 -2
- package/src/utils/__tests__/secureDataAccess.unit.test.ts +3 -2
- package/src/utils/file-reference/index.ts +4 -4
- package/src/utils/google-places/googlePlacesUtils.ts +1 -1
- package/src/utils/google-places/types.ts +1 -1
- package/src/utils/request-deduplication.ts +4 -4
- package/src/utils/security/secureDataAccess.test.ts +1 -1
- package/src/utils/security/secureDataAccess.ts +7 -4
- package/src/utils/storage/README.md +1 -1
- package/dist/chunk-4QYC5L4K.js.map +0 -1
- package/dist/chunk-J2XXC7R5.js.map +0 -1
- package/dist/chunk-NIU6J6OX.js.map +0 -1
- package/dist/chunk-RUYZKXOD.js.map +0 -1
- package/dist/chunk-STYK4OH2.js.map +0 -1
- package/dist/chunk-VVBAW5A5.js.map +0 -1
- /package/dist/{DataTable-ON3IXISJ.js.map → DataTable-WKRZD47S.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-X5NXANVI.js.map → UnifiedAuthProvider-FTSG5XH7.js.map} +0 -0
- /package/dist/{api-I6UCQ5S6.js.map → api-IHKALJZD.js.map} +0 -0
- /package/dist/{chunk-73HSNNOQ.js.map → chunk-LOMZXPSN.js.map} +0 -0
- /package/dist/{chunk-Y4BUBBHD.js.map → chunk-OETXORNB.js.map} +0 -0
- /package/dist/{chunk-HQVPB5MZ.js.map → chunk-VRGWKHDB.js.map} +0 -0
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Event Context Utilities Tests
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module RBAC/EventContext/Tests
|
|
5
|
+
* @since 1.0.0
|
|
6
|
+
*
|
|
7
|
+
* Comprehensive tests for event context utilities in the RBAC system.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
11
|
+
import { SupabaseClient } from '@supabase/supabase-js';
|
|
12
|
+
import { Database } from '../../../types/database';
|
|
13
|
+
import { UUID, Scope } from '../../types';
|
|
14
|
+
import {
|
|
15
|
+
getOrganisationFromEvent,
|
|
16
|
+
createScopeFromEvent,
|
|
17
|
+
isEventBasedScope,
|
|
18
|
+
isValidEventBasedScope,
|
|
19
|
+
clearAllOrgDerivationCache,
|
|
20
|
+
clearOrgDerivationCache
|
|
21
|
+
} from '../eventContext';
|
|
22
|
+
|
|
23
|
+
// Mock Supabase client
|
|
24
|
+
const createMockSupabaseClient = () => {
|
|
25
|
+
const mockQuery = {
|
|
26
|
+
select: vi.fn().mockReturnThis(),
|
|
27
|
+
eq: vi.fn().mockReturnThis(),
|
|
28
|
+
single: vi.fn()
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const fromMock = vi.fn().mockReturnValue(mockQuery);
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
from: fromMock,
|
|
35
|
+
query: mockQuery
|
|
36
|
+
} as unknown as SupabaseClient<Database>;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
describe('Event Context Utilities', () => {
|
|
40
|
+
let mockSupabase: SupabaseClient<Database>;
|
|
41
|
+
let mockQuery: any;
|
|
42
|
+
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
// Clear cache before each test
|
|
45
|
+
clearAllOrgDerivationCache();
|
|
46
|
+
|
|
47
|
+
mockSupabase = createMockSupabaseClient();
|
|
48
|
+
// Reset mockQuery to get a fresh query builder for each test
|
|
49
|
+
mockQuery = {
|
|
50
|
+
select: vi.fn().mockReturnThis(),
|
|
51
|
+
eq: vi.fn().mockReturnThis(),
|
|
52
|
+
single: vi.fn()
|
|
53
|
+
};
|
|
54
|
+
(mockSupabase.from as any).mockReturnValue(mockQuery);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
afterEach(() => {
|
|
58
|
+
vi.clearAllMocks();
|
|
59
|
+
// Clear cache after each test
|
|
60
|
+
clearAllOrgDerivationCache();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('getOrganisationFromEvent', () => {
|
|
64
|
+
it('should return organisation ID when event exists', async () => {
|
|
65
|
+
const eventId = 'event-123';
|
|
66
|
+
const organisationId = 'org-456';
|
|
67
|
+
|
|
68
|
+
mockQuery.single.mockResolvedValue({
|
|
69
|
+
data: { organisation_id: organisationId },
|
|
70
|
+
error: null
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const result = await getOrganisationFromEvent(mockSupabase, eventId);
|
|
74
|
+
|
|
75
|
+
expect(result).toBe(organisationId);
|
|
76
|
+
expect(mockSupabase.from).toHaveBeenCalledWith('core_events');
|
|
77
|
+
expect(mockQuery.select).toHaveBeenCalledWith('organisation_id');
|
|
78
|
+
expect(mockQuery.eq).toHaveBeenCalledWith('event_id', eventId);
|
|
79
|
+
expect(mockQuery.single).toHaveBeenCalled();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should return null when event does not exist', async () => {
|
|
83
|
+
const eventId = 'nonexistent-event';
|
|
84
|
+
|
|
85
|
+
mockQuery.single.mockResolvedValue({
|
|
86
|
+
data: null,
|
|
87
|
+
error: { message: 'Event not found' }
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const result = await getOrganisationFromEvent(mockSupabase, eventId);
|
|
91
|
+
|
|
92
|
+
expect(result).toBeNull();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should return null when data is null', async () => {
|
|
96
|
+
const eventId = 'event-123';
|
|
97
|
+
|
|
98
|
+
// Clear cache for this specific test
|
|
99
|
+
clearOrgDerivationCache(eventId);
|
|
100
|
+
|
|
101
|
+
mockQuery.single.mockResolvedValue({
|
|
102
|
+
data: null,
|
|
103
|
+
error: null
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const result = await getOrganisationFromEvent(mockSupabase, eventId);
|
|
107
|
+
|
|
108
|
+
expect(result).toBeNull();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should handle database errors gracefully', async () => {
|
|
112
|
+
const eventId = 'event-123';
|
|
113
|
+
const dbError = new Error('Database connection failed');
|
|
114
|
+
|
|
115
|
+
// Clear cache for this specific test
|
|
116
|
+
clearOrgDerivationCache(eventId);
|
|
117
|
+
|
|
118
|
+
mockQuery.single.mockRejectedValue(dbError);
|
|
119
|
+
|
|
120
|
+
await expect(getOrganisationFromEvent(mockSupabase, eventId))
|
|
121
|
+
.rejects.toThrow('Database connection failed');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should handle empty organisation_id', async () => {
|
|
125
|
+
const eventId = 'event-123';
|
|
126
|
+
|
|
127
|
+
// Clear cache for this specific test
|
|
128
|
+
clearOrgDerivationCache(eventId);
|
|
129
|
+
|
|
130
|
+
mockQuery.single.mockResolvedValue({
|
|
131
|
+
data: { organisation_id: null },
|
|
132
|
+
error: null
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const result = await getOrganisationFromEvent(mockSupabase, eventId);
|
|
136
|
+
|
|
137
|
+
expect(result).toBeNull();
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe('createScopeFromEvent', () => {
|
|
142
|
+
it('should create complete scope when event exists', async () => {
|
|
143
|
+
const eventId = 'event-123';
|
|
144
|
+
const organisationId = 'org-456';
|
|
145
|
+
const appId = 'app-789';
|
|
146
|
+
|
|
147
|
+
mockQuery.single.mockResolvedValue({
|
|
148
|
+
data: { organisation_id: organisationId },
|
|
149
|
+
error: null
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const result = await createScopeFromEvent(mockSupabase, eventId, appId);
|
|
153
|
+
|
|
154
|
+
expect(result).toEqual({
|
|
155
|
+
organisationId,
|
|
156
|
+
eventId,
|
|
157
|
+
appId
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('should create scope without appId when not provided', async () => {
|
|
162
|
+
const eventId = 'event-123';
|
|
163
|
+
const organisationId = 'org-456';
|
|
164
|
+
|
|
165
|
+
mockQuery.single.mockResolvedValue({
|
|
166
|
+
data: { organisation_id: organisationId },
|
|
167
|
+
error: null
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const result = await createScopeFromEvent(mockSupabase, eventId);
|
|
171
|
+
|
|
172
|
+
expect(result).toEqual({
|
|
173
|
+
organisationId,
|
|
174
|
+
eventId,
|
|
175
|
+
appId: undefined
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('should return null when event does not exist', async () => {
|
|
180
|
+
const eventId = 'nonexistent-event';
|
|
181
|
+
|
|
182
|
+
mockQuery.single.mockResolvedValue({
|
|
183
|
+
data: null,
|
|
184
|
+
error: { message: 'Event not found' }
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const result = await createScopeFromEvent(mockSupabase, eventId);
|
|
188
|
+
|
|
189
|
+
expect(result).toBeNull();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('should return null when organisation lookup fails', async () => {
|
|
193
|
+
const eventId = 'event-123';
|
|
194
|
+
|
|
195
|
+
// Clear cache for this specific test
|
|
196
|
+
clearOrgDerivationCache(eventId);
|
|
197
|
+
|
|
198
|
+
mockQuery.single.mockResolvedValue({
|
|
199
|
+
data: { organisation_id: null },
|
|
200
|
+
error: null
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const result = await createScopeFromEvent(mockSupabase, eventId);
|
|
204
|
+
|
|
205
|
+
expect(result).toBeNull();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('should handle database errors gracefully', async () => {
|
|
209
|
+
const eventId = 'event-123';
|
|
210
|
+
const dbError = new Error('Database connection failed');
|
|
211
|
+
|
|
212
|
+
// Clear cache for this specific test
|
|
213
|
+
clearOrgDerivationCache(eventId);
|
|
214
|
+
|
|
215
|
+
mockQuery.single.mockRejectedValue(dbError);
|
|
216
|
+
|
|
217
|
+
await expect(createScopeFromEvent(mockSupabase, eventId))
|
|
218
|
+
.rejects.toThrow('Database connection failed');
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe('isEventBasedScope', () => {
|
|
223
|
+
it('should return true for event-based scope (no organisationId, has eventId)', () => {
|
|
224
|
+
const scope: Scope = {
|
|
225
|
+
eventId: 'event-123',
|
|
226
|
+
appId: 'app-456'
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
expect(isEventBasedScope(scope)).toBe(true);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should return false when organisationId is present', () => {
|
|
233
|
+
const scope: Scope = {
|
|
234
|
+
organisationId: 'org-123',
|
|
235
|
+
eventId: 'event-123',
|
|
236
|
+
appId: 'app-456'
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
expect(isEventBasedScope(scope)).toBe(false);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('should return false when eventId is missing', () => {
|
|
243
|
+
const scope: Scope = {
|
|
244
|
+
appId: 'app-456'
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
expect(isEventBasedScope(scope)).toBe(false);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('should return false when both organisationId and eventId are missing', () => {
|
|
251
|
+
const scope: Scope = {
|
|
252
|
+
appId: 'app-456'
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
expect(isEventBasedScope(scope)).toBe(false);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('should return false when eventId is null', () => {
|
|
259
|
+
const scope: Scope = {
|
|
260
|
+
eventId: null,
|
|
261
|
+
appId: 'app-456'
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
expect(isEventBasedScope(scope)).toBe(false);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('should return false when eventId is undefined', () => {
|
|
268
|
+
const scope: Scope = {
|
|
269
|
+
eventId: undefined,
|
|
270
|
+
appId: 'app-456'
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
expect(isEventBasedScope(scope)).toBe(false);
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
describe('isValidEventBasedScope', () => {
|
|
278
|
+
it('should return true for valid event-based scope', () => {
|
|
279
|
+
const scope: Scope = {
|
|
280
|
+
eventId: 'event-123',
|
|
281
|
+
appId: 'app-456'
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
expect(isValidEventBasedScope(scope)).toBe(true);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('should return false when eventId is missing', () => {
|
|
288
|
+
const scope: Scope = {
|
|
289
|
+
appId: 'app-456'
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
expect(isValidEventBasedScope(scope)).toBe(false);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('should return false when eventId is null', () => {
|
|
296
|
+
const scope: Scope = {
|
|
297
|
+
eventId: null,
|
|
298
|
+
appId: 'app-456'
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
expect(isValidEventBasedScope(scope)).toBe(false);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('should return false when eventId is undefined', () => {
|
|
305
|
+
const scope: Scope = {
|
|
306
|
+
eventId: undefined,
|
|
307
|
+
appId: 'app-456'
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
expect(isValidEventBasedScope(scope)).toBe(false);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('should return false when organisationId is present (not event-based)', () => {
|
|
314
|
+
const scope: Scope = {
|
|
315
|
+
organisationId: 'org-123',
|
|
316
|
+
eventId: 'event-123',
|
|
317
|
+
appId: 'app-456'
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
expect(isValidEventBasedScope(scope)).toBe(false);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('should return true for event-based scope without appId', () => {
|
|
324
|
+
const scope: Scope = {
|
|
325
|
+
eventId: 'event-123'
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
expect(isValidEventBasedScope(scope)).toBe(true);
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
describe('Edge Cases and Error Handling', () => {
|
|
333
|
+
it('should handle malformed event IDs', async () => {
|
|
334
|
+
const malformedEventId = '';
|
|
335
|
+
|
|
336
|
+
mockQuery.single.mockResolvedValue({
|
|
337
|
+
data: null,
|
|
338
|
+
error: { message: 'Invalid event ID' }
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
const result = await getOrganisationFromEvent(mockSupabase, malformedEventId);
|
|
342
|
+
|
|
343
|
+
expect(result).toBeNull();
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('should handle very long event IDs', async () => {
|
|
347
|
+
const longEventId = 'a'.repeat(1000);
|
|
348
|
+
const organisationId = 'org-456';
|
|
349
|
+
|
|
350
|
+
mockQuery.single.mockResolvedValue({
|
|
351
|
+
data: { organisation_id: organisationId },
|
|
352
|
+
error: null
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
const result = await getOrganisationFromEvent(mockSupabase, longEventId);
|
|
356
|
+
|
|
357
|
+
expect(result).toBe(organisationId);
|
|
358
|
+
expect(mockQuery.eq).toHaveBeenCalledWith('event_id', longEventId);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it('should handle special characters in event IDs', async () => {
|
|
362
|
+
const specialEventId = 'event-123!@#$%^&*()';
|
|
363
|
+
const organisationId = 'org-456';
|
|
364
|
+
|
|
365
|
+
mockQuery.single.mockResolvedValue({
|
|
366
|
+
data: { organisation_id: organisationId },
|
|
367
|
+
error: null
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
const result = await getOrganisationFromEvent(mockSupabase, specialEventId);
|
|
371
|
+
|
|
372
|
+
expect(result).toBe(organisationId);
|
|
373
|
+
expect(mockQuery.eq).toHaveBeenCalledWith('event_id', specialEventId);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it('should handle concurrent calls to getOrganisationFromEvent', async () => {
|
|
377
|
+
const eventId1 = 'event-123';
|
|
378
|
+
const eventId2 = 'event-456';
|
|
379
|
+
const organisationId1 = 'org-123';
|
|
380
|
+
const organisationId2 = 'org-456';
|
|
381
|
+
|
|
382
|
+
// Clear cache for these specific events
|
|
383
|
+
clearOrgDerivationCache(eventId1);
|
|
384
|
+
clearOrgDerivationCache(eventId2);
|
|
385
|
+
|
|
386
|
+
// Create separate query builders for concurrent calls
|
|
387
|
+
const mockQuery1 = {
|
|
388
|
+
select: vi.fn().mockReturnThis(),
|
|
389
|
+
eq: vi.fn().mockReturnThis(),
|
|
390
|
+
single: vi.fn().mockResolvedValue({
|
|
391
|
+
data: { organisation_id: organisationId1 },
|
|
392
|
+
error: null
|
|
393
|
+
})
|
|
394
|
+
};
|
|
395
|
+
const mockQuery2 = {
|
|
396
|
+
select: vi.fn().mockReturnThis(),
|
|
397
|
+
eq: vi.fn().mockReturnThis(),
|
|
398
|
+
single: vi.fn().mockResolvedValue({
|
|
399
|
+
data: { organisation_id: organisationId2 },
|
|
400
|
+
error: null
|
|
401
|
+
})
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
(mockSupabase.from as any)
|
|
405
|
+
.mockReturnValueOnce(mockQuery1)
|
|
406
|
+
.mockReturnValueOnce(mockQuery2);
|
|
407
|
+
|
|
408
|
+
const [result1, result2] = await Promise.all([
|
|
409
|
+
getOrganisationFromEvent(mockSupabase, eventId1),
|
|
410
|
+
getOrganisationFromEvent(mockSupabase, eventId2)
|
|
411
|
+
]);
|
|
412
|
+
|
|
413
|
+
expect(result1).toBe(organisationId1);
|
|
414
|
+
expect(result2).toBe(organisationId2);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it('should handle concurrent calls to createScopeFromEvent', async () => {
|
|
418
|
+
const eventId1 = 'event-123';
|
|
419
|
+
const eventId2 = 'event-456';
|
|
420
|
+
const organisationId1 = 'org-123';
|
|
421
|
+
const organisationId2 = 'org-456';
|
|
422
|
+
const appId1 = 'app-123';
|
|
423
|
+
const appId2 = 'app-456';
|
|
424
|
+
|
|
425
|
+
// Clear cache for these specific events
|
|
426
|
+
clearOrgDerivationCache(eventId1);
|
|
427
|
+
clearOrgDerivationCache(eventId2);
|
|
428
|
+
|
|
429
|
+
// Create separate query builders for concurrent calls
|
|
430
|
+
const mockQuery1 = {
|
|
431
|
+
select: vi.fn().mockReturnThis(),
|
|
432
|
+
eq: vi.fn().mockReturnThis(),
|
|
433
|
+
single: vi.fn().mockResolvedValue({
|
|
434
|
+
data: { organisation_id: organisationId1 },
|
|
435
|
+
error: null
|
|
436
|
+
})
|
|
437
|
+
};
|
|
438
|
+
const mockQuery2 = {
|
|
439
|
+
select: vi.fn().mockReturnThis(),
|
|
440
|
+
eq: vi.fn().mockReturnThis(),
|
|
441
|
+
single: vi.fn().mockResolvedValue({
|
|
442
|
+
data: { organisation_id: organisationId2 },
|
|
443
|
+
error: null
|
|
444
|
+
})
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
(mockSupabase.from as any)
|
|
448
|
+
.mockReturnValueOnce(mockQuery1)
|
|
449
|
+
.mockReturnValueOnce(mockQuery2);
|
|
450
|
+
|
|
451
|
+
const [result1, result2] = await Promise.all([
|
|
452
|
+
createScopeFromEvent(mockSupabase, eventId1, appId1),
|
|
453
|
+
createScopeFromEvent(mockSupabase, eventId2, appId2)
|
|
454
|
+
]);
|
|
455
|
+
|
|
456
|
+
expect(result1).toEqual({
|
|
457
|
+
organisationId: organisationId1,
|
|
458
|
+
eventId: eventId1,
|
|
459
|
+
appId: appId1
|
|
460
|
+
});
|
|
461
|
+
expect(result2).toEqual({
|
|
462
|
+
organisationId: organisationId2,
|
|
463
|
+
eventId: eventId2,
|
|
464
|
+
appId: appId2
|
|
465
|
+
});
|
|
466
|
+
});
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
describe('Type Safety', () => {
|
|
470
|
+
it('should handle UUID types correctly', () => {
|
|
471
|
+
const validUUID = '123e4567-e89b-12d3-a456-426614174000';
|
|
472
|
+
const scope: Scope = {
|
|
473
|
+
eventId: validUUID,
|
|
474
|
+
appId: validUUID
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
expect(isValidEventBasedScope(scope)).toBe(true);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it('should handle string types correctly', () => {
|
|
481
|
+
const stringId = 'event-123';
|
|
482
|
+
const scope: Scope = {
|
|
483
|
+
eventId: stringId,
|
|
484
|
+
appId: 'app-456'
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
expect(isValidEventBasedScope(scope)).toBe(true);
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
});
|
|
@@ -56,7 +56,7 @@ export async function getOrganisationFromEvent(
|
|
|
56
56
|
|
|
57
57
|
// Query database
|
|
58
58
|
const { data, error } = await supabase
|
|
59
|
-
.from('
|
|
59
|
+
.from('core_events')
|
|
60
60
|
.select('organisation_id')
|
|
61
61
|
.eq('event_id', eventId)
|
|
62
62
|
.single() as { data: { organisation_id: string } | null; error: any };
|
|
@@ -65,8 +65,11 @@ export async function getOrganisationFromEvent(
|
|
|
65
65
|
|
|
66
66
|
if (error || !data) {
|
|
67
67
|
organisationId = null;
|
|
68
|
-
} else {
|
|
68
|
+
} else if (data.organisation_id) {
|
|
69
69
|
organisationId = data.organisation_id;
|
|
70
|
+
} else {
|
|
71
|
+
// organisation_id is null or undefined
|
|
72
|
+
organisationId = null;
|
|
70
73
|
}
|
|
71
74
|
|
|
72
75
|
// Cache the result (with size limit to prevent memory leaks)
|
|
@@ -299,7 +299,15 @@ export class AuthService extends BaseService implements IAuthService {
|
|
|
299
299
|
// Lifecycle methods
|
|
300
300
|
async initialize(): Promise<void> {
|
|
301
301
|
await super.initialize();
|
|
302
|
+
// Set loading to true before starting session restoration
|
|
303
|
+
// This ensures ProtectedRoute shows loading state while session is being restored
|
|
304
|
+
this.authLoading = true;
|
|
305
|
+
this.notify();
|
|
306
|
+
|
|
307
|
+
// Setup auth state listener first - this will receive INITIAL_SESSION event
|
|
302
308
|
await this.setupAuthStateListener();
|
|
309
|
+
|
|
310
|
+
// Then restore session - this will trigger INITIAL_SESSION event if session exists
|
|
303
311
|
await this.restoreSession();
|
|
304
312
|
}
|
|
305
313
|
|
|
@@ -431,17 +439,25 @@ export class AuthService extends BaseService implements IAuthService {
|
|
|
431
439
|
this.sessionRestorationState.restorationError ||
|
|
432
440
|
(hasTimeoutError && session)) {
|
|
433
441
|
this.finishSessionRestoration();
|
|
434
|
-
|
|
442
|
+
}
|
|
443
|
+
} else {
|
|
444
|
+
// No session in INITIAL_SESSION event - user is not authenticated
|
|
445
|
+
// Finish restoration to clear loading state
|
|
446
|
+
if (this.sessionRestorationState.isRestoring) {
|
|
447
|
+
this.finishSessionRestoration();
|
|
435
448
|
}
|
|
436
449
|
}
|
|
437
450
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
451
|
+
// CRITICAL: Set loading to false AFTER handling INITIAL_SESSION
|
|
452
|
+
// This ensures ProtectedRoute waits for session restoration to complete
|
|
453
|
+
// before checking authentication state
|
|
454
|
+
this.authLoading = false;
|
|
455
|
+
this.notify();
|
|
456
|
+
return; // Return early to avoid setting loading to false again below
|
|
442
457
|
}
|
|
443
458
|
|
|
444
|
-
//
|
|
459
|
+
// For other events (SIGNED_IN, SIGNED_OUT, TOKEN_REFRESHED), set loading to false
|
|
460
|
+
// INITIAL_SESSION is handled above and returns early
|
|
445
461
|
this.authLoading = false;
|
|
446
462
|
this.notify();
|
|
447
463
|
} catch (error) {
|
|
@@ -519,8 +535,21 @@ export class AuthService extends BaseService implements IAuthService {
|
|
|
519
535
|
this.authError = null;
|
|
520
536
|
}
|
|
521
537
|
|
|
522
|
-
//
|
|
523
|
-
|
|
538
|
+
// CRITICAL FIX: Don't finish restoration here - wait for INITIAL_SESSION event
|
|
539
|
+
// The INITIAL_SESSION event should fire when the auth state listener is set up
|
|
540
|
+
// However, if it doesn't fire within a short delay (e.g., edge case), we'll finish it
|
|
541
|
+
// This ensures ProtectedRoute waits for the event before checking auth state
|
|
542
|
+
|
|
543
|
+
// Set a short fallback timeout to finish restoration if INITIAL_SESSION doesn't fire
|
|
544
|
+
// This handles edge cases where the event might be delayed or not fire
|
|
545
|
+
setTimeout(() => {
|
|
546
|
+
// Only finish if restoration is still in progress and INITIAL_SESSION hasn't fired
|
|
547
|
+
// The INITIAL_SESSION handler will have set restorationComplete to true if it fired
|
|
548
|
+
if (this.sessionRestorationState.isRestoring && !this.sessionRestorationState.restorationComplete) {
|
|
549
|
+
logger.debug('AuthService', 'INITIAL_SESSION event did not fire, finishing restoration');
|
|
550
|
+
this.finishSessionRestoration();
|
|
551
|
+
}
|
|
552
|
+
}, 100); // 100ms fallback - INITIAL_SESSION should fire immediately when listener is set up
|
|
524
553
|
} catch (error) {
|
|
525
554
|
const restorationError = error instanceof Error
|
|
526
555
|
? error
|