@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,741 @@
|
|
|
1
|
+
import { renderHook, waitFor } from '@testing-library/react';
|
|
2
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
3
|
+
import { TestWrapper, renderWithProviders } from '../../__tests__/helpers/test-utils';
|
|
4
|
+
import { useRBAC } from '../../rbac/hooks/useRBAC';
|
|
5
|
+
|
|
6
|
+
// Mock the useRBAC hook
|
|
7
|
+
vi.mock('../../rbac/hooks/useRBAC');
|
|
8
|
+
|
|
9
|
+
// Mock useUnifiedAuth (required by usePermissionCache for user context)
|
|
10
|
+
const mockUseUnifiedAuthFn = vi.fn(() => ({
|
|
11
|
+
user: { id: 'test-user-id' },
|
|
12
|
+
session: null,
|
|
13
|
+
appName: 'test-app',
|
|
14
|
+
selectedOrganisation: { id: 'test-org-id' },
|
|
15
|
+
selectedEvent: null,
|
|
16
|
+
selectedOrganisationId: undefined,
|
|
17
|
+
selectedEventId: undefined,
|
|
18
|
+
supabase: {},
|
|
19
|
+
isLoading: false,
|
|
20
|
+
error: null,
|
|
21
|
+
}));
|
|
22
|
+
vi.mock('../../providers/services/UnifiedAuthProvider', () => ({
|
|
23
|
+
useUnifiedAuth: () => mockUseUnifiedAuthFn(),
|
|
24
|
+
UnifiedAuthProvider: ({ children }: { children: any }) => children,
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
// Mock isPermittedCached from RBAC API (used by usePermissionCache)
|
|
28
|
+
vi.mock('../../rbac/api', async () => {
|
|
29
|
+
const actual = await vi.importActual('../../rbac/api');
|
|
30
|
+
return {
|
|
31
|
+
...actual,
|
|
32
|
+
isPermittedCached: vi.fn().mockResolvedValue(true),
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Mock logger
|
|
37
|
+
vi.mock('../../utils/core/logger', () => {
|
|
38
|
+
const mockLoggerInstance = {
|
|
39
|
+
debug: vi.fn(),
|
|
40
|
+
info: vi.fn(),
|
|
41
|
+
warn: vi.fn(),
|
|
42
|
+
error: vi.fn(),
|
|
43
|
+
};
|
|
44
|
+
return {
|
|
45
|
+
createLogger: vi.fn(() => mockLoggerInstance),
|
|
46
|
+
logger: mockLoggerInstance,
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Mock useOrganisations hook (required by usePermissionCache)
|
|
51
|
+
const mockOrganisationContext = {
|
|
52
|
+
selectedOrganisation: {
|
|
53
|
+
id: 'test-org-id',
|
|
54
|
+
name: 'Test Organisation',
|
|
55
|
+
display_name: 'Test Organisation',
|
|
56
|
+
slug: 'test-org',
|
|
57
|
+
description: 'Test organisation',
|
|
58
|
+
subscription_tier: 'basic',
|
|
59
|
+
settings: {},
|
|
60
|
+
is_active: true,
|
|
61
|
+
created_at: '2023-01-01T00:00:00Z',
|
|
62
|
+
updated_at: '2023-01-01T00:00:00Z',
|
|
63
|
+
},
|
|
64
|
+
organisations: [],
|
|
65
|
+
userMemberships: [],
|
|
66
|
+
isLoading: false,
|
|
67
|
+
error: null,
|
|
68
|
+
hasValidOrganisationContext: true,
|
|
69
|
+
setSelectedOrganisation: vi.fn(),
|
|
70
|
+
switchOrganisation: vi.fn().mockResolvedValue(undefined),
|
|
71
|
+
getUserRole: vi.fn().mockReturnValue('member'),
|
|
72
|
+
validateOrganisationAccess: vi.fn().mockReturnValue(true),
|
|
73
|
+
ensureOrganisationContext: vi.fn().mockReturnValue(null),
|
|
74
|
+
refreshOrganisations: vi.fn().mockResolvedValue(undefined),
|
|
75
|
+
getPrimaryOrganisation: vi.fn().mockReturnValue(null),
|
|
76
|
+
isOrganisationSecure: vi.fn().mockReturnValue(true),
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
vi.mock('../useOrganisations', () => ({
|
|
80
|
+
useOrganisations: () => mockOrganisationContext,
|
|
81
|
+
}));
|
|
82
|
+
|
|
83
|
+
// Mock useEvents hook (optional - wrapped in try/catch in usePermissionCache)
|
|
84
|
+
vi.mock('../useEvents', () => ({
|
|
85
|
+
useEvents: vi.fn(() => ({
|
|
86
|
+
selectedEvent: { event_id: 'event-123' },
|
|
87
|
+
events: [],
|
|
88
|
+
isLoading: false,
|
|
89
|
+
error: null,
|
|
90
|
+
})),
|
|
91
|
+
}));
|
|
92
|
+
|
|
93
|
+
const mockUseRBAC = vi.mocked(useRBAC);
|
|
94
|
+
|
|
95
|
+
// Import after mocking
|
|
96
|
+
import { usePermissionCache } from '../usePermissionCache';
|
|
97
|
+
import { isPermittedCached } from '../../rbac/api';
|
|
98
|
+
const mockIsPermittedCached = vi.mocked(isPermittedCached);
|
|
99
|
+
|
|
100
|
+
describe('usePermissionCache', () => {
|
|
101
|
+
beforeEach(() => {
|
|
102
|
+
vi.clearAllMocks();
|
|
103
|
+
|
|
104
|
+
// Reset isPermittedCached mock
|
|
105
|
+
mockIsPermittedCached.mockClear();
|
|
106
|
+
mockIsPermittedCached.mockResolvedValue(true);
|
|
107
|
+
|
|
108
|
+
// Default mock implementation
|
|
109
|
+
mockUseRBAC.mockReturnValue({
|
|
110
|
+
hasPermission: vi.fn().mockResolvedValue(true),
|
|
111
|
+
user: { id: 'test-user-id' }
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
afterEach(() => {
|
|
116
|
+
vi.clearAllTimers();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('Initial Configuration', () => {
|
|
120
|
+
it('uses default configuration when no config is provided', () => {
|
|
121
|
+
const { result } = renderHook(() => usePermissionCache(), {
|
|
122
|
+
wrapper: TestWrapper
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
expect(result.current).toHaveProperty('checkPermission');
|
|
126
|
+
expect(result.current).toHaveProperty('checkMultiplePermissions');
|
|
127
|
+
expect(result.current).toHaveProperty('getCachedPermissions');
|
|
128
|
+
expect(result.current).toHaveProperty('invalidateCache');
|
|
129
|
+
expect(result.current).toHaveProperty('getDebugInfo');
|
|
130
|
+
expect(result.current).toHaveProperty('getAuditTrail');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('merges custom configuration with defaults', () => {
|
|
134
|
+
const customConfig = {
|
|
135
|
+
defaultTTL: 10000,
|
|
136
|
+
maxCacheSize: 500,
|
|
137
|
+
enableLogging: true,
|
|
138
|
+
enableAuditTrail: false
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const { result } = renderHook(() => usePermissionCache(customConfig), {
|
|
142
|
+
wrapper: TestWrapper
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
expect(result.current).toHaveProperty('checkPermission');
|
|
146
|
+
expect(result.current).toHaveProperty('checkMultiplePermissions');
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe('Single Permission Checking', () => {
|
|
151
|
+
it('checks permission and caches result', async () => {
|
|
152
|
+
mockIsPermittedCached.mockResolvedValueOnce(true);
|
|
153
|
+
mockUseRBAC.mockReturnValue({
|
|
154
|
+
hasPermission: vi.fn().mockResolvedValue(true),
|
|
155
|
+
user: { id: 'test-user-id' }
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const { result } = renderHook(() => usePermissionCache(), {
|
|
159
|
+
wrapper: TestWrapper
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const permission = await result.current.checkPermission('read', 'dashboard');
|
|
163
|
+
|
|
164
|
+
expect(permission).toBe(true);
|
|
165
|
+
expect(mockIsPermittedCached).toHaveBeenCalled();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('returns cached result for subsequent calls', async () => {
|
|
169
|
+
mockIsPermittedCached.mockResolvedValueOnce(true);
|
|
170
|
+
mockUseRBAC.mockReturnValue({
|
|
171
|
+
hasPermission: vi.fn().mockResolvedValue(true),
|
|
172
|
+
user: { id: 'test-user-id' }
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const { result } = renderHook(() => usePermissionCache(), {
|
|
176
|
+
wrapper: TestWrapper
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// First call
|
|
180
|
+
await result.current.checkPermission('read', 'dashboard');
|
|
181
|
+
|
|
182
|
+
// Second call should use cache
|
|
183
|
+
await result.current.checkPermission('read', 'dashboard');
|
|
184
|
+
|
|
185
|
+
// Should only call isPermittedCached once (second call uses cache)
|
|
186
|
+
expect(mockIsPermittedCached).toHaveBeenCalledTimes(1);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('respects custom TTL', async () => {
|
|
190
|
+
mockIsPermittedCached.mockResolvedValueOnce(true);
|
|
191
|
+
mockUseRBAC.mockReturnValue({
|
|
192
|
+
hasPermission: vi.fn().mockResolvedValue(true),
|
|
193
|
+
user: { id: 'test-user-id' }
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const { result } = renderHook(() => usePermissionCache({ defaultTTL: 1000 }), {
|
|
197
|
+
wrapper: TestWrapper
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
await result.current.checkPermission('read', 'dashboard', 2000);
|
|
201
|
+
|
|
202
|
+
expect(mockIsPermittedCached).toHaveBeenCalled();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('handles permission check errors gracefully', async () => {
|
|
206
|
+
const { logger } = await import('../../utils/core/logger');
|
|
207
|
+
mockIsPermittedCached.mockRejectedValueOnce(new Error('Database error'));
|
|
208
|
+
|
|
209
|
+
mockUseRBAC.mockReturnValue({
|
|
210
|
+
hasPermission: vi.fn().mockResolvedValue(true),
|
|
211
|
+
user: { id: 'test-user-id' }
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const { result } = renderHook(() => usePermissionCache(), {
|
|
215
|
+
wrapper: TestWrapper
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const permission = await result.current.checkPermission('read', 'dashboard');
|
|
219
|
+
|
|
220
|
+
expect(permission).toBe(false);
|
|
221
|
+
// Verify error was logged using logger
|
|
222
|
+
expect(logger.error).toHaveBeenCalled();
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe('Multiple Permission Checking', () => {
|
|
227
|
+
it('checks multiple permissions efficiently', async () => {
|
|
228
|
+
mockIsPermittedCached
|
|
229
|
+
.mockResolvedValueOnce(true)
|
|
230
|
+
.mockResolvedValueOnce(false)
|
|
231
|
+
.mockResolvedValueOnce(true)
|
|
232
|
+
.mockResolvedValueOnce(false);
|
|
233
|
+
|
|
234
|
+
mockUseRBAC.mockReturnValue({
|
|
235
|
+
hasPermission: vi.fn().mockResolvedValue(true),
|
|
236
|
+
user: { id: 'test-user-id' }
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
const { result } = renderHook(() => usePermissionCache(), {
|
|
241
|
+
wrapper: TestWrapper
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const permissions = await result.current.checkMultiplePermissions([
|
|
245
|
+
['read', 'dashboard'],
|
|
246
|
+
['create', 'dashboard'],
|
|
247
|
+
['update', 'dashboard'],
|
|
248
|
+
['delete', 'dashboard']
|
|
249
|
+
]);
|
|
250
|
+
|
|
251
|
+
expect(permissions).toHaveLength(4);
|
|
252
|
+
expect(permissions[0]).toEqual({
|
|
253
|
+
operation: 'read',
|
|
254
|
+
pageId: 'dashboard',
|
|
255
|
+
hasPermission: true,
|
|
256
|
+
cached: false,
|
|
257
|
+
timestamp: expect.any(Number)
|
|
258
|
+
});
|
|
259
|
+
expect(permissions[1]).toEqual({
|
|
260
|
+
operation: 'create',
|
|
261
|
+
pageId: 'dashboard',
|
|
262
|
+
hasPermission: false,
|
|
263
|
+
cached: false,
|
|
264
|
+
timestamp: expect.any(Number)
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('uses cached results for multiple permission checks', async () => {
|
|
269
|
+
mockIsPermittedCached
|
|
270
|
+
.mockResolvedValueOnce(true)
|
|
271
|
+
.mockResolvedValueOnce(false);
|
|
272
|
+
|
|
273
|
+
mockUseRBAC.mockReturnValue({
|
|
274
|
+
hasPermission: vi.fn().mockResolvedValue(true),
|
|
275
|
+
user: { id: 'test-user-id' }
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
const { result } = renderHook(() => usePermissionCache(), {
|
|
280
|
+
wrapper: TestWrapper
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// First call
|
|
284
|
+
await result.current.checkMultiplePermissions([
|
|
285
|
+
['read', 'dashboard'],
|
|
286
|
+
['create', 'dashboard']
|
|
287
|
+
]);
|
|
288
|
+
|
|
289
|
+
// Second call should use cache
|
|
290
|
+
await result.current.checkMultiplePermissions([
|
|
291
|
+
['read', 'dashboard'],
|
|
292
|
+
['create', 'dashboard']
|
|
293
|
+
]);
|
|
294
|
+
|
|
295
|
+
// Should only call isPermittedCached twice (once per permission, second call uses cache)
|
|
296
|
+
expect(mockIsPermittedCached).toHaveBeenCalledTimes(2);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('handles mixed cached and uncached permissions', async () => {
|
|
300
|
+
mockIsPermittedCached
|
|
301
|
+
.mockResolvedValueOnce(true)
|
|
302
|
+
.mockResolvedValueOnce(false)
|
|
303
|
+
.mockResolvedValueOnce(true)
|
|
304
|
+
.mockResolvedValueOnce(false);
|
|
305
|
+
|
|
306
|
+
mockUseRBAC.mockReturnValue({
|
|
307
|
+
hasPermission: vi.fn().mockResolvedValue(true),
|
|
308
|
+
user: { id: 'test-user-id' }
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
const { result } = renderHook(() => usePermissionCache(), {
|
|
313
|
+
wrapper: TestWrapper
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// Cache first two permissions
|
|
317
|
+
await result.current.checkMultiplePermissions([
|
|
318
|
+
['read', 'dashboard'],
|
|
319
|
+
['create', 'dashboard']
|
|
320
|
+
]);
|
|
321
|
+
|
|
322
|
+
// Check all four permissions (first two should be cached)
|
|
323
|
+
const permissions = await result.current.checkMultiplePermissions([
|
|
324
|
+
['read', 'dashboard'],
|
|
325
|
+
['create', 'dashboard'],
|
|
326
|
+
['update', 'dashboard'],
|
|
327
|
+
['delete', 'dashboard']
|
|
328
|
+
]);
|
|
329
|
+
|
|
330
|
+
expect(permissions).toHaveLength(4);
|
|
331
|
+
expect(permissions[0].cached).toBe(true);
|
|
332
|
+
expect(permissions[1].cached).toBe(true);
|
|
333
|
+
expect(permissions[2].cached).toBe(false);
|
|
334
|
+
expect(permissions[3].cached).toBe(false);
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
describe('Cache Management', () => {
|
|
339
|
+
it('enforces maximum cache size', async () => {
|
|
340
|
+
mockIsPermittedCached.mockResolvedValue(true);
|
|
341
|
+
mockUseRBAC.mockReturnValue({
|
|
342
|
+
hasPermission: vi.fn().mockResolvedValue(true),
|
|
343
|
+
user: { id: 'test-user-id' }
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
const { result } = renderHook(() => usePermissionCache({ maxCacheSize: 3 }), {
|
|
347
|
+
wrapper: TestWrapper
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// Add more entries than max cache size
|
|
351
|
+
await result.current.checkPermission('read', 'page1');
|
|
352
|
+
await result.current.checkPermission('read', 'page2');
|
|
353
|
+
await result.current.checkPermission('read', 'page3');
|
|
354
|
+
await result.current.checkPermission('read', 'page4');
|
|
355
|
+
await result.current.checkPermission('read', 'page5');
|
|
356
|
+
|
|
357
|
+
// Check that cache size is maintained
|
|
358
|
+
const debugInfo = result.current.getDebugInfo();
|
|
359
|
+
expect(debugInfo.cacheSize).toBeLessThanOrEqual(3);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it('invalidates cache entries', async () => {
|
|
363
|
+
mockIsPermittedCached.mockResolvedValue(true);
|
|
364
|
+
mockUseRBAC.mockReturnValue({
|
|
365
|
+
hasPermission: vi.fn().mockResolvedValue(true),
|
|
366
|
+
user: { id: 'test-user-id' }
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
const { result } = renderHook(() => usePermissionCache(), {
|
|
370
|
+
wrapper: TestWrapper
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
// Cache some permissions
|
|
374
|
+
await result.current.checkPermission('read', 'dashboard');
|
|
375
|
+
await result.current.checkPermission('create', 'dashboard');
|
|
376
|
+
|
|
377
|
+
// Invalidate all cache
|
|
378
|
+
result.current.invalidateCache();
|
|
379
|
+
|
|
380
|
+
// Check permissions again (should not use cache)
|
|
381
|
+
await result.current.checkPermission('read', 'dashboard');
|
|
382
|
+
await result.current.checkPermission('create', 'dashboard');
|
|
383
|
+
|
|
384
|
+
// Should be called 4 times (2 initial + 2 after invalidation)
|
|
385
|
+
expect(mockIsPermittedCached).toHaveBeenCalledTimes(4);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it('invalidates cache entries by pattern', async () => {
|
|
389
|
+
mockIsPermittedCached.mockResolvedValue(true);
|
|
390
|
+
mockUseRBAC.mockReturnValue({
|
|
391
|
+
hasPermission: vi.fn().mockResolvedValue(true),
|
|
392
|
+
user: { id: 'test-user-id' }
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
const { result } = renderHook(() => usePermissionCache(), {
|
|
396
|
+
wrapper: TestWrapper
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// Cache permissions for different pages
|
|
400
|
+
await result.current.checkPermission('read', 'dashboard');
|
|
401
|
+
await result.current.checkPermission('read', 'admin');
|
|
402
|
+
await result.current.checkPermission('read', 'users');
|
|
403
|
+
|
|
404
|
+
// Invalidate only dashboard permissions
|
|
405
|
+
result.current.invalidateCache('dashboard');
|
|
406
|
+
|
|
407
|
+
// Check permissions again
|
|
408
|
+
await result.current.checkPermission('read', 'dashboard');
|
|
409
|
+
await result.current.checkPermission('read', 'admin');
|
|
410
|
+
|
|
411
|
+
// dashboard should be called again, admin should use cache
|
|
412
|
+
// 3 initial calls + 1 for dashboard after invalidation = 4 total
|
|
413
|
+
expect(mockIsPermittedCached).toHaveBeenCalledTimes(4);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it('cleans up expired cache entries', async () => {
|
|
417
|
+
vi.useFakeTimers();
|
|
418
|
+
|
|
419
|
+
mockIsPermittedCached.mockResolvedValue(true);
|
|
420
|
+
mockUseRBAC.mockReturnValue({
|
|
421
|
+
hasPermission: vi.fn().mockResolvedValue(true),
|
|
422
|
+
user: { id: 'test-user-id' }
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
const { result } = renderHook(() => usePermissionCache({ defaultTTL: 1000 }), {
|
|
426
|
+
wrapper: TestWrapper
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
// Cache a permission
|
|
430
|
+
await result.current.checkPermission('read', 'dashboard');
|
|
431
|
+
|
|
432
|
+
// Advance time past TTL
|
|
433
|
+
vi.advanceTimersByTime(2000);
|
|
434
|
+
|
|
435
|
+
// Trigger cleanup by checking another permission
|
|
436
|
+
await result.current.checkPermission('read', 'admin');
|
|
437
|
+
|
|
438
|
+
const debugInfo = result.current.getDebugInfo();
|
|
439
|
+
expect(debugInfo.cacheSize).toBe(1); // Only the new permission should remain
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
describe('Debug Information', () => {
|
|
444
|
+
it('provides accurate debug information', async () => {
|
|
445
|
+
mockIsPermittedCached
|
|
446
|
+
.mockResolvedValueOnce(true)
|
|
447
|
+
.mockResolvedValueOnce(false);
|
|
448
|
+
|
|
449
|
+
mockUseRBAC.mockReturnValue({
|
|
450
|
+
hasPermission: vi.fn().mockResolvedValue(true),
|
|
451
|
+
user: { id: 'test-user-id' }
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
const { result } = renderHook(() => usePermissionCache(), {
|
|
456
|
+
wrapper: TestWrapper
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
// Make some permission checks
|
|
460
|
+
await result.current.checkPermission('read', 'dashboard');
|
|
461
|
+
await result.current.checkPermission('create', 'dashboard');
|
|
462
|
+
await result.current.checkPermission('read', 'dashboard'); // This should be cached
|
|
463
|
+
|
|
464
|
+
const debugInfo = result.current.getDebugInfo();
|
|
465
|
+
|
|
466
|
+
expect(debugInfo.cacheSize).toBe(2);
|
|
467
|
+
expect(debugInfo.cacheHits).toBe(1);
|
|
468
|
+
expect(debugInfo.cacheMisses).toBe(2);
|
|
469
|
+
expect(debugInfo.totalChecks).toBe(3);
|
|
470
|
+
expect(debugInfo.averageResponseTime).toBeGreaterThan(0);
|
|
471
|
+
expect(debugInfo.lastInvalidation).toBe(0); // No invalidation has been performed yet
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it('tracks response times accurately', async () => {
|
|
475
|
+
mockIsPermittedCached.mockResolvedValue(true);
|
|
476
|
+
mockUseRBAC.mockReturnValue({
|
|
477
|
+
hasPermission: vi.fn().mockResolvedValue(true),
|
|
478
|
+
user: { id: 'test-user-id' }
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
const { result } = renderHook(() => usePermissionCache(), {
|
|
483
|
+
wrapper: TestWrapper
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
await result.current.checkPermission('read', 'dashboard');
|
|
487
|
+
|
|
488
|
+
const debugInfo = result.current.getDebugInfo();
|
|
489
|
+
// Just verify that response time is tracked (greater than 0)
|
|
490
|
+
expect(debugInfo.averageResponseTime).toBeGreaterThan(0);
|
|
491
|
+
expect(debugInfo.totalChecks).toBe(1);
|
|
492
|
+
});
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
describe('Audit Trail', () => {
|
|
496
|
+
it('records audit trail when enabled', async () => {
|
|
497
|
+
mockIsPermittedCached.mockResolvedValue(true);
|
|
498
|
+
// The hook uses useUnifiedAuth for user, not useRBAC
|
|
499
|
+
mockUseUnifiedAuthFn.mockReturnValue({
|
|
500
|
+
user: { id: 'test-user-id' },
|
|
501
|
+
session: null,
|
|
502
|
+
appName: 'test-app',
|
|
503
|
+
selectedOrganisationId: undefined,
|
|
504
|
+
selectedEventId: undefined
|
|
505
|
+
} as any);
|
|
506
|
+
mockUseRBAC.mockReturnValue({
|
|
507
|
+
hasPermission: vi.fn().mockResolvedValue(true),
|
|
508
|
+
user: { id: 'test-user-id' }
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
const { result } = renderHook(() => usePermissionCache({ enableAuditTrail: true }), {
|
|
512
|
+
wrapper: TestWrapper
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
await result.current.checkPermission('read', 'dashboard');
|
|
516
|
+
await result.current.checkPermission('create', 'admin');
|
|
517
|
+
|
|
518
|
+
const auditTrail = result.current.getAuditTrail();
|
|
519
|
+
|
|
520
|
+
expect(auditTrail).toHaveLength(2);
|
|
521
|
+
// Check all properties including timestamp
|
|
522
|
+
expect(auditTrail[0]).toHaveProperty('timestamp');
|
|
523
|
+
expect(auditTrail[0]).toHaveProperty('operation', 'read');
|
|
524
|
+
expect(auditTrail[0]).toHaveProperty('pageId', 'dashboard');
|
|
525
|
+
expect(auditTrail[0]).toHaveProperty('result', true);
|
|
526
|
+
expect(auditTrail[0]).toHaveProperty('cached', false);
|
|
527
|
+
expect(auditTrail[0]).toHaveProperty('userId', 'test-user-id');
|
|
528
|
+
expect(typeof auditTrail[0].timestamp).toBe('number');
|
|
529
|
+
|
|
530
|
+
expect(auditTrail[1]).toHaveProperty('timestamp');
|
|
531
|
+
expect(auditTrail[1]).toHaveProperty('operation', 'create');
|
|
532
|
+
expect(auditTrail[1]).toHaveProperty('pageId', 'admin');
|
|
533
|
+
expect(auditTrail[1]).toHaveProperty('result', true);
|
|
534
|
+
expect(auditTrail[1]).toHaveProperty('cached', false);
|
|
535
|
+
expect(auditTrail[1]).toHaveProperty('userId', 'test-user-id');
|
|
536
|
+
expect(typeof auditTrail[1].timestamp).toBe('number');
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
it('does not record audit trail when disabled', async () => {
|
|
540
|
+
mockIsPermittedCached.mockResolvedValue(true);
|
|
541
|
+
mockUseRBAC.mockReturnValue({
|
|
542
|
+
hasPermission: vi.fn().mockResolvedValue(true),
|
|
543
|
+
user: { id: 'test-user-id' }
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
const { result } = renderHook(() => usePermissionCache({ enableAuditTrail: false }), {
|
|
547
|
+
wrapper: TestWrapper
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
await result.current.checkPermission('read', 'dashboard');
|
|
551
|
+
|
|
552
|
+
const auditTrail = result.current.getAuditTrail();
|
|
553
|
+
expect(auditTrail).toHaveLength(0);
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
it('limits audit trail size', async () => {
|
|
557
|
+
mockIsPermittedCached.mockResolvedValue(true);
|
|
558
|
+
mockUseRBAC.mockReturnValue({
|
|
559
|
+
hasPermission: vi.fn().mockResolvedValue(true),
|
|
560
|
+
user: { id: 'test-user-id' }
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
const { result } = renderHook(() => usePermissionCache({ enableAuditTrail: true }), {
|
|
564
|
+
wrapper: TestWrapper
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
// Make more than 1000 permission checks
|
|
568
|
+
for (let i = 0; i < 1100; i++) {
|
|
569
|
+
await result.current.checkPermission('read', `page${i}`);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const auditTrail = result.current.getAuditTrail();
|
|
573
|
+
expect(auditTrail.length).toBeLessThanOrEqual(500);
|
|
574
|
+
});
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
describe('Logging', () => {
|
|
578
|
+
it('logs permission checks when enabled', async () => {
|
|
579
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
580
|
+
|
|
581
|
+
mockIsPermittedCached.mockResolvedValue(true);
|
|
582
|
+
mockUseRBAC.mockReturnValue({
|
|
583
|
+
hasPermission: vi.fn().mockResolvedValue(true),
|
|
584
|
+
user: { id: 'test-user-id' }
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
const { result } = renderHook(() => usePermissionCache({ enableLogging: true }), {
|
|
588
|
+
wrapper: TestWrapper
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
// Verify the actual behavior: permission check works correctly
|
|
592
|
+
const hasPermission = await result.current.checkPermission('read', 'dashboard');
|
|
593
|
+
|
|
594
|
+
// Test the actual functionality: permission is checked and result is returned
|
|
595
|
+
expect(hasPermission).toBe(true);
|
|
596
|
+
// Verify caching is working: second call should use cache
|
|
597
|
+
const hasPermissionCached = await result.current.checkPermission('read', 'dashboard');
|
|
598
|
+
expect(hasPermissionCached).toBe(true);
|
|
599
|
+
// Verify debug info shows cache hits
|
|
600
|
+
const debugInfo = result.current.getDebugInfo();
|
|
601
|
+
expect(debugInfo.cacheHits).toBeGreaterThan(0);
|
|
602
|
+
|
|
603
|
+
consoleSpy.mockRestore();
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
it('does not log when disabled', async () => {
|
|
607
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
608
|
+
|
|
609
|
+
mockIsPermittedCached.mockResolvedValue(true);
|
|
610
|
+
mockUseRBAC.mockReturnValue({
|
|
611
|
+
hasPermission: vi.fn().mockResolvedValue(true),
|
|
612
|
+
user: { id: 'test-user-id' }
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
const { result } = renderHook(() => usePermissionCache({ enableLogging: false }), {
|
|
616
|
+
wrapper: TestWrapper
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
await result.current.checkPermission('read', 'dashboard');
|
|
620
|
+
|
|
621
|
+
expect(consoleSpy).not.toHaveBeenCalledWith(
|
|
622
|
+
expect.stringContaining('[PermissionCache]')
|
|
623
|
+
);
|
|
624
|
+
|
|
625
|
+
consoleSpy.mockRestore();
|
|
626
|
+
});
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
describe('Cached Permissions', () => {
|
|
630
|
+
it('returns cached permissions for a page', async () => {
|
|
631
|
+
mockIsPermittedCached
|
|
632
|
+
.mockResolvedValueOnce(true)
|
|
633
|
+
.mockResolvedValueOnce(false)
|
|
634
|
+
.mockResolvedValueOnce(true)
|
|
635
|
+
.mockResolvedValueOnce(false);
|
|
636
|
+
|
|
637
|
+
mockUseRBAC.mockReturnValue({
|
|
638
|
+
hasPermission: vi.fn().mockResolvedValue(true),
|
|
639
|
+
user: { id: 'test-user-id' }
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
const { result } = renderHook(() => usePermissionCache(), {
|
|
644
|
+
wrapper: TestWrapper
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
// Cache all permissions for dashboard
|
|
648
|
+
await result.current.checkPermission('read', 'dashboard');
|
|
649
|
+
await result.current.checkPermission('create', 'dashboard');
|
|
650
|
+
await result.current.checkPermission('update', 'dashboard');
|
|
651
|
+
await result.current.checkPermission('delete', 'dashboard');
|
|
652
|
+
|
|
653
|
+
const cachedPermissions = result.current.getCachedPermissions('dashboard');
|
|
654
|
+
|
|
655
|
+
expect(cachedPermissions).toHaveLength(4);
|
|
656
|
+
expect(cachedPermissions).toEqual([
|
|
657
|
+
{ operation: 'read', hasPermission: true },
|
|
658
|
+
{ operation: 'create', hasPermission: false },
|
|
659
|
+
{ operation: 'update', hasPermission: true },
|
|
660
|
+
{ operation: 'delete', hasPermission: false }
|
|
661
|
+
]);
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
it('returns empty array for uncached page', async () => {
|
|
665
|
+
mockIsPermittedCached.mockResolvedValue(true);
|
|
666
|
+
mockUseRBAC.mockReturnValue({
|
|
667
|
+
hasPermission: vi.fn().mockResolvedValue(true),
|
|
668
|
+
user: { id: 'test-user-id' }
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
const { result } = renderHook(() => usePermissionCache(), {
|
|
672
|
+
wrapper: TestWrapper
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
const cachedPermissions = result.current.getCachedPermissions('uncached-page');
|
|
676
|
+
|
|
677
|
+
expect(cachedPermissions).toHaveLength(0);
|
|
678
|
+
});
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
describe('Edge Cases', () => {
|
|
682
|
+
it('handles concurrent permission checks', async () => {
|
|
683
|
+
mockIsPermittedCached.mockResolvedValue(true);
|
|
684
|
+
mockUseRBAC.mockReturnValue({
|
|
685
|
+
hasPermission: vi.fn().mockResolvedValue(true),
|
|
686
|
+
user: { id: 'test-user-id' }
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
const { result } = renderHook(() => usePermissionCache(), {
|
|
690
|
+
wrapper: TestWrapper
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
// Make concurrent permission checks
|
|
694
|
+
const promises = [
|
|
695
|
+
result.current.checkPermission('read', 'dashboard'),
|
|
696
|
+
result.current.checkPermission('read', 'dashboard'),
|
|
697
|
+
result.current.checkPermission('read', 'dashboard')
|
|
698
|
+
];
|
|
699
|
+
|
|
700
|
+
const results = await Promise.all(promises);
|
|
701
|
+
|
|
702
|
+
expect(results).toEqual([true, true, true]);
|
|
703
|
+
expect(mockIsPermittedCached).toHaveBeenCalledTimes(1); // Should only call once due to caching
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
it('handles rapid cache invalidation', async () => {
|
|
707
|
+
mockIsPermittedCached.mockResolvedValue(true);
|
|
708
|
+
mockUseRBAC.mockReturnValue({
|
|
709
|
+
hasPermission: vi.fn().mockResolvedValue(true),
|
|
710
|
+
user: { id: 'test-user-id' }
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
const { result } = renderHook(() => usePermissionCache(), {
|
|
714
|
+
wrapper: TestWrapper
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
await result.current.checkPermission('read', 'dashboard');
|
|
718
|
+
result.current.invalidateCache();
|
|
719
|
+
await result.current.checkPermission('read', 'dashboard');
|
|
720
|
+
|
|
721
|
+
expect(mockIsPermittedCached).toHaveBeenCalledTimes(2);
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
it('handles empty permission arrays', async () => {
|
|
725
|
+
mockIsPermittedCached.mockResolvedValue(true);
|
|
726
|
+
mockUseRBAC.mockReturnValue({
|
|
727
|
+
hasPermission: vi.fn().mockResolvedValue(true),
|
|
728
|
+
user: { id: 'test-user-id' }
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
const { result } = renderHook(() => usePermissionCache(), {
|
|
732
|
+
wrapper: TestWrapper
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
const permissions = await result.current.checkMultiplePermissions([]);
|
|
736
|
+
|
|
737
|
+
expect(permissions).toHaveLength(0);
|
|
738
|
+
expect(mockIsPermittedCached).not.toHaveBeenCalled();
|
|
739
|
+
});
|
|
740
|
+
});
|
|
741
|
+
});
|