@jmruthers/pace-core 0.5.1 → 0.5.4
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-GX3XERFJ.js → DataTable-ZQDRE46Q.js} +7 -6
- package/dist/{PublicLoadingSpinner-DztrzuJr.d.ts → PublicLoadingSpinner-Bq_-BeK-.d.ts} +1 -1
- package/dist/RBACProvider-BO4ilsQB.d.ts +63 -0
- package/dist/{UnifiedAuthProvider-w66zSCUf.d.ts → UnifiedAuthProvider-DGQsy-vY.d.ts} +2 -59
- package/dist/{api-ETQ6YJ3C.js → api-H5A3H4IR.js} +2 -2
- package/dist/{chunk-T3XIA4AJ.js → chunk-5H3C2SWM.js} +14 -16
- package/dist/chunk-5H3C2SWM.js.map +1 -0
- package/dist/chunk-5SIXIV7R.js +1925 -0
- package/dist/chunk-5SIXIV7R.js.map +1 -0
- package/dist/chunk-GNTALZV3.js +17 -0
- package/dist/chunk-GNTALZV3.js.map +1 -0
- package/dist/{chunk-C5G2A4PO.js → chunk-GWSBHC4J.js} +6 -6
- package/dist/{chunk-XJK2J4N6.js → chunk-HD7PYDUV.js} +4 -6
- package/dist/{chunk-XJK2J4N6.js.map → chunk-HD7PYDUV.js.map} +1 -1
- package/dist/{chunk-TGDCLPP2.js → chunk-HXX35Q2M.js} +6 -21
- package/dist/chunk-HXX35Q2M.js.map +1 -0
- package/dist/{chunk-5EL3KHOQ.js → chunk-K6B7BLSE.js} +2 -2
- package/dist/{chunk-GSNM5D6H.js → chunk-M4RW7PIP.js} +4 -4
- package/dist/{chunk-U6JDHVC2.js → chunk-PVMYVQSM.js} +6 -8
- package/dist/{chunk-U6JDHVC2.js.map → chunk-PVMYVQSM.js.map} +1 -1
- package/dist/{chunk-6CR3MRZN.js → chunk-QKHFMQ5R.js} +372 -11
- package/dist/{chunk-6CR3MRZN.js.map → chunk-QKHFMQ5R.js.map} +1 -1
- package/dist/chunk-QVYBYGT2.js +428 -0
- package/dist/chunk-QVYBYGT2.js.map +1 -0
- package/dist/{chunk-OEGRKULD.js → chunk-WJARTBCT.js} +56 -1
- package/dist/chunk-WJARTBCT.js.map +1 -0
- package/dist/components.d.ts +4 -3
- package/dist/components.js +16 -162
- package/dist/components.js.map +1 -1
- package/dist/hooks.d.ts +2 -2
- package/dist/hooks.js +7 -9
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +8 -6
- package/dist/index.js +152 -17
- package/dist/index.js.map +1 -1
- package/dist/providers.d.ts +3 -2
- package/dist/providers.js +6 -12
- package/dist/rbac/index.d.ts +167 -98
- package/dist/rbac/index.js +48 -1881
- package/dist/rbac/index.js.map +1 -1
- package/dist/styles/core.css +0 -55
- package/dist/types.d.ts +2 -2
- package/dist/{unified-CM7T0aTK.d.ts → unified-CMPjE_fv.d.ts} +1 -1
- package/dist/{usePublicRouteParams-B6i0KtXW.d.ts → usePublicRouteParams-B2OcAsur.d.ts} +1 -1
- package/dist/utils.js +12 -14
- package/dist/utils.js.map +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +73 -0
- package/docs/api/classes/MissingUserContextError.md +66 -0
- package/docs/api/classes/OrganisationContextRequiredError.md +66 -0
- package/docs/api/classes/PermissionDeniedError.md +73 -0
- package/docs/api/classes/PublicErrorBoundary.md +1 -1
- package/docs/api/classes/RBACAuditManager.md +270 -0
- package/docs/api/classes/RBACCache.md +284 -0
- package/docs/api/classes/RBACEngine.md +141 -0
- package/docs/api/classes/RBACError.md +76 -0
- package/docs/api/classes/RBACNotInitializedError.md +66 -0
- package/docs/api/classes/SecureSupabaseClient.md +135 -0
- 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 +96 -0
- 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 +235 -0
- package/docs/api/interfaces/EventContextType.md +1 -1
- package/docs/api/interfaces/EventLogoProps.md +1 -1
- package/docs/api/interfaces/EventProviderProps.md +1 -1
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadProps.md +1 -1
- package/docs/api/interfaces/FooterProps.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/LoginFormProps.md +1 -1
- package/docs/api/interfaces/NavigationAccessRecord.md +107 -0
- package/docs/api/interfaces/NavigationContextType.md +164 -0
- package/docs/api/interfaces/NavigationGuardProps.md +139 -0
- package/docs/api/interfaces/NavigationItem.md +1 -1
- package/docs/api/interfaces/NavigationMenuProps.md +1 -1
- package/docs/api/interfaces/NavigationProviderProps.md +117 -0
- package/docs/api/interfaces/Organisation.md +1 -1
- package/docs/api/interfaces/OrganisationContextType.md +1 -1
- package/docs/api/interfaces/OrganisationMembership.md +2 -2
- 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 +85 -0
- package/docs/api/interfaces/PagePermissionContextType.md +140 -0
- package/docs/api/interfaces/PagePermissionGuardProps.md +153 -0
- package/docs/api/interfaces/PagePermissionProviderProps.md +119 -0
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/PermissionEnforcerProps.md +153 -0
- 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 +99 -0
- package/docs/api/interfaces/RBACContextType.md +474 -0
- package/docs/api/interfaces/RBACLogger.md +112 -0
- package/docs/api/interfaces/RBACProviderProps.md +107 -0
- package/docs/api/interfaces/RoleBasedRouterContextType.md +151 -0
- package/docs/api/interfaces/RoleBasedRouterProps.md +156 -0
- package/docs/api/interfaces/RouteAccessRecord.md +107 -0
- package/docs/api/interfaces/RouteConfig.md +121 -0
- package/docs/api/interfaces/SecureDataContextType.md +168 -0
- package/docs/api/interfaces/SecureDataProviderProps.md +132 -0
- 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/ToastActionElement.md +1 -1
- package/docs/api/interfaces/ToastProps.md +1 -1
- package/docs/api/interfaces/UnifiedAuthContextType.md +85 -85
- 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/UsePublicEventLogoOptions.md +1 -1
- 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/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UserEventAccess.md +11 -11
- package/docs/api/interfaces/UserMenuProps.md +1 -1
- package/docs/api/interfaces/UserProfile.md +1 -1
- package/docs/api/modules.md +2244 -3
- package/docs/migration-guide.md +43 -18
- package/docs/styles/README.md +187 -98
- package/docs/usage.md +32 -7
- package/package.json +2 -2
- package/src/components/Footer/Footer.test.tsx +482 -0
- package/src/components/Form/Form.test.tsx +1158 -0
- package/src/components/Header/Header.test.tsx +582 -0
- package/src/components/Header/Header.tsx +1 -1
- package/src/components/InactivityWarningModal/InactivityWarningModal.test.tsx +489 -0
- package/src/components/Input/Input.test.tsx +466 -0
- package/src/components/LoadingSpinner/LoadingSpinner.test.tsx +450 -0
- package/src/components/LoginForm/LoginForm.test.tsx +816 -0
- package/src/components/NavigationMenu/NavigationMenu.test.tsx +883 -0
- package/src/components/OrganisationSelector/OrganisationSelector.test.tsx +748 -0
- package/src/components/PaceAppLayout/PaceAppLayout.test.tsx +891 -0
- package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +475 -0
- package/src/components/PasswordReset/PasswordChangeForm.test.tsx +621 -0
- package/src/components/PasswordReset/PasswordResetForm.test.tsx +605 -0
- package/src/components/Select/Select.test.tsx +948 -0
- package/src/components/SuperAdminGuard.tsx +1 -1
- package/src/components/Toast/Toast.test.tsx +586 -0
- package/src/components/Tooltip/Tooltip.test.tsx +852 -0
- package/src/components/UserMenu/UserMenu.test.tsx +702 -0
- package/src/components/UserMenu/UserMenu.tsx +2 -2
- package/src/hooks/useDebounce.test.ts +375 -0
- package/src/hooks/useOrganisationPermissions.test.ts +528 -0
- package/src/hooks/useOrganisationSecurity.test.ts +734 -0
- package/src/hooks/usePermissionCache.test.ts +542 -0
- package/src/hooks/usePermissionCache.ts +1 -1
- package/src/index.ts +2 -3
- package/src/providers/UnifiedAuthProvider.tsx +2 -2
- package/src/providers/index.ts +3 -1
- package/src/rbac/__tests__/integration.test.tsx +218 -0
- package/src/rbac/api.test.ts +952 -0
- package/src/rbac/components/__tests__/NavigationGuard.test.tsx +843 -0
- package/src/rbac/components/__tests__/PagePermissionGuard.test.tsx +1007 -0
- package/src/rbac/components/__tests__/PermissionEnforcer.test.tsx +806 -0
- package/src/rbac/components/__tests__/RoleBasedRouter.test.tsx +741 -0
- package/src/rbac/hooks/index.ts +21 -0
- package/src/rbac/hooks/useCan.test.ts +461 -0
- package/src/rbac/hooks/usePermissions.test.ts +364 -0
- package/src/rbac/hooks/usePermissions.ts +567 -0
- package/src/rbac/hooks/useRBAC.simple.test.ts +90 -0
- package/src/rbac/hooks/useRBAC.test.ts +551 -0
- package/src/{hooks → rbac/hooks}/useRBAC.ts +7 -7
- package/src/rbac/index.ts +5 -10
- package/src/{providers → rbac/providers}/RBACProvider.tsx +6 -6
- package/src/rbac/providers/__tests__/RBACProvider.test.tsx +687 -0
- package/src/rbac/providers/index.ts +11 -0
- package/src/styles/core.css +0 -55
- package/src/utils/formatDate.test.ts +241 -0
- package/dist/chunk-AUE24LVR.js +0 -268
- package/dist/chunk-AUE24LVR.js.map +0 -1
- package/dist/chunk-COBPIXXQ.js +0 -379
- package/dist/chunk-COBPIXXQ.js.map +0 -1
- package/dist/chunk-OEGRKULD.js.map +0 -1
- package/dist/chunk-OYRY44Q2.js +0 -62
- package/dist/chunk-OYRY44Q2.js.map +0 -1
- package/dist/chunk-T3XIA4AJ.js.map +0 -1
- package/dist/chunk-TGDCLPP2.js.map +0 -1
- package/src/components/RBAC/PagePermissionGuard.tsx +0 -287
- package/src/components/RBAC/RBACGuard.tsx +0 -143
- package/src/components/RBAC/RBACProvider.tsx +0 -186
- package/src/components/RBAC/RoleBasedContent.tsx +0 -129
- package/src/components/RBAC/index.ts +0 -23
- package/src/rbac/hooks.ts +0 -570
- /package/dist/{DataTable-GX3XERFJ.js.map → DataTable-ZQDRE46Q.js.map} +0 -0
- /package/dist/{api-ETQ6YJ3C.js.map → api-H5A3H4IR.js.map} +0 -0
- /package/dist/{chunk-C5G2A4PO.js.map → chunk-GWSBHC4J.js.map} +0 -0
- /package/dist/{chunk-5EL3KHOQ.js.map → chunk-K6B7BLSE.js.map} +0 -0
- /package/dist/{chunk-GSNM5D6H.js.map → chunk-M4RW7PIP.js.map} +0 -0
|
@@ -0,0 +1,741 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file RoleBasedRouter Component Tests
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module RBAC/Components/RoleBasedRouter
|
|
5
|
+
* @since 2.0.0
|
|
6
|
+
*
|
|
7
|
+
* Comprehensive tests for the RoleBasedRouter component covering all critical functionality.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
11
|
+
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
12
|
+
import { ReactNode } from 'react';
|
|
13
|
+
import { MemoryRouter, Routes, Route } from 'react-router-dom';
|
|
14
|
+
import { RoleBasedRouter, useRoleBasedRouter } from '../RoleBasedRouter';
|
|
15
|
+
import { useCan } from '../../hooks';
|
|
16
|
+
import { useUnifiedAuth } from '../../../providers/UnifiedAuthProvider';
|
|
17
|
+
|
|
18
|
+
// Mock the RBAC hooks
|
|
19
|
+
vi.mock('../../hooks', () => ({
|
|
20
|
+
useCan: vi.fn()
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
// Mock the auth provider
|
|
24
|
+
vi.mock('../../../providers/UnifiedAuthProvider', () => ({
|
|
25
|
+
useUnifiedAuth: vi.fn()
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
// Mock React Router
|
|
29
|
+
vi.mock('react-router-dom', async () => {
|
|
30
|
+
const actual = await vi.importActual('react-router-dom');
|
|
31
|
+
return {
|
|
32
|
+
...actual,
|
|
33
|
+
useLocation: vi.fn(),
|
|
34
|
+
useNavigate: vi.fn(),
|
|
35
|
+
Outlet: vi.fn()
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
import { useLocation, useNavigate, Outlet } from 'react-router-dom';
|
|
40
|
+
|
|
41
|
+
// Mock data
|
|
42
|
+
const mockUser = {
|
|
43
|
+
id: 'user-123',
|
|
44
|
+
email: 'test@example.com'
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const mockScope = {
|
|
48
|
+
organisationId: 'org-123',
|
|
49
|
+
eventId: 'event-123',
|
|
50
|
+
appId: 'app-123'
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const mockRoutes = [
|
|
54
|
+
{
|
|
55
|
+
path: '/dashboard',
|
|
56
|
+
component: () => <div data-testid="dashboard">Dashboard</div>,
|
|
57
|
+
permissions: ['read:dashboard'] as const,
|
|
58
|
+
pageId: 'dashboard'
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
path: '/admin',
|
|
62
|
+
component: () => <div data-testid="admin">Admin</div>,
|
|
63
|
+
permissions: ['admin:system'] as const,
|
|
64
|
+
pageId: 'admin',
|
|
65
|
+
strictMode: true
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
path: '/public',
|
|
69
|
+
component: () => <div data-testid="public">Public</div>,
|
|
70
|
+
permissions: [] as const
|
|
71
|
+
}
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
// Test components
|
|
75
|
+
const TestComponent = ({ children }: { children: ReactNode }) => (
|
|
76
|
+
<div data-testid="test-component">{children}</div>
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const TestUnauthorized = ({ route, reason }: { route: string; reason: string }) => (
|
|
80
|
+
<div data-testid="test-unauthorized">
|
|
81
|
+
Unauthorized: {route} - {reason}
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const TestOutlet = () => <div data-testid="test-outlet">Outlet</div>;
|
|
86
|
+
|
|
87
|
+
// Mock Outlet component
|
|
88
|
+
vi.mocked(Outlet).mockImplementation(TestOutlet);
|
|
89
|
+
|
|
90
|
+
describe('RoleBasedRouter Component', () => {
|
|
91
|
+
const mockUseCan = vi.mocked(useCan);
|
|
92
|
+
const mockUseUnifiedAuth = vi.mocked(useUnifiedAuth);
|
|
93
|
+
const mockUseLocation = vi.mocked(useLocation);
|
|
94
|
+
const mockUseNavigate = vi.mocked(useNavigate);
|
|
95
|
+
|
|
96
|
+
beforeEach(() => {
|
|
97
|
+
vi.clearAllMocks();
|
|
98
|
+
|
|
99
|
+
// Default mock implementations
|
|
100
|
+
mockUseUnifiedAuth.mockReturnValue({
|
|
101
|
+
user: mockUser,
|
|
102
|
+
selectedOrganisationId: 'org-123',
|
|
103
|
+
selectedEventId: 'event-123'
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
mockUseLocation.mockReturnValue({
|
|
107
|
+
pathname: '/dashboard',
|
|
108
|
+
search: '',
|
|
109
|
+
hash: '',
|
|
110
|
+
state: null,
|
|
111
|
+
key: 'test'
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
mockUseNavigate.mockReturnValue(vi.fn());
|
|
115
|
+
|
|
116
|
+
mockUseCan.mockReturnValue({
|
|
117
|
+
can: true,
|
|
118
|
+
isLoading: false,
|
|
119
|
+
error: null
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
afterEach(() => {
|
|
124
|
+
vi.restoreAllMocks();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('Rendering', () => {
|
|
128
|
+
it('renders children when user has permission', async () => {
|
|
129
|
+
mockUseCan.mockReturnValue({
|
|
130
|
+
can: true,
|
|
131
|
+
isLoading: false,
|
|
132
|
+
error: null
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
render(
|
|
136
|
+
<MemoryRouter initialEntries={['/dashboard']}>
|
|
137
|
+
<RoleBasedRouter routes={mockRoutes}>
|
|
138
|
+
<TestComponent>App Content</TestComponent>
|
|
139
|
+
</RoleBasedRouter>
|
|
140
|
+
</MemoryRouter>
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
await waitFor(() => {
|
|
144
|
+
expect(screen.getByTestId('test-component')).toBeInTheDocument();
|
|
145
|
+
expect(screen.getByText('App Content')).toBeInTheDocument();
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('renders unauthorized component when user lacks permission', async () => {
|
|
150
|
+
mockUseCan.mockReturnValue({
|
|
151
|
+
can: false,
|
|
152
|
+
isLoading: false,
|
|
153
|
+
error: null
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
render(
|
|
157
|
+
<MemoryRouter initialEntries={['/admin']}>
|
|
158
|
+
<RoleBasedRouter
|
|
159
|
+
routes={mockRoutes}
|
|
160
|
+
unauthorizedComponent={TestUnauthorized}
|
|
161
|
+
>
|
|
162
|
+
<TestComponent>App Content</TestComponent>
|
|
163
|
+
</RoleBasedRouter>
|
|
164
|
+
</MemoryRouter>
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
await waitFor(() => {
|
|
168
|
+
expect(screen.getByTestId('test-unauthorized')).toBeInTheDocument();
|
|
169
|
+
expect(screen.getByText('Unauthorized: /dashboard - Insufficient permissions')).toBeInTheDocument();
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('shows loading state while checking permissions', () => {
|
|
174
|
+
mockUseCan.mockReturnValue({
|
|
175
|
+
can: false,
|
|
176
|
+
isLoading: true,
|
|
177
|
+
error: null
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
render(
|
|
181
|
+
<MemoryRouter initialEntries={['/dashboard']}>
|
|
182
|
+
<RoleBasedRouter routes={mockRoutes}>
|
|
183
|
+
<TestComponent>App Content</TestComponent>
|
|
184
|
+
</RoleBasedRouter>
|
|
185
|
+
</MemoryRouter>
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
expect(screen.getByText('Checking permissions...')).toBeInTheDocument();
|
|
189
|
+
expect(screen.queryByTestId('test-component')).not.toBeInTheDocument();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('renders outlet for route components', async () => {
|
|
193
|
+
mockUseCan.mockReturnValue({
|
|
194
|
+
can: true,
|
|
195
|
+
isLoading: false,
|
|
196
|
+
error: null
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
render(
|
|
200
|
+
<MemoryRouter initialEntries={['/dashboard']}>
|
|
201
|
+
<RoleBasedRouter routes={mockRoutes}>
|
|
202
|
+
<TestComponent>App Content</TestComponent>
|
|
203
|
+
</RoleBasedRouter>
|
|
204
|
+
</MemoryRouter>
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
await waitFor(() => {
|
|
208
|
+
expect(screen.getByTestId('test-component')).toBeInTheDocument();
|
|
209
|
+
expect(screen.getByText('App Content')).toBeInTheDocument();
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe('Route Protection', () => {
|
|
215
|
+
it('protects routes based on permissions', async () => {
|
|
216
|
+
mockUseCan.mockReturnValue({
|
|
217
|
+
can: true,
|
|
218
|
+
isLoading: false,
|
|
219
|
+
error: null
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
render(
|
|
223
|
+
<MemoryRouter initialEntries={['/dashboard']}>
|
|
224
|
+
<RoleBasedRouter routes={mockRoutes}>
|
|
225
|
+
<TestComponent>App Content</TestComponent>
|
|
226
|
+
</RoleBasedRouter>
|
|
227
|
+
</MemoryRouter>
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
await waitFor(() => {
|
|
231
|
+
expect(screen.getByTestId('test-component')).toBeInTheDocument();
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
expect(mockUseCan).toHaveBeenCalledWith(
|
|
235
|
+
'user-123',
|
|
236
|
+
expect.objectContaining({
|
|
237
|
+
organisationId: 'org-123',
|
|
238
|
+
eventId: 'event-123',
|
|
239
|
+
appId: undefined
|
|
240
|
+
}),
|
|
241
|
+
'read:dashboard',
|
|
242
|
+
'dashboard'
|
|
243
|
+
);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('denies access to routes without permissions', async () => {
|
|
247
|
+
mockUseCan.mockReturnValue({
|
|
248
|
+
can: false,
|
|
249
|
+
isLoading: false,
|
|
250
|
+
error: null
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
render(
|
|
254
|
+
<MemoryRouter initialEntries={['/public']}>
|
|
255
|
+
<RoleBasedRouter routes={mockRoutes}>
|
|
256
|
+
<TestComponent>App Content</TestComponent>
|
|
257
|
+
</RoleBasedRouter>
|
|
258
|
+
</MemoryRouter>
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
await waitFor(() => {
|
|
262
|
+
expect(screen.getByText('Access Denied')).toBeInTheDocument();
|
|
263
|
+
expect(screen.getByText('You don\'t have permission to access')).toBeInTheDocument();
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('handles routes not found in configuration', async () => {
|
|
268
|
+
mockUseLocation.mockReturnValue({
|
|
269
|
+
pathname: '/unknown',
|
|
270
|
+
search: '',
|
|
271
|
+
hash: '',
|
|
272
|
+
state: null,
|
|
273
|
+
key: 'test'
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
277
|
+
|
|
278
|
+
render(
|
|
279
|
+
<MemoryRouter initialEntries={['/unknown']}>
|
|
280
|
+
<RoleBasedRouter routes={mockRoutes} strictMode={true}>
|
|
281
|
+
<TestComponent>App Content</TestComponent>
|
|
282
|
+
</RoleBasedRouter>
|
|
283
|
+
</MemoryRouter>
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
await waitFor(() => {
|
|
287
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
288
|
+
expect.stringContaining('STRICT MODE VIOLATION'),
|
|
289
|
+
expect.objectContaining({
|
|
290
|
+
route: '/unknown',
|
|
291
|
+
userId: 'user-123'
|
|
292
|
+
})
|
|
293
|
+
);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
consoleSpy.mockRestore();
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('redirects to fallback route when unauthorized', async () => {
|
|
300
|
+
const mockNavigate = vi.fn();
|
|
301
|
+
mockUseNavigate.mockReturnValue(mockNavigate);
|
|
302
|
+
|
|
303
|
+
mockUseCan.mockReturnValue({
|
|
304
|
+
can: false,
|
|
305
|
+
isLoading: false,
|
|
306
|
+
error: null
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
render(
|
|
310
|
+
<MemoryRouter initialEntries={['/admin']}>
|
|
311
|
+
<RoleBasedRouter
|
|
312
|
+
routes={mockRoutes}
|
|
313
|
+
fallbackRoute="/unauthorized"
|
|
314
|
+
>
|
|
315
|
+
<TestComponent>App Content</TestComponent>
|
|
316
|
+
</RoleBasedRouter>
|
|
317
|
+
</MemoryRouter>
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
await waitFor(() => {
|
|
321
|
+
expect(mockNavigate).toHaveBeenCalledWith('/unauthorized', { replace: true });
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
describe('Context Provider', () => {
|
|
327
|
+
it('provides router context to children', async () => {
|
|
328
|
+
const TestConsumer = () => {
|
|
329
|
+
const context = useRoleBasedRouter();
|
|
330
|
+
return (
|
|
331
|
+
<div data-testid="context-consumer">
|
|
332
|
+
<div data-testid="strict-mode">{context.isStrictMode.toString()}</div>
|
|
333
|
+
<div data-testid="audit-log">{context.isAuditLogEnabled.toString()}</div>
|
|
334
|
+
</div>
|
|
335
|
+
);
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
mockUseCan.mockReturnValue({
|
|
339
|
+
can: true,
|
|
340
|
+
isLoading: false,
|
|
341
|
+
error: null
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
render(
|
|
345
|
+
<MemoryRouter initialEntries={['/dashboard']}>
|
|
346
|
+
<RoleBasedRouter routes={mockRoutes} strictMode={true} auditLog={false}>
|
|
347
|
+
<TestConsumer />
|
|
348
|
+
</RoleBasedRouter>
|
|
349
|
+
</MemoryRouter>
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
await waitFor(() => {
|
|
353
|
+
expect(screen.getByTestId('context-consumer')).toBeInTheDocument();
|
|
354
|
+
expect(screen.getByTestId('strict-mode')).toHaveTextContent('true');
|
|
355
|
+
expect(screen.getByTestId('audit-log')).toHaveTextContent('false');
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('throws error when useRoleBasedRouter is used outside provider', () => {
|
|
360
|
+
const TestConsumer = () => {
|
|
361
|
+
useRoleBasedRouter();
|
|
362
|
+
return <div>Should not render</div>;
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
expect(() => {
|
|
366
|
+
render(<TestConsumer />);
|
|
367
|
+
}).toThrow('useRoleBasedRouter must be used within a RoleBasedRouter');
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
describe('Route Access Management', () => {
|
|
372
|
+
it('records route access attempts', async () => {
|
|
373
|
+
const onRouteAccessSpy = vi.fn();
|
|
374
|
+
|
|
375
|
+
mockUseCan.mockReturnValue({
|
|
376
|
+
can: true,
|
|
377
|
+
isLoading: false,
|
|
378
|
+
error: null
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
render(
|
|
382
|
+
<MemoryRouter initialEntries={['/dashboard']}>
|
|
383
|
+
<RoleBasedRouter
|
|
384
|
+
routes={mockRoutes}
|
|
385
|
+
onRouteAccess={onRouteAccessSpy}
|
|
386
|
+
auditLog={true}
|
|
387
|
+
>
|
|
388
|
+
<TestComponent>App Content</TestComponent>
|
|
389
|
+
</RoleBasedRouter>
|
|
390
|
+
</MemoryRouter>
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
await waitFor(() => {
|
|
394
|
+
expect(screen.getByTestId('test-component')).toBeInTheDocument();
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
expect(onRouteAccessSpy).toHaveBeenCalledWith(
|
|
398
|
+
'/dashboard',
|
|
399
|
+
true,
|
|
400
|
+
expect.objectContaining({
|
|
401
|
+
route: '/dashboard',
|
|
402
|
+
permissions: ['read:dashboard'],
|
|
403
|
+
userId: 'user-123',
|
|
404
|
+
allowed: true
|
|
405
|
+
})
|
|
406
|
+
);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it('handles strict mode violations', async () => {
|
|
410
|
+
const onStrictModeViolationSpy = vi.fn();
|
|
411
|
+
|
|
412
|
+
mockUseCan.mockReturnValue({
|
|
413
|
+
can: false,
|
|
414
|
+
isLoading: false,
|
|
415
|
+
error: null
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
render(
|
|
419
|
+
<MemoryRouter initialEntries={['/admin']}>
|
|
420
|
+
<RoleBasedRouter
|
|
421
|
+
routes={mockRoutes}
|
|
422
|
+
onStrictModeViolation={onStrictModeViolationSpy}
|
|
423
|
+
strictMode={true}
|
|
424
|
+
>
|
|
425
|
+
<TestComponent>App Content</TestComponent>
|
|
426
|
+
</RoleBasedRouter>
|
|
427
|
+
</MemoryRouter>
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
await waitFor(() => {
|
|
431
|
+
expect(onStrictModeViolationSpy).toHaveBeenCalledWith(
|
|
432
|
+
'/dashboard',
|
|
433
|
+
expect.objectContaining({
|
|
434
|
+
route: '/dashboard',
|
|
435
|
+
permissions: ['read:dashboard'],
|
|
436
|
+
userId: 'user-123',
|
|
437
|
+
allowed: false
|
|
438
|
+
})
|
|
439
|
+
);
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it('manages route access history', async () => {
|
|
444
|
+
mockUseCan.mockReturnValue({
|
|
445
|
+
can: true,
|
|
446
|
+
isLoading: false,
|
|
447
|
+
error: null
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
const TestHistoryConsumer = () => {
|
|
451
|
+
const context = useRoleBasedRouter();
|
|
452
|
+
const history = context.getRouteAccessHistory();
|
|
453
|
+
return (
|
|
454
|
+
<div data-testid="history-length">{history.length}</div>
|
|
455
|
+
);
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
render(
|
|
459
|
+
<MemoryRouter initialEntries={['/dashboard']}>
|
|
460
|
+
<RoleBasedRouter routes={mockRoutes}>
|
|
461
|
+
<TestHistoryConsumer />
|
|
462
|
+
</RoleBasedRouter>
|
|
463
|
+
</MemoryRouter>
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
await waitFor(() => {
|
|
467
|
+
expect(screen.getByTestId('history-length')).toHaveTextContent('1');
|
|
468
|
+
});
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
it('provides accessible routes', async () => {
|
|
472
|
+
mockUseCan.mockReturnValue({
|
|
473
|
+
can: true,
|
|
474
|
+
isLoading: false,
|
|
475
|
+
error: null
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
const TestAccessibleConsumer = () => {
|
|
479
|
+
const context = useRoleBasedRouter();
|
|
480
|
+
const accessibleRoutes = context.getAccessibleRoutes();
|
|
481
|
+
return (
|
|
482
|
+
<div data-testid="accessible-routes">{accessibleRoutes.length}</div>
|
|
483
|
+
);
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
render(
|
|
487
|
+
<MemoryRouter initialEntries={['/dashboard']}>
|
|
488
|
+
<RoleBasedRouter routes={mockRoutes}>
|
|
489
|
+
<TestAccessibleConsumer />
|
|
490
|
+
</RoleBasedRouter>
|
|
491
|
+
</MemoryRouter>
|
|
492
|
+
);
|
|
493
|
+
|
|
494
|
+
await waitFor(() => {
|
|
495
|
+
expect(screen.getByTestId('accessible-routes')).toHaveTextContent('3');
|
|
496
|
+
});
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it('provides route configuration', async () => {
|
|
500
|
+
mockUseCan.mockReturnValue({
|
|
501
|
+
can: true,
|
|
502
|
+
isLoading: false,
|
|
503
|
+
error: null
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
const TestConfigConsumer = () => {
|
|
507
|
+
const context = useRoleBasedRouter();
|
|
508
|
+
const routeConfig = context.getRouteConfig('/dashboard');
|
|
509
|
+
return (
|
|
510
|
+
<div data-testid="route-config">{routeConfig?.path}</div>
|
|
511
|
+
);
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
render(
|
|
515
|
+
<MemoryRouter initialEntries={['/dashboard']}>
|
|
516
|
+
<RoleBasedRouter routes={mockRoutes}>
|
|
517
|
+
<TestConfigConsumer />
|
|
518
|
+
</RoleBasedRouter>
|
|
519
|
+
</MemoryRouter>
|
|
520
|
+
);
|
|
521
|
+
|
|
522
|
+
await waitFor(() => {
|
|
523
|
+
expect(screen.getByTestId('route-config')).toHaveTextContent('/dashboard');
|
|
524
|
+
});
|
|
525
|
+
});
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
describe('Configuration Options', () => {
|
|
529
|
+
it('respects strictMode setting', async () => {
|
|
530
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
531
|
+
|
|
532
|
+
mockUseCan.mockReturnValue({
|
|
533
|
+
can: false,
|
|
534
|
+
isLoading: false,
|
|
535
|
+
error: null
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
render(
|
|
539
|
+
<MemoryRouter initialEntries={['/admin']}>
|
|
540
|
+
<RoleBasedRouter routes={mockRoutes} strictMode={false}>
|
|
541
|
+
<TestComponent>App Content</TestComponent>
|
|
542
|
+
</RoleBasedRouter>
|
|
543
|
+
</MemoryRouter>
|
|
544
|
+
);
|
|
545
|
+
|
|
546
|
+
await waitFor(() => {
|
|
547
|
+
expect(screen.getByText('Access Denied')).toBeInTheDocument();
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
expect(consoleSpy).not.toHaveBeenCalledWith(
|
|
551
|
+
expect.stringContaining('STRICT MODE VIOLATION')
|
|
552
|
+
);
|
|
553
|
+
|
|
554
|
+
consoleSpy.mockRestore();
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
it('respects auditLog setting', async () => {
|
|
558
|
+
const onRouteAccessSpy = vi.fn();
|
|
559
|
+
|
|
560
|
+
mockUseCan.mockReturnValue({
|
|
561
|
+
can: true,
|
|
562
|
+
isLoading: false,
|
|
563
|
+
error: null
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
render(
|
|
567
|
+
<MemoryRouter initialEntries={['/dashboard']}>
|
|
568
|
+
<RoleBasedRouter
|
|
569
|
+
routes={mockRoutes}
|
|
570
|
+
onRouteAccess={onRouteAccessSpy}
|
|
571
|
+
auditLog={false}
|
|
572
|
+
>
|
|
573
|
+
<TestComponent>App Content</TestComponent>
|
|
574
|
+
</RoleBasedRouter>
|
|
575
|
+
</MemoryRouter>
|
|
576
|
+
);
|
|
577
|
+
|
|
578
|
+
await waitFor(() => {
|
|
579
|
+
expect(screen.getByTestId('test-component')).toBeInTheDocument();
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
expect(onRouteAccessSpy).not.toHaveBeenCalled();
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
it('respects maxHistorySize setting', async () => {
|
|
586
|
+
mockUseCan.mockReturnValue({
|
|
587
|
+
can: true,
|
|
588
|
+
isLoading: false,
|
|
589
|
+
error: null
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
const TestHistoryConsumer = () => {
|
|
593
|
+
const context = useRoleBasedRouter();
|
|
594
|
+
const history = context.getRouteAccessHistory();
|
|
595
|
+
return (
|
|
596
|
+
<div data-testid="history-length">{history.length}</div>
|
|
597
|
+
);
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
render(
|
|
601
|
+
<MemoryRouter initialEntries={['/dashboard']}>
|
|
602
|
+
<RoleBasedRouter routes={mockRoutes} maxHistorySize={5}>
|
|
603
|
+
<TestHistoryConsumer />
|
|
604
|
+
</RoleBasedRouter>
|
|
605
|
+
</MemoryRouter>
|
|
606
|
+
);
|
|
607
|
+
|
|
608
|
+
await waitFor(() => {
|
|
609
|
+
expect(screen.getByTestId('history-length')).toHaveTextContent('1');
|
|
610
|
+
});
|
|
611
|
+
});
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
describe('Error Handling', () => {
|
|
615
|
+
it('handles missing user gracefully', async () => {
|
|
616
|
+
mockUseUnifiedAuth.mockReturnValue({
|
|
617
|
+
user: null,
|
|
618
|
+
selectedOrganisationId: 'org-123',
|
|
619
|
+
selectedEventId: 'event-123'
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
mockUseCan.mockReturnValue({
|
|
623
|
+
can: false,
|
|
624
|
+
isLoading: false,
|
|
625
|
+
error: null
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
render(
|
|
629
|
+
<MemoryRouter initialEntries={['/dashboard']}>
|
|
630
|
+
<RoleBasedRouter routes={mockRoutes}>
|
|
631
|
+
<TestComponent>App Content</TestComponent>
|
|
632
|
+
</RoleBasedRouter>
|
|
633
|
+
</MemoryRouter>
|
|
634
|
+
);
|
|
635
|
+
|
|
636
|
+
await waitFor(() => {
|
|
637
|
+
expect(screen.getByText('Access Denied')).toBeInTheDocument();
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
expect(mockUseCan).toHaveBeenCalledWith(
|
|
641
|
+
'',
|
|
642
|
+
expect.objectContaining({
|
|
643
|
+
organisationId: 'org-123',
|
|
644
|
+
eventId: 'event-123',
|
|
645
|
+
appId: undefined
|
|
646
|
+
}),
|
|
647
|
+
'read:dashboard',
|
|
648
|
+
'dashboard'
|
|
649
|
+
);
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
it('handles missing organisation context', async () => {
|
|
653
|
+
mockUseUnifiedAuth.mockReturnValue({
|
|
654
|
+
user: mockUser,
|
|
655
|
+
selectedOrganisationId: null,
|
|
656
|
+
selectedEventId: null
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
mockUseCan.mockReturnValue({
|
|
660
|
+
can: false,
|
|
661
|
+
isLoading: false,
|
|
662
|
+
error: null
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
render(
|
|
666
|
+
<MemoryRouter initialEntries={['/dashboard']}>
|
|
667
|
+
<RoleBasedRouter routes={mockRoutes}>
|
|
668
|
+
<TestComponent>App Content</TestComponent>
|
|
669
|
+
</RoleBasedRouter>
|
|
670
|
+
</MemoryRouter>
|
|
671
|
+
);
|
|
672
|
+
|
|
673
|
+
await waitFor(() => {
|
|
674
|
+
expect(screen.getByText('Access Denied')).toBeInTheDocument();
|
|
675
|
+
});
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
it('handles permission check errors', async () => {
|
|
679
|
+
const error = new Error('Permission check failed');
|
|
680
|
+
mockUseCan.mockReturnValue({
|
|
681
|
+
can: false,
|
|
682
|
+
isLoading: false,
|
|
683
|
+
error
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
render(
|
|
687
|
+
<MemoryRouter initialEntries={['/dashboard']}>
|
|
688
|
+
<RoleBasedRouter routes={mockRoutes}>
|
|
689
|
+
<TestComponent>App Content</TestComponent>
|
|
690
|
+
</RoleBasedRouter>
|
|
691
|
+
</MemoryRouter>
|
|
692
|
+
);
|
|
693
|
+
|
|
694
|
+
await waitFor(() => {
|
|
695
|
+
expect(screen.getByText('Access Denied')).toBeInTheDocument();
|
|
696
|
+
});
|
|
697
|
+
});
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
describe('Route Changes', () => {
|
|
701
|
+
it('handles route changes correctly', async () => {
|
|
702
|
+
const { rerender } = render(
|
|
703
|
+
<MemoryRouter initialEntries={['/dashboard']}>
|
|
704
|
+
<RoleBasedRouter routes={mockRoutes}>
|
|
705
|
+
<TestComponent>App Content</TestComponent>
|
|
706
|
+
</RoleBasedRouter>
|
|
707
|
+
</MemoryRouter>
|
|
708
|
+
);
|
|
709
|
+
|
|
710
|
+
// Change route
|
|
711
|
+
mockUseLocation.mockReturnValue({
|
|
712
|
+
pathname: '/admin',
|
|
713
|
+
search: '',
|
|
714
|
+
hash: '',
|
|
715
|
+
state: null,
|
|
716
|
+
key: 'test2'
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
rerender(
|
|
720
|
+
<MemoryRouter initialEntries={['/admin']}>
|
|
721
|
+
<RoleBasedRouter routes={mockRoutes}>
|
|
722
|
+
<TestComponent>App Content</TestComponent>
|
|
723
|
+
</RoleBasedRouter>
|
|
724
|
+
</MemoryRouter>
|
|
725
|
+
);
|
|
726
|
+
|
|
727
|
+
await waitFor(() => {
|
|
728
|
+
expect(mockUseCan).toHaveBeenCalledWith(
|
|
729
|
+
'user-123',
|
|
730
|
+
expect.objectContaining({
|
|
731
|
+
organisationId: 'org-123',
|
|
732
|
+
eventId: 'event-123',
|
|
733
|
+
appId: undefined
|
|
734
|
+
}),
|
|
735
|
+
'admin:system',
|
|
736
|
+
'admin'
|
|
737
|
+
);
|
|
738
|
+
});
|
|
739
|
+
});
|
|
740
|
+
});
|
|
741
|
+
});
|