diff --git a/src/App.tsx b/src/App.tsx index 086f5bb30..e984cf6ae 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -76,6 +76,7 @@ const ComplianceReviewScreen = lazy(() => import('./screens/compliance-review.sc const ComplianceCallQueuesScreen = lazy(() => import('./screens/compliance-call-queues.screen')); const ComplianceCallQueueScreen = lazy(() => import('./screens/compliance-call-queue.screen')); const ComplianceCallQueueDetailScreen = lazy(() => import('./screens/compliance-call-queue-detail.screen')); +const ComplianceNotFoundScreen = lazy(() => import('./screens/compliance-not-found.screen')); const SupportDashboardOverviewScreen = lazy(() => import('./screens/support-dashboard-overview.screen')); const SupportDashboardScreen = lazy(() => import('./screens/support-dashboard.screen')); const SupportDashboardIssueScreen = lazy(() => import('./screens/support-dashboard-issue.screen')); @@ -462,6 +463,12 @@ export const Routes = [ path: 'compliance/call-queues/:queue/:userDataId', element: withSuspense(), }, + // Must stay last among the compliance routes: React Router ranks static segments above a splat, + // so this only matches paths none of the screens above claim. + { + path: 'compliance/*', + element: withSuspense(), + }, { path: 'sitemap', element: withSuspense(), diff --git a/src/__tests__/compliance-chargeback-list.screen.test.tsx b/src/__tests__/compliance-chargeback-list.screen.test.tsx index 16435eebc..86ba778d7 100644 --- a/src/__tests__/compliance-chargeback-list.screen.test.tsx +++ b/src/__tests__/compliance-chargeback-list.screen.test.tsx @@ -5,6 +5,7 @@ let mockIsLoggedIn = true; const mockGetPendingChargebacks = jest.fn(); const mockNavigate = jest.fn(); +const mockUseComplianceGuard = jest.fn(); jest.mock('@dfx.swiss/react', () => ({ useSessionContext: () => ({ isLoggedIn: mockIsLoggedIn }), @@ -23,7 +24,7 @@ jest.mock('src/components/error-hint', () => ({ })); jest.mock('src/hooks/guard.hook', () => ({ - useComplianceGuard: () => undefined, + useComplianceGuard: (...args: unknown[]) => mockUseComplianceGuard(...args), })); jest.mock('src/hooks/layout-config.hook', () => ({ @@ -104,6 +105,18 @@ describe('ComplianceChargebackListScreen', () => { mockGetPendingChargebacks.mockResolvedValue(asTransport([])); }); + // The role guard is all that stands between an unauthorised role and this screen, and it is mocked + // out in every other test here — so without this assertion the screen could lose the call and the + // whole suite would stay green (issue #1306 suspected exactly that). Asserting the argument list is + // empty covers the quieter mutation too: the hook's signature is + // `useComplianceGuard(redirectPath = '/', isActive = true)`, so `useComplianceGuard(undefined, false)` + // would keep the call and still leave the guard inert. + it('calls the compliance guard on render, with the guard left active', () => { + render(); + + expect(mockUseComplianceGuard).toHaveBeenCalledWith(); + }); + it('does not fetch when not logged in', () => { mockIsLoggedIn = false; render(); diff --git a/src/__tests__/compliance-not-found.screen.test.tsx b/src/__tests__/compliance-not-found.screen.test.tsx new file mode 100644 index 000000000..041d456f0 --- /dev/null +++ b/src/__tests__/compliance-not-found.screen.test.tsx @@ -0,0 +1,47 @@ +// Unit tests for ComplianceNotFoundScreen: the guarded catch-all for unknown /compliance paths. + +const mockUseComplianceGuard = jest.fn(); + +jest.mock('@dfx.swiss/react-components', () => ({ + StyledVerticalStack: ({ children }: { children: React.ReactNode }) =>
{children}
, +})); + +jest.mock('react-router-dom', () => ({ + useLocation: () => ({ pathname: '/compliance/does-not-exist' }), +})); + +jest.mock('src/contexts/settings.context', () => ({ + useSettingsContext: () => ({ translate: (_ns: string, key: string) => key }), +})); + +jest.mock('src/hooks/guard.hook', () => ({ + useComplianceGuard: (...args: unknown[]) => mockUseComplianceGuard(...args), +})); + +jest.mock('src/hooks/layout-config.hook', () => ({ + useLayoutOptions: () => undefined, +})); + +import { render, screen } from '@testing-library/react'; +import ComplianceNotFoundScreen from 'src/screens/compliance-not-found.screen'; + +describe('ComplianceNotFoundScreen', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + // The whole point of this screen: an unknown compliance path must still run the role guard, so an + // unauthorised role is sent away instead of parked on a /compliance URL (issue #1306). + it('calls the compliance guard on render, with the guard left active', () => { + render(); + + expect(mockUseComplianceGuard).toHaveBeenCalledWith(); + }); + + it('names the missing page and shows the path that was requested', () => { + render(); + + expect(screen.getByText('This compliance page does not exist')).toBeInTheDocument(); + expect(screen.getByText('/compliance/does-not-exist')).toBeInTheDocument(); + }); +}); diff --git a/src/screens/compliance-not-found.screen.tsx b/src/screens/compliance-not-found.screen.tsx new file mode 100644 index 000000000..3c5c83943 --- /dev/null +++ b/src/screens/compliance-not-found.screen.tsx @@ -0,0 +1,24 @@ +import { StyledVerticalStack } from '@dfx.swiss/react-components'; +import { useLocation } from 'react-router-dom'; +import { useSettingsContext } from 'src/contexts/settings.context'; +import { useComplianceGuard } from 'src/hooks/guard.hook'; +import { useLayoutOptions } from 'src/hooks/layout-config.hook'; + +// Catch-all for every unknown path below /compliance. Without it such a URL matches no route at all, +// so the router falls back to its errorElement — a screen that runs outside every guard and leaves the +// address bar untouched, which parks an unauthorised role on a /compliance URL (issue #1306). Guarding +// the catch-all makes the role check independent of whether a given compliance screen exists. +export default function ComplianceNotFoundScreen(): JSX.Element { + useComplianceGuard(); + useLayoutOptions({ title: 'Not found', backButton: true }); + + const { translate } = useSettingsContext(); + const { pathname } = useLocation(); + + return ( + +

{translate('screens/compliance', 'This compliance page does not exist')}

+

{pathname}

+
+ ); +}