From 4f1eb9281e3fae182db653d2ed8335c30a995de3 Mon Sep 17 00:00:00 2001 From: Rio Knightley <128376976+RioKnightleyNHS@users.noreply.github.com> Date: Thu, 9 Nov 2023 13:45:27 +0000 Subject: [PATCH] PRMDR-436 UI RBACS Test Coverage (#141) Add RBACS test coverage for role based components --- app/src/App.tsx | 55 ++-- .../DeleteDocumentsStage.test.tsx | 256 ++++++++---------- .../DeleteDocumentsStage.tsx | 12 +- .../DeletionConfirmationStage.test.tsx | 179 ++++++++---- .../DeletionConfirmationStage.tsx | 14 +- .../LloydGeorgeDownloadAllStage.test.tsx | 22 +- app/src/helpers/hooks/useRole.test.tsx | 45 +++ app/src/helpers/hooks/useRole.tsx | 10 + .../requests/deleteAllDocuments.test.ts | 68 +++++ app/src/helpers/requests/getAuthToken.test.ts | 84 ++++++ .../requests/getDocumentSearchResults.test.ts | 86 ++++++ ...Results.ts => getDocumentSearchResults.ts} | 8 +- .../DocumentSearchResultsPage.tsx | 4 +- .../LloydGeorgeRecordPage.tsx | 2 - .../PatientResultPage.test.tsx | 256 ++++++++++-------- .../patientResultPage/PatientResultPage.tsx | 8 +- .../PatientSearchPage.test.tsx | 141 +++++----- .../patientSearchPage/PatientSearchPage.tsx | 8 +- .../pages/roleSelectPage/RoleSelectPage.tsx | 3 + app/src/types/generic/authRole.ts | 2 + app/src/types/generic/routes.ts | 1 - 21 files changed, 801 insertions(+), 463 deletions(-) create mode 100644 app/src/helpers/hooks/useRole.test.tsx create mode 100644 app/src/helpers/hooks/useRole.tsx create mode 100644 app/src/helpers/requests/deleteAllDocuments.test.ts create mode 100644 app/src/helpers/requests/getAuthToken.test.ts create mode 100644 app/src/helpers/requests/getDocumentSearchResults.test.ts rename app/src/helpers/requests/{documentSearchResults.ts => getDocumentSearchResults.ts} (88%) diff --git a/app/src/App.tsx b/app/src/App.tsx index 8d43e1af5..d05aecca8 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -20,7 +20,6 @@ import UploadDocumentsPage from './pages/uploadDocumentsPage/UploadDocumentsPage import DocumentSearchResultsPage from './pages/documentSearchResultsPage/DocumentSearchResultsPage'; import AuthErrorPage from './pages/authErrorPage/AuthErrorPage'; import LloydGeorgeRecordPage from './pages/lloydGeorgeRecordPage/LloydGeorgeRecordPage'; -import { REPOSITORY_ROLE } from './types/generic/authRole'; function App() { return ( @@ -45,22 +44,15 @@ function App() { } > - } - path={routes.DOWNLOAD_SEARCH} - /> - - } - path={routes.UPLOAD_SEARCH} - /> - - } - path={routes.UPLOAD_SEARCH} - /> + {[routes.DOWNLOAD_SEARCH, routes.UPLOAD_SEARCH].map( + (searchRoute) => ( + } + path={searchRoute} + /> + ), + )} } path={routes.LOGOUT} /> } > - - } - path={routes.DOWNLOAD_VERIFY} - /> - ( + } + path={searchResultRoute} /> - } - path={routes.UPLOAD_VERIFY} - /> - - } - path={routes.UPLOAD_VERIFY} - /> + ), + )} } path={routes.LLOYD_GEORGE} diff --git a/app/src/components/blocks/deleteDocumentsStage/DeleteDocumentsStage.test.tsx b/app/src/components/blocks/deleteDocumentsStage/DeleteDocumentsStage.test.tsx index a8c079141..e85c8b3c0 100644 --- a/app/src/components/blocks/deleteDocumentsStage/DeleteDocumentsStage.test.tsx +++ b/app/src/components/blocks/deleteDocumentsStage/DeleteDocumentsStage.test.tsx @@ -1,10 +1,5 @@ import { render, screen, waitFor } from '@testing-library/react'; -import SessionProvider, { Session } from '../../../providers/sessionProvider/SessionProvider'; -import { - buildLgSearchResult, - buildPatientDetails, - buildUserAuth, -} from '../../../helpers/test/testBuilders'; +import { buildLgSearchResult, buildPatientDetails } from '../../../helpers/test/testBuilders'; import DeleteDocumentsStage, { Props } from './DeleteDocumentsStage'; import { getFormattedDate } from '../../../helpers/utils/formatDate'; import { act } from 'react-dom/test-utils'; @@ -12,21 +7,26 @@ import userEvent from '@testing-library/user-event'; import { LG_RECORD_STAGE } from '../../../pages/lloydGeorgeRecordPage/LloydGeorgeRecordPage'; import { DOCUMENT_TYPE } from '../../../types/pages/UploadDocumentsPage/types'; import axios from 'axios/index'; -import { USER_ROLE } from '../../../types/generic/roles'; import * as ReactRouter from 'react-router'; import { createMemoryHistory } from 'history'; +import useRole from '../../../helpers/hooks/useRole'; +import { REPOSITORY_ROLE, authorisedRoles } from '../../../types/generic/authRole'; import { routes } from '../../../types/generic/routes'; +jest.mock('../../../helpers/hooks/useBaseAPIHeaders'); +jest.mock('../../../helpers/hooks/useRole'); jest.mock('axios'); +const mockedUseRole = useRole as jest.Mock; +const mockedAxios = axios as jest.Mocked; const mockPatientDetails = buildPatientDetails(); const mockLgSearchResult = buildLgSearchResult(); + const mockSetStage = jest.fn(); const mockSetIsDeletingDocuments = jest.fn(); const mockSetDownloadStage = jest.fn(); -const mockedAxios = axios as jest.Mocked; -describe('DeleteAllDocumentsStage', () => { +describe('DeleteDocumentsStage', () => { beforeEach(() => { process.env.REACT_APP_ENVIRONMENT = 'jest'; }); @@ -35,82 +35,53 @@ describe('DeleteAllDocumentsStage', () => { jest.clearAllMocks(); }); - describe('GP USER', () => { - it('renders the page with patient details', async () => { - const patientName = `${mockPatientDetails.givenName} ${mockPatientDetails.familyName}`; - const dob = getFormattedDate(new Date(mockPatientDetails.birthDate)); - - renderComponent(USER_ROLE.GP, DOCUMENT_TYPE.LLOYD_GEORGE); - - await waitFor(async () => { - expect( - screen.getByText('Are you sure you want to permanently delete files for:'), - ).toBeInTheDocument(); - }); - - expect(screen.getByText(patientName)).toBeInTheDocument(); - expect(screen.getByText(`Date of birth: ${dob}`)).toBeInTheDocument(); - expect(screen.getByText(/NHS number/)).toBeInTheDocument(); - expect(screen.getByRole('radio', { name: 'Yes' })).toBeInTheDocument(); - expect(screen.getByRole('radio', { name: 'No' })).toBeInTheDocument(); - expect(screen.getByRole('button', { name: 'Continue' })).toBeInTheDocument(); - }); - - it('renders LgRecordStage when the No radio button is selected and Continue button clicked', async () => { - renderComponent(USER_ROLE.GP, DOCUMENT_TYPE.LLOYD_GEORGE); - - act(() => { - userEvent.click(screen.getByRole('radio', { name: 'No' })); - userEvent.click(screen.getByRole('button', { name: 'Continue' })); - }); - - await waitFor(() => { - expect(mockSetStage).toHaveBeenCalledWith(LG_RECORD_STAGE.RECORD); - }); - }); - - it('renders DeletionConfirmationStage when the Yes radio button is selected and Continue button clicked', async () => { - mockedAxios.delete.mockReturnValue(Promise.resolve({ status: 200, data: 'Success' })); - - renderComponent(USER_ROLE.GP, DOCUMENT_TYPE.LLOYD_GEORGE); - - expect(screen.getByRole('radio', { name: 'Yes' })).toBeInTheDocument(); - expect(screen.getByRole('button', { name: 'Continue' })).toBeInTheDocument(); - - act(() => { - userEvent.click(screen.getByRole('radio', { name: 'Yes' })); - userEvent.click(screen.getByRole('button', { name: 'Continue' })); - }); - - await waitFor(() => { - expect(screen.getByText('Deletion complete')).toBeInTheDocument(); - }); - }); - }); - - describe('PCSE USER', () => { - it('renders the page with patient details', async () => { - const patientName = `${mockPatientDetails.givenName} ${mockPatientDetails.familyName}`; - const dob = getFormattedDate(new Date(mockPatientDetails.birthDate)); - - renderComponent(USER_ROLE.PCSE, DOCUMENT_TYPE.ALL); - - await waitFor(async () => { - expect( - screen.getByText('Are you sure you want to permanently delete files for:'), - ).toBeInTheDocument(); - }); - - expect(screen.getByText(patientName)).toBeInTheDocument(); - expect(screen.getByText(`Date of birth: ${dob}`)).toBeInTheDocument(); - expect(screen.getByText(/NHS number/)).toBeInTheDocument(); - expect(screen.getByRole('radio', { name: 'Yes' })).toBeInTheDocument(); - expect(screen.getByRole('radio', { name: 'No' })).toBeInTheDocument(); - expect(screen.getByRole('button', { name: 'Continue' })).toBeInTheDocument(); - }); - - it('renders Document search results when the No radio button is selected and Continue button clicked', async () => { - renderComponent(USER_ROLE.PCSE, DOCUMENT_TYPE.ALL); + describe('Render', () => { + it.each(authorisedRoles)( + "renders the page with patient details when user role is '%s'", + async (role) => { + const patientName = `${mockPatientDetails.givenName} ${mockPatientDetails.familyName}`; + const dob = getFormattedDate(new Date(mockPatientDetails.birthDate)); + mockedUseRole.mockReturnValue(role); + + renderComponent(DOCUMENT_TYPE.LLOYD_GEORGE); + + await waitFor(async () => { + expect( + screen.getByText('Are you sure you want to permanently delete files for:'), + ).toBeInTheDocument(); + }); + + expect(screen.getByText(patientName)).toBeInTheDocument(); + expect(screen.getByText(`Date of birth: ${dob}`)).toBeInTheDocument(); + expect(screen.getByText(/NHS number/)).toBeInTheDocument(); + expect(screen.getByRole('radio', { name: 'Yes' })).toBeInTheDocument(); + expect(screen.getByRole('radio', { name: 'No' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Continue' })).toBeInTheDocument(); + }, + ); + + it.each([REPOSITORY_ROLE.GP_ADMIN, REPOSITORY_ROLE.GP_CLINICAL])( + "renders LgRecordStage when No is selected and Continue clicked, when user role is '%s'", + async (role) => { + mockedUseRole.mockReturnValue(role); + + renderComponent(DOCUMENT_TYPE.LLOYD_GEORGE); + + act(() => { + userEvent.click(screen.getByRole('radio', { name: 'No' })); + userEvent.click(screen.getByRole('button', { name: 'Continue' })); + }); + + await waitFor(() => { + expect(mockSetStage).toHaveBeenCalledWith(LG_RECORD_STAGE.RECORD); + }); + }, + ); + + it('renders DocumentSearchResults when No is selected and Continue clicked, when user role is PCSE', async () => { + mockedUseRole.mockReturnValue(REPOSITORY_ROLE.PCSE); + + renderComponent(DOCUMENT_TYPE.ALL); act(() => { userEvent.click(screen.getByRole('radio', { name: 'No' })); @@ -122,23 +93,29 @@ describe('DeleteAllDocumentsStage', () => { }); }); - it('renders DeletionConfirmationStage when the Yes radio button is selected and Continue button clicked', async () => { - mockedAxios.delete.mockReturnValue(Promise.resolve({ status: 200, data: 'Success' })); + it.each([REPOSITORY_ROLE.GP_ADMIN, REPOSITORY_ROLE.GP_CLINICAL, REPOSITORY_ROLE.PCSE])( + "renders DeletionConfirmationStage when the Yes is selected and Continue clicked, when user role is '%s'", + async (role) => { + mockedAxios.delete.mockReturnValue( + Promise.resolve({ status: 200, data: 'Success' }), + ); + mockedUseRole.mockReturnValue(role); - renderComponent(USER_ROLE.PCSE, DOCUMENT_TYPE.ALL); + renderComponent(DOCUMENT_TYPE.LLOYD_GEORGE); - expect(screen.getByRole('radio', { name: 'Yes' })).toBeInTheDocument(); - expect(screen.getByRole('button', { name: 'Continue' })).toBeInTheDocument(); + expect(screen.getByRole('radio', { name: 'Yes' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Continue' })).toBeInTheDocument(); - act(() => { - userEvent.click(screen.getByRole('radio', { name: 'Yes' })); - userEvent.click(screen.getByRole('button', { name: 'Continue' })); - }); + act(() => { + userEvent.click(screen.getByRole('radio', { name: 'Yes' })); + userEvent.click(screen.getByRole('button', { name: 'Continue' })); + }); - await waitFor(() => { - expect(screen.getByText('Deletion complete')).toBeInTheDocument(); - }); - }); + await waitFor(() => { + expect(screen.getByText('Deletion complete')).toBeInTheDocument(); + }); + }, + ); it('renders a service error when service is down', async () => { const errorResponse = { @@ -148,8 +125,8 @@ describe('DeleteAllDocumentsStage', () => { }, }; mockedAxios.delete.mockImplementation(() => Promise.reject(errorResponse)); - - renderComponent(USER_ROLE.PCSE, DOCUMENT_TYPE.ALL); + mockedUseRole.mockReturnValue(REPOSITORY_ROLE.PCSE); + renderComponent(DOCUMENT_TYPE.ALL); expect(screen.getByRole('radio', { name: 'Yes' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Continue' })).toBeInTheDocument(); @@ -166,80 +143,63 @@ describe('DeleteAllDocumentsStage', () => { }); }); }); +}); - describe('Navigation', () => { - it('navigates to home page when API call returns 403', async () => { - const history = createMemoryHistory({ - initialEntries: ['/example'], - initialIndex: 1, - }); +describe('Navigation', () => { + it('navigates to home page when API call returns 403', async () => { + const history = createMemoryHistory({ + initialEntries: ['/example'], + initialIndex: 1, + }); - const errorResponse = { - response: { - status: 403, - message: 'Forbidden', - }, - }; - mockedAxios.delete.mockImplementation(() => Promise.reject(errorResponse)); + const errorResponse = { + response: { + status: 403, + message: 'Forbidden', + }, + }; + mockedAxios.delete.mockImplementation(() => Promise.reject(errorResponse)); + mockedUseRole.mockReturnValue(REPOSITORY_ROLE.PCSE); - renderComponent(USER_ROLE.PCSE, DOCUMENT_TYPE.ALL, history); + renderComponent(DOCUMENT_TYPE.ALL, history); - expect(history.location.pathname).toBe('/example'); + expect(history.location.pathname).toBe('/example'); - expect(screen.getByRole('radio', { name: 'Yes' })).toBeInTheDocument(); - expect(screen.getByRole('button', { name: 'Continue' })).toBeInTheDocument(); + expect(screen.getByRole('radio', { name: 'Yes' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Continue' })).toBeInTheDocument(); - act(() => { - userEvent.click(screen.getByRole('radio', { name: 'Yes' })); - userEvent.click(screen.getByRole('button', { name: 'Continue' })); - }); + act(() => { + userEvent.click(screen.getByRole('radio', { name: 'Yes' })); + userEvent.click(screen.getByRole('button', { name: 'Continue' })); + }); - await waitFor(() => { - expect(history.location.pathname).toBe(routes.HOME); - }); + await waitFor(() => { + expect(history.location.pathname).toBe(routes.HOME); }); }); }); -const TestApp = ( - props: Omit, -) => { - return ( - - ); -}; - const homeRoute = '/example'; const renderComponent = ( - userType: USER_ROLE, docType: DOCUMENT_TYPE, history = createMemoryHistory({ initialEntries: [homeRoute], - initialIndex: 1, }), ) => { - const auth: Session = { - auth: buildUserAuth(), - isLoggedIn: true, - }; - const props: Omit = { patientDetails: mockPatientDetails, numberOfFiles: mockLgSearchResult.number_of_files, - userType, docType, }; render( - - - + , ); }; diff --git a/app/src/components/blocks/deleteDocumentsStage/DeleteDocumentsStage.tsx b/app/src/components/blocks/deleteDocumentsStage/DeleteDocumentsStage.tsx index 0e74e09f0..332d79168 100644 --- a/app/src/components/blocks/deleteDocumentsStage/DeleteDocumentsStage.tsx +++ b/app/src/components/blocks/deleteDocumentsStage/DeleteDocumentsStage.tsx @@ -13,11 +13,12 @@ import { DOWNLOAD_STAGE } from '../../../types/generic/downloadStage'; import SpinnerButton from '../../generic/spinnerButton/SpinnerButton'; import ServiceError from '../../layout/serviceErrorBox/ServiceErrorBox'; import { SUBMISSION_STATE } from '../../../types/pages/documentSearchResultsPage/types'; -import { USER_ROLE } from '../../../types/generic/roles'; import { formatNhsNumber } from '../../../helpers/utils/formatNhsNumber'; import { AxiosError } from 'axios'; import { routes } from '../../../types/generic/routes'; import { useNavigate } from 'react-router-dom'; +import useRole from '../../../helpers/hooks/useRole'; +import { REPOSITORY_ROLE } from '../../../types/generic/authRole'; export type Props = { docType: DOCUMENT_TYPE; @@ -25,7 +26,6 @@ export type Props = { patientDetails: PatientDetails; setStage?: Dispatch>; setIsDeletingDocuments?: Dispatch>; - userType: USER_ROLE; setDownloadStage?: Dispatch>; }; @@ -40,9 +40,9 @@ function DeleteDocumentsStage({ patientDetails, setStage, setIsDeletingDocuments, - userType, setDownloadStage, }: Props) { + const role = useRole(); const { register, handleSubmit } = useForm(); const { ref: deleteDocsRef, ...radioProps } = register('deleteDocs'); const [deletionStage, setDeletionStage] = useState(SUBMISSION_STATE.INITIAL); @@ -96,9 +96,10 @@ function DeleteDocumentsStage({ }; const handleNoOption = () => { - if (userType === USER_ROLE.GP && setStage) { + const isGp = role === REPOSITORY_ROLE.GP_ADMIN || role === REPOSITORY_ROLE.GP_CLINICAL; + if (isGp && setStage) { setStage(LG_RECORD_STAGE.RECORD); - } else if (userType === USER_ROLE.PCSE && setIsDeletingDocuments) { + } else if (role === REPOSITORY_ROLE.PCSE && setIsDeletingDocuments) { setIsDeletingDocuments(false); } }; @@ -156,7 +157,6 @@ function DeleteDocumentsStage({ numberOfFiles={numberOfFiles} patientDetails={patientDetails} setStage={setStage} - userType={userType} /> ); } diff --git a/app/src/components/blocks/deletionConfirmationStage/DeletionConfirmationStage.test.tsx b/app/src/components/blocks/deletionConfirmationStage/DeletionConfirmationStage.test.tsx index f038bb45d..da6407666 100644 --- a/app/src/components/blocks/deletionConfirmationStage/DeletionConfirmationStage.test.tsx +++ b/app/src/components/blocks/deletionConfirmationStage/DeletionConfirmationStage.test.tsx @@ -1,18 +1,17 @@ import { render, screen, waitFor } from '@testing-library/react'; -import SessionProvider, { Session } from '../../../providers/sessionProvider/SessionProvider'; -import { - buildLgSearchResult, - buildPatientDetails, - buildUserAuth, -} from '../../../helpers/test/testBuilders'; +import { buildLgSearchResult, buildPatientDetails } from '../../../helpers/test/testBuilders'; import DeletionConfirmationStage from './DeletionConfirmationStage'; import { act } from 'react-dom/test-utils'; import userEvent from '@testing-library/user-event'; import { LG_RECORD_STAGE } from '../../../pages/lloydGeorgeRecordPage/LloydGeorgeRecordPage'; -import { USER_ROLE } from '../../../types/generic/roles'; import * as ReactRouter from 'react-router'; import { createMemoryHistory } from 'history'; import { routes } from '../../../types/generic/routes'; +import useRole from '../../../helpers/hooks/useRole'; +import { REPOSITORY_ROLE } from '../../../types/generic/authRole'; + +jest.mock('../../../helpers/hooks/useRole'); +const mockedUseRole = useRole as jest.Mock; const mockPatientDetails = buildPatientDetails(); const mockLgSearchResult = buildLgSearchResult(); @@ -22,73 +21,106 @@ describe('DeletionConfirmationStage', () => { beforeEach(() => { process.env.REACT_APP_ENVIRONMENT = 'jest'; }); - afterEach(() => { jest.clearAllMocks(); }); - describe('GP USER', () => { - it('renders the page with patient details', async () => { + describe('Rendering', () => { + it.each([REPOSITORY_ROLE.GP_ADMIN, REPOSITORY_ROLE.GP_CLINICAL])( + "renders the page with Lloyd George patient details when user role is '%s'", + async (role) => { + const patientName = `${mockPatientDetails.givenName} ${mockPatientDetails.familyName}`; + const numberOfFiles = mockLgSearchResult.number_of_files; + + mockedUseRole.mockReturnValue(role); + renderComponent(numberOfFiles); + + await waitFor(async () => { + expect(screen.getByText('Deletion complete')).toBeInTheDocument(); + }); + + expect( + screen.getByText(`${numberOfFiles} files from the Lloyd George record of:`), + ).toBeInTheDocument(); + expect(screen.getByText(patientName)).toBeInTheDocument(); + expect(screen.getByText(/NHS number/)).toBeInTheDocument(); + expect( + screen.getByRole('button', { + name: "Return to patient's Lloyd George record page", + }), + ).toBeInTheDocument(); + }, + ); + it('renders the page with ARF patient details, when user role is PCSE', async () => { const patientName = `${mockPatientDetails.givenName} ${mockPatientDetails.familyName}`; - const numberOfFiles = mockLgSearchResult.number_of_files; + const numberOfFiles = 1; - renderComponent(USER_ROLE.GP, numberOfFiles); + mockedUseRole.mockReturnValue(REPOSITORY_ROLE.PCSE); + renderComponent(numberOfFiles); await waitFor(async () => { expect(screen.getByText('Deletion complete')).toBeInTheDocument(); }); expect( - screen.getByText(`${numberOfFiles} files from the Lloyd George record of:`), + screen.getByText(`${numberOfFiles} file from the record of:`), ).toBeInTheDocument(); expect(screen.getByText(patientName)).toBeInTheDocument(); expect(screen.getByText(/NHS number/)).toBeInTheDocument(); expect( - screen.getByRole('button', { - name: "Return to patient's Lloyd George record page", + screen.getByRole('link', { + name: 'Start Again', }), ).toBeInTheDocument(); }); - it('sets stage back to LgRecordStage when button is clicked', async () => { - const numberOfFiles = mockLgSearchResult.number_of_files; + it.each([REPOSITORY_ROLE.GP_ADMIN, REPOSITORY_ROLE.GP_CLINICAL])( + "renders the return to Lloyd George Record button, when user role is '%s'", + async (role) => { + const numberOfFiles = mockLgSearchResult.number_of_files; + mockedUseRole.mockReturnValue(role); - renderComponent(USER_ROLE.GP, numberOfFiles); + renderComponent(numberOfFiles); - await waitFor(async () => { - expect(screen.getByText('Deletion complete')).toBeInTheDocument(); - }); + await waitFor(async () => { + expect(screen.getByText('Deletion complete')).toBeInTheDocument(); + }); - act(() => { - userEvent.click( + expect( screen.getByRole('button', { name: "Return to patient's Lloyd George record page", }), - ); - }); + ).toBeInTheDocument(); + }, + ); - await waitFor(() => { - expect(mockSetStage).toHaveBeenCalledWith(LG_RECORD_STAGE.RECORD); + it('does not render the return to Lloyd George Record button, when user role is PCSE', async () => { + const numberOfFiles = mockLgSearchResult.number_of_files; + mockedUseRole.mockReturnValue(REPOSITORY_ROLE.PCSE); + + renderComponent(numberOfFiles); + + await waitFor(async () => { + expect(screen.getByText('Deletion complete')).toBeInTheDocument(); }); + + expect( + screen.queryByRole('button', { + name: "Return to patient's Lloyd George record page", + }), + ).not.toBeInTheDocument(); }); - }); - describe('PCSE USER', () => { - it('renders the page with patient details', async () => { - const patientName = `${mockPatientDetails.givenName} ${mockPatientDetails.familyName}`; - const numberOfFiles = 1; + it('renders the Start Again button, when user role is PCSE', async () => { + const numberOfFiles = 7; + mockedUseRole.mockReturnValue(REPOSITORY_ROLE.PCSE); - renderComponent(USER_ROLE.PCSE, numberOfFiles); + renderComponent(numberOfFiles); await waitFor(async () => { expect(screen.getByText('Deletion complete')).toBeInTheDocument(); }); - expect( - screen.getByText(`${numberOfFiles} file from the record of:`), - ).toBeInTheDocument(); - expect(screen.getByText(patientName)).toBeInTheDocument(); - expect(screen.getByText(/NHS number/)).toBeInTheDocument(); expect( screen.getByRole('link', { name: 'Start Again', @@ -96,15 +128,64 @@ describe('DeletionConfirmationStage', () => { ).toBeInTheDocument(); }); - it('navigates to Home page when link is clicked', async () => { + it.each([REPOSITORY_ROLE.GP_ADMIN, REPOSITORY_ROLE.GP_CLINICAL])( + "does not render the Start Again button, when user role is '%s'", + async (role) => { + const numberOfFiles = 7; + mockedUseRole.mockReturnValue(role); + + renderComponent(numberOfFiles); + + await waitFor(async () => { + expect(screen.getByText('Deletion complete')).toBeInTheDocument(); + }); + + expect( + screen.queryByRole('link', { + name: 'Start Again', + }), + ).not.toBeInTheDocument(); + }, + ); + + it.each([REPOSITORY_ROLE.GP_ADMIN, REPOSITORY_ROLE.GP_CLINICAL])( + "displays the LgRecordStage when return button is clicked, when user role is '%s'", + async (role) => { + const numberOfFiles = mockLgSearchResult.number_of_files; + mockedUseRole.mockReturnValue(role); + + renderComponent(numberOfFiles); + + await waitFor(async () => { + expect(screen.getByText('Deletion complete')).toBeInTheDocument(); + }); + + act(() => { + userEvent.click( + screen.getByRole('button', { + name: "Return to patient's Lloyd George record page", + }), + ); + }); + + await waitFor(() => { + expect(mockSetStage).toHaveBeenCalledWith(LG_RECORD_STAGE.RECORD); + }); + }, + ); + }); + + describe('Navigation', () => { + it('navigates to Home page when link is clicked when user role is PCSE', async () => { const history = createMemoryHistory({ initialEntries: ['/'], initialIndex: 0, }); const numberOfFiles = 7; + mockedUseRole.mockReturnValue(REPOSITORY_ROLE.PCSE); - renderComponent(USER_ROLE.PCSE, numberOfFiles, history); + renderComponent(numberOfFiles, history); await waitFor(async () => { expect(screen.getByText('Deletion complete')).toBeInTheDocument(); @@ -126,27 +207,19 @@ describe('DeletionConfirmationStage', () => { }); const renderComponent = ( - userType: USER_ROLE, numberOfFiles: number, history = createMemoryHistory({ initialEntries: ['/'], initialIndex: 0, }), ) => { - const auth: Session = { - auth: buildUserAuth(), - isLoggedIn: true, - }; render( - - - + , ); }; diff --git a/app/src/components/blocks/deletionConfirmationStage/DeletionConfirmationStage.tsx b/app/src/components/blocks/deletionConfirmationStage/DeletionConfirmationStage.tsx index ad0104007..6f84b7992 100644 --- a/app/src/components/blocks/deletionConfirmationStage/DeletionConfirmationStage.tsx +++ b/app/src/components/blocks/deletionConfirmationStage/DeletionConfirmationStage.tsx @@ -2,30 +2,30 @@ import React, { Dispatch, SetStateAction } from 'react'; import { ButtonLink, Card } from 'nhsuk-react-components'; import { PatientDetails } from '../../../types/generic/patientDetails'; import { LG_RECORD_STAGE } from '../../../pages/lloydGeorgeRecordPage/LloydGeorgeRecordPage'; -import { USER_ROLE } from '../../../types/generic/roles'; import { routes } from '../../../types/generic/routes'; import { Link } from 'react-router-dom'; import { useNavigate } from 'react-router'; import { formatNhsNumber } from '../../../helpers/utils/formatNhsNumber'; +import useRole from '../../../helpers/hooks/useRole'; +import { REPOSITORY_ROLE } from '../../../types/generic/authRole'; export type Props = { numberOfFiles: number; patientDetails: PatientDetails; setStage?: Dispatch>; - userType: USER_ROLE; }; -function DeletionConfirmationStage({ numberOfFiles, patientDetails, setStage, userType }: Props) { +function DeletionConfirmationStage({ numberOfFiles, patientDetails, setStage }: Props) { const navigate = useNavigate(); const nhsNumber: string = patientDetails?.nhsNumber || ''; const formattedNhsNumber = formatNhsNumber(nhsNumber); - + const role = useRole(); const handleClick = () => { if (setStage) { setStage(LG_RECORD_STAGE.RECORD); } }; - + const isGP = role === REPOSITORY_ROLE.GP_ADMIN || role === REPOSITORY_ROLE.GP_CLINICAL; return (
@@ -33,7 +33,7 @@ function DeletionConfirmationStage({ numberOfFiles, patientDetails, setStage, us Deletion complete {numberOfFiles} file{numberOfFiles !== 1 && 's'} from the{' '} - {userType === USER_ROLE.GP && 'Lloyd George '} + {isGP && 'Lloyd George '} record of:{' '} @@ -46,7 +46,7 @@ function DeletionConfirmationStage({ numberOfFiles, patientDetails, setStage, us

- {userType === USER_ROLE.GP ? ( + {isGP ? ( Return to patient's Lloyd George record page diff --git a/app/src/components/blocks/lloydGeorgeDownloadAllStage/LloydGeorgeDownloadAllStage.test.tsx b/app/src/components/blocks/lloydGeorgeDownloadAllStage/LloydGeorgeDownloadAllStage.test.tsx index cbca4a826..2c7953c4d 100644 --- a/app/src/components/blocks/lloydGeorgeDownloadAllStage/LloydGeorgeDownloadAllStage.test.tsx +++ b/app/src/components/blocks/lloydGeorgeDownloadAllStage/LloydGeorgeDownloadAllStage.test.tsx @@ -3,12 +3,12 @@ import LgDownloadAllStage, { Props } from './LloydGeorgeDownloadAllStage'; import { buildLgSearchResult, buildPatientDetails } from '../../../helpers/test/testBuilders'; import { createMemoryHistory } from 'history'; import * as ReactRouter from 'react-router'; -import SessionProvider from '../../../providers/sessionProvider/SessionProvider'; import axios from 'axios'; -import userEvent from '@testing-library/user-event'; import { act } from 'react-dom/test-utils'; -jest.mock('axios'); +import userEvent from '@testing-library/user-event'; +jest.mock('axios'); +jest.mock('../../../helpers/hooks/useBaseAPIHeaders'); const mockedAxios = axios as jest.Mocked; const mockPdf = buildLgSearchResult(); const mockPatient = buildPatientDetails(); @@ -49,6 +49,7 @@ describe('LloydGeorgeDownloadAllStage', () => { }); it('renders download complete on zip success', async () => { + window.HTMLAnchorElement.prototype.click = jest.fn(); mockedAxios.get.mockImplementation(() => Promise.resolve({ data: mockPdf.presign_url })); jest.useFakeTimers(); @@ -68,10 +69,15 @@ describe('LloydGeorgeDownloadAllStage', () => { expect(screen.queryByText('0% downloaded...')).not.toBeInTheDocument(); expect(screen.getByTestId(mockPdf.presign_url)).toBeInTheDocument(); + const urlLink = screen.getByTestId(mockPdf.presign_url); + urlLink.addEventListener('click', (e) => { + e.preventDefault(); + }); act(() => { - userEvent.click(screen.getByTestId(mockPdf.presign_url)); + userEvent.click(urlLink); }); + await waitFor(async () => { expect(screen.queryByText('Downloading documents')).not.toBeInTheDocument(); }); @@ -85,11 +91,9 @@ const TestApp = (props: Omit) => { initialIndex: 0, }); return ( - - - - - + + + ); }; diff --git a/app/src/helpers/hooks/useRole.test.tsx b/app/src/helpers/hooks/useRole.test.tsx new file mode 100644 index 000000000..6ced71853 --- /dev/null +++ b/app/src/helpers/hooks/useRole.test.tsx @@ -0,0 +1,45 @@ +import { render, screen } from '@testing-library/react'; +import { REPOSITORY_ROLE, authorisedRoles } from '../../types/generic/authRole'; +import useRole from './useRole'; +import SessionProvider, { Session } from '../../providers/sessionProvider/SessionProvider'; +import { buildUserAuth } from '../test/testBuilders'; + +describe('useRole', () => { + beforeEach(() => { + sessionStorage.setItem('UserSession', ''); + process.env.REACT_APP_ENVIRONMENT = 'jest'; + }); + afterEach(() => { + jest.clearAllMocks(); + }); + + it.each(authorisedRoles)( + "returns a role when there is an authorised session for user role '%s'", + async (role) => { + renderHook(role); + expect(screen.getByText(`ROLE: ${role}`)).toBeInTheDocument(); + }, + ); + + it('returns null when there is no session', () => { + renderHook(); + expect(screen.getByText(`ROLE: null`)).toBeInTheDocument(); + }); +}); + +const TestApp = () => { + const role = useRole(); + return

{`ROLE: ${role}`.normalize()}
; +}; + +const renderHook = (role?: REPOSITORY_ROLE) => { + const session: Session = { + auth: buildUserAuth({ role }), + isLoggedIn: true, + }; + return render( + + + , + ); +}; diff --git a/app/src/helpers/hooks/useRole.tsx b/app/src/helpers/hooks/useRole.tsx new file mode 100644 index 000000000..574d6aa5f --- /dev/null +++ b/app/src/helpers/hooks/useRole.tsx @@ -0,0 +1,10 @@ +import { useSessionContext } from '../../providers/sessionProvider/SessionProvider'; + +function useRole() { + const [session] = useSessionContext(); + + const role = session.auth ? session.auth.role : null; + return role; +} + +export default useRole; diff --git a/app/src/helpers/requests/deleteAllDocuments.test.ts b/app/src/helpers/requests/deleteAllDocuments.test.ts new file mode 100644 index 000000000..13d6e043e --- /dev/null +++ b/app/src/helpers/requests/deleteAllDocuments.test.ts @@ -0,0 +1,68 @@ +import axios, { AxiosError } from 'axios'; +import deleteAllDocuments, { DeleteResponse } from './deleteAllDocuments'; +import { DOCUMENT_TYPE } from '../../types/pages/UploadDocumentsPage/types'; + +// Mock out all top level functions, such as get, put, delete and post: +jest.mock('axios'); +const mockedAxios = axios as jest.Mocked; + +// ... + +describe('[DELETE] deleteAllDocuments', () => { + test('Delete all documents handles a 2XX response', async () => { + mockedAxios.delete.mockImplementation(() => Promise.resolve({ status: 200, data: '' })); + const args = { + docType: DOCUMENT_TYPE.ARF, + nhsNumber: '90000000009', + baseUrl: '/test', + baseHeaders: { 'Content-Type': 'application/json', test: 'test' }, + }; + let response: DeleteResponse | AxiosError; + try { + response = await deleteAllDocuments(args); + } catch (e) { + const error = e as AxiosError; + response = error; + } + expect(response).toHaveProperty('status'); + expect(response?.status).toBe(200); + }); + + test('Delete all documents catches a 4XX response', async () => { + mockedAxios.delete.mockImplementation(() => Promise.reject({ status: 403, data: '' })); + const args = { + docType: DOCUMENT_TYPE.ARF, + nhsNumber: '', + baseUrl: '/test', + baseHeaders: { 'Content-Type': 'application/json', test: 'test' }, + }; + let response: DeleteResponse | AxiosError; + try { + response = await deleteAllDocuments(args); + } catch (e) { + const error = e as AxiosError; + response = error; + } + expect(response).toHaveProperty('status'); + expect(response?.status).toBe(403); + }); + + test('Delete all documents catches a 5XX response', async () => { + mockedAxios.delete.mockImplementation(() => Promise.reject({ status: 500, data: '' })); + const args = { + docType: DOCUMENT_TYPE.ARF, + nhsNumber: '', + baseUrl: '/test', + baseHeaders: { 'Content-Type': 'application/json', test: 'test' }, + }; + let response: DeleteResponse | AxiosError; + try { + response = await deleteAllDocuments(args); + } catch (e) { + const error = e as AxiosError; + response = error; + } + expect(response).toHaveProperty('status'); + expect(response?.status).toBe(500); + }); +}); diff --git a/app/src/helpers/requests/getAuthToken.test.ts b/app/src/helpers/requests/getAuthToken.test.ts new file mode 100644 index 000000000..d2814b3d2 --- /dev/null +++ b/app/src/helpers/requests/getAuthToken.test.ts @@ -0,0 +1,84 @@ +import axios, { AxiosError } from 'axios'; +import getDocumentSearchResults from './getDocumentSearchResults'; +import { SearchResult } from '../../types/generic/searchResult'; +import { buildUserAuth } from '../test/testBuilders'; +import { UserAuth } from '../../types/blocks/userAuth'; +import getAuthToken from './getAuthToken'; + +// Mock out all top level functions, such as get, put, delete and post: +jest.mock('axios'); +const mockedAxios = axios as jest.Mocked; + +// ... + +describe('[GET] getDocumentSearchResults', () => { + test('Document search results handles a 2XX response', async () => { + const mockAuth = buildUserAuth(); + mockedAxios.get.mockImplementation(() => Promise.resolve({ status: 200, data: mockAuth })); + const args = { + baseUrl: '/test', + code: 'xx', + state: 'xx', + }; + let response: UserAuth | AxiosError; + try { + response = await getAuthToken(args); + } catch (e) { + const error = e as AxiosError; + response = error; + } + expect(response).not.toHaveProperty('status'); + expect(response).toHaveProperty('authorisation_token'); + expect(response).toHaveProperty('role'); + + const data = response as UserAuth; + expect(data.authorisation_token).toBe(mockAuth.authorisation_token); + expect(data.role).toBe(mockAuth.role); + }); + + test('Document search results catches a 4XX response', async () => { + mockedAxios.get.mockImplementation(() => Promise.reject({ status: 403 })); + const args = { + nhsNumber: '', + baseUrl: '/test', + baseHeaders: { 'Content-Type': 'application/json', test: 'test' }, + }; + let response: SearchResult[] | AxiosError; + try { + response = await getDocumentSearchResults(args); + } catch (e) { + const error = e as AxiosError; + response = error; + } + + expect(response).not.toHaveProperty('authorisation_token'); + expect(response).not.toHaveProperty('role'); + expect(response).toHaveProperty('status'); + + const { status } = response as AxiosError; + expect(status).toBe(403); + }); + + test('Document search results catches a 5XX response', async () => { + mockedAxios.get.mockImplementation(() => Promise.reject({ status: 500 })); + const args = { + nhsNumber: '', + baseUrl: '/test', + baseHeaders: { 'Content-Type': 'application/json', test: 'test' }, + }; + let response: SearchResult[] | AxiosError; + try { + response = await getDocumentSearchResults(args); + } catch (e) { + const error = e as AxiosError; + response = error; + } + + expect(response).not.toHaveProperty('authorisation_token'); + expect(response).not.toHaveProperty('role'); + expect(response).toHaveProperty('status'); + + const { status } = response as AxiosError; + expect(status).toBe(500); + }); +}); diff --git a/app/src/helpers/requests/getDocumentSearchResults.test.ts b/app/src/helpers/requests/getDocumentSearchResults.test.ts new file mode 100644 index 000000000..be8ce7167 --- /dev/null +++ b/app/src/helpers/requests/getDocumentSearchResults.test.ts @@ -0,0 +1,86 @@ +import axios, { AxiosError } from 'axios'; +import getDocumentSearchResults from './getDocumentSearchResults'; +import { SearchResult } from '../../types/generic/searchResult'; +import { buildSearchResult } from '../test/testBuilders'; + +// Mock out all top level functions, such as get, put, delete and post: +jest.mock('axios'); +const mockedAxios = axios as jest.Mocked; + +// ... + +describe('[GET] getDocumentSearchResults', () => { + test('Document search results handles a 2XX response', async () => { + const searchResult = buildSearchResult(); + const mockResults = [searchResult]; + mockedAxios.get.mockImplementation(() => + Promise.resolve({ status: 200, data: mockResults }), + ); + const args = { + nhsNumber: '', + baseUrl: '/test', + baseHeaders: { 'Content-Type': 'application/json', test: 'test' }, + }; + let response: SearchResult[] | AxiosError; + try { + response = await getDocumentSearchResults(args); + } catch (e) { + const error = e as AxiosError; + response = error; + } + expect(response).toBeInstanceOf(Array); + expect(response).toHaveLength(1); + + const data = response as SearchResult[]; + expect(data[0]).toHaveProperty('fileName'); + expect(data[0].fileName).toBe(searchResult.fileName); + expect(data[0]).toHaveProperty('created'); + expect(data[0].created).toBe(searchResult.created); + expect(data[0]).toHaveProperty('virusScannerResult'); + expect(data[0].virusScannerResult).toBe(searchResult.virusScannerResult); + }); + + test('Document search results catches a 4XX response', async () => { + mockedAxios.get.mockImplementation(() => Promise.reject({ status: 403 })); + const args = { + nhsNumber: '', + baseUrl: '/test', + baseHeaders: { 'Content-Type': 'application/json', test: 'test' }, + }; + let response: SearchResult[] | AxiosError; + try { + response = await getDocumentSearchResults(args); + } catch (e) { + const error = e as AxiosError; + response = error; + } + + expect(response).not.toBeInstanceOf(Array); + expect(response).toHaveProperty('status'); + + const { status } = response as AxiosError; + expect(status).toBe(403); + }); + + test('Document search results catches a 5XX response', async () => { + mockedAxios.get.mockImplementation(() => Promise.reject({ status: 500 })); + const args = { + nhsNumber: '', + baseUrl: '/test', + baseHeaders: { 'Content-Type': 'application/json', test: 'test' }, + }; + let response: SearchResult[] | AxiosError; + try { + response = await getDocumentSearchResults(args); + } catch (e) { + const error = e as AxiosError; + response = error; + } + + expect(response).not.toBeInstanceOf(Array); + expect(response).toHaveProperty('status'); + + const { status } = response as AxiosError; + expect(status).toBe(500); + }); +}); diff --git a/app/src/helpers/requests/documentSearchResults.ts b/app/src/helpers/requests/getDocumentSearchResults.ts similarity index 88% rename from app/src/helpers/requests/documentSearchResults.ts rename to app/src/helpers/requests/getDocumentSearchResults.ts index 2058f4bf8..8672e9daf 100644 --- a/app/src/helpers/requests/documentSearchResults.ts +++ b/app/src/helpers/requests/getDocumentSearchResults.ts @@ -10,11 +10,9 @@ type Args = { baseHeaders: AuthHeaders; }; -type GetDocumentSearchResultsResponse = - | { - data: Array; - } - | undefined; +export type GetDocumentSearchResultsResponse = { + data: Array; +}; const getDocumentSearchResults = async ({ nhsNumber, baseUrl, baseHeaders }: Args) => { const gatewayUrl = baseUrl + endpoints.DOCUMENT_SEARCH; diff --git a/app/src/pages/documentSearchResultsPage/DocumentSearchResultsPage.tsx b/app/src/pages/documentSearchResultsPage/DocumentSearchResultsPage.tsx index 8dd396296..812e74a65 100644 --- a/app/src/pages/documentSearchResultsPage/DocumentSearchResultsPage.tsx +++ b/app/src/pages/documentSearchResultsPage/DocumentSearchResultsPage.tsx @@ -12,10 +12,9 @@ import ServiceError from '../../components/layout/serviceErrorBox/ServiceErrorBo import { useBaseAPIUrl } from '../../providers/configProvider/ConfigProvider'; import DocumentSearchResultsOptions from '../../components/blocks/documentSearchResultsOptions/DocumentSearchResultsOptions'; import { AxiosError } from 'axios'; -import getDocumentSearchResults from '../../helpers/requests/documentSearchResults'; +import getDocumentSearchResults from '../../helpers/requests/getDocumentSearchResults'; import useBaseAPIHeaders from '../../helpers/hooks/useBaseAPIHeaders'; import DeleteDocumentsStage from '../../components/blocks/deleteDocumentsStage/DeleteDocumentsStage'; -import { USER_ROLE } from '../../types/generic/roles'; import { DOCUMENT_TYPE } from '../../types/pages/UploadDocumentsPage/types'; function DocumentSearchResultsPage() { @@ -130,7 +129,6 @@ function DocumentSearchResultsPage() { diff --git a/app/src/pages/lloydGeorgeRecordPage/LloydGeorgeRecordPage.tsx b/app/src/pages/lloydGeorgeRecordPage/LloydGeorgeRecordPage.tsx index 14ea051c1..f64db89e1 100644 --- a/app/src/pages/lloydGeorgeRecordPage/LloydGeorgeRecordPage.tsx +++ b/app/src/pages/lloydGeorgeRecordPage/LloydGeorgeRecordPage.tsx @@ -10,7 +10,6 @@ import getLloydGeorgeRecord from '../../helpers/requests/getLloydGeorgeRecord'; import LloydGeorgeRecordStage from '../../components/blocks/lloydGeorgeRecordStage/LloydGeorgeRecordStage'; import LloydGeorgeDownloadAllStage from '../../components/blocks/lloydGeorgeDownloadAllStage/LloydGeorgeDownloadAllStage'; import { DOCUMENT_TYPE } from '../../types/pages/UploadDocumentsPage/types'; -import { USER_ROLE } from '../../types/generic/roles'; export enum LG_RECORD_STAGE { RECORD = 0, @@ -104,7 +103,6 @@ function LloydGeorgeRecordPage() { numberOfFiles={numberOfFiles} patientDetails={patientDetails} setStage={setStage} - userType={USER_ROLE.GP} setDownloadStage={setDownloadStage} /> ) diff --git a/app/src/pages/patientResultPage/PatientResultPage.test.tsx b/app/src/pages/patientResultPage/PatientResultPage.test.tsx index 966c79e89..5713c9bd0 100644 --- a/app/src/pages/patientResultPage/PatientResultPage.test.tsx +++ b/app/src/pages/patientResultPage/PatientResultPage.test.tsx @@ -1,6 +1,5 @@ import { render, screen, waitFor } from '@testing-library/react'; import PatientResultPage from './PatientResultPage'; -import { USER_ROLE } from '../../types/generic/roles'; import PatientDetailsProvider from '../../providers/patientProvider/PatientProvider'; import { buildPatientDetails } from '../../helpers/test/testBuilders'; import { PatientDetails } from '../../types/generic/patientDetails'; @@ -9,9 +8,17 @@ import { createMemoryHistory } from 'history'; import userEvent from '@testing-library/user-event'; import { routes } from '../../types/generic/routes'; import { act } from 'react-dom/test-utils'; -import { REPOSITORY_ROLE } from '../../types/generic/authRole'; +import { REPOSITORY_ROLE, authorisedRoles } from '../../types/generic/authRole'; +import useRole from '../../helpers/hooks/useRole'; + +jest.mock('../../helpers/hooks/useRole'); +const mockedUseRole = useRole as jest.Mock; describe('PatientResultPage', () => { + beforeEach(() => { + process.env.REACT_APP_ENVIRONMENT = 'jest'; + mockedUseRole.mockReturnValue(REPOSITORY_ROLE.PCSE); + }); afterEach(() => { jest.clearAllMocks(); }); @@ -23,56 +30,66 @@ describe('PatientResultPage', () => { expect(screen.getByText('Verify patient details')).toBeInTheDocument(); }); - it('displays the patient details page when patient data is found', async () => { - const nhsNumber = '9000000000'; - const familyName = 'Smith'; - const patientDetails = buildPatientDetails({ familyName, nhsNumber }); + it.each(authorisedRoles)( + "displays the patient details page when patient data is found when user role is '%s'", + async (role) => { + const nhsNumber = '9000000000'; + const familyName = 'Smith'; + const patientDetails = buildPatientDetails({ familyName, nhsNumber }); + mockedUseRole.mockReturnValue(role); - renderPatientResultPage(patientDetails); + renderPatientResultPage(patientDetails); - expect( - screen.getByRole('heading', { name: 'Verify patient details' }), - ).toBeInTheDocument(); - expect(screen.getByText(familyName)).toBeInTheDocument(); - expect( - screen.getByRole('button', { name: 'Accept details are correct' }), - ).toBeInTheDocument(); - expect(screen.getByText(/If patient details are incorrect/)).toBeInTheDocument(); + expect( + screen.getByRole('heading', { name: 'Verify patient details' }), + ).toBeInTheDocument(); + expect(screen.getByText(familyName)).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: 'Accept details are correct' }), + ).toBeInTheDocument(); + expect(screen.getByText(/If patient details are incorrect/)).toBeInTheDocument(); - const nationalServiceDeskLink = screen.getByRole('link', { - name: /National Service Desk/, - }); + const nationalServiceDeskLink = screen.getByRole('link', { + name: /National Service Desk/, + }); - expect(nationalServiceDeskLink).toHaveAttribute( - 'href', - 'https://digital.nhs.uk/about-nhs-digital/contact-us#nhs-digital-service-desks', - ); - expect(nationalServiceDeskLink).toHaveAttribute('target', '_blank'); - }); + expect(nationalServiceDeskLink).toHaveAttribute( + 'href', + 'https://digital.nhs.uk/about-nhs-digital/contact-us#nhs-digital-service-desks', + ); + expect(nationalServiceDeskLink).toHaveAttribute('target', '_blank'); + }, + ); - it('displays text specific to upload path if user has selected upload', async () => { - const nhsNumber = '9000000000'; - const patientDetails = buildPatientDetails({ nhsNumber }); - const uploadRole = REPOSITORY_ROLE.GP_ADMIN; + it.each([REPOSITORY_ROLE.GP_ADMIN, REPOSITORY_ROLE.GP_CLINICAL])( + "displays text specific to upload path when user role is '%s'", + async (role) => { + const nhsNumber = '9000000000'; + const patientDetails = buildPatientDetails({ nhsNumber }); - renderPatientResultPage(patientDetails, uploadRole); + mockedUseRole.mockReturnValue(role); - expect( - screen.getByRole('heading', { name: 'Verify patient details' }), - ).toBeInTheDocument(); + renderPatientResultPage(patientDetails); - expect( - screen.getByText( - 'Ensure these patient details match the records and attachments that you upload', - ), - ).toBeInTheDocument(); - }); + expect( + screen.getByRole('heading', { name: 'Verify patient details' }), + ).toBeInTheDocument(); + expect( + screen.getByText( + 'Ensure these patient details match the records and attachments that you upload', + ), + ).toBeInTheDocument(); + }, + ); - it("doesn't display text specific to upload path if user has selected download", async () => { + it("doesn't display text specific to upload path when user role is PCSE", async () => { const nhsNumber = '9000000000'; const patientDetails = buildPatientDetails({ nhsNumber }); - const downloadRole = REPOSITORY_ROLE.PCSE; - renderPatientResultPage(patientDetails, downloadRole); + + const role = REPOSITORY_ROLE.PCSE; + mockedUseRole.mockReturnValue(role); + + renderPatientResultPage(patientDetails); expect( screen.getByRole('heading', { name: 'Verify patient details' }), @@ -83,7 +100,6 @@ describe('PatientResultPage', () => { expect( screen.queryByRole('radio', { name: 'Inactive patient' }), ).not.toBeInTheDocument(); - expect( screen.queryByText( 'Ensure these patient details match the electronic health records and attachments you are about to upload.', @@ -91,6 +107,32 @@ describe('PatientResultPage', () => { ).not.toBeInTheDocument(); }); + it.each([REPOSITORY_ROLE.GP_ADMIN, REPOSITORY_ROLE.GP_CLINICAL])( + "displays an error message if 'active' boolean is missing on the patient, when role is '%s'", + async (role) => { + const history = createMemoryHistory({ initialEntries: ['/example'] }); + + mockedUseRole.mockReturnValue(role); + + renderPatientResultPage({ active: undefined }, history); + expect(history.location.pathname).toBe('/example'); + + act(() => { + userEvent.click( + screen.getByRole('button', { + name: 'Accept details are correct', + }), + ); + }); + + await waitFor(() => { + expect( + screen.getByText('We cannot determine the active state of this patient'), + ).toBeInTheDocument(); + }); + }, + ); + it('displays a message when NHS number is superseded', async () => { const nhsNumber = '9000000012'; const patientDetails = buildPatientDetails({ superseded: true, nhsNumber }); @@ -127,76 +169,58 @@ describe('PatientResultPage', () => { }); describe('Navigation', () => { - it('navigates to LG record page when GP user selects Active patient and submits', async () => { - const history = createMemoryHistory({ - initialEntries: ['/example'], - initialIndex: 1, - }); - - const uploadRole = REPOSITORY_ROLE.GP_ADMIN; - - renderPatientResultPage({ active: true }, uploadRole, history); - expect(history.location.pathname).toBe('/example'); - - act(() => { - userEvent.click(screen.getByRole('button', { name: 'Accept details are correct' })); - }); - - await waitFor(() => { - expect(history.location.pathname).toBe(routes.LLOYD_GEORGE); - }); - }); - - it('navigates to Upload docs page when GP user selects Inactive patient and submits', async () => { - const history = createMemoryHistory({ - initialEntries: ['/example'], - initialIndex: 1, - }); - - const uploadRole = REPOSITORY_ROLE.GP_ADMIN; - - renderPatientResultPage({ active: false }, uploadRole, history); - expect(history.location.pathname).toBe('/example'); - act(() => { - userEvent.click(screen.getByRole('button', { name: 'Accept details are correct' })); - }); - - await waitFor(() => { - expect(history.location.pathname).toBe(routes.UPLOAD_DOCUMENTS); - }); - }); - - it('Shows an error message if the active field is missing on the patient', async () => { - const history = createMemoryHistory({ - initialEntries: ['/example'], - initialIndex: 1, - }); - - const uploadRole = REPOSITORY_ROLE.GP_ADMIN; - - renderPatientResultPage({ active: undefined }, uploadRole, history); - expect(history.location.pathname).toBe('/example'); - - act(() => { - userEvent.click(screen.getByRole('button', { name: 'Accept details are correct' })); - }); - - await waitFor(() => { - expect( - screen.getByText('We cannot determine the active state of this patient'), - ).toBeInTheDocument(); - }); - }); - - it('navigates to download page when user has verified download patient', async () => { - const history = createMemoryHistory({ - initialEntries: ['/example'], - initialIndex: 1, - }); - - const downloadRole = REPOSITORY_ROLE.PCSE; - - renderPatientResultPage({}, downloadRole, history); + it.each([REPOSITORY_ROLE.GP_ADMIN, REPOSITORY_ROLE.GP_CLINICAL])( + "navigates to Upload docs page after user selects Inactive patient when role is '%s'", + async (role) => { + const history = createMemoryHistory({ + initialEntries: ['/example'], + }); + + mockedUseRole.mockReturnValue(role); + renderPatientResultPage({ active: false }, history); + expect(history.location.pathname).toBe('/example'); + act(() => { + userEvent.click( + screen.getByRole('button', { + name: 'Accept details are correct', + }), + ); + }); + + await waitFor(() => { + expect(history.location.pathname).toBe(routes.UPLOAD_DOCUMENTS); + }); + }, + ); + + it.each([REPOSITORY_ROLE.GP_ADMIN, REPOSITORY_ROLE.GP_CLINICAL])( + "navigates to Lloyd George Record page after user selects Active patient, when role is '%s'", + async (role) => { + const history = createMemoryHistory({ initialEntries: ['/example'] }); + mockedUseRole.mockReturnValue(role); + + renderPatientResultPage({ active: true }, history); + expect(history.location.pathname).toBe('/example'); + + act(() => { + userEvent.click( + screen.getByRole('button', { name: 'Accept details are correct' }), + ); + }); + + await waitFor(() => { + expect(history.location.pathname).toBe(routes.LLOYD_GEORGE); + }); + }, + ); + + it('navigates to ARF Download page when user selects Verify patient, when role is PCSE', async () => { + const history = createMemoryHistory({ initialEntries: ['/example'] }); + + const role = REPOSITORY_ROLE.PCSE; + mockedUseRole.mockReturnValue(role); + + renderPatientResultPage({}, history); expect(history.location.pathname).toBe('/example'); userEvent.click(screen.getByRole('button', { name: 'Accept details are correct' })); @@ -211,11 +235,7 @@ describe('PatientResultPage', () => { const homeRoute = '/example'; const renderPatientResultPage = ( patientOverride: Partial = {}, - role: REPOSITORY_ROLE = REPOSITORY_ROLE.PCSE, - history = createMemoryHistory({ - initialEntries: [homeRoute], - initialIndex: 1, - }), + history = createMemoryHistory({ initialEntries: ['/example'] }), ) => { const patient: PatientDetails = { ...buildPatientDetails(), @@ -225,7 +245,7 @@ const renderPatientResultPage = ( render( - + , ); diff --git a/app/src/pages/patientResultPage/PatientResultPage.tsx b/app/src/pages/patientResultPage/PatientResultPage.tsx index 698bf73b2..8f53162cb 100644 --- a/app/src/pages/patientResultPage/PatientResultPage.tsx +++ b/app/src/pages/patientResultPage/PatientResultPage.tsx @@ -8,12 +8,10 @@ import BackButton from '../../components/generic/backButton/BackButton'; import { useForm } from 'react-hook-form'; import ErrorBox from '../../components/layout/errorBox/ErrorBox'; import { REPOSITORY_ROLE } from '../../types/generic/authRole'; +import useRole from '../../helpers/hooks/useRole'; -type Props = { - role: REPOSITORY_ROLE; -}; - -function PatientResultPage({ role }: Props) { +function PatientResultPage() { + const role = useRole(); const userIsPCSE = role === REPOSITORY_ROLE.PCSE; const userIsGPAdmin = role === REPOSITORY_ROLE.GP_ADMIN; const userIsGPClinical = role === REPOSITORY_ROLE.GP_CLINICAL; diff --git a/app/src/pages/patientSearchPage/PatientSearchPage.test.tsx b/app/src/pages/patientSearchPage/PatientSearchPage.test.tsx index ac7aed051..8fb777daf 100644 --- a/app/src/pages/patientSearchPage/PatientSearchPage.test.tsx +++ b/app/src/pages/patientSearchPage/PatientSearchPage.test.tsx @@ -1,5 +1,4 @@ import { render, screen, waitFor } from '@testing-library/react'; -import { USER_ROLE } from '../../types/generic/roles'; import PatientDetailsProvider from '../../providers/patientProvider/PatientProvider'; import { PatientDetails } from '../../types/generic/patientDetails'; import * as ReactRouter from 'react-router'; @@ -9,50 +8,68 @@ import userEvent from '@testing-library/user-event'; import { buildPatientDetails } from '../../helpers/test/testBuilders'; import axios from 'axios'; import { routes } from '../../types/generic/routes'; -import { REPOSITORY_ROLE } from '../../types/generic/authRole'; +import { REPOSITORY_ROLE, authorisedRoles } from '../../types/generic/authRole'; +import useRole from '../../helpers/hooks/useRole'; jest.mock('../../helpers/hooks/useBaseAPIHeaders'); +jest.mock('../../helpers/hooks/useRole'); jest.mock('axios'); const mockedAxios = axios as jest.Mocked; +const mockedUseRole = useRole as jest.Mock; describe('PatientSearchPage', () => { beforeEach(() => { process.env.REACT_APP_ENVIRONMENT = 'jest'; + mockedUseRole.mockReturnValue(REPOSITORY_ROLE.PCSE); }); afterEach(() => { jest.clearAllMocks(); }); describe('Rendering', () => { - it('displays component with patient details', () => { - renderPatientSearchPage(); - expect(screen.getByText('Search for patient')).toBeInTheDocument(); - expect(screen.getByRole('textbox', { name: 'Enter NHS number' })).toBeInTheDocument(); - expect( - screen.getByText('A 10-digit number, for example, 485 777 3456'), - ).toBeInTheDocument(); - expect(screen.getByRole('button', { name: 'Search' })).toBeInTheDocument(); - }); + it.each(authorisedRoles)( + "renders the page with patient search form when user role is '%s'", + async (role) => { + mockedUseRole.mockReturnValue(role); - it('displays a loading spinner when the patients details are being requested', async () => { - mockedAxios.get.mockImplementation(async () => { - await new Promise((resolve) => { - setTimeout(() => { - // To delay the mock request, and give a chance for the spinner to appear - resolve(null); - }, 500); + renderPatientSearchPage(); + expect(screen.getByText('Search for patient')).toBeInTheDocument(); + expect( + screen.getByRole('textbox', { name: 'Enter NHS number' }), + ).toBeInTheDocument(); + expect( + screen.getByText('A 10-digit number, for example, 485 777 3456'), + ).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Search' })).toBeInTheDocument(); + }, + ); + it.each(authorisedRoles)( + "rdisplays a loading spinner when the patients details are being requested when user role is '%s'", + async (role) => { + mockedUseRole.mockReturnValue(role); + + mockedAxios.get.mockImplementation(async () => { + await new Promise((resolve) => { + setTimeout(() => { + // To delay the mock request, and give a chance for the spinner to appear + resolve(null); + }, 500); + }); + return Promise.resolve({ data: buildPatientDetails() }); }); - return Promise.resolve({ data: buildPatientDetails() }); - }); - renderPatientSearchPage(); + renderPatientSearchPage(); - userEvent.type(screen.getByRole('textbox', { name: 'Enter NHS number' }), '9000000009'); - userEvent.click(screen.getByRole('button', { name: 'Search' })); + userEvent.type( + screen.getByRole('textbox', { name: 'Enter NHS number' }), + '9000000009', + ); + userEvent.click(screen.getByRole('button', { name: 'Search' })); - await waitFor(() => { - expect(screen.getByRole('SpinnerButton')).toBeInTheDocument(); - }); - }); + await waitFor(() => { + expect(screen.getByRole('SpinnerButton')).toBeInTheDocument(); + }); + }, + ); it('displays an input error when user attempts to submit an invalid NHS number', async () => { renderPatientSearchPage(); @@ -117,17 +134,16 @@ describe('PatientSearchPage', () => { }); describe('Navigation', () => { - it('navigates to verify page when download patient is requested', async () => { - const history = createMemoryHistory({ - initialEntries: ['/example'], - initialIndex: 1, - }); + it('navigates to download journey when role is PCSE', async () => { + const history = createMemoryHistory({ initialEntries: ['/example'] }); mockedAxios.get.mockImplementation(() => Promise.resolve({ data: buildPatientDetails() }), ); + const role = REPOSITORY_ROLE.PCSE; + mockedUseRole.mockReturnValue(role); - renderPatientSearchPage({}, REPOSITORY_ROLE.PCSE, history); + renderPatientSearchPage({}, history); expect(history.location.pathname).toBe('/example'); userEvent.type(screen.getByRole('textbox', { name: 'Enter NHS number' }), '9000000000'); userEvent.click(screen.getByRole('button', { name: 'Search' })); @@ -137,30 +153,34 @@ describe('PatientSearchPage', () => { }); }); - it('navigates to verify page when upload patient is requested', async () => { - const history = createMemoryHistory({ - initialEntries: ['/example'], - initialIndex: 1, - }); + it.each([REPOSITORY_ROLE.GP_ADMIN, REPOSITORY_ROLE.GP_CLINICAL])( + "navigates to upload journey when role is '%s'", + async (role) => { + const history = createMemoryHistory({ + initialEntries: ['/example'], + }); - mockedAxios.get.mockImplementation(() => - Promise.resolve({ data: buildPatientDetails() }), - ); - renderPatientSearchPage({}, REPOSITORY_ROLE.GP_ADMIN, history); - expect(history.location.pathname).toBe('/example'); - userEvent.type(screen.getByRole('textbox', { name: 'Enter NHS number' }), '9000000000'); - userEvent.click(screen.getByRole('button', { name: 'Search' })); + mockedAxios.get.mockImplementation(() => + Promise.resolve({ data: buildPatientDetails() }), + ); + mockedUseRole.mockReturnValue(role); + renderPatientSearchPage({}, history); + expect(history.location.pathname).toBe('/example'); + userEvent.type( + screen.getByRole('textbox', { name: 'Enter NHS number' }), + '9000000000', + ); + userEvent.click(screen.getByRole('button', { name: 'Search' })); - await waitFor(() => { - expect(history.location.pathname).toBe(routes.UPLOAD_VERIFY); - }); - }); + await waitFor(() => { + expect(history.location.pathname).toBe(routes.UPLOAD_VERIFY); + }); + }, + ); it('navigates to start page when user is unauthorized to make request', async () => { - const history = createMemoryHistory({ - initialEntries: ['/example'], - initialIndex: 1, - }); + const history = createMemoryHistory({ initialEntries: ['/example'] }); + const errorResponse = { response: { status: 403, @@ -170,7 +190,7 @@ describe('PatientSearchPage', () => { mockedAxios.get.mockImplementation(() => Promise.reject(errorResponse)); - renderPatientSearchPage({}, REPOSITORY_ROLE.GP_ADMIN, history); + renderPatientSearchPage({}, history); expect(history.location.pathname).toBe('/example'); userEvent.type(screen.getByRole('textbox', { name: 'Enter NHS number' }), '9000000000'); userEvent.click(screen.getByRole('button', { name: 'Search' })); @@ -193,7 +213,7 @@ describe('PatientSearchPage', () => { Promise.resolve({ data: buildPatientDetails() }), ); - renderPatientSearchPage({}, REPOSITORY_ROLE.PCSE, history); + renderPatientSearchPage({}, history); expect(history.location.pathname).toBe('/example'); userEvent.type(screen.getByRole('textbox', { name: 'Enter NHS number' }), testNumber); userEvent.click(screen.getByRole('button', { name: 'Search' })); @@ -205,16 +225,13 @@ describe('PatientSearchPage', () => { it('allows NHS number with dashes to be submitted', async () => { const testNumber = '900-000-0000'; - const history = createMemoryHistory({ - initialEntries: ['/example'], - initialIndex: 1, - }); + const history = createMemoryHistory({ initialEntries: ['/example'] }); mockedAxios.get.mockImplementation(() => Promise.resolve({ data: buildPatientDetails() }), ); - renderPatientSearchPage({}, REPOSITORY_ROLE.PCSE, history); + renderPatientSearchPage({}, history); expect(history.location.pathname).toBe('/example'); userEvent.type(screen.getByRole('textbox', { name: 'Enter NHS number' }), testNumber); userEvent.click(screen.getByRole('button', { name: 'Search' })); @@ -252,10 +269,8 @@ describe('PatientSearchPage', () => { const testRoute = '/example'; const renderPatientSearchPage = ( patientOverride: Partial = {}, - role: REPOSITORY_ROLE = REPOSITORY_ROLE.PCSE, history = createMemoryHistory({ initialEntries: [testRoute], - initialIndex: 1, }), ) => { const patient: PatientDetails = { @@ -266,7 +281,7 @@ const renderPatientSearchPage = ( render( - + , ); diff --git a/app/src/pages/patientSearchPage/PatientSearchPage.tsx b/app/src/pages/patientSearchPage/PatientSearchPage.tsx index e238b26ea..0461f843f 100644 --- a/app/src/pages/patientSearchPage/PatientSearchPage.tsx +++ b/app/src/pages/patientSearchPage/PatientSearchPage.tsx @@ -19,15 +19,13 @@ import { PatientDetails } from '../../types/generic/patientDetails'; import { buildPatientDetails } from '../../helpers/test/testBuilders'; import { isMock } from '../../helpers/utils/isLocal'; import useBaseAPIHeaders from '../../helpers/hooks/useBaseAPIHeaders'; - -type Props = { - role: REPOSITORY_ROLE; -}; +import useRole from '../../helpers/hooks/useRole'; export const incorrectFormatMessage = "Enter patient's 10 digit NHS number"; -function PatientSearchPage({ role }: Props) { +function PatientSearchPage() { const [, setPatientDetails] = usePatientDetailsContext(); + const role = useRole(); const [submissionState, setSubmissionState] = useState(SEARCH_STATES.IDLE); const [statusCode, setStatusCode] = useState(null); const [inputError, setInputError] = useState(null); diff --git a/app/src/pages/roleSelectPage/RoleSelectPage.tsx b/app/src/pages/roleSelectPage/RoleSelectPage.tsx index 7fe37bcee..54e94db41 100644 --- a/app/src/pages/roleSelectPage/RoleSelectPage.tsx +++ b/app/src/pages/roleSelectPage/RoleSelectPage.tsx @@ -7,6 +7,9 @@ import ErrorBox from '../../components/layout/errorBox/ErrorBox'; import { useSessionContext } from '../../providers/sessionProvider/SessionProvider'; function RoleSelectPage() { + /** + * [DEPRECATED] Removed from routing + */ const navigate = useNavigate(); const [inputError, setInputError] = useState(''); const [session, setSession] = useSessionContext(); diff --git a/app/src/types/generic/authRole.ts b/app/src/types/generic/authRole.ts index 16e3ccaff..67b665860 100644 --- a/app/src/types/generic/authRole.ts +++ b/app/src/types/generic/authRole.ts @@ -3,3 +3,5 @@ export enum REPOSITORY_ROLE { GP_CLINICAL = 'GP_CLINICAL', PCSE = 'PCSE', } + +export const authorisedRoles: Array = Object.values(REPOSITORY_ROLE); diff --git a/app/src/types/generic/routes.ts b/app/src/types/generic/routes.ts index fd73f1492..ab05ac056 100644 --- a/app/src/types/generic/routes.ts +++ b/app/src/types/generic/routes.ts @@ -1,6 +1,5 @@ export enum routes { HOME = '/', - SELECT_ORG = '/select-organisation', AUTH_CALLBACK = '/auth-callback', NOT_FOUND = '/*', UNAUTHORISED = '/unauthorised',