Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
Expand Down Expand Up @@ -462,6 +463,12 @@ export const Routes = [
path: 'compliance/call-queues/:queue/:userDataId',
element: withSuspense(<ComplianceCallQueueDetailScreen />),
},
// 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(<ComplianceNotFoundScreen />),
},
{
path: 'sitemap',
element: withSuspense(<SitemapScreen />),
Expand Down
15 changes: 14 additions & 1 deletion src/__tests__/compliance-chargeback-list.screen.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 }),
Expand All @@ -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', () => ({
Expand Down Expand Up @@ -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(<ComplianceChargebackListScreen />);

expect(mockUseComplianceGuard).toHaveBeenCalledWith();
});

it('does not fetch when not logged in', () => {
mockIsLoggedIn = false;
render(<ComplianceChargebackListScreen />);
Expand Down
47 changes: 47 additions & 0 deletions src/__tests__/compliance-not-found.screen.test.tsx
Original file line number Diff line number Diff line change
@@ -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 }) => <div>{children}</div>,
}));

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(<ComplianceNotFoundScreen />);

expect(mockUseComplianceGuard).toHaveBeenCalledWith();
});

it('names the missing page and shows the path that was requested', () => {
render(<ComplianceNotFoundScreen />);

expect(screen.getByText('This compliance page does not exist')).toBeInTheDocument();
expect(screen.getByText('/compliance/does-not-exist')).toBeInTheDocument();
});
});
24 changes: 24 additions & 0 deletions src/screens/compliance-not-found.screen.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<StyledVerticalStack gap={4} full center>
<h2 className="text-dfxBlue-800">{translate('screens/compliance', 'This compliance page does not exist')}</h2>
<p className="text-dfxGray-700 text-sm">{pathname}</p>
</StyledVerticalStack>
);
}
Loading