@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
|
@@ -26,6 +26,50 @@ import { useEvents } from './useEvents';
|
|
|
26
26
|
import { applyPalette, clearPalette } from '../theming/runtime';
|
|
27
27
|
import type { PaletteData, ColorPalette } from '../theming/runtime';
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Parse and normalize event_colours to PaletteData (supports ev-* keys and string JSON)
|
|
31
|
+
* This matches the logic in EventService.parseAndNormalizeEventColours()
|
|
32
|
+
*/
|
|
33
|
+
function parseAndNormalizeEventColours(input: unknown): { main: any; sec: any; acc: any } | null {
|
|
34
|
+
try {
|
|
35
|
+
if (!input) return null;
|
|
36
|
+
let obj: any = input;
|
|
37
|
+
if (typeof input === 'string') {
|
|
38
|
+
try {
|
|
39
|
+
obj = JSON.parse(input);
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
} else if (typeof input !== 'object') {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const pick = (o: any, pref: string, plain: string) => (o?.[pref] ?? o?.[plain]) || null;
|
|
48
|
+
const main = pick(obj, 'ev-main', 'main');
|
|
49
|
+
const sec = pick(obj, 'ev-sec', 'sec');
|
|
50
|
+
const acc = pick(obj, 'ev-acc', 'acc');
|
|
51
|
+
if (!main && !sec && !acc) return null;
|
|
52
|
+
|
|
53
|
+
// Fill helper: return palette as-is, only return empty object if null/undefined/empty
|
|
54
|
+
const fill = (p: any) => {
|
|
55
|
+
if (!p) return {};
|
|
56
|
+
// If object is empty or has no actual color values, return empty object
|
|
57
|
+
const keys = Object.keys(p);
|
|
58
|
+
if (keys.length === 0) return {};
|
|
59
|
+
// Check if any values are truthy (not null/undefined)
|
|
60
|
+
const hasValues = keys.some(k => p[k] != null);
|
|
61
|
+
if (!hasValues) return {};
|
|
62
|
+
// Return the object as-is (don't fill missing shades)
|
|
63
|
+
return p;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
return { main: fill(main), sec: fill(sec), acc: fill(acc) };
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.warn('[useEventTheme] Failed to parse/normalize event colours:', error);
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
29
73
|
/**
|
|
30
74
|
* Hook that automatically applies event-specific theming
|
|
31
75
|
*
|
|
@@ -62,30 +106,17 @@ export function useEventTheme(): void {
|
|
|
62
106
|
// Check if the event has theme colors
|
|
63
107
|
const eventColours = selectedEvent.event_colours;
|
|
64
108
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Validate that event_colours has the expected structure
|
|
71
|
-
const palette = eventColours as Partial<PaletteData>;
|
|
109
|
+
// Parse and normalize event_colours (same logic as EventService)
|
|
110
|
+
const normalized = parseAndNormalizeEventColours(eventColours);
|
|
72
111
|
|
|
73
|
-
|
|
74
|
-
if (!palette.main && !palette.sec && !palette.acc) {
|
|
112
|
+
if (!normalized) {
|
|
75
113
|
clearPalette();
|
|
76
114
|
return;
|
|
77
115
|
}
|
|
78
116
|
|
|
79
|
-
// Apply the palette
|
|
80
|
-
// The system expects main, sec, and acc, so we ensure all are present (empty if needed)
|
|
81
|
-
const fullPalette: PaletteData = {
|
|
82
|
-
main: (palette.main as ColorPalette) || {},
|
|
83
|
-
sec: (palette.sec as ColorPalette) || {},
|
|
84
|
-
acc: (palette.acc as ColorPalette) || {},
|
|
85
|
-
};
|
|
86
|
-
|
|
117
|
+
// Apply the normalized palette
|
|
87
118
|
try {
|
|
88
|
-
applyPalette(
|
|
119
|
+
applyPalette(normalized);
|
|
89
120
|
} catch (error) {
|
|
90
121
|
console.error('[useEventTheme] Failed to apply event palette:', error);
|
|
91
122
|
}
|
|
@@ -322,7 +322,8 @@ export function usePermissionCache(config: Partial<CacheConfig> = {}) {
|
|
|
322
322
|
}));
|
|
323
323
|
}
|
|
324
324
|
|
|
325
|
-
|
|
325
|
+
// Pre-size results array to maintain order and length
|
|
326
|
+
const results: PermissionResult[] = new Array(permissions.length);
|
|
326
327
|
const startTime = Date.now();
|
|
327
328
|
|
|
328
329
|
// Check cache for all permissions first
|
|
@@ -334,13 +335,14 @@ export function usePermissionCache(config: Partial<CacheConfig> = {}) {
|
|
|
334
335
|
const entry = cache.current.get(cacheKey);
|
|
335
336
|
|
|
336
337
|
if (entry && isCacheValid(entry)) {
|
|
337
|
-
|
|
338
|
+
// Use index assignment to maintain order
|
|
339
|
+
results[i] = {
|
|
338
340
|
operation,
|
|
339
341
|
pageId,
|
|
340
342
|
hasPermission: entry.result,
|
|
341
343
|
cached: true,
|
|
342
344
|
timestamp: entry.timestamp
|
|
343
|
-
}
|
|
345
|
+
};
|
|
344
346
|
stats.current.cacheHits++;
|
|
345
347
|
} else {
|
|
346
348
|
uncachedPermissions.push([operation, pageId, i]);
|
|
@@ -51,9 +51,10 @@
|
|
|
51
51
|
* - Type-safe database operations
|
|
52
52
|
*/
|
|
53
53
|
|
|
54
|
-
import { useCallback, useState } from 'react';
|
|
54
|
+
import { useCallback, useState, useContext } from 'react';
|
|
55
55
|
import { useUnifiedAuth } from '../providers';
|
|
56
56
|
import { useOrganisations } from './useOrganisations';
|
|
57
|
+
import { EventServiceContext } from '../providers/services/EventServiceProvider';
|
|
57
58
|
import { setOrganisationContext } from '../utils/organisationContext';
|
|
58
59
|
import type { Permission } from '../rbac/types';
|
|
59
60
|
import type { OrganisationSecurityError } from '../types/organisation';
|
|
@@ -149,6 +150,11 @@ export interface DataAccessRecord {
|
|
|
149
150
|
export function useSecureDataAccess(): SecureDataAccessReturn {
|
|
150
151
|
const { supabase, user, session } = useUnifiedAuth();
|
|
151
152
|
const { ensureOrganisationContext } = useOrganisations();
|
|
153
|
+
|
|
154
|
+
// Get selected event for event-scoped RPC calls
|
|
155
|
+
// Use useContext directly to safely check if EventServiceProvider is available
|
|
156
|
+
const eventServiceContext = useContext(EventServiceContext);
|
|
157
|
+
const selectedEvent = eventServiceContext?.eventService?.getSelectedEvent() || null;
|
|
152
158
|
|
|
153
159
|
const validateContext = useCallback((): void => {
|
|
154
160
|
if (!supabase) {
|
|
@@ -459,10 +465,57 @@ export function useSecureDataAccess(): SecureDataAccessReturn {
|
|
|
459
465
|
await setOrganisationContextInSession(organisationId);
|
|
460
466
|
|
|
461
467
|
// Include organisation_id in RPC parameters
|
|
468
|
+
// Some functions use p_organisation_id instead of organisation_id (to avoid conflicts with RETURNS TABLE columns)
|
|
469
|
+
const functionsWithPOrganisationId = [
|
|
470
|
+
'data_cake_diners_list',
|
|
471
|
+
'data_cake_mealplans_list'
|
|
472
|
+
];
|
|
473
|
+
|
|
474
|
+
const paramName = functionsWithPOrganisationId.includes(functionName)
|
|
475
|
+
? 'p_organisation_id'
|
|
476
|
+
: 'organisation_id';
|
|
477
|
+
|
|
478
|
+
// Functions that need p_event_id for event-app role permission checks
|
|
479
|
+
// Note: Even org-scoped functions (like items, packages, suppliers) need event_id
|
|
480
|
+
// for permission checks when users have event-app roles
|
|
481
|
+
const functionsNeedingEventId = [
|
|
482
|
+
'data_cake_items_list',
|
|
483
|
+
'data_cake_packages_list',
|
|
484
|
+
'data_cake_suppliers_list',
|
|
485
|
+
'data_cake_diettypes_list',
|
|
486
|
+
'data_cake_mealtypes_list',
|
|
487
|
+
'data_cake_diners_list',
|
|
488
|
+
'data_cake_mealplans_list',
|
|
489
|
+
'data_cake_dishes_list',
|
|
490
|
+
'data_cake_recipes_list',
|
|
491
|
+
'data_cake_meals_list',
|
|
492
|
+
'data_cake_units_list',
|
|
493
|
+
'data_cake_orders_list',
|
|
494
|
+
'app_cake_item_create',
|
|
495
|
+
'app_cake_item_update',
|
|
496
|
+
'app_cake_package_create',
|
|
497
|
+
'app_cake_package_update',
|
|
498
|
+
'app_cake_supplier_create',
|
|
499
|
+
'app_cake_supplier_update',
|
|
500
|
+
'app_cake_supplier_delete',
|
|
501
|
+
'app_cake_meal_create',
|
|
502
|
+
'app_cake_meal_update',
|
|
503
|
+
'app_cake_meal_delete',
|
|
504
|
+
'app_cake_unit_create',
|
|
505
|
+
'app_cake_unit_update',
|
|
506
|
+
'app_cake_unit_delete',
|
|
507
|
+
'app_cake_delivery_upsert'
|
|
508
|
+
];
|
|
509
|
+
|
|
462
510
|
const secureParams = {
|
|
463
511
|
...params,
|
|
464
|
-
|
|
512
|
+
[paramName]: organisationId
|
|
465
513
|
};
|
|
514
|
+
|
|
515
|
+
// Add p_event_id if function needs it and event is selected
|
|
516
|
+
if (functionsNeedingEventId.includes(functionName) && selectedEvent?.event_id) {
|
|
517
|
+
secureParams.p_event_id = selectedEvent.event_id;
|
|
518
|
+
}
|
|
466
519
|
|
|
467
520
|
const { data, error } = await supabase!.rpc(functionName, secureParams);
|
|
468
521
|
|
|
@@ -476,7 +529,7 @@ export function useSecureDataAccess(): SecureDataAccessReturn {
|
|
|
476
529
|
});
|
|
477
530
|
|
|
478
531
|
return data as T;
|
|
479
|
-
}, [validateContext, getCurrentOrganisationId, setOrganisationContextInSession, supabase]);
|
|
532
|
+
}, [validateContext, getCurrentOrganisationId, setOrganisationContextInSession, supabase, selectedEvent?.event_id]);
|
|
480
533
|
|
|
481
534
|
// NEW: Phase 1 - Enhanced Security Features
|
|
482
535
|
const [dataAccessHistory, setDataAccessHistory] = useState<DataAccessRecord[]>([]);
|
package/src/hooks/useToast.ts
CHANGED
|
@@ -34,7 +34,7 @@ type ToasterToast = ToastProps & {
|
|
|
34
34
|
/** Unique identifier for the toast */
|
|
35
35
|
id: string
|
|
36
36
|
/** Duration before automatic dismissal in milliseconds */
|
|
37
|
-
duration
|
|
37
|
+
duration?: number
|
|
38
38
|
/** Optional title content */
|
|
39
39
|
title?: React.ReactNode
|
|
40
40
|
/** Optional description content */
|
|
@@ -52,16 +52,23 @@ export function EventServiceProvider({
|
|
|
52
52
|
|
|
53
53
|
// Update service dependencies and initialize when dependencies change
|
|
54
54
|
useEffect(() => {
|
|
55
|
-
eventService.updateDependencies(supabaseClient, user, session, appName, selectedOrganisation, setSelectedEventId);
|
|
56
|
-
|
|
57
|
-
// Re-initialize service when dependencies change
|
|
58
55
|
let isMounted = true;
|
|
59
56
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
57
|
+
const updateAndInitialize = async () => {
|
|
58
|
+
// Update dependencies (now async to handle user change cleanup)
|
|
59
|
+
await eventService.updateDependencies(supabaseClient, user, session, appName, selectedOrganisation, setSelectedEventId);
|
|
60
|
+
|
|
61
|
+
if (!isMounted) return;
|
|
62
|
+
|
|
63
|
+
// Re-initialize service when dependencies change
|
|
64
|
+
await eventService.initialize().catch(error => {
|
|
65
|
+
if (isMounted) {
|
|
66
|
+
console.error('[EventServiceProvider] Failed to initialize event service:', error);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
updateAndInitialize();
|
|
65
72
|
|
|
66
73
|
return () => {
|
|
67
74
|
isMounted = false;
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file RBAC Cache Invalidation Tests
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module RBAC/CacheInvalidation
|
|
5
|
+
* @since 1.0.0
|
|
6
|
+
*
|
|
7
|
+
* Comprehensive tests for the RBAC cache invalidation system.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
11
|
+
import { createMockSupabaseClient } from '@test/helpers';
|
|
12
|
+
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
13
|
+
import type { Database } from '../types/database';
|
|
14
|
+
import {
|
|
15
|
+
RBACCacheInvalidationManager,
|
|
16
|
+
INVALIDATION_PATTERNS,
|
|
17
|
+
initializeCacheInvalidation,
|
|
18
|
+
getCacheInvalidationManager,
|
|
19
|
+
} from '../cache-invalidation';
|
|
20
|
+
import { rbacCache, CACHE_PATTERNS } from '../cache';
|
|
21
|
+
import { emitAuditEvent } from '../audit';
|
|
22
|
+
|
|
23
|
+
// Mock the cache module
|
|
24
|
+
vi.mock('../cache', () => ({
|
|
25
|
+
rbacCache: {
|
|
26
|
+
invalidate: vi.fn(),
|
|
27
|
+
clear: vi.fn(),
|
|
28
|
+
},
|
|
29
|
+
CACHE_PATTERNS: {
|
|
30
|
+
USER: (userId: string) => `user:${userId}`,
|
|
31
|
+
ORGANISATION: (orgId: string) => `org:${orgId}`,
|
|
32
|
+
EVENT: (eventId: string) => `event:${eventId}`,
|
|
33
|
+
APP: (appId: string) => `app:${appId}`,
|
|
34
|
+
},
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
// Mock the audit module
|
|
38
|
+
vi.mock('../audit', () => ({
|
|
39
|
+
emitAuditEvent: vi.fn(() => Promise.resolve(undefined)),
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
describe('RBAC Cache Invalidation', () => {
|
|
43
|
+
let mockSupabase: ReturnType<typeof createMockSupabaseClient>;
|
|
44
|
+
let manager: RBACCacheInvalidationManager;
|
|
45
|
+
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
vi.clearAllMocks();
|
|
48
|
+
mockSupabase = createMockSupabaseClient();
|
|
49
|
+
manager = new RBACCacheInvalidationManager(mockSupabase as unknown as SupabaseClient<Database>);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
afterEach(() => {
|
|
53
|
+
vi.clearAllMocks();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('INVALIDATION_PATTERNS', () => {
|
|
57
|
+
describe('USER_ROLES_CHANGED', () => {
|
|
58
|
+
it('generates correct patterns for user invalidation', () => {
|
|
59
|
+
const userId = 'user-123' as const;
|
|
60
|
+
const patterns = INVALIDATION_PATTERNS.USER_ROLES_CHANGED(userId);
|
|
61
|
+
|
|
62
|
+
expect(patterns).toContain('user:user-123');
|
|
63
|
+
expect(patterns).toContain('perm:user-123:*');
|
|
64
|
+
expect(patterns).toContain('access:user-123:*');
|
|
65
|
+
expect(patterns).toContain('map:user-123:*');
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('ORGANISATION_PERMISSIONS_CHANGED', () => {
|
|
70
|
+
it('generates correct patterns for organisation invalidation', () => {
|
|
71
|
+
const orgId = 'org-456' as const;
|
|
72
|
+
const patterns = INVALIDATION_PATTERNS.ORGANISATION_PERMISSIONS_CHANGED(orgId);
|
|
73
|
+
|
|
74
|
+
expect(patterns).toContain('org:org-456');
|
|
75
|
+
expect(patterns).toContain('perm:*:org-456:*');
|
|
76
|
+
expect(patterns).toContain('access:*:org-456:*');
|
|
77
|
+
expect(patterns).toContain('map:*:org-456:*');
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('EVENT_PERMISSIONS_CHANGED', () => {
|
|
82
|
+
it('generates correct patterns for event invalidation', () => {
|
|
83
|
+
const eventId = 'event-789';
|
|
84
|
+
const patterns = INVALIDATION_PATTERNS.EVENT_PERMISSIONS_CHANGED(eventId);
|
|
85
|
+
|
|
86
|
+
expect(patterns).toContain('event:event-789');
|
|
87
|
+
expect(patterns).toContain('perm:*:*:event-789:*');
|
|
88
|
+
expect(patterns).toContain('access:*:*:event-789:*');
|
|
89
|
+
expect(patterns).toContain('map:*:*:event-789:*');
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('APP_PERMISSIONS_CHANGED', () => {
|
|
94
|
+
it('generates correct patterns for app invalidation', () => {
|
|
95
|
+
const appId = 'app-abc' as const;
|
|
96
|
+
const patterns = INVALIDATION_PATTERNS.APP_PERMISSIONS_CHANGED(appId);
|
|
97
|
+
|
|
98
|
+
expect(patterns).toContain('app:app-abc');
|
|
99
|
+
expect(patterns).toContain('perm:*:*:*:app-abc:*');
|
|
100
|
+
expect(patterns).toContain('access:*:*:*:app-abc');
|
|
101
|
+
expect(patterns).toContain('map:*:*:*:app-abc');
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('PAGE_PERMISSIONS_CHANGED', () => {
|
|
106
|
+
it('generates correct patterns for page invalidation', () => {
|
|
107
|
+
const pageId = 'page-xyz' as const;
|
|
108
|
+
const patterns = INVALIDATION_PATTERNS.PAGE_PERMISSIONS_CHANGED(pageId);
|
|
109
|
+
|
|
110
|
+
expect(patterns).toContain('perm:*:*:*:*:page-xyz');
|
|
111
|
+
expect(patterns).toContain('map:*:*:*:*');
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('RBACCacheInvalidationManager', () => {
|
|
117
|
+
describe('onInvalidation', () => {
|
|
118
|
+
it('adds callback and returns unsubscribe function', () => {
|
|
119
|
+
const callback = vi.fn();
|
|
120
|
+
const unsubscribe = manager.onInvalidation(callback);
|
|
121
|
+
|
|
122
|
+
expect(typeof unsubscribe).toBe('function');
|
|
123
|
+
|
|
124
|
+
// Trigger invalidation
|
|
125
|
+
manager.invalidateUser('user-123' as const, 'test');
|
|
126
|
+
|
|
127
|
+
expect(callback).toHaveBeenCalled();
|
|
128
|
+
|
|
129
|
+
// Unsubscribe
|
|
130
|
+
unsubscribe();
|
|
131
|
+
|
|
132
|
+
// Clear mocks
|
|
133
|
+
vi.clearAllMocks();
|
|
134
|
+
|
|
135
|
+
// Trigger again - callback should not be called
|
|
136
|
+
manager.invalidateUser('user-123' as const, 'test');
|
|
137
|
+
expect(callback).not.toHaveBeenCalled();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('handles multiple callbacks', () => {
|
|
141
|
+
const callback1 = vi.fn();
|
|
142
|
+
const callback2 = vi.fn();
|
|
143
|
+
|
|
144
|
+
manager.onInvalidation(callback1);
|
|
145
|
+
manager.onInvalidation(callback2);
|
|
146
|
+
|
|
147
|
+
manager.invalidateUser('user-123' as const, 'test');
|
|
148
|
+
|
|
149
|
+
expect(callback1).toHaveBeenCalled();
|
|
150
|
+
expect(callback2).toHaveBeenCalled();
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe('invalidateUser', () => {
|
|
155
|
+
it('invalidates cache for user with correct patterns', () => {
|
|
156
|
+
const userId = 'user-123' as const;
|
|
157
|
+
manager.invalidateUser(userId, 'test-reason');
|
|
158
|
+
|
|
159
|
+
expect(rbacCache.invalidate).toHaveBeenCalledWith('user:user-123');
|
|
160
|
+
expect(rbacCache.invalidate).toHaveBeenCalledWith('perm:user-123:*');
|
|
161
|
+
expect(rbacCache.invalidate).toHaveBeenCalledWith('access:user-123:*');
|
|
162
|
+
expect(rbacCache.invalidate).toHaveBeenCalledWith('map:user-123:*');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('emits audit event for user invalidation', async () => {
|
|
166
|
+
const userId = 'user-123' as const;
|
|
167
|
+
manager.invalidateUser(userId, 'test-reason');
|
|
168
|
+
|
|
169
|
+
await vi.waitFor(() => {
|
|
170
|
+
expect(emitAuditEvent).toHaveBeenCalled();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const auditCall = vi.mocked(emitAuditEvent).mock.calls[0][0];
|
|
174
|
+
expect(auditCall.type).toBe('permission_check');
|
|
175
|
+
expect(auditCall.permission).toBe('cache:invalidate');
|
|
176
|
+
expect(auditCall.metadata?.reason).toBe('test-reason');
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe('invalidateOrganisation', () => {
|
|
181
|
+
it('invalidates cache for organisation with correct patterns', () => {
|
|
182
|
+
const orgId = 'org-456' as const;
|
|
183
|
+
manager.invalidateOrganisation(orgId, 'test-reason');
|
|
184
|
+
|
|
185
|
+
expect(rbacCache.invalidate).toHaveBeenCalledWith('org:org-456');
|
|
186
|
+
expect(rbacCache.invalidate).toHaveBeenCalledWith('perm:*:org-456:*');
|
|
187
|
+
expect(rbacCache.invalidate).toHaveBeenCalledWith('access:*:org-456:*');
|
|
188
|
+
expect(rbacCache.invalidate).toHaveBeenCalledWith('map:*:org-456:*');
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe('invalidateEvent', () => {
|
|
193
|
+
it('invalidates cache for event with correct patterns', () => {
|
|
194
|
+
const eventId = 'event-789';
|
|
195
|
+
manager.invalidateEvent(eventId, 'test-reason');
|
|
196
|
+
|
|
197
|
+
expect(rbacCache.invalidate).toHaveBeenCalledWith('event:event-789');
|
|
198
|
+
expect(rbacCache.invalidate).toHaveBeenCalledWith('perm:*:*:event-789:*');
|
|
199
|
+
expect(rbacCache.invalidate).toHaveBeenCalledWith('access:*:*:event-789:*');
|
|
200
|
+
expect(rbacCache.invalidate).toHaveBeenCalledWith('map:*:*:event-789:*');
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe('invalidateApp', () => {
|
|
205
|
+
it('invalidates cache for app with correct patterns', () => {
|
|
206
|
+
const appId = 'app-abc' as const;
|
|
207
|
+
manager.invalidateApp(appId, 'test-reason');
|
|
208
|
+
|
|
209
|
+
expect(rbacCache.invalidate).toHaveBeenCalledWith('app:app-abc');
|
|
210
|
+
expect(rbacCache.invalidate).toHaveBeenCalledWith('perm:*:*:*:app-abc:*');
|
|
211
|
+
expect(rbacCache.invalidate).toHaveBeenCalledWith('access:*:*:*:app-abc');
|
|
212
|
+
expect(rbacCache.invalidate).toHaveBeenCalledWith('map:*:*:*:app-abc');
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe('invalidatePage', () => {
|
|
217
|
+
it('invalidates cache for page with correct patterns', () => {
|
|
218
|
+
const pageId = 'page-xyz' as const;
|
|
219
|
+
manager.invalidatePage(pageId, 'test-reason');
|
|
220
|
+
|
|
221
|
+
expect(rbacCache.invalidate).toHaveBeenCalledWith('perm:*:*:*:*:page-xyz');
|
|
222
|
+
expect(rbacCache.invalidate).toHaveBeenCalledWith('map:*:*:*:*');
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe('invalidateAllUsersInOrganisation', () => {
|
|
227
|
+
it('invalidates cache for all users in organisation', async () => {
|
|
228
|
+
const orgId = 'org-456' as const;
|
|
229
|
+
const mockUsers = [
|
|
230
|
+
{ user_id: 'user-1' as const },
|
|
231
|
+
{ user_id: 'user-2' as const },
|
|
232
|
+
];
|
|
233
|
+
|
|
234
|
+
mockSupabase.from.mockReturnValue({
|
|
235
|
+
select: vi.fn().mockReturnValue({
|
|
236
|
+
eq: vi.fn().mockReturnValue({
|
|
237
|
+
eq: vi.fn().mockResolvedValue({
|
|
238
|
+
data: mockUsers,
|
|
239
|
+
error: null,
|
|
240
|
+
}),
|
|
241
|
+
}),
|
|
242
|
+
}),
|
|
243
|
+
} as any);
|
|
244
|
+
|
|
245
|
+
await manager.invalidateAllUsersInOrganisation(orgId, 'test-reason');
|
|
246
|
+
|
|
247
|
+
// Should invalidate each user
|
|
248
|
+
expect(rbacCache.invalidate).toHaveBeenCalledWith('user:user-1');
|
|
249
|
+
expect(rbacCache.invalidate).toHaveBeenCalledWith('user:user-2');
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('handles empty user list gracefully', async () => {
|
|
253
|
+
const orgId = 'org-456' as const;
|
|
254
|
+
|
|
255
|
+
mockSupabase.from.mockReturnValue({
|
|
256
|
+
select: vi.fn().mockReturnValue({
|
|
257
|
+
eq: vi.fn().mockReturnValue({
|
|
258
|
+
eq: vi.fn().mockResolvedValue({
|
|
259
|
+
data: [],
|
|
260
|
+
error: null,
|
|
261
|
+
}),
|
|
262
|
+
}),
|
|
263
|
+
}),
|
|
264
|
+
} as any);
|
|
265
|
+
|
|
266
|
+
await manager.invalidateAllUsersInOrganisation(orgId, 'test-reason');
|
|
267
|
+
|
|
268
|
+
// Should not throw, but should not call invalidate for users
|
|
269
|
+
expect(rbacCache.invalidate).not.toHaveBeenCalledWith(expect.stringContaining('user:'));
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('handles null data gracefully', async () => {
|
|
273
|
+
const orgId = 'org-456' as const;
|
|
274
|
+
|
|
275
|
+
mockSupabase.from.mockReturnValue({
|
|
276
|
+
select: vi.fn().mockReturnValue({
|
|
277
|
+
eq: vi.fn().mockReturnValue({
|
|
278
|
+
eq: vi.fn().mockResolvedValue({
|
|
279
|
+
data: null,
|
|
280
|
+
error: null,
|
|
281
|
+
}),
|
|
282
|
+
}),
|
|
283
|
+
}),
|
|
284
|
+
} as any);
|
|
285
|
+
|
|
286
|
+
await manager.invalidateAllUsersInOrganisation(orgId, 'test-reason');
|
|
287
|
+
|
|
288
|
+
// Should not throw
|
|
289
|
+
expect(rbacCache.invalidate).not.toHaveBeenCalledWith(expect.stringContaining('user:'));
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
describe('clearAllCache', () => {
|
|
294
|
+
it('clears all cache entries', () => {
|
|
295
|
+
manager.clearAllCache();
|
|
296
|
+
expect(rbacCache.clear).toHaveBeenCalled();
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
describe('setupRealtimeSubscriptions', () => {
|
|
301
|
+
it('skips subscriptions when realtime is not available', () => {
|
|
302
|
+
const mockSupabaseNoRealtime = createMockSupabaseClient();
|
|
303
|
+
mockSupabaseNoRealtime.channel = undefined;
|
|
304
|
+
|
|
305
|
+
const managerNoRealtime = new RBACCacheInvalidationManager(
|
|
306
|
+
mockSupabaseNoRealtime as unknown as SupabaseClient<Database>
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
// Should not throw and should log that realtime is not available
|
|
310
|
+
expect(managerNoRealtime).toBeDefined();
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('sets up subscriptions when realtime is available', () => {
|
|
314
|
+
const mockChannel = {
|
|
315
|
+
on: vi.fn().mockReturnThis(),
|
|
316
|
+
subscribe: vi.fn(),
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
const mockSupabaseWithRealtime = createMockSupabaseClient();
|
|
320
|
+
mockSupabaseWithRealtime.channel = vi.fn(() => mockChannel);
|
|
321
|
+
|
|
322
|
+
const managerWithRealtime = new RBACCacheInvalidationManager(
|
|
323
|
+
mockSupabaseWithRealtime as unknown as SupabaseClient<Database>
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
expect(mockSupabaseWithRealtime.channel).toHaveBeenCalled();
|
|
327
|
+
expect(mockChannel.on).toHaveBeenCalled();
|
|
328
|
+
expect(mockChannel.subscribe).toHaveBeenCalled();
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
describe('Global Functions', () => {
|
|
334
|
+
describe('initializeCacheInvalidation', () => {
|
|
335
|
+
it('initializes and returns cache invalidation manager', () => {
|
|
336
|
+
const manager = initializeCacheInvalidation(mockSupabase as unknown as SupabaseClient<Database>);
|
|
337
|
+
|
|
338
|
+
expect(manager).toBeInstanceOf(RBACCacheInvalidationManager);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it('sets global manager instance', () => {
|
|
342
|
+
initializeCacheInvalidation(mockSupabase as unknown as SupabaseClient<Database>);
|
|
343
|
+
|
|
344
|
+
const globalManager = getCacheInvalidationManager();
|
|
345
|
+
expect(globalManager).toBeInstanceOf(RBACCacheInvalidationManager);
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
describe('getCacheInvalidationManager', () => {
|
|
350
|
+
it('returns null when manager is not initialized', () => {
|
|
351
|
+
// Reset global state by creating new instance without initialize
|
|
352
|
+
const manager = getCacheInvalidationManager();
|
|
353
|
+
// May be null or the previous instance
|
|
354
|
+
expect(manager === null || manager instanceof RBACCacheInvalidationManager).toBe(true);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it('returns manager instance after initialization', () => {
|
|
358
|
+
const manager = initializeCacheInvalidation(mockSupabase as unknown as SupabaseClient<Database>);
|
|
359
|
+
const retrieved = getCacheInvalidationManager();
|
|
360
|
+
|
|
361
|
+
expect(retrieved).toBe(manager);
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
describe('Error Handling', () => {
|
|
367
|
+
it('handles audit event errors gracefully', async () => {
|
|
368
|
+
vi.mocked(emitAuditEvent).mockRejectedValue(new Error('Audit error'));
|
|
369
|
+
|
|
370
|
+
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
371
|
+
|
|
372
|
+
manager.invalidateUser('user-123' as const, 'test');
|
|
373
|
+
|
|
374
|
+
await vi.waitFor(() => {
|
|
375
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
376
|
+
expect.stringContaining('[RBAC Cache] Failed to log cache invalidation audit event'),
|
|
377
|
+
expect.any(Error)
|
|
378
|
+
);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
consoleWarnSpy.mockRestore();
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
|