@jmruthers/pace-core 0.5.114 → 0.5.116
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{AuthService-CVgsgtaZ.d.ts → AuthService-D4646R4b.d.ts} +9 -4
- package/dist/{DataTable-3JRLZXER.js → DataTable-ZOAKQ3SU.js} +10 -9
- package/dist/{UnifiedAuthProvider-KZZUO27W.js → UnifiedAuthProvider-YFN7YGVN.js} +4 -3
- package/dist/{api-PKU4PUBO.js → api-TNIBJWLM.js} +3 -3
- package/dist/{audit-H4YJJF7R.js → audit-T36HM7IM.js} +2 -2
- package/dist/{chunk-4OX5PXHX.js → chunk-2GJ5GL77.js} +4 -5
- package/dist/chunk-2GJ5GL77.js.map +1 -0
- package/dist/{chunk-5YIZFEUQ.js → chunk-2LM4QQGH.js} +31 -35
- package/dist/chunk-2LM4QQGH.js.map +1 -0
- package/dist/{chunk-3OGQLOJM.js → chunk-3DBFLLLU.js} +30 -1
- package/dist/chunk-3DBFLLLU.js.map +1 -0
- package/dist/{chunk-KTHLNIMA.js → chunk-ECOVPXYS.js} +13 -62
- package/dist/chunk-ECOVPXYS.js.map +1 -0
- package/dist/{chunk-OO3V7W4H.js → chunk-KA3PSVNV.js} +87 -40
- package/dist/chunk-KA3PSVNV.js.map +1 -0
- package/dist/{chunk-HKWQN44G.js → chunk-KMPWND3F.js} +15 -15
- package/dist/{chunk-L36JW4KV.js → chunk-LFS45U62.js} +2 -2
- package/dist/{chunk-NEONKMTU.js → chunk-LZYHAL7Y.js} +9 -4
- package/dist/{chunk-NEONKMTU.js.map → chunk-LZYHAL7Y.js.map} +1 -1
- package/dist/{chunk-BUN7NMV7.js → chunk-O3FTRYEU.js} +2 -2
- package/dist/{chunk-F6QB26OS.js → chunk-P3PUOL6B.js} +80 -8
- package/dist/chunk-P3PUOL6B.js.map +1 -0
- package/dist/{chunk-ZPXWJA4H.js → chunk-PHDAXDHB.js} +131 -5
- package/dist/chunk-PHDAXDHB.js.map +1 -0
- package/dist/chunk-UJI6WSMD.js +201 -0
- package/dist/{chunk-5CDJCTOO.js.map → chunk-UJI6WSMD.js.map} +1 -1
- package/dist/{chunk-JHWQNJP3.js → chunk-UKZWNQMB.js} +65 -19
- package/dist/{chunk-JHWQNJP3.js.map → chunk-UKZWNQMB.js.map} +1 -1
- package/dist/{chunk-7H75SHXZ.js → chunk-VN3OOE35.js} +2 -2
- package/dist/{chunk-QKIVSZ2O.js → chunk-WP5I5GLN.js} +2 -2
- package/dist/components.d.ts +1 -1
- package/dist/components.js +12 -11
- package/dist/components.js.map +1 -1
- package/dist/hooks.d.ts +1 -1
- package/dist/hooks.js +10 -9
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +19 -16
- package/dist/index.js.map +1 -1
- package/dist/providers.d.ts +2 -2
- package/dist/providers.js +3 -2
- package/dist/rbac/index.d.ts +82 -1
- package/dist/rbac/index.js +13 -10
- package/dist/{useToast-DRah6K-g.d.ts → useToast-Cs_g32bg.d.ts} +8 -6
- package/dist/utils.js +6 -4
- package/dist/utils.js.map +1 -1
- package/dist/validation.js +3 -1
- package/dist/validation.js.map +1 -1
- package/docs/README.md +4 -0
- package/docs/api/classes/ColumnFactory.md +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +1 -1
- package/docs/api/classes/MissingUserContextError.md +1 -1
- package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
- package/docs/api/classes/PermissionDeniedError.md +1 -1
- package/docs/api/classes/PublicErrorBoundary.md +1 -1
- package/docs/api/classes/RBACAuditManager.md +35 -12
- package/docs/api/classes/RBACCache.md +1 -1
- package/docs/api/classes/RBACEngine.md +1 -1
- package/docs/api/classes/RBACError.md +1 -1
- package/docs/api/classes/RBACNotInitializedError.md +1 -1
- package/docs/api/classes/SecureSupabaseClient.md +1 -1
- package/docs/api/classes/StorageUtils.md +1 -1
- package/docs/api/enums/FileCategory.md +1 -1
- package/docs/api/interfaces/AggregateConfig.md +1 -1
- package/docs/api/interfaces/ButtonProps.md +1 -1
- package/docs/api/interfaces/CardProps.md +1 -1
- package/docs/api/interfaces/ColorPalette.md +1 -1
- package/docs/api/interfaces/ColorShade.md +1 -1
- package/docs/api/interfaces/DataAccessRecord.md +1 -1
- package/docs/api/interfaces/DataRecord.md +1 -1
- package/docs/api/interfaces/DataTableAction.md +1 -1
- package/docs/api/interfaces/DataTableColumn.md +1 -1
- package/docs/api/interfaces/DataTableProps.md +1 -1
- package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
- package/docs/api/interfaces/EmptyStateConfig.md +1 -1
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
- package/docs/api/interfaces/EventAppRoleData.md +71 -0
- package/docs/api/interfaces/FileDisplayProps.md +1 -1
- package/docs/api/interfaces/FileMetadata.md +1 -1
- package/docs/api/interfaces/FileReference.md +1 -1
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadOptions.md +1 -1
- package/docs/api/interfaces/FileUploadProps.md +1 -1
- package/docs/api/interfaces/FooterProps.md +1 -1
- package/docs/api/interfaces/GrantEventAppRoleParams.md +122 -0
- package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
- package/docs/api/interfaces/InputProps.md +1 -1
- package/docs/api/interfaces/LabelProps.md +1 -1
- package/docs/api/interfaces/LoginFormProps.md +1 -1
- package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
- package/docs/api/interfaces/NavigationContextType.md +1 -1
- package/docs/api/interfaces/NavigationGuardProps.md +1 -1
- package/docs/api/interfaces/NavigationItem.md +1 -1
- package/docs/api/interfaces/NavigationMenuProps.md +1 -1
- package/docs/api/interfaces/NavigationProviderProps.md +1 -1
- package/docs/api/interfaces/Organisation.md +1 -1
- package/docs/api/interfaces/OrganisationContextType.md +1 -1
- package/docs/api/interfaces/OrganisationMembership.md +1 -1
- package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
- package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
- package/docs/api/interfaces/PaceAppLayoutProps.md +27 -27
- package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
- package/docs/api/interfaces/PageAccessRecord.md +1 -1
- package/docs/api/interfaces/PagePermissionContextType.md +1 -1
- package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
- package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
- package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
- package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
- package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
- package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
- package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
- package/docs/api/interfaces/RBACConfig.md +1 -1
- package/docs/api/interfaces/RBACLogger.md +1 -1
- package/docs/api/interfaces/RevokeEventAppRoleParams.md +100 -0
- package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
- package/docs/api/interfaces/RoleManagementResult.md +52 -0
- package/docs/api/interfaces/RouteAccessRecord.md +1 -1
- package/docs/api/interfaces/RouteConfig.md +1 -1
- package/docs/api/interfaces/SecureDataContextType.md +1 -1
- package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
- package/docs/api/interfaces/StorageConfig.md +1 -1
- package/docs/api/interfaces/StorageFileInfo.md +1 -1
- package/docs/api/interfaces/StorageFileMetadata.md +1 -1
- package/docs/api/interfaces/StorageListOptions.md +1 -1
- package/docs/api/interfaces/StorageListResult.md +1 -1
- package/docs/api/interfaces/StorageUploadOptions.md +1 -1
- package/docs/api/interfaces/StorageUploadResult.md +1 -1
- package/docs/api/interfaces/StorageUrlOptions.md +1 -1
- package/docs/api/interfaces/StyleImport.md +1 -1
- package/docs/api/interfaces/SwitchProps.md +1 -1
- package/docs/api/interfaces/ToastActionElement.md +1 -1
- package/docs/api/interfaces/ToastProps.md +1 -1
- package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
- package/docs/api/interfaces/UserEventAccess.md +1 -1
- package/docs/api/interfaces/UserMenuProps.md +1 -1
- package/docs/api/interfaces/UserProfile.md +1 -1
- package/docs/api/modules.md +43 -16
- package/docs/architecture/rpc-function-standards.md +193 -0
- package/package.json +1 -1
- package/src/__tests__/TEST_STANDARD.md +244 -2
- package/src/components/DataTable/__tests__/a11y.basic.test.tsx +46 -16
- package/src/components/DataTable/__tests__/keyboard.test.tsx +276 -217
- package/src/components/DataTable/components/DataTableCore.tsx +32 -17
- package/src/components/DataTable/components/DataTableToolbar.tsx +3 -2
- package/src/components/DataTable/components/EditableRow.tsx +18 -1
- package/src/components/DataTable/components/ImportModal.tsx +25 -2
- package/src/components/DataTable/components/ViewRowModal.tsx +1 -1
- package/src/components/DataTable/components/__tests__/AccessDeniedPage.test.tsx +735 -0
- package/src/components/DataTable/components/__tests__/BulkOperationsDropdown.test.tsx +572 -0
- package/src/components/DataTable/components/__tests__/ColumnVisibilityDropdown.test.tsx +708 -0
- package/src/components/DataTable/components/__tests__/DataTableErrorBoundary.test.tsx +451 -0
- package/src/components/DataTable/components/__tests__/DataTableModals.test.tsx +456 -0
- package/src/components/DataTable/components/__tests__/EditableRow.test.tsx +454 -0
- package/src/components/DataTable/components/__tests__/ExpandButton.test.tsx +462 -0
- package/src/components/DataTable/components/__tests__/FilterRow.test.tsx +423 -0
- package/src/components/DataTable/components/__tests__/GroupHeader.test.tsx +393 -0
- package/src/components/DataTable/components/__tests__/GroupingDropdown.test.tsx +617 -0
- package/src/components/DataTable/components/__tests__/ImportModal.test.tsx +734 -0
- package/src/components/DataTable/components/__tests__/ViewRowModal.test.tsx +412 -0
- package/src/components/DataTable/hooks/useTableHandlers.ts +4 -0
- package/src/components/EventSelector/EventSelector.tsx +5 -25
- package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +12 -7
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +4 -0
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.accessibility.test.tsx +7 -2
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.integration.test.tsx +13 -8
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.performance.test.tsx +109 -100
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.security.test.tsx +18 -13
- package/src/components/PaceAppLayout/__tests__/PaceAppLayout.unit.test.tsx +17 -12
- package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +2 -0
- package/src/components/PaceLoginPage/PaceLoginPage.tsx +11 -1
- package/src/components/PasswordReset/PasswordChangeForm.test.tsx +2 -2
- package/src/components/ProtectedRoute/ProtectedRoute.test.tsx +648 -0
- package/src/components/ProtectedRoute/ProtectedRoute.tsx +10 -7
- package/src/components/PublicLayout/__tests__/PublicErrorBoundary.test.tsx +4 -12
- package/src/components/Select/Select.tsx +8 -0
- package/src/components/Toast/Toast.test.tsx +8 -7
- package/src/components/Toast/Toast.tsx +4 -4
- package/src/hooks/__tests__/usePublicEvent.simple.test.ts +367 -3
- package/src/hooks/__tests__/usePublicFileDisplay.test.ts +916 -0
- package/src/hooks/useEventTheme.ts +49 -18
- package/src/hooks/usePermissionCache.ts +5 -3
- package/src/hooks/useSecureDataAccess.ts +11 -1
- package/src/hooks/useToast.ts +11 -12
- package/src/providers/services/EventServiceProvider.tsx +15 -8
- package/src/rbac/__tests__/cache-invalidation.test.ts +385 -0
- package/src/rbac/audit.test.ts +206 -0
- package/src/rbac/audit.ts +37 -2
- package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +26 -23
- package/src/rbac/errors.test.ts +340 -0
- package/src/rbac/hooks/index.ts +9 -0
- package/src/rbac/hooks/useResolvedScope.test.ts +1063 -0
- package/src/rbac/hooks/useRoleManagement.test.ts +908 -0
- package/src/rbac/hooks/useRoleManagement.ts +255 -0
- package/src/services/AuthService.ts +10 -0
- package/src/services/EventService.ts +111 -50
- package/src/services/__tests__/AuthService.test.ts +1 -1
- package/src/services/__tests__/EventService.test.ts +60 -45
- package/src/services/interfaces/IEventService.ts +1 -1
- package/src/utils/__tests__/deviceFingerprint.unit.test.ts +320 -0
- package/src/utils/__tests__/logger.unit.test.ts +398 -0
- package/src/utils/__tests__/validation.unit.test.ts +225 -1
- package/src/utils/file-reference.test.ts +214 -0
- package/dist/chunk-3OGQLOJM.js.map +0 -1
- package/dist/chunk-4OX5PXHX.js.map +0 -1
- package/dist/chunk-5CDJCTOO.js +0 -190
- package/dist/chunk-5YIZFEUQ.js.map +0 -1
- package/dist/chunk-F6QB26OS.js.map +0 -1
- package/dist/chunk-KTHLNIMA.js.map +0 -1
- package/dist/chunk-OO3V7W4H.js.map +0 -1
- package/dist/chunk-ZPXWJA4H.js.map +0 -1
- package/src/rbac/audit-enhanced.ts +0 -351
- /package/dist/{DataTable-3JRLZXER.js.map → DataTable-ZOAKQ3SU.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-KZZUO27W.js.map → UnifiedAuthProvider-YFN7YGVN.js.map} +0 -0
- /package/dist/{api-PKU4PUBO.js.map → api-TNIBJWLM.js.map} +0 -0
- /package/dist/{audit-H4YJJF7R.js.map → audit-T36HM7IM.js.map} +0 -0
- /package/dist/{chunk-HKWQN44G.js.map → chunk-KMPWND3F.js.map} +0 -0
- /package/dist/{chunk-L36JW4KV.js.map → chunk-LFS45U62.js.map} +0 -0
- /package/dist/{chunk-BUN7NMV7.js.map → chunk-O3FTRYEU.js.map} +0 -0
- /package/dist/{chunk-7H75SHXZ.js.map → chunk-VN3OOE35.js.map} +0 -0
- /package/dist/{chunk-QKIVSZ2O.js.map → chunk-WP5I5GLN.js.map} +0 -0
|
@@ -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]);
|
|
@@ -459,9 +459,19 @@ export function useSecureDataAccess(): SecureDataAccessReturn {
|
|
|
459
459
|
await setOrganisationContextInSession(organisationId);
|
|
460
460
|
|
|
461
461
|
// Include organisation_id in RPC parameters
|
|
462
|
+
// Some functions use p_organisation_id instead of organisation_id (to avoid conflicts with RETURNS TABLE columns)
|
|
463
|
+
const functionsWithPOrganisationId = [
|
|
464
|
+
'data_cake_diners_list',
|
|
465
|
+
'data_cake_mealplans_list'
|
|
466
|
+
];
|
|
467
|
+
|
|
468
|
+
const paramName = functionsWithPOrganisationId.includes(functionName)
|
|
469
|
+
? 'p_organisation_id'
|
|
470
|
+
: 'organisation_id';
|
|
471
|
+
|
|
462
472
|
const secureParams = {
|
|
463
473
|
...params,
|
|
464
|
-
|
|
474
|
+
[paramName]: organisationId
|
|
465
475
|
};
|
|
466
476
|
|
|
467
477
|
const { data, error } = await supabase!.rpc(functionName, secureParams);
|
package/src/hooks/useToast.ts
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* @package @jmruthers/pace-core
|
|
5
5
|
* @module Hooks
|
|
6
6
|
* @since 0.1.0
|
|
7
|
-
*
|
|
7
|
+
*
|
|
8
8
|
* Toast notifications automatically dismiss after 10 seconds by default.
|
|
9
|
-
*
|
|
9
|
+
* The duration is fixed to the pace-core default to ensure consistent behaviour.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import * as React from "react"
|
|
@@ -22,7 +22,6 @@ export interface ToastProps {
|
|
|
22
22
|
title?: React.ReactNode;
|
|
23
23
|
description?: React.ReactNode;
|
|
24
24
|
variant?: 'default' | 'destructive' | 'success';
|
|
25
|
-
duration?: number;
|
|
26
25
|
onClose?: () => void;
|
|
27
26
|
action?: React.ReactElement;
|
|
28
27
|
}
|
|
@@ -34,6 +33,8 @@ export interface ToastProps {
|
|
|
34
33
|
type ToasterToast = ToastProps & {
|
|
35
34
|
/** Unique identifier for the toast */
|
|
36
35
|
id: string
|
|
36
|
+
/** Duration before automatic dismissal in milliseconds */
|
|
37
|
+
duration?: number
|
|
37
38
|
/** Optional title content */
|
|
38
39
|
title?: React.ReactNode
|
|
39
40
|
/** Optional description content */
|
|
@@ -181,32 +182,30 @@ function reducer(state: State, action: {
|
|
|
181
182
|
|
|
182
183
|
/**
|
|
183
184
|
* Toast configuration without ID
|
|
185
|
+
* Duration is fixed to DEFAULT_TOAST_DURATION and cannot be overridden
|
|
184
186
|
*/
|
|
185
|
-
type Toast = Omit<ToasterToast, "id">
|
|
187
|
+
type Toast = Omit<ToasterToast, "id" | "duration">
|
|
186
188
|
|
|
187
189
|
/**
|
|
188
190
|
* Creates a new toast notification
|
|
189
|
-
* @param props - Toast configuration. Duration
|
|
191
|
+
* @param props - Toast configuration. Duration is automatically set to 10 seconds (10000ms) and cannot be customized.
|
|
190
192
|
* @returns Object with toast ID and control methods
|
|
191
193
|
*/
|
|
192
|
-
function toast(
|
|
194
|
+
function toast(props: Toast) {
|
|
193
195
|
const id = genId()
|
|
194
196
|
|
|
195
|
-
const update = (props: Partial<Omit<ToasterToast, "id">>) =>
|
|
197
|
+
const update = (props: Partial<Omit<ToasterToast, "id" | "duration">>) =>
|
|
196
198
|
dispatch({
|
|
197
199
|
type: "UPDATE_TOAST",
|
|
198
|
-
toast: { ...props, id },
|
|
200
|
+
toast: { ...props, duration: DEFAULT_TOAST_DURATION, id },
|
|
199
201
|
})
|
|
200
202
|
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
|
|
201
203
|
|
|
202
|
-
// Use provided duration or default to 10 seconds
|
|
203
|
-
const toastDuration = duration ?? DEFAULT_TOAST_DURATION
|
|
204
|
-
|
|
205
204
|
dispatch({
|
|
206
205
|
type: "ADD_TOAST",
|
|
207
206
|
toast: {
|
|
208
207
|
...props,
|
|
209
|
-
duration:
|
|
208
|
+
duration: DEFAULT_TOAST_DURATION,
|
|
210
209
|
id,
|
|
211
210
|
open: true,
|
|
212
211
|
dismiss,
|
|
@@ -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
|
+
|