From 484ace584c3d0da994451f6fbd41e24be72eff3c Mon Sep 17 00:00:00 2001 From: Rio Knightley Date: Thu, 9 Nov 2023 13:49:11 +0000 Subject: [PATCH 01/18] Add roleGuard and sitemap function --- app/package-lock.json | 7 ++ app/package.json | 3 +- app/src/App.tsx | 81 +------------------ app/src/router/Router.tsx | 75 +++++++++++++++++ .../guards}/authGuard/AuthGuard.test.tsx | 0 .../guards}/authGuard/AuthGuard.tsx | 0 .../patientGuard/PatientGuard.test.tsx | 0 .../guards}/patientGuard/PatientGuard.tsx | 0 app/src/router/guards/roleGuard/RoleGuard.tsx | 20 +++++ 9 files changed, 108 insertions(+), 78 deletions(-) create mode 100644 app/src/router/Router.tsx rename app/src/{components/blocks => router/guards}/authGuard/AuthGuard.test.tsx (100%) rename app/src/{components/blocks => router/guards}/authGuard/AuthGuard.tsx (100%) rename app/src/{components/blocks => router/guards}/patientGuard/PatientGuard.test.tsx (100%) rename app/src/{components/blocks => router/guards}/patientGuard/PatientGuard.tsx (100%) create mode 100644 app/src/router/guards/roleGuard/RoleGuard.tsx diff --git a/app/package-lock.json b/app/package-lock.json index d0efbd5e5..df8cd7132 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -59,6 +59,7 @@ "lint-staged": "^14.0.1", "prettier": "^3.0.3", "prop-types": "^15.8.1", + "react-router-to-array": "^0.1.3", "storybook": "^7.4.0", "webpack": "^5.88.2" } @@ -24765,6 +24766,12 @@ "react-dom": ">=16.8" } }, + "node_modules/react-router-to-array": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/react-router-to-array/-/react-router-to-array-0.1.3.tgz", + "integrity": "sha512-rg4zwEzRApWrenY1rGO32t9z+R156wsZUUj3eTD3H2tv497ItWiEJkn5ekvGKZ6aYJXz7Fhb1GW0dqJeVPExog==", + "dev": true + }, "node_modules/react-scripts": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", diff --git a/app/package.json b/app/package.json index b8203b973..ff7226d63 100644 --- a/app/package.json +++ b/app/package.json @@ -32,6 +32,7 @@ "react-hook-form": "^7.45.4", "react-router": "^6.14.2", "react-router-dom": "^6.14.2", + "react-router-to-array": "^0.1.3", "react-scripts": "^5.0.1", "sass": "^1.66.1", "serve": "^14.2.1", @@ -87,4 +88,4 @@ "axios": "axios/dist/node/axios.cjs" } } -} \ No newline at end of file +} diff --git a/app/src/App.tsx b/app/src/App.tsx index d05aecca8..0afb5c029 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -1,93 +1,20 @@ import React from 'react'; import './styles/App.scss'; -import HomePage from './pages/homePage/HomePage'; import ConfigProvider from './providers/configProvider/ConfigProvider'; import config from './config'; -import { routes } from './types/generic/routes'; import Layout from './components/layout/Layout'; import PatientDetailsProvider from './providers/patientProvider/PatientProvider'; -import { BrowserRouter as Router, Route, Routes, Outlet } from 'react-router-dom'; import SessionProvider from './providers/sessionProvider/SessionProvider'; -import AuthCallbackPage from './pages/authCallbackPage/AuthCallbackPage'; -import NotFoundPage from './pages/notFoundPage/NotFoundPage'; -import UnauthorisedPage from './pages/unauthorisedPage/UnauthorisedPage'; -import AuthGuard from './components/blocks/authGuard/AuthGuard'; -import PatientSearchPage from './pages/patientSearchPage/PatientSearchPage'; -import LogoutPage from './pages/logoutPage/LogoutPage'; -import PatientGuard from './components/blocks/patientGuard/PatientGuard'; -import PatientResultPage from './pages/patientResultPage/PatientResultPage'; -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 AppRouter from './router/Router'; function App() { return ( - - - - } path={routes.HOME} /> - - } path={routes.NOT_FOUND} /> - } path={routes.UNAUTHORISED} /> - } path={routes.AUTH_ERROR} /> - - } path={routes.AUTH_CALLBACK} /> - - - - - } - > - {[routes.DOWNLOAD_SEARCH, routes.UPLOAD_SEARCH].map( - (searchRoute) => ( - } - path={searchRoute} - /> - ), - )} - - } path={routes.LOGOUT} /> - - - - } - > - {[routes.DOWNLOAD_VERIFY, routes.UPLOAD_VERIFY].map( - (searchResultRoute) => ( - } - path={searchResultRoute} - /> - ), - )} - } - path={routes.LLOYD_GEORGE} - /> - } - path={routes.UPLOAD_DOCUMENTS} - /> - } - path={routes.DOWNLOAD_DOCUMENTS} - /> - - - - - + + + diff --git a/app/src/router/Router.tsx b/app/src/router/Router.tsx new file mode 100644 index 000000000..592e1c852 --- /dev/null +++ b/app/src/router/Router.tsx @@ -0,0 +1,75 @@ +import React from 'react'; +import { Outlet, Route, Routes as Switch } from 'react-router'; +import HomePage from '../pages/homePage/HomePage'; +import NotFoundPage from '../pages/notFoundPage/NotFoundPage'; +import UnauthorisedPage from '../pages/unauthorisedPage/UnauthorisedPage'; +import AuthErrorPage from '../pages/authErrorPage/AuthErrorPage'; +import AuthCallbackPage from '../pages/authCallbackPage/AuthCallbackPage'; +import AuthGuard from './guards/authGuard/AuthGuard'; +import PatientSearchPage from '../pages/patientSearchPage/PatientSearchPage'; +import { routes } from '../types/generic/routes'; +import LogoutPage from '../pages/logoutPage/LogoutPage'; +import PatientGuard from './guards/patientGuard/PatientGuard'; +import PatientResultPage from '../pages/patientResultPage/PatientResultPage'; +import LloydGeorgeRecordPage from '../pages/lloydGeorgeRecordPage/LloydGeorgeRecordPage'; +import UploadDocumentsPage from '../pages/uploadDocumentsPage/UploadDocumentsPage'; +import DocumentSearchResultsPage from '../pages/documentSearchResultsPage/DocumentSearchResultsPage'; +import { BrowserRouter } from 'react-router-dom'; + +const reactRouterToArray = require('react-router-to-array'); + +export const Routes = () => ( + + } path={routes.HOME} /> + + } path={routes.NOT_FOUND} /> + } path={routes.UNAUTHORISED} /> + } path={routes.AUTH_ERROR} /> + + } path={routes.AUTH_CALLBACK} /> + + + + + } + > + {[routes.DOWNLOAD_SEARCH, routes.UPLOAD_SEARCH].map((searchRoute) => ( + } path={searchRoute} /> + ))} + + } path={routes.LOGOUT} /> + + + + } + > + {[routes.DOWNLOAD_VERIFY, routes.UPLOAD_VERIFY].map((searchResultRoute) => ( + } + path={searchResultRoute} + /> + ))} + } path={routes.LLOYD_GEORGE} /> + } path={routes.UPLOAD_DOCUMENTS} /> + } path={routes.DOWNLOAD_DOCUMENTS} /> + + + +); + +export const sitemap = reactRouterToArray(); + +const AppRouter = () => { + return ( + + + + ); +}; + +export default AppRouter; diff --git a/app/src/components/blocks/authGuard/AuthGuard.test.tsx b/app/src/router/guards/authGuard/AuthGuard.test.tsx similarity index 100% rename from app/src/components/blocks/authGuard/AuthGuard.test.tsx rename to app/src/router/guards/authGuard/AuthGuard.test.tsx diff --git a/app/src/components/blocks/authGuard/AuthGuard.tsx b/app/src/router/guards/authGuard/AuthGuard.tsx similarity index 100% rename from app/src/components/blocks/authGuard/AuthGuard.tsx rename to app/src/router/guards/authGuard/AuthGuard.tsx diff --git a/app/src/components/blocks/patientGuard/PatientGuard.test.tsx b/app/src/router/guards/patientGuard/PatientGuard.test.tsx similarity index 100% rename from app/src/components/blocks/patientGuard/PatientGuard.test.tsx rename to app/src/router/guards/patientGuard/PatientGuard.test.tsx diff --git a/app/src/components/blocks/patientGuard/PatientGuard.tsx b/app/src/router/guards/patientGuard/PatientGuard.tsx similarity index 100% rename from app/src/components/blocks/patientGuard/PatientGuard.tsx rename to app/src/router/guards/patientGuard/PatientGuard.tsx diff --git a/app/src/router/guards/roleGuard/RoleGuard.tsx b/app/src/router/guards/roleGuard/RoleGuard.tsx new file mode 100644 index 000000000..256b0701d --- /dev/null +++ b/app/src/router/guards/roleGuard/RoleGuard.tsx @@ -0,0 +1,20 @@ +import { type ReactNode } from 'react'; +import { REPOSITORY_ROLE } from '../../../types/generic/authRole'; +import { useNavigate } from 'react-router'; +type Props = { + children: ReactNode; +}; + +function RoleGuard({ children }: Props) { + const role = REPOSITORY_ROLE.PCSE; + const navigate = useNavigate(); + + // useEffect(() => { + // if (!patient) { + // navigate(routes.UNAUTHORISED); + // } + // }, [role, navigate]); + return <>{children}; +} + +export default RoleGuard; From cfef6aa2fb42a2a71dd25febe920fcaf7f3e2a4c Mon Sep 17 00:00:00 2001 From: Rio Knightley Date: Thu, 9 Nov 2023 15:11:05 +0000 Subject: [PATCH 02/18] Add app routes component --- app/src/App.tsx | 5 +- app/src/router/AppRoutes.tsx | 85 +++++++++++++++++++ app/src/router/Router.tsx | 34 ++++---- app/src/router/guards/roleGuard/RoleGuard.tsx | 17 ++-- app/src/types/generic/routes.ts | 15 +++- 5 files changed, 129 insertions(+), 27 deletions(-) create mode 100644 app/src/router/AppRoutes.tsx diff --git a/app/src/App.tsx b/app/src/App.tsx index 0afb5c029..f3d65dbdc 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -2,7 +2,6 @@ import React from 'react'; import './styles/App.scss'; import ConfigProvider from './providers/configProvider/ConfigProvider'; import config from './config'; -import Layout from './components/layout/Layout'; import PatientDetailsProvider from './providers/patientProvider/PatientProvider'; import SessionProvider from './providers/sessionProvider/SessionProvider'; import AppRouter from './router/Router'; @@ -12,9 +11,7 @@ function App() { - - - + diff --git a/app/src/router/AppRoutes.tsx b/app/src/router/AppRoutes.tsx new file mode 100644 index 000000000..5c059183e --- /dev/null +++ b/app/src/router/AppRoutes.tsx @@ -0,0 +1,85 @@ +import AuthCallbackPage from '../pages/authCallbackPage/AuthCallbackPage'; +import AuthErrorPage from '../pages/authErrorPage/AuthErrorPage'; +import DocumentSearchResultsPage from '../pages/documentSearchResultsPage/DocumentSearchResultsPage'; +import HomePage from '../pages/homePage/HomePage'; +import LloydGeorgeRecordPage from '../pages/lloydGeorgeRecordPage/LloydGeorgeRecordPage'; +import LogoutPage from '../pages/logoutPage/LogoutPage'; +import NotFoundPage from '../pages/notFoundPage/NotFoundPage'; +import PatientResultPage from '../pages/patientResultPage/PatientResultPage'; +import PatientSearchPage from '../pages/patientSearchPage/PatientSearchPage'; +import UnauthorisedPage from '../pages/unauthorisedPage/UnauthorisedPage'; +import UploadDocumentsPage from '../pages/uploadDocumentsPage/UploadDocumentsPage'; +import { ROUTE_GUARD, routes, route } from '../types/generic/routes'; +const { + HOME, + AUTH_CALLBACK, + NOT_FOUND, + UNAUTHORISED, + AUTH_ERROR, + LOGOUT, + DOWNLOAD_SEARCH, + DOWNLOAD_VERIFY, + DOWNLOAD_DOCUMENTS, + LLOYD_GEORGE, + UPLOAD_SEARCH, + UPLOAD_VERIFY, + UPLOAD_DOCUMENTS, +} = routes; + +type AppRoutes = { + [key in routes]: route; +}; + +export const appRoutes: AppRoutes = { + [HOME]: { + page: , + }, + [AUTH_CALLBACK]: { + page: , + }, + [NOT_FOUND]: { + page: , + }, + [AUTH_ERROR]: { + page: , + }, + [UNAUTHORISED]: { + page: , + }, + [LOGOUT]: { + page: , + guards: [ROUTE_GUARD.AUTH], + }, + + [DOWNLOAD_SEARCH]: { + page: , + guards: [ROUTE_GUARD.AUTH, ROUTE_GUARD.ROLE], + }, + [UPLOAD_SEARCH]: { + page: , + guards: [ROUTE_GUARD.AUTH, ROUTE_GUARD.ROLE], + }, + [UPLOAD_VERIFY]: { + page: , + guards: [ROUTE_GUARD.AUTH, ROUTE_GUARD.ROLE, ROUTE_GUARD.PATIENT], + }, + [DOWNLOAD_VERIFY]: { + page: , + guards: [ROUTE_GUARD.AUTH, ROUTE_GUARD.ROLE, ROUTE_GUARD.PATIENT], + }, + + [UPLOAD_DOCUMENTS]: { + page: , + guards: [ROUTE_GUARD.AUTH, ROUTE_GUARD.ROLE, ROUTE_GUARD.PATIENT], + }, + [DOWNLOAD_DOCUMENTS]: { + page: , + guards: [ROUTE_GUARD.AUTH, ROUTE_GUARD.ROLE, ROUTE_GUARD.PATIENT], + }, + [LLOYD_GEORGE]: { + page: , + guards: [ROUTE_GUARD.AUTH, ROUTE_GUARD.ROLE, ROUTE_GUARD.PATIENT], + }, +}; + +export default appRoutes; diff --git a/app/src/router/Router.tsx b/app/src/router/Router.tsx index 592e1c852..152007b28 100644 --- a/app/src/router/Router.tsx +++ b/app/src/router/Router.tsx @@ -1,5 +1,7 @@ import React from 'react'; -import { Outlet, Route, Routes as Switch } from 'react-router'; +import { BrowserRouter as Router, Routes, Route, Outlet } from 'react-router-dom'; +import Layout from '../components/layout/Layout'; +import { routes } from '../types/generic/routes'; import HomePage from '../pages/homePage/HomePage'; import NotFoundPage from '../pages/notFoundPage/NotFoundPage'; import UnauthorisedPage from '../pages/unauthorisedPage/UnauthorisedPage'; @@ -7,19 +9,18 @@ import AuthErrorPage from '../pages/authErrorPage/AuthErrorPage'; import AuthCallbackPage from '../pages/authCallbackPage/AuthCallbackPage'; import AuthGuard from './guards/authGuard/AuthGuard'; import PatientSearchPage from '../pages/patientSearchPage/PatientSearchPage'; -import { routes } from '../types/generic/routes'; import LogoutPage from '../pages/logoutPage/LogoutPage'; import PatientGuard from './guards/patientGuard/PatientGuard'; import PatientResultPage from '../pages/patientResultPage/PatientResultPage'; import LloydGeorgeRecordPage from '../pages/lloydGeorgeRecordPage/LloydGeorgeRecordPage'; import UploadDocumentsPage from '../pages/uploadDocumentsPage/UploadDocumentsPage'; import DocumentSearchResultsPage from '../pages/documentSearchResultsPage/DocumentSearchResultsPage'; -import { BrowserRouter } from 'react-router-dom'; +import RoleGuard from './guards/roleGuard/RoleGuard'; const reactRouterToArray = require('react-router-to-array'); -export const Routes = () => ( - +const AppRoutes = () => ( + } path={routes.HOME} /> } path={routes.NOT_FOUND} /> @@ -42,9 +43,11 @@ export const Routes = () => ( } path={routes.LOGOUT} /> - - + + + + + } > {[routes.DOWNLOAD_VERIFY, routes.UPLOAD_VERIFY].map((searchResultRoute) => ( @@ -59,17 +62,18 @@ export const Routes = () => ( } path={routes.DOWNLOAD_DOCUMENTS} /> - + ); - -export const sitemap = reactRouterToArray(); - const AppRouter = () => { return ( - - - + + + + + ); }; +export const sitemap = reactRouterToArray(AppRoutes); + export default AppRouter; diff --git a/app/src/router/guards/roleGuard/RoleGuard.tsx b/app/src/router/guards/roleGuard/RoleGuard.tsx index 256b0701d..41f5f41e1 100644 --- a/app/src/router/guards/roleGuard/RoleGuard.tsx +++ b/app/src/router/guards/roleGuard/RoleGuard.tsx @@ -1,6 +1,8 @@ -import { type ReactNode } from 'react'; +import { useEffect, type ReactNode } from 'react'; import { REPOSITORY_ROLE } from '../../../types/generic/authRole'; import { useNavigate } from 'react-router'; +import { useLocation } from 'react-router-dom'; + type Props = { children: ReactNode; }; @@ -8,12 +10,13 @@ type Props = { function RoleGuard({ children }: Props) { const role = REPOSITORY_ROLE.PCSE; const navigate = useNavigate(); - - // useEffect(() => { - // if (!patient) { - // navigate(routes.UNAUTHORISED); - // } - // }, [role, navigate]); + const location = useLocation(); + useEffect(() => { + console.log(location.pathname); + // if (!patient) { + // navigate(routes.UNAUTHORISED); + // } + }, [role, location]); return <>{children}; } diff --git a/app/src/types/generic/routes.ts b/app/src/types/generic/routes.ts index ab05ac056..2de37d70f 100644 --- a/app/src/types/generic/routes.ts +++ b/app/src/types/generic/routes.ts @@ -1,3 +1,5 @@ +import { REPOSITORY_ROLE } from './authRole'; + export enum routes { HOME = '/', AUTH_CALLBACK = '/auth-callback', @@ -10,7 +12,6 @@ export enum routes { DOWNLOAD_SEARCH = '/search/patient', DOWNLOAD_VERIFY = '/search/patient/result', DOWNLOAD_DOCUMENTS = '/search/results', - DELETE_DOCUMENTS = '/search/results/delete', LLOYD_GEORGE = '/search/patient/lloyd-george-record', @@ -18,3 +19,15 @@ export enum routes { UPLOAD_VERIFY = '/search/upload/result', UPLOAD_DOCUMENTS = '/upload/submit', } + +export enum ROUTE_GUARD { + AUTH = 0, + ROLE = 1, + PATIENT = 2, +} + +export type route = { + page: JSX.Element; + guards?: Array; + unauthorized?: Array; +}; From 09c07c4b87bb822a278f345e291f77f34a597259 Mon Sep 17 00:00:00 2001 From: Rio Knightley Date: Fri, 10 Nov 2023 14:03:28 +0000 Subject: [PATCH 03/18] Add roleGuard logic --- app/src/router/AppRoutes.tsx | 82 ++++++++++++++++++- app/src/router/guards/roleGuard/RoleGuard.tsx | 15 ++-- 2 files changed, 89 insertions(+), 8 deletions(-) diff --git a/app/src/router/AppRoutes.tsx b/app/src/router/AppRoutes.tsx index 5c059183e..8479e8192 100644 --- a/app/src/router/AppRoutes.tsx +++ b/app/src/router/AppRoutes.tsx @@ -10,6 +10,11 @@ import PatientSearchPage from '../pages/patientSearchPage/PatientSearchPage'; import UnauthorisedPage from '../pages/unauthorisedPage/UnauthorisedPage'; import UploadDocumentsPage from '../pages/uploadDocumentsPage/UploadDocumentsPage'; import { ROUTE_GUARD, routes, route } from '../types/generic/routes'; +import { Routes as Switch, Route, Outlet } from 'react-router-dom'; +import AuthGuard from './guards/authGuard/AuthGuard'; +import PatientGuard from './guards/patientGuard/PatientGuard'; +import RoleGuard from './guards/roleGuard/RoleGuard'; + const { HOME, AUTH_CALLBACK, @@ -26,11 +31,14 @@ const { UPLOAD_DOCUMENTS, } = routes; -type AppRoutes = { +type Routes = { [key in routes]: route; }; -export const appRoutes: AppRoutes = { +export const routeMap: Routes = { + /** + * Public routes + */ [HOME]: { page: , }, @@ -46,11 +54,18 @@ export const appRoutes: AppRoutes = { [UNAUTHORISED]: { page: , }, + + /** + * Auth guarded routes + */ [LOGOUT]: { page: , guards: [ROUTE_GUARD.AUTH], }, + /** + * Role guarded routes + */ [DOWNLOAD_SEARCH]: { page: , guards: [ROUTE_GUARD.AUTH, ROUTE_GUARD.ROLE], @@ -68,6 +83,9 @@ export const appRoutes: AppRoutes = { guards: [ROUTE_GUARD.AUTH, ROUTE_GUARD.ROLE, ROUTE_GUARD.PATIENT], }, + /** + * Patient guarded routes + */ [UPLOAD_DOCUMENTS]: { page: , guards: [ROUTE_GUARD.AUTH, ROUTE_GUARD.ROLE, ROUTE_GUARD.PATIENT], @@ -82,4 +100,62 @@ export const appRoutes: AppRoutes = { }, }; -export default appRoutes; +const AppRoutes = () => { + let unusedPaths = Object.keys(routeMap); + const appRoutesArr = Object.entries(routeMap); + const publicRoutes = appRoutesArr.map(([path, route]) => { + if (!route.guards && unusedPaths.includes(path)) { + unusedPaths = unusedPaths.filter((p) => p !== path); + return ; + } + }); + const patientRoutes = appRoutesArr.map(([path, route]) => { + if ( + route.guards && + route.guards.includes(ROUTE_GUARD.PATIENT) && + unusedPaths.includes(path) + ) { + unusedPaths = unusedPaths.filter((p) => p !== path); + return ; + } + }); + const roleRoutes = appRoutesArr.map(([path, route]) => { + if (route.guards && route.guards.includes(ROUTE_GUARD.ROLE) && unusedPaths.includes(path)) { + unusedPaths = unusedPaths.filter((p) => p !== path); + return ; + } + }); + const privateRoutes = appRoutesArr.map(([path, route]) => { + if (route.guards && route.guards.includes(ROUTE_GUARD.AUTH) && unusedPaths.includes(path)) { + unusedPaths = unusedPaths.filter((p) => p !== path); + return ; + } + }); + return ( + + {...publicRoutes} + + + + } + > + {...privateRoutes} + + + + + + } + > + {...[...patientRoutes, ...roleRoutes]} + + + + ); +}; + +export default AppRoutes; diff --git a/app/src/router/guards/roleGuard/RoleGuard.tsx b/app/src/router/guards/roleGuard/RoleGuard.tsx index 41f5f41e1..a0998903b 100644 --- a/app/src/router/guards/roleGuard/RoleGuard.tsx +++ b/app/src/router/guards/roleGuard/RoleGuard.tsx @@ -2,6 +2,8 @@ import { useEffect, type ReactNode } from 'react'; import { REPOSITORY_ROLE } from '../../../types/generic/authRole'; import { useNavigate } from 'react-router'; import { useLocation } from 'react-router-dom'; +import { routes } from '../../../types/generic/routes'; +import { routeMap } from '../../appRoutes'; type Props = { children: ReactNode; @@ -12,11 +14,14 @@ function RoleGuard({ children }: Props) { const navigate = useNavigate(); const location = useLocation(); useEffect(() => { - console.log(location.pathname); - // if (!patient) { - // navigate(routes.UNAUTHORISED); - // } - }, [role, location]); + const routeKey = location.pathname as keyof typeof routeMap; + const { unauthorized } = routeMap[routeKey]; + const denyResource = + !!unauthorized && Array.isArray(unauthorized) && unauthorized.includes(role); + if (denyResource) { + navigate(routes.UNAUTHORISED); + } + }, [role, location, navigate]); return <>{children}; } From 509decd501208e3ae774e64ebe0d54ae7d734232 Mon Sep 17 00:00:00 2001 From: Rio Knightley Date: Fri, 10 Nov 2023 14:05:19 +0000 Subject: [PATCH 04/18] Fix import error --- app/src/router/guards/roleGuard/RoleGuard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/router/guards/roleGuard/RoleGuard.tsx b/app/src/router/guards/roleGuard/RoleGuard.tsx index a0998903b..86a2811c1 100644 --- a/app/src/router/guards/roleGuard/RoleGuard.tsx +++ b/app/src/router/guards/roleGuard/RoleGuard.tsx @@ -3,7 +3,7 @@ import { REPOSITORY_ROLE } from '../../../types/generic/authRole'; import { useNavigate } from 'react-router'; import { useLocation } from 'react-router-dom'; import { routes } from '../../../types/generic/routes'; -import { routeMap } from '../../appRoutes'; +import { routeMap } from '../../AppRoutes'; type Props = { children: ReactNode; From b382e6f204a42b090904e39b515aad3c7785ffc8 Mon Sep 17 00:00:00 2001 From: Rio Knightley Date: Fri, 10 Nov 2023 15:38:19 +0000 Subject: [PATCH 05/18] Add unauthorised to link handlers --- app/src/App.tsx | 2 +- .../DeleteDocumentsStage.tsx | 4 + .../DeletionConfirmationStage.tsx | 4 + .../LloydGeorgeDownloadAllStage.tsx | 4 + .../LloydGeorgeRecordDetails.tsx | 35 ++++---- .../router/{AppRoutes.tsx => AppRouter.tsx} | 34 +++++--- app/src/router/Router.tsx | 79 ------------------- 7 files changed, 56 insertions(+), 106 deletions(-) rename app/src/router/{AppRoutes.tsx => AppRouter.tsx} (92%) delete mode 100644 app/src/router/Router.tsx diff --git a/app/src/App.tsx b/app/src/App.tsx index f3d65dbdc..80d97f430 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -4,7 +4,7 @@ import ConfigProvider from './providers/configProvider/ConfigProvider'; import config from './config'; import PatientDetailsProvider from './providers/patientProvider/PatientProvider'; import SessionProvider from './providers/sessionProvider/SessionProvider'; -import AppRouter from './router/Router'; +import AppRouter from './router/AppRouter'; function App() { return ( diff --git a/app/src/components/blocks/deleteDocumentsStage/DeleteDocumentsStage.tsx b/app/src/components/blocks/deleteDocumentsStage/DeleteDocumentsStage.tsx index 332d79168..cc050f146 100644 --- a/app/src/components/blocks/deleteDocumentsStage/DeleteDocumentsStage.tsx +++ b/app/src/components/blocks/deleteDocumentsStage/DeleteDocumentsStage.tsx @@ -20,6 +20,10 @@ import { useNavigate } from 'react-router-dom'; import useRole from '../../../helpers/hooks/useRole'; import { REPOSITORY_ROLE } from '../../../types/generic/authRole'; +/** + * TODO: REMOVE GP CLINICAL FROM COMPONENT & TESTS + */ + export type Props = { docType: DOCUMENT_TYPE; numberOfFiles: number; diff --git a/app/src/components/blocks/deletionConfirmationStage/DeletionConfirmationStage.tsx b/app/src/components/blocks/deletionConfirmationStage/DeletionConfirmationStage.tsx index 6f84b7992..1c4277579 100644 --- a/app/src/components/blocks/deletionConfirmationStage/DeletionConfirmationStage.tsx +++ b/app/src/components/blocks/deletionConfirmationStage/DeletionConfirmationStage.tsx @@ -15,6 +15,10 @@ export type Props = { setStage?: Dispatch>; }; +/** + * TODO: REMOVE GP CLINICAL FROM COMPONENT & TESTS + */ + function DeletionConfirmationStage({ numberOfFiles, patientDetails, setStage }: Props) { const navigate = useNavigate(); const nhsNumber: string = patientDetails?.nhsNumber || ''; diff --git a/app/src/components/blocks/lloydGeorgeDownloadAllStage/LloydGeorgeDownloadAllStage.tsx b/app/src/components/blocks/lloydGeorgeDownloadAllStage/LloydGeorgeDownloadAllStage.tsx index 79799622b..9444699f0 100644 --- a/app/src/components/blocks/lloydGeorgeDownloadAllStage/LloydGeorgeDownloadAllStage.tsx +++ b/app/src/components/blocks/lloydGeorgeDownloadAllStage/LloydGeorgeDownloadAllStage.tsx @@ -18,6 +18,10 @@ import { DOCUMENT_TYPE } from '../../../types/pages/UploadDocumentsPage/types'; import LgDownloadComplete from '../lloydGeorgeDownloadComplete/LloydGeorgeDownloadComplete'; const FakeProgress = require('fake-progress'); +/** + * TODO: REMOVE GP CLINICAL FROM COMPONENT & TESTS + * + */ export type Props = { numberOfFiles: number; setStage: Dispatch>; diff --git a/app/src/components/blocks/lloydGeorgeRecordDetails/LloydGeorgeRecordDetails.tsx b/app/src/components/blocks/lloydGeorgeRecordDetails/LloydGeorgeRecordDetails.tsx index 21813e29c..31dc32555 100644 --- a/app/src/components/blocks/lloydGeorgeRecordDetails/LloydGeorgeRecordDetails.tsx +++ b/app/src/components/blocks/lloydGeorgeRecordDetails/LloydGeorgeRecordDetails.tsx @@ -5,6 +5,8 @@ import { LG_RECORD_STAGE } from '../../../pages/lloydGeorgeRecordPage/LloydGeorg import { useOnClickOutside } from 'usehooks-ts'; import { Card } from 'nhsuk-react-components'; import { Link } from 'react-router-dom'; +import { REPOSITORY_ROLE } from '../../../types/generic/authRole'; +import useRole from '../../../helpers/hooks/useRole'; export type Props = { lastUpdated: string; @@ -17,6 +19,7 @@ type PdfActionLink = { label: string; key: string; handler: () => void; + unauthorised?: Array; }; function LloydGeorgeRecordDetails({ lastUpdated, @@ -26,7 +29,7 @@ function LloydGeorgeRecordDetails({ }: Props) { const [showActionsMenu, setShowActionsMenu] = useState(false); const actionsRef = useRef(null); - + const role = useRole(); const handleMoreActions = () => { setShowActionsMenu(!showActionsMenu); }; @@ -44,11 +47,13 @@ function LloydGeorgeRecordDetails({ label: 'Download all files', key: 'download-all-files-link', handler: () => setStage(LG_RECORD_STAGE.DOWNLOAD_ALL), + unauthorised: [REPOSITORY_ROLE.GP_CLINICAL], }, { label: 'Delete all files', key: 'delete-all-files-link', handler: () => setStage(LG_RECORD_STAGE.DELETE_ALL), + unauthorised: [REPOSITORY_ROLE.GP_CLINICAL], }, ]; @@ -92,19 +97,21 @@ function LloydGeorgeRecordDetails({
    - {actionLinks.map((link) => ( -
  1. - { - e.preventDefault(); - link.handler(); - }} - > - {link.label} - -
  2. - ))} + {actionLinks.map((link) => + role && !link.unauthorised?.includes(role) ? ( +
  3. + { + e.preventDefault(); + link.handler(); + }} + > + {link.label} + +
  4. + ) : null, + )}
diff --git a/app/src/router/AppRoutes.tsx b/app/src/router/AppRouter.tsx similarity index 92% rename from app/src/router/AppRoutes.tsx rename to app/src/router/AppRouter.tsx index 8479e8192..cce8526f4 100644 --- a/app/src/router/AppRoutes.tsx +++ b/app/src/router/AppRouter.tsx @@ -1,20 +1,21 @@ +import React from 'react'; +import { BrowserRouter as Router, Routes as Switch, Route, Outlet } from 'react-router-dom'; +import Layout from '../components/layout/Layout'; +import { ROUTE_GUARD, route, routes } from '../types/generic/routes'; +import HomePage from '../pages/homePage/HomePage'; import AuthCallbackPage from '../pages/authCallbackPage/AuthCallbackPage'; +import NotFoundPage from '../pages/notFoundPage/NotFoundPage'; import AuthErrorPage from '../pages/authErrorPage/AuthErrorPage'; -import DocumentSearchResultsPage from '../pages/documentSearchResultsPage/DocumentSearchResultsPage'; -import HomePage from '../pages/homePage/HomePage'; -import LloydGeorgeRecordPage from '../pages/lloydGeorgeRecordPage/LloydGeorgeRecordPage'; +import UnauthorisedPage from '../pages/unauthorisedPage/UnauthorisedPage'; import LogoutPage from '../pages/logoutPage/LogoutPage'; -import NotFoundPage from '../pages/notFoundPage/NotFoundPage'; -import PatientResultPage from '../pages/patientResultPage/PatientResultPage'; import PatientSearchPage from '../pages/patientSearchPage/PatientSearchPage'; -import UnauthorisedPage from '../pages/unauthorisedPage/UnauthorisedPage'; +import PatientResultPage from '../pages/patientResultPage/PatientResultPage'; import UploadDocumentsPage from '../pages/uploadDocumentsPage/UploadDocumentsPage'; -import { ROUTE_GUARD, routes, route } from '../types/generic/routes'; -import { Routes as Switch, Route, Outlet } from 'react-router-dom'; +import DocumentSearchResultsPage from '../pages/documentSearchResultsPage/DocumentSearchResultsPage'; +import LloydGeorgeRecordPage from '../pages/lloydGeorgeRecordPage/LloydGeorgeRecordPage'; import AuthGuard from './guards/authGuard/AuthGuard'; -import PatientGuard from './guards/patientGuard/PatientGuard'; import RoleGuard from './guards/roleGuard/RoleGuard'; - +import PatientGuard from './guards/patientGuard/PatientGuard'; const { HOME, AUTH_CALLBACK, @@ -30,7 +31,6 @@ const { UPLOAD_VERIFY, UPLOAD_DOCUMENTS, } = routes; - type Routes = { [key in routes]: route; }; @@ -158,4 +158,14 @@ const AppRoutes = () => { ); }; -export default AppRoutes; +const AppRouter = () => { + return ( + + + + + + ); +}; + +export default AppRouter; diff --git a/app/src/router/Router.tsx b/app/src/router/Router.tsx deleted file mode 100644 index 152007b28..000000000 --- a/app/src/router/Router.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import React from 'react'; -import { BrowserRouter as Router, Routes, Route, Outlet } from 'react-router-dom'; -import Layout from '../components/layout/Layout'; -import { routes } from '../types/generic/routes'; -import HomePage from '../pages/homePage/HomePage'; -import NotFoundPage from '../pages/notFoundPage/NotFoundPage'; -import UnauthorisedPage from '../pages/unauthorisedPage/UnauthorisedPage'; -import AuthErrorPage from '../pages/authErrorPage/AuthErrorPage'; -import AuthCallbackPage from '../pages/authCallbackPage/AuthCallbackPage'; -import AuthGuard from './guards/authGuard/AuthGuard'; -import PatientSearchPage from '../pages/patientSearchPage/PatientSearchPage'; -import LogoutPage from '../pages/logoutPage/LogoutPage'; -import PatientGuard from './guards/patientGuard/PatientGuard'; -import PatientResultPage from '../pages/patientResultPage/PatientResultPage'; -import LloydGeorgeRecordPage from '../pages/lloydGeorgeRecordPage/LloydGeorgeRecordPage'; -import UploadDocumentsPage from '../pages/uploadDocumentsPage/UploadDocumentsPage'; -import DocumentSearchResultsPage from '../pages/documentSearchResultsPage/DocumentSearchResultsPage'; -import RoleGuard from './guards/roleGuard/RoleGuard'; - -const reactRouterToArray = require('react-router-to-array'); - -const AppRoutes = () => ( - - } path={routes.HOME} /> - - } path={routes.NOT_FOUND} /> - } path={routes.UNAUTHORISED} /> - } path={routes.AUTH_ERROR} /> - - } path={routes.AUTH_CALLBACK} /> - - - - - } - > - {[routes.DOWNLOAD_SEARCH, routes.UPLOAD_SEARCH].map((searchRoute) => ( - } path={searchRoute} /> - ))} - - } path={routes.LOGOUT} /> - - - - - - } - > - {[routes.DOWNLOAD_VERIFY, routes.UPLOAD_VERIFY].map((searchResultRoute) => ( - } - path={searchResultRoute} - /> - ))} - } path={routes.LLOYD_GEORGE} /> - } path={routes.UPLOAD_DOCUMENTS} /> - } path={routes.DOWNLOAD_DOCUMENTS} /> - - - -); -const AppRouter = () => { - return ( - - - - - - ); -}; - -export const sitemap = reactRouterToArray(AppRoutes); - -export default AppRouter; From 4fff314d73d3da3ce24b41bf07a8bfa49ee1366d Mon Sep 17 00:00:00 2001 From: Rio Knightley Date: Fri, 10 Nov 2023 17:11:01 +0000 Subject: [PATCH 06/18] Change route typing --- app/src/router/AppRouter.tsx | 199 +++++++++++------- app/src/router/guards/roleGuard/RoleGuard.tsx | 2 +- app/src/types/generic/routes.ts | 13 +- 3 files changed, 131 insertions(+), 83 deletions(-) diff --git a/app/src/router/AppRouter.tsx b/app/src/router/AppRouter.tsx index cce8526f4..763e9a7f2 100644 --- a/app/src/router/AppRouter.tsx +++ b/app/src/router/AppRouter.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { BrowserRouter as Router, Routes as Switch, Route, Outlet } from 'react-router-dom'; import Layout from '../components/layout/Layout'; -import { ROUTE_GUARD, route, routes } from '../types/generic/routes'; +import { ROUTE_TYPE, route, routes } from '../types/generic/routes'; import HomePage from '../pages/homePage/HomePage'; import AuthCallbackPage from '../pages/authCallbackPage/AuthCallbackPage'; import NotFoundPage from '../pages/notFoundPage/NotFoundPage'; @@ -14,8 +14,8 @@ import UploadDocumentsPage from '../pages/uploadDocumentsPage/UploadDocumentsPag import DocumentSearchResultsPage from '../pages/documentSearchResultsPage/DocumentSearchResultsPage'; import LloydGeorgeRecordPage from '../pages/lloydGeorgeRecordPage/LloydGeorgeRecordPage'; import AuthGuard from './guards/authGuard/AuthGuard'; -import RoleGuard from './guards/roleGuard/RoleGuard'; import PatientGuard from './guards/patientGuard/PatientGuard'; +import { REPOSITORY_ROLE } from '../types/generic/authRole'; const { HOME, AUTH_CALLBACK, @@ -31,138 +31,183 @@ const { UPLOAD_VERIFY, UPLOAD_DOCUMENTS, } = routes; + type Routes = { [key in routes]: route; }; export const routeMap: Routes = { - /** - * Public routes - */ + // Public routes [HOME]: { page: , + type: ROUTE_TYPE.PUBLIC, }, [AUTH_CALLBACK]: { page: , + type: ROUTE_TYPE.PUBLIC, }, [NOT_FOUND]: { page: , + type: ROUTE_TYPE.PUBLIC, }, [AUTH_ERROR]: { page: , + type: ROUTE_TYPE.PUBLIC, }, [UNAUTHORISED]: { page: , + type: ROUTE_TYPE.PUBLIC, }, - /** - * Auth guarded routes - */ + // Auth guard routes [LOGOUT]: { page: , - guards: [ROUTE_GUARD.AUTH], + type: ROUTE_TYPE.PRIVATE, }, - /** - * Role guarded routes - */ + // App guard routes [DOWNLOAD_SEARCH]: { page: , - guards: [ROUTE_GUARD.AUTH, ROUTE_GUARD.ROLE], + type: ROUTE_TYPE.APP, + unauthorized: [REPOSITORY_ROLE.GP_ADMIN, REPOSITORY_ROLE.GP_CLINICAL], }, [UPLOAD_SEARCH]: { page: , - guards: [ROUTE_GUARD.AUTH, ROUTE_GUARD.ROLE], + type: ROUTE_TYPE.APP, + unauthorized: [REPOSITORY_ROLE.PCSE], }, - [UPLOAD_VERIFY]: { + [DOWNLOAD_VERIFY]: { page: , - guards: [ROUTE_GUARD.AUTH, ROUTE_GUARD.ROLE, ROUTE_GUARD.PATIENT], + type: ROUTE_TYPE.APP, + unauthorized: [REPOSITORY_ROLE.GP_ADMIN, REPOSITORY_ROLE.GP_CLINICAL], }, - [DOWNLOAD_VERIFY]: { + [UPLOAD_VERIFY]: { page: , - guards: [ROUTE_GUARD.AUTH, ROUTE_GUARD.ROLE, ROUTE_GUARD.PATIENT], + type: ROUTE_TYPE.APP, + unauthorized: [REPOSITORY_ROLE.PCSE], }, - - /** - * Patient guarded routes - */ [UPLOAD_DOCUMENTS]: { page: , - guards: [ROUTE_GUARD.AUTH, ROUTE_GUARD.ROLE, ROUTE_GUARD.PATIENT], + type: ROUTE_TYPE.APP, }, [DOWNLOAD_DOCUMENTS]: { page: , - guards: [ROUTE_GUARD.AUTH, ROUTE_GUARD.ROLE, ROUTE_GUARD.PATIENT], + type: ROUTE_TYPE.APP, }, [LLOYD_GEORGE]: { page: , - guards: [ROUTE_GUARD.AUTH, ROUTE_GUARD.ROLE, ROUTE_GUARD.PATIENT], + type: ROUTE_TYPE.APP, }, }; -const AppRoutes = () => { - let unusedPaths = Object.keys(routeMap); - const appRoutesArr = Object.entries(routeMap); - const publicRoutes = appRoutesArr.map(([path, route]) => { - if (!route.guards && unusedPaths.includes(path)) { - unusedPaths = unusedPaths.filter((p) => p !== path); - return ; - } - }); - const patientRoutes = appRoutesArr.map(([path, route]) => { - if ( - route.guards && - route.guards.includes(ROUTE_GUARD.PATIENT) && - unusedPaths.includes(path) - ) { - unusedPaths = unusedPaths.filter((p) => p !== path); - return ; - } - }); - const roleRoutes = appRoutesArr.map(([path, route]) => { - if (route.guards && route.guards.includes(ROUTE_GUARD.ROLE) && unusedPaths.includes(path)) { - unusedPaths = unusedPaths.filter((p) => p !== path); - return ; - } - }); - const privateRoutes = appRoutesArr.map(([path, route]) => { - if (route.guards && route.guards.includes(ROUTE_GUARD.AUTH) && unusedPaths.includes(path)) { - unusedPaths = unusedPaths.filter((p) => p !== path); - return ; - } - }); - return ( - - {...publicRoutes} +// const AppRoutes = () => { +// let unusedPaths = Object.keys(routeMap); +// const appRoutesArr = Object.entries(routeMap); +// const publicRoutes = appRoutesArr.map(([path, route]) => { +// if (!route.guards && unusedPaths.includes(path)) { +// unusedPaths = unusedPaths.filter((p) => p !== path); +// return ; +// } +// }); +// const patientRoutes = appRoutesArr.map(([path, route]) => { +// if ( +// route.guards && +// route.guards.includes(ROUTE_GUARD.PATIENT) && +// unusedPaths.includes(path) +// ) { +// unusedPaths = unusedPaths.filter((p) => p !== path); +// return ; +// } +// }); +// const roleRoutes = appRoutesArr.map(([path, route]) => { +// if (route.guards && route.guards.includes(ROUTE_GUARD.ROLE) && unusedPaths.includes(path)) { +// unusedPaths = unusedPaths.filter((p) => p !== path); +// return ; +// } +// }); +// const privateRoutes = appRoutesArr.map(([path, route]) => { +// if (route.guards && route.guards.includes(ROUTE_GUARD.AUTH) && unusedPaths.includes(path)) { +// unusedPaths = unusedPaths.filter((p) => p !== path); +// return ; +// } +// }); +// return ( +// +// {publicRoutes} +// +// +// +// } +// > +// {privateRoutes} +// +// +// +// +// +// } +// > +// {roleRoutes} +// {patientRoutes} +// +// +// +// ); +// }; + +const PrevRoutes = () => ( + + } path={routes.HOME} /> + + } path={routes.NOT_FOUND} /> + } path={routes.UNAUTHORISED} /> + } path={routes.AUTH_ERROR} /> + + } path={routes.AUTH_CALLBACK} /> + + + + + } + > + {[routes.DOWNLOAD_SEARCH, routes.UPLOAD_SEARCH].map((searchRoute) => ( + } path={searchRoute} /> + ))} + + } path={routes.LOGOUT} /> + - + } > - {...privateRoutes} - - - - - - } - > - {...[...patientRoutes, ...roleRoutes]} - + {[routes.DOWNLOAD_VERIFY, routes.UPLOAD_VERIFY].map((searchResultRoute) => ( + } + path={searchResultRoute} + /> + ))} + } path={routes.LLOYD_GEORGE} /> + } path={routes.UPLOAD_DOCUMENTS} /> + } path={routes.DOWNLOAD_DOCUMENTS} /> - - ); -}; + + +); const AppRouter = () => { return ( - + ); diff --git a/app/src/router/guards/roleGuard/RoleGuard.tsx b/app/src/router/guards/roleGuard/RoleGuard.tsx index 86a2811c1..337f807e2 100644 --- a/app/src/router/guards/roleGuard/RoleGuard.tsx +++ b/app/src/router/guards/roleGuard/RoleGuard.tsx @@ -3,7 +3,7 @@ import { REPOSITORY_ROLE } from '../../../types/generic/authRole'; import { useNavigate } from 'react-router'; import { useLocation } from 'react-router-dom'; import { routes } from '../../../types/generic/routes'; -import { routeMap } from '../../AppRoutes'; +import { routeMap } from '../../AppRouter'; type Props = { children: ReactNode; diff --git a/app/src/types/generic/routes.ts b/app/src/types/generic/routes.ts index 2de37d70f..f5ad28513 100644 --- a/app/src/types/generic/routes.ts +++ b/app/src/types/generic/routes.ts @@ -20,14 +20,17 @@ export enum routes { UPLOAD_DOCUMENTS = '/upload/submit', } -export enum ROUTE_GUARD { - AUTH = 0, - ROLE = 1, - PATIENT = 2, +export enum ROUTE_TYPE { + // No guard + PUBLIC = 0, + // Auth route guard + PRIVATE = 1, + // All route guards + APP = 2, } export type route = { page: JSX.Element; - guards?: Array; + type: ROUTE_TYPE; unauthorized?: Array; }; From 9d6f62981b9f9b0080a74ce7be4c51b1b8ba048a Mon Sep 17 00:00:00 2001 From: Rio Knightley Date: Mon, 13 Nov 2023 11:12:46 +0000 Subject: [PATCH 07/18] Finish route type mapping --- app/src/router/AppRouter.tsx | 145 +++++++++----------------------- app/src/types/generic/routes.ts | 2 +- 2 files changed, 43 insertions(+), 104 deletions(-) diff --git a/app/src/router/AppRouter.tsx b/app/src/router/AppRouter.tsx index 763e9a7f2..46c7f99c6 100644 --- a/app/src/router/AppRouter.tsx +++ b/app/src/router/AppRouter.tsx @@ -16,6 +16,7 @@ import LloydGeorgeRecordPage from '../pages/lloydGeorgeRecordPage/LloydGeorgeRec import AuthGuard from './guards/authGuard/AuthGuard'; import PatientGuard from './guards/patientGuard/PatientGuard'; import { REPOSITORY_ROLE } from '../types/generic/authRole'; +import RoleGuard from './guards/roleGuard/RoleGuard'; const { HOME, AUTH_CALLBACK, @@ -68,146 +69,84 @@ export const routeMap: Routes = { // App guard routes [DOWNLOAD_SEARCH]: { page: , - type: ROUTE_TYPE.APP, + type: ROUTE_TYPE.PRIVATE, unauthorized: [REPOSITORY_ROLE.GP_ADMIN, REPOSITORY_ROLE.GP_CLINICAL], }, [UPLOAD_SEARCH]: { page: , - type: ROUTE_TYPE.APP, + type: ROUTE_TYPE.PRIVATE, unauthorized: [REPOSITORY_ROLE.PCSE], }, [DOWNLOAD_VERIFY]: { page: , - type: ROUTE_TYPE.APP, + type: ROUTE_TYPE.PATIENT, unauthorized: [REPOSITORY_ROLE.GP_ADMIN, REPOSITORY_ROLE.GP_CLINICAL], }, [UPLOAD_VERIFY]: { page: , - type: ROUTE_TYPE.APP, + type: ROUTE_TYPE.PATIENT, unauthorized: [REPOSITORY_ROLE.PCSE], }, [UPLOAD_DOCUMENTS]: { page: , - type: ROUTE_TYPE.APP, + type: ROUTE_TYPE.PATIENT, }, [DOWNLOAD_DOCUMENTS]: { page: , - type: ROUTE_TYPE.APP, + type: ROUTE_TYPE.PATIENT, }, [LLOYD_GEORGE]: { page: , - type: ROUTE_TYPE.APP, + type: ROUTE_TYPE.PATIENT, }, }; -// const AppRoutes = () => { -// let unusedPaths = Object.keys(routeMap); -// const appRoutesArr = Object.entries(routeMap); -// const publicRoutes = appRoutesArr.map(([path, route]) => { -// if (!route.guards && unusedPaths.includes(path)) { -// unusedPaths = unusedPaths.filter((p) => p !== path); -// return ; -// } -// }); -// const patientRoutes = appRoutesArr.map(([path, route]) => { -// if ( -// route.guards && -// route.guards.includes(ROUTE_GUARD.PATIENT) && -// unusedPaths.includes(path) -// ) { -// unusedPaths = unusedPaths.filter((p) => p !== path); -// return ; -// } -// }); -// const roleRoutes = appRoutesArr.map(([path, route]) => { -// if (route.guards && route.guards.includes(ROUTE_GUARD.ROLE) && unusedPaths.includes(path)) { -// unusedPaths = unusedPaths.filter((p) => p !== path); -// return ; -// } -// }); -// const privateRoutes = appRoutesArr.map(([path, route]) => { -// if (route.guards && route.guards.includes(ROUTE_GUARD.AUTH) && unusedPaths.includes(path)) { -// unusedPaths = unusedPaths.filter((p) => p !== path); -// return ; -// } -// }); -// return ( -// -// {publicRoutes} -// -// -// -// } -// > -// {privateRoutes} -// -// -// -// -// -// } -// > -// {roleRoutes} -// {patientRoutes} -// -// -// -// ); -// }; - -const PrevRoutes = () => ( - - } path={routes.HOME} /> - - } path={routes.NOT_FOUND} /> - } path={routes.UNAUTHORISED} /> - } path={routes.AUTH_ERROR} /> - - } path={routes.AUTH_CALLBACK} /> +const createRoutesFromType = (routeType: ROUTE_TYPE) => + Object.entries(routeMap).reduce( + (acc, [path, route]) => + route.type === routeType + ? [...acc, ] + : acc, + [] as Array, + ); - - - - } - > - {[routes.DOWNLOAD_SEARCH, routes.UPLOAD_SEARCH].map((searchRoute) => ( - } path={searchRoute} /> - ))} +const AppRoutes = () => { + const publicRoutes = createRoutesFromType(ROUTE_TYPE.PUBLIC); + const privateRoutes = createRoutesFromType(ROUTE_TYPE.PRIVATE); + const patientRoutes = createRoutesFromType(ROUTE_TYPE.PATIENT); - } path={routes.LOGOUT} /> + return ( + + {publicRoutes} - - + + + + + } > - {[routes.DOWNLOAD_VERIFY, routes.UPLOAD_VERIFY].map((searchResultRoute) => ( - } - path={searchResultRoute} - /> - ))} - } path={routes.LLOYD_GEORGE} /> - } path={routes.UPLOAD_DOCUMENTS} /> - } path={routes.DOWNLOAD_DOCUMENTS} /> + {privateRoutes} + + + + } + > + {patientRoutes} + - - -); + + ); +}; const AppRouter = () => { return ( - + ); diff --git a/app/src/types/generic/routes.ts b/app/src/types/generic/routes.ts index f5ad28513..f174cb859 100644 --- a/app/src/types/generic/routes.ts +++ b/app/src/types/generic/routes.ts @@ -26,7 +26,7 @@ export enum ROUTE_TYPE { // Auth route guard PRIVATE = 1, // All route guards - APP = 2, + PATIENT = 2, } export type route = { From 6379b607e8df1ae5a4232c12d2028b3f9882f2db Mon Sep 17 00:00:00 2001 From: Rio Knightley Date: Mon, 13 Nov 2023 11:18:32 +0000 Subject: [PATCH 08/18] Fix unauthorised array --- app/src/router/AppRouter.tsx | 8 ++++---- app/src/router/guards/roleGuard/RoleGuard.tsx | 5 +++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/app/src/router/AppRouter.tsx b/app/src/router/AppRouter.tsx index 46c7f99c6..8886904a4 100644 --- a/app/src/router/AppRouter.tsx +++ b/app/src/router/AppRouter.tsx @@ -70,22 +70,22 @@ export const routeMap: Routes = { [DOWNLOAD_SEARCH]: { page: , type: ROUTE_TYPE.PRIVATE, - unauthorized: [REPOSITORY_ROLE.GP_ADMIN, REPOSITORY_ROLE.GP_CLINICAL], + unauthorized: [REPOSITORY_ROLE.PCSE], }, [UPLOAD_SEARCH]: { page: , type: ROUTE_TYPE.PRIVATE, - unauthorized: [REPOSITORY_ROLE.PCSE], + unauthorized: [REPOSITORY_ROLE.GP_ADMIN, REPOSITORY_ROLE.GP_CLINICAL], }, [DOWNLOAD_VERIFY]: { page: , type: ROUTE_TYPE.PATIENT, - unauthorized: [REPOSITORY_ROLE.GP_ADMIN, REPOSITORY_ROLE.GP_CLINICAL], + unauthorized: [REPOSITORY_ROLE.PCSE], }, [UPLOAD_VERIFY]: { page: , type: ROUTE_TYPE.PATIENT, - unauthorized: [REPOSITORY_ROLE.PCSE], + unauthorized: [REPOSITORY_ROLE.GP_ADMIN, REPOSITORY_ROLE.GP_CLINICAL], }, [UPLOAD_DOCUMENTS]: { page: , diff --git a/app/src/router/guards/roleGuard/RoleGuard.tsx b/app/src/router/guards/roleGuard/RoleGuard.tsx index 337f807e2..c57dc8521 100644 --- a/app/src/router/guards/roleGuard/RoleGuard.tsx +++ b/app/src/router/guards/roleGuard/RoleGuard.tsx @@ -16,8 +16,9 @@ function RoleGuard({ children }: Props) { useEffect(() => { const routeKey = location.pathname as keyof typeof routeMap; const { unauthorized } = routeMap[routeKey]; - const denyResource = - !!unauthorized && Array.isArray(unauthorized) && unauthorized.includes(role); + const denyResource = Array.isArray(unauthorized) && unauthorized.includes(role); + console.log(unauthorized); + console.log('DENY RESOURCE?:', denyResource); if (denyResource) { navigate(routes.UNAUTHORISED); } From 9e78f8b530cdb0132b4e8c05fd3445dd9d3b4ee4 Mon Sep 17 00:00:00 2001 From: Rio Knightley Date: Mon, 13 Nov 2023 11:45:46 +0000 Subject: [PATCH 09/18] Fix record stage tests --- .../lloydGeorgeRecordStage/LloydGeorgeRecordStage.test.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/components/blocks/lloydGeorgeRecordStage/LloydGeorgeRecordStage.test.tsx b/app/src/components/blocks/lloydGeorgeRecordStage/LloydGeorgeRecordStage.test.tsx index 9a6de02a1..21f7a1ff1 100644 --- a/app/src/components/blocks/lloydGeorgeRecordStage/LloydGeorgeRecordStage.test.tsx +++ b/app/src/components/blocks/lloydGeorgeRecordStage/LloydGeorgeRecordStage.test.tsx @@ -11,6 +11,8 @@ import { act } from 'react-dom/test-utils'; const mockPdf = buildLgSearchResult(); const mockPatientDetails = buildPatientDetails(); +jest.mock('../../../helpers/hooks/useRole'); + describe('LloydGeorgeRecordStage', () => { beforeEach(() => { process.env.REACT_APP_ENVIRONMENT = 'jest'; From f71a83447baa2d7447de073e5899e1aef8fb8150 Mon Sep 17 00:00:00 2001 From: Rio Knightley Date: Mon, 13 Nov 2023 13:51:49 +0000 Subject: [PATCH 10/18] Add Lloyd George action links role tests --- .../LloydGeorgeRecordDetails.test.tsx | 73 +++++++++++++++++-- 1 file changed, 65 insertions(+), 8 deletions(-) diff --git a/app/src/components/blocks/lloydGeorgeRecordDetails/LloydGeorgeRecordDetails.test.tsx b/app/src/components/blocks/lloydGeorgeRecordDetails/LloydGeorgeRecordDetails.test.tsx index 12b91e652..5b65f250d 100644 --- a/app/src/components/blocks/lloydGeorgeRecordDetails/LloydGeorgeRecordDetails.test.tsx +++ b/app/src/components/blocks/lloydGeorgeRecordDetails/LloydGeorgeRecordDetails.test.tsx @@ -7,18 +7,42 @@ import * as ReactRouter from 'react-router'; import { createMemoryHistory } from 'history'; import userEvent from '@testing-library/user-event'; import { act } from 'react-dom/test-utils'; -const mockPdf = buildLgSearchResult(); +import { REPOSITORY_ROLE } from '../../../types/generic/authRole'; +import useRole from '../../../helpers/hooks/useRole'; +jest.mock('../../../helpers/hooks/useRole'); +const mockPdf = buildLgSearchResult(); const mockSetStaqe = jest.fn(); +const mockedUseRole = useRole as jest.Mock; describe('LloydGeorgeRecordDetails', () => { - const actionLinkStrings = [ + const actionLinks = [ { label: 'See all files', expectedStage: LG_RECORD_STAGE.SEE_ALL }, - { label: 'Download all files', expectedStage: LG_RECORD_STAGE.DOWNLOAD_ALL }, - { label: 'Delete all files', expectedStage: LG_RECORD_STAGE.DELETE_ALL }, + { + label: 'Download all files', + expectedStage: LG_RECORD_STAGE.DOWNLOAD_ALL, + }, + { + label: 'Delete all files', + expectedStage: LG_RECORD_STAGE.DELETE_ALL, + }, + ]; + + const actionLinksAuth = [ + { + label: 'Download all files', + expectedStage: LG_RECORD_STAGE.DOWNLOAD_ALL, + unauthorised: [REPOSITORY_ROLE.GP_CLINICAL], + }, + { + label: 'Delete all files', + expectedStage: LG_RECORD_STAGE.DELETE_ALL, + unauthorised: [REPOSITORY_ROLE.GP_CLINICAL], + }, ]; beforeEach(() => { + mockedUseRole.mockReturnValue(REPOSITORY_ROLE.PCSE); process.env.REACT_APP_ENVIRONMENT = 'jest'; }); afterEach(() => { @@ -41,7 +65,7 @@ describe('LloydGeorgeRecordDetails', () => { expect(screen.getByText(`Select an action...`)).toBeInTheDocument(); expect(screen.getByTestId('actions-menu')).toBeInTheDocument(); - actionLinkStrings.forEach((action) => { + actionLinks.forEach((action) => { expect(screen.queryByText(action.label)).not.toBeInTheDocument(); }); @@ -49,14 +73,28 @@ describe('LloydGeorgeRecordDetails', () => { userEvent.click(screen.getByTestId('actions-menu')); }); await waitFor(async () => { - actionLinkStrings.forEach((action) => { + actionLinks.forEach((action) => { expect(screen.getByText(action.label)).toBeInTheDocument(); }); }); }); - it.each(actionLinkStrings)( - "navigates to a required stage when action '%s' is clicked", + it.each(actionLinks)("renders actionLink '$label'", async (action) => { + renderComponent(); + + expect(screen.getByText(`Select an action...`)).toBeInTheDocument(); + expect(screen.getByTestId('actions-menu')).toBeInTheDocument(); + + act(() => { + userEvent.click(screen.getByTestId('actions-menu')); + }); + await waitFor(async () => { + expect(screen.getByText(action.label)).toBeInTheDocument(); + }); + }); + + it.each(actionLinks)( + "navigates to expected stage when action '$label' is clicked", async (action) => { renderComponent(); @@ -78,6 +116,25 @@ describe('LloydGeorgeRecordDetails', () => { }); }, ); + + it.each(actionLinksAuth)( + "does not render actionLink '$label' if role is unauthorised", + async (action) => { + mockedUseRole.mockReturnValue(action.unauthorised[0]); + + renderComponent(); + + expect(screen.getByText(`Select an action...`)).toBeInTheDocument(); + expect(screen.getByTestId('actions-menu')).toBeInTheDocument(); + + act(() => { + userEvent.click(screen.getByTestId('actions-menu')); + }); + await waitFor(async () => { + expect(screen.queryByText(action.label)).not.toBeInTheDocument(); + }); + }, + ); }); const TestApp = (props: Omit) => { From 91c43ad8c24cf20955beb2e2ed18063e212d6084 Mon Sep 17 00:00:00 2001 From: Rio Knightley Date: Wed, 15 Nov 2023 12:05:44 +0000 Subject: [PATCH 11/18] Update record details tests to include rbacs coverage --- .../LloydGeorgeRecordDetails.test.tsx | 37 +++------------ .../LloydGeorgeRecordDetails.tsx | 46 +++++++++---------- 2 files changed, 30 insertions(+), 53 deletions(-) diff --git a/app/src/components/blocks/lloydGeorgeRecordDetails/LloydGeorgeRecordDetails.test.tsx b/app/src/components/blocks/lloydGeorgeRecordDetails/LloydGeorgeRecordDetails.test.tsx index 5b65f250d..9346b1cb7 100644 --- a/app/src/components/blocks/lloydGeorgeRecordDetails/LloydGeorgeRecordDetails.test.tsx +++ b/app/src/components/blocks/lloydGeorgeRecordDetails/LloydGeorgeRecordDetails.test.tsx @@ -1,6 +1,5 @@ import { render, screen, waitFor } from '@testing-library/react'; -import LgRecordDetails, { Props } from './LloydGeorgeRecordDetails'; -import { LG_RECORD_STAGE } from '../../../pages/lloydGeorgeRecordPage/LloydGeorgeRecordPage'; +import LgRecordDetails, { Props, actionLinks } from './LloydGeorgeRecordDetails'; import { buildLgSearchResult } from '../../../helpers/test/testBuilders'; import formatFileSize from '../../../helpers/utils/formatFileSize'; import * as ReactRouter from 'react-router'; @@ -16,31 +15,6 @@ const mockSetStaqe = jest.fn(); const mockedUseRole = useRole as jest.Mock; describe('LloydGeorgeRecordDetails', () => { - const actionLinks = [ - { label: 'See all files', expectedStage: LG_RECORD_STAGE.SEE_ALL }, - { - label: 'Download all files', - expectedStage: LG_RECORD_STAGE.DOWNLOAD_ALL, - }, - { - label: 'Delete all files', - expectedStage: LG_RECORD_STAGE.DELETE_ALL, - }, - ]; - - const actionLinksAuth = [ - { - label: 'Download all files', - expectedStage: LG_RECORD_STAGE.DOWNLOAD_ALL, - unauthorised: [REPOSITORY_ROLE.GP_CLINICAL], - }, - { - label: 'Delete all files', - expectedStage: LG_RECORD_STAGE.DELETE_ALL, - unauthorised: [REPOSITORY_ROLE.GP_CLINICAL], - }, - ]; - beforeEach(() => { mockedUseRole.mockReturnValue(REPOSITORY_ROLE.PCSE); process.env.REACT_APP_ENVIRONMENT = 'jest'; @@ -112,15 +86,18 @@ describe('LloydGeorgeRecordDetails', () => { userEvent.click(screen.getByText(action.label)); }); await waitFor(async () => { - expect(mockSetStaqe).toHaveBeenCalledWith(action.expectedStage); + expect(mockSetStaqe).toHaveBeenCalledWith(action.stage); }); }, ); - it.each(actionLinksAuth)( + const unauthorisedLinks = actionLinks.filter((a) => Array.isArray(a.unauthorised)); + + it.each(unauthorisedLinks)( "does not render actionLink '$label' if role is unauthorised", async (action) => { - mockedUseRole.mockReturnValue(action.unauthorised[0]); + const [unauthorisedRole] = action.unauthorised ?? [REPOSITORY_ROLE.GP_CLINICAL]; + mockedUseRole.mockReturnValue(unauthorisedRole); renderComponent(); diff --git a/app/src/components/blocks/lloydGeorgeRecordDetails/LloydGeorgeRecordDetails.tsx b/app/src/components/blocks/lloydGeorgeRecordDetails/LloydGeorgeRecordDetails.tsx index 31dc32555..fd48f9ebe 100644 --- a/app/src/components/blocks/lloydGeorgeRecordDetails/LloydGeorgeRecordDetails.tsx +++ b/app/src/components/blocks/lloydGeorgeRecordDetails/LloydGeorgeRecordDetails.tsx @@ -18,9 +18,30 @@ export type Props = { type PdfActionLink = { label: string; key: string; - handler: () => void; + stage: LG_RECORD_STAGE; unauthorised?: Array; }; + +export const actionLinks: Array = [ + { + label: 'See all files', + key: 'see-all-files-link', + stage: LG_RECORD_STAGE.SEE_ALL, + }, + { + label: 'Download all files', + key: 'download-all-files-link', + stage: LG_RECORD_STAGE.DOWNLOAD_ALL, + unauthorised: [REPOSITORY_ROLE.GP_CLINICAL], + }, + { + label: 'Delete all files', + key: 'delete-all-files-link', + stage: LG_RECORD_STAGE.DELETE_ALL, + unauthorised: [REPOSITORY_ROLE.GP_CLINICAL], + }, +]; + function LloydGeorgeRecordDetails({ lastUpdated, numberOfFiles, @@ -36,27 +57,6 @@ function LloydGeorgeRecordDetails({ useOnClickOutside(actionsRef, (e) => { setShowActionsMenu(false); }); - - const actionLinks: Array = [ - { - label: 'See all files', - key: 'see-all-files-link', - handler: () => setStage(LG_RECORD_STAGE.SEE_ALL), - }, - { - label: 'Download all files', - key: 'download-all-files-link', - handler: () => setStage(LG_RECORD_STAGE.DOWNLOAD_ALL), - unauthorised: [REPOSITORY_ROLE.GP_CLINICAL], - }, - { - label: 'Delete all files', - key: 'delete-all-files-link', - handler: () => setStage(LG_RECORD_STAGE.DELETE_ALL), - unauthorised: [REPOSITORY_ROLE.GP_CLINICAL], - }, - ]; - return (
@@ -104,7 +104,7 @@ function LloydGeorgeRecordDetails({ to="#" onClick={(e) => { e.preventDefault(); - link.handler(); + setStage(link.stage); }} > {link.label} From 97a47f4f6a0d67c92286d6980aa582e48e24475b Mon Sep 17 00:00:00 2001 From: Rio Knightley Date: Wed, 15 Nov 2023 14:06:05 +0000 Subject: [PATCH 12/18] Fix circular import conflicts causing undefined on LG records --- .../DeleteDocumentsStage.test.tsx | 2 +- .../DeleteDocumentsStage.tsx | 2 +- .../DeletionConfirmationStage.test.tsx | 3 +- .../DeletionConfirmationStage.tsx | 2 +- .../LloydGeorgeDownloadComplete.test.tsx | 2 +- .../LloydGeorgeDownloadComplete.tsx | 2 +- .../LloydGeorgeRecordDetails.test.tsx | 3 +- .../LloydGeorgeRecordDetails.tsx | 31 ++----------------- .../LloydGeorgeRecordStage.test.tsx | 2 +- .../LloydGeorgeRecordPage.tsx | 7 +---- app/src/types/blocks/lloydGeorgeActions.ts | 29 +++++++++++++++++ app/src/types/blocks/lloydGeorgeStages.ts | 6 ++++ 12 files changed, 47 insertions(+), 44 deletions(-) create mode 100644 app/src/types/blocks/lloydGeorgeActions.ts create mode 100644 app/src/types/blocks/lloydGeorgeStages.ts diff --git a/app/src/components/blocks/deleteDocumentsStage/DeleteDocumentsStage.test.tsx b/app/src/components/blocks/deleteDocumentsStage/DeleteDocumentsStage.test.tsx index e85c8b3c0..fb16bc73c 100644 --- a/app/src/components/blocks/deleteDocumentsStage/DeleteDocumentsStage.test.tsx +++ b/app/src/components/blocks/deleteDocumentsStage/DeleteDocumentsStage.test.tsx @@ -4,7 +4,6 @@ import DeleteDocumentsStage, { Props } from './DeleteDocumentsStage'; import { getFormattedDate } from '../../../helpers/utils/formatDate'; import { act } from 'react-dom/test-utils'; 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 * as ReactRouter from 'react-router'; @@ -12,6 +11,7 @@ import { createMemoryHistory } from 'history'; import useRole from '../../../helpers/hooks/useRole'; import { REPOSITORY_ROLE, authorisedRoles } from '../../../types/generic/authRole'; import { routes } from '../../../types/generic/routes'; +import { LG_RECORD_STAGE } from '../../../types/blocks/lloydGeorgeStages'; jest.mock('../../../helpers/hooks/useBaseAPIHeaders'); jest.mock('../../../helpers/hooks/useRole'); diff --git a/app/src/components/blocks/deleteDocumentsStage/DeleteDocumentsStage.tsx b/app/src/components/blocks/deleteDocumentsStage/DeleteDocumentsStage.tsx index cc050f146..4edc9135d 100644 --- a/app/src/components/blocks/deleteDocumentsStage/DeleteDocumentsStage.tsx +++ b/app/src/components/blocks/deleteDocumentsStage/DeleteDocumentsStage.tsx @@ -3,7 +3,6 @@ import { FieldValues, useForm } from 'react-hook-form'; import { Button, Fieldset, Radios } from 'nhsuk-react-components'; import { getFormattedDate } from '../../../helpers/utils/formatDate'; import { PatientDetails } from '../../../types/generic/patientDetails'; -import { LG_RECORD_STAGE } from '../../../pages/lloydGeorgeRecordPage/LloydGeorgeRecordPage'; import DeletionConfirmationStage from '../deletionConfirmationStage/DeletionConfirmationStage'; import deleteAllDocuments, { DeleteResponse } from '../../../helpers/requests/deleteAllDocuments'; import { useBaseAPIUrl } from '../../../providers/configProvider/ConfigProvider'; @@ -19,6 +18,7 @@ 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'; +import { LG_RECORD_STAGE } from '../../../types/blocks/lloydGeorgeStages'; /** * TODO: REMOVE GP CLINICAL FROM COMPONENT & TESTS diff --git a/app/src/components/blocks/deletionConfirmationStage/DeletionConfirmationStage.test.tsx b/app/src/components/blocks/deletionConfirmationStage/DeletionConfirmationStage.test.tsx index da6407666..d1615ad66 100644 --- a/app/src/components/blocks/deletionConfirmationStage/DeletionConfirmationStage.test.tsx +++ b/app/src/components/blocks/deletionConfirmationStage/DeletionConfirmationStage.test.tsx @@ -3,13 +3,12 @@ import { buildLgSearchResult, buildPatientDetails } from '../../../helpers/test/ 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 * 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'; - +import { LG_RECORD_STAGE } from '../../../types/blocks/lloydGeorgeStages'; jest.mock('../../../helpers/hooks/useRole'); const mockedUseRole = useRole as jest.Mock; diff --git a/app/src/components/blocks/deletionConfirmationStage/DeletionConfirmationStage.tsx b/app/src/components/blocks/deletionConfirmationStage/DeletionConfirmationStage.tsx index 1c4277579..708909485 100644 --- a/app/src/components/blocks/deletionConfirmationStage/DeletionConfirmationStage.tsx +++ b/app/src/components/blocks/deletionConfirmationStage/DeletionConfirmationStage.tsx @@ -1,13 +1,13 @@ 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 { 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'; +import { LG_RECORD_STAGE } from '../../../types/blocks/lloydGeorgeStages'; export type Props = { numberOfFiles: number; diff --git a/app/src/components/blocks/lloydGeorgeDownloadComplete/LloydGeorgeDownloadComplete.test.tsx b/app/src/components/blocks/lloydGeorgeDownloadComplete/LloydGeorgeDownloadComplete.test.tsx index e390a0225..bfbbee83d 100644 --- a/app/src/components/blocks/lloydGeorgeDownloadComplete/LloydGeorgeDownloadComplete.test.tsx +++ b/app/src/components/blocks/lloydGeorgeDownloadComplete/LloydGeorgeDownloadComplete.test.tsx @@ -1,5 +1,5 @@ import { buildPatientDetails } from '../../../helpers/test/testBuilders'; -import { LG_RECORD_STAGE } from '../../../pages/lloydGeorgeRecordPage/LloydGeorgeRecordPage'; +import { LG_RECORD_STAGE } from '../../../types/blocks/lloydGeorgeStages'; import { PatientDetails } from '../../../types/generic/patientDetails'; import LgDownloadComplete, { Props } from './LloydGeorgeDownloadComplete'; import { render, screen, waitFor } from '@testing-library/react'; diff --git a/app/src/components/blocks/lloydGeorgeDownloadComplete/LloydGeorgeDownloadComplete.tsx b/app/src/components/blocks/lloydGeorgeDownloadComplete/LloydGeorgeDownloadComplete.tsx index 78f2def51..ad8246e1a 100644 --- a/app/src/components/blocks/lloydGeorgeDownloadComplete/LloydGeorgeDownloadComplete.tsx +++ b/app/src/components/blocks/lloydGeorgeDownloadComplete/LloydGeorgeDownloadComplete.tsx @@ -1,7 +1,7 @@ import React, { Dispatch, SetStateAction } from 'react'; import { PatientDetails } from '../../../types/generic/patientDetails'; import { Button, Card } from 'nhsuk-react-components'; -import { LG_RECORD_STAGE } from '../../../pages/lloydGeorgeRecordPage/LloydGeorgeRecordPage'; +import { LG_RECORD_STAGE } from '../../../types/blocks/lloydGeorgeStages'; export type Props = { patientDetails: PatientDetails; diff --git a/app/src/components/blocks/lloydGeorgeRecordDetails/LloydGeorgeRecordDetails.test.tsx b/app/src/components/blocks/lloydGeorgeRecordDetails/LloydGeorgeRecordDetails.test.tsx index 9346b1cb7..ac917d0c3 100644 --- a/app/src/components/blocks/lloydGeorgeRecordDetails/LloydGeorgeRecordDetails.test.tsx +++ b/app/src/components/blocks/lloydGeorgeRecordDetails/LloydGeorgeRecordDetails.test.tsx @@ -1,5 +1,5 @@ import { render, screen, waitFor } from '@testing-library/react'; -import LgRecordDetails, { Props, actionLinks } from './LloydGeorgeRecordDetails'; +import LgRecordDetails, { Props } from './LloydGeorgeRecordDetails'; import { buildLgSearchResult } from '../../../helpers/test/testBuilders'; import formatFileSize from '../../../helpers/utils/formatFileSize'; import * as ReactRouter from 'react-router'; @@ -8,6 +8,7 @@ import userEvent from '@testing-library/user-event'; import { act } from 'react-dom/test-utils'; import { REPOSITORY_ROLE } from '../../../types/generic/authRole'; import useRole from '../../../helpers/hooks/useRole'; +import { actionLinks } from '../../../types/blocks/lloydGeorgeActions'; jest.mock('../../../helpers/hooks/useRole'); const mockPdf = buildLgSearchResult(); diff --git a/app/src/components/blocks/lloydGeorgeRecordDetails/LloydGeorgeRecordDetails.tsx b/app/src/components/blocks/lloydGeorgeRecordDetails/LloydGeorgeRecordDetails.tsx index fd48f9ebe..1879e61e9 100644 --- a/app/src/components/blocks/lloydGeorgeRecordDetails/LloydGeorgeRecordDetails.tsx +++ b/app/src/components/blocks/lloydGeorgeRecordDetails/LloydGeorgeRecordDetails.tsx @@ -1,12 +1,12 @@ import React, { Dispatch, SetStateAction, useRef, useState } from 'react'; import { ReactComponent as Chevron } from '../../../styles/down-chevron.svg'; import formatFileSize from '../../../helpers/utils/formatFileSize'; -import { LG_RECORD_STAGE } from '../../../pages/lloydGeorgeRecordPage/LloydGeorgeRecordPage'; import { useOnClickOutside } from 'usehooks-ts'; import { Card } from 'nhsuk-react-components'; import { Link } from 'react-router-dom'; -import { REPOSITORY_ROLE } from '../../../types/generic/authRole'; import useRole from '../../../helpers/hooks/useRole'; +import { actionLinks } from '../../../types/blocks/lloydGeorgeActions'; +import { LG_RECORD_STAGE } from '../../../types/blocks/lloydGeorgeStages'; export type Props = { lastUpdated: string; @@ -15,33 +15,6 @@ export type Props = { setStage: Dispatch>; }; -type PdfActionLink = { - label: string; - key: string; - stage: LG_RECORD_STAGE; - unauthorised?: Array; -}; - -export const actionLinks: Array = [ - { - label: 'See all files', - key: 'see-all-files-link', - stage: LG_RECORD_STAGE.SEE_ALL, - }, - { - label: 'Download all files', - key: 'download-all-files-link', - stage: LG_RECORD_STAGE.DOWNLOAD_ALL, - unauthorised: [REPOSITORY_ROLE.GP_CLINICAL], - }, - { - label: 'Delete all files', - key: 'delete-all-files-link', - stage: LG_RECORD_STAGE.DELETE_ALL, - unauthorised: [REPOSITORY_ROLE.GP_CLINICAL], - }, -]; - function LloydGeorgeRecordDetails({ lastUpdated, numberOfFiles, diff --git a/app/src/components/blocks/lloydGeorgeRecordStage/LloydGeorgeRecordStage.test.tsx b/app/src/components/blocks/lloydGeorgeRecordStage/LloydGeorgeRecordStage.test.tsx index 21f7a1ff1..8e38cb8b8 100644 --- a/app/src/components/blocks/lloydGeorgeRecordStage/LloydGeorgeRecordStage.test.tsx +++ b/app/src/components/blocks/lloydGeorgeRecordStage/LloydGeorgeRecordStage.test.tsx @@ -5,9 +5,9 @@ import LgRecordStage, { Props } from './LloydGeorgeRecordStage'; import { getFormattedDate } from '../../../helpers/utils/formatDate'; import { DOWNLOAD_STAGE } from '../../../types/generic/downloadStage'; import { useState } from 'react'; -import { LG_RECORD_STAGE } from '../../../pages/lloydGeorgeRecordPage/LloydGeorgeRecordPage'; import formatFileSize from '../../../helpers/utils/formatFileSize'; import { act } from 'react-dom/test-utils'; +import { LG_RECORD_STAGE } from '../../../types/blocks/lloydGeorgeStages'; const mockPdf = buildLgSearchResult(); const mockPatientDetails = buildPatientDetails(); diff --git a/app/src/pages/lloydGeorgeRecordPage/LloydGeorgeRecordPage.tsx b/app/src/pages/lloydGeorgeRecordPage/LloydGeorgeRecordPage.tsx index f64db89e1..421387875 100644 --- a/app/src/pages/lloydGeorgeRecordPage/LloydGeorgeRecordPage.tsx +++ b/app/src/pages/lloydGeorgeRecordPage/LloydGeorgeRecordPage.tsx @@ -10,13 +10,8 @@ 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 { LG_RECORD_STAGE } from '../../types/blocks/lloydGeorgeStages'; -export enum LG_RECORD_STAGE { - RECORD = 0, - DOWNLOAD_ALL = 1, - SEE_ALL = 2, - DELETE_ALL = 3, -} function LloydGeorgeRecordPage() { const [patientDetails] = usePatientDetailsContext(); const [downloadStage, setDownloadStage] = useState(DOWNLOAD_STAGE.INITIAL); diff --git a/app/src/types/blocks/lloydGeorgeActions.ts b/app/src/types/blocks/lloydGeorgeActions.ts new file mode 100644 index 000000000..8c7f34df5 --- /dev/null +++ b/app/src/types/blocks/lloydGeorgeActions.ts @@ -0,0 +1,29 @@ +import { REPOSITORY_ROLE } from '../generic/authRole'; +import { LG_RECORD_STAGE } from './lloydGeorgeStages'; + +type PdfActionLink = { + label: string; + key: string; + stage: LG_RECORD_STAGE; + unauthorised?: Array; +}; + +export const actionLinks: Array = [ + { + label: 'See all files', + key: 'see-all-files-link', + stage: LG_RECORD_STAGE.SEE_ALL, + }, + { + label: 'Download all files', + key: 'download-all-files-link', + stage: LG_RECORD_STAGE.DOWNLOAD_ALL, + unauthorised: [REPOSITORY_ROLE.GP_CLINICAL], + }, + { + label: 'Delete all files', + key: 'delete-all-files-link', + stage: LG_RECORD_STAGE.DELETE_ALL, + unauthorised: [REPOSITORY_ROLE.GP_CLINICAL], + }, +]; diff --git a/app/src/types/blocks/lloydGeorgeStages.ts b/app/src/types/blocks/lloydGeorgeStages.ts new file mode 100644 index 000000000..662c9e86c --- /dev/null +++ b/app/src/types/blocks/lloydGeorgeStages.ts @@ -0,0 +1,6 @@ +export enum LG_RECORD_STAGE { + RECORD = 0, + DOWNLOAD_ALL = 1, + SEE_ALL = 2, + DELETE_ALL = 3, +} From 1ca5601ab089ab7ca012deb9fb09600a573885ac Mon Sep 17 00:00:00 2001 From: Rio Knightley Date: Wed, 15 Nov 2023 15:25:27 +0000 Subject: [PATCH 13/18] Add GP Clinical denied test for LG actions --- .../DeleteDocumentsStage.tsx | 3 +- .../LloydGeorgeRecordDetails.test.tsx | 144 ++++++++++-------- 2 files changed, 79 insertions(+), 68 deletions(-) diff --git a/app/src/components/blocks/deleteDocumentsStage/DeleteDocumentsStage.tsx b/app/src/components/blocks/deleteDocumentsStage/DeleteDocumentsStage.tsx index 4edc9135d..3ba14665f 100644 --- a/app/src/components/blocks/deleteDocumentsStage/DeleteDocumentsStage.tsx +++ b/app/src/components/blocks/deleteDocumentsStage/DeleteDocumentsStage.tsx @@ -100,8 +100,7 @@ function DeleteDocumentsStage({ }; const handleNoOption = () => { - const isGp = role === REPOSITORY_ROLE.GP_ADMIN || role === REPOSITORY_ROLE.GP_CLINICAL; - if (isGp && setStage) { + if (role === REPOSITORY_ROLE.GP_ADMIN && setStage) { setStage(LG_RECORD_STAGE.RECORD); } else if (role === REPOSITORY_ROLE.PCSE && setIsDeletingDocuments) { setIsDeletingDocuments(false); diff --git a/app/src/components/blocks/lloydGeorgeRecordDetails/LloydGeorgeRecordDetails.test.tsx b/app/src/components/blocks/lloydGeorgeRecordDetails/LloydGeorgeRecordDetails.test.tsx index ac917d0c3..fd6971d0c 100644 --- a/app/src/components/blocks/lloydGeorgeRecordDetails/LloydGeorgeRecordDetails.test.tsx +++ b/app/src/components/blocks/lloydGeorgeRecordDetails/LloydGeorgeRecordDetails.test.tsx @@ -23,83 +23,38 @@ describe('LloydGeorgeRecordDetails', () => { afterEach(() => { jest.clearAllMocks(); }); + describe('Rendering', () => { + it('renders the record details component', () => { + renderComponent(); - it('renders the record details component', () => { - renderComponent(); - - expect(screen.getByText(`Last updated: ${mockPdf.last_updated}`)).toBeInTheDocument(); - expect(screen.getByText(`${mockPdf.number_of_files} files`)).toBeInTheDocument(); - expect( - screen.getByText(`File size: ${formatFileSize(mockPdf.total_file_size_in_byte)}`), - ).toBeInTheDocument(); - expect(screen.getByText('File format: PDF')).toBeInTheDocument(); - }); - - it('renders record details actions menu', async () => { - renderComponent(); - - expect(screen.getByText(`Select an action...`)).toBeInTheDocument(); - expect(screen.getByTestId('actions-menu')).toBeInTheDocument(); - actionLinks.forEach((action) => { - expect(screen.queryByText(action.label)).not.toBeInTheDocument(); - }); - - act(() => { - userEvent.click(screen.getByTestId('actions-menu')); - }); - await waitFor(async () => { - actionLinks.forEach((action) => { - expect(screen.getByText(action.label)).toBeInTheDocument(); - }); - }); - }); - - it.each(actionLinks)("renders actionLink '$label'", async (action) => { - renderComponent(); - - expect(screen.getByText(`Select an action...`)).toBeInTheDocument(); - expect(screen.getByTestId('actions-menu')).toBeInTheDocument(); - - act(() => { - userEvent.click(screen.getByTestId('actions-menu')); - }); - await waitFor(async () => { - expect(screen.getByText(action.label)).toBeInTheDocument(); + expect(screen.getByText(`Last updated: ${mockPdf.last_updated}`)).toBeInTheDocument(); + expect(screen.getByText(`${mockPdf.number_of_files} files`)).toBeInTheDocument(); + expect( + screen.getByText(`File size: ${formatFileSize(mockPdf.total_file_size_in_byte)}`), + ).toBeInTheDocument(); + expect(screen.getByText('File format: PDF')).toBeInTheDocument(); }); - }); - it.each(actionLinks)( - "navigates to expected stage when action '$label' is clicked", - async (action) => { + it('renders record details actions menu', async () => { renderComponent(); expect(screen.getByText(`Select an action...`)).toBeInTheDocument(); expect(screen.getByTestId('actions-menu')).toBeInTheDocument(); - - act(() => { - userEvent.click(screen.getByTestId('actions-menu')); - }); - await waitFor(async () => { - expect(screen.getByText(action.label)).toBeInTheDocument(); + actionLinks.forEach((action) => { + expect(screen.queryByText(action.label)).not.toBeInTheDocument(); }); act(() => { - userEvent.click(screen.getByText(action.label)); + userEvent.click(screen.getByTestId('actions-menu')); }); await waitFor(async () => { - expect(mockSetStaqe).toHaveBeenCalledWith(action.stage); + actionLinks.forEach((action) => { + expect(screen.getByText(action.label)).toBeInTheDocument(); + }); }); - }, - ); - - const unauthorisedLinks = actionLinks.filter((a) => Array.isArray(a.unauthorised)); - - it.each(unauthorisedLinks)( - "does not render actionLink '$label' if role is unauthorised", - async (action) => { - const [unauthorisedRole] = action.unauthorised ?? [REPOSITORY_ROLE.GP_CLINICAL]; - mockedUseRole.mockReturnValue(unauthorisedRole); + }); + it.each(actionLinks)("renders actionLink '$label'", async (action) => { renderComponent(); expect(screen.getByText(`Select an action...`)).toBeInTheDocument(); @@ -109,10 +64,67 @@ describe('LloydGeorgeRecordDetails', () => { userEvent.click(screen.getByTestId('actions-menu')); }); await waitFor(async () => { - expect(screen.queryByText(action.label)).not.toBeInTheDocument(); + expect(screen.getByText(action.label)).toBeInTheDocument(); }); - }, - ); + }); + }); + + describe('Navigation', () => { + it.each(actionLinks)( + "navigates to '$stage' when action '$label' is clicked", + async (action) => { + renderComponent(); + + expect(screen.getByText(`Select an action...`)).toBeInTheDocument(); + expect(screen.getByTestId('actions-menu')).toBeInTheDocument(); + + act(() => { + userEvent.click(screen.getByTestId('actions-menu')); + }); + await waitFor(async () => { + expect(screen.getByText(action.label)).toBeInTheDocument(); + }); + + act(() => { + userEvent.click(screen.getByText(action.label)); + }); + await waitFor(async () => { + expect(mockSetStaqe).toHaveBeenCalledWith(action.stage); + }); + }, + ); + }); + + describe('Unauthorised', () => { + const unauthorisedLinks = actionLinks.filter((a) => Array.isArray(a.unauthorised)); + + it.each(unauthorisedLinks)( + "does not render actionLink '$label' if role is unauthorised", + async (action) => { + const [unauthorisedRole] = action.unauthorised ?? []; + mockedUseRole.mockReturnValue(unauthorisedRole); + + renderComponent(); + + expect(screen.getByText(`Select an action...`)).toBeInTheDocument(); + expect(screen.getByTestId('actions-menu')).toBeInTheDocument(); + + act(() => { + userEvent.click(screen.getByTestId('actions-menu')); + }); + await waitFor(async () => { + expect(screen.queryByText(action.label)).not.toBeInTheDocument(); + }); + }, + ); + + it.each(unauthorisedLinks)( + "does not render actionLink '$label' for GP Clinical Role", + async (action) => { + expect(action.unauthorised).toContain(REPOSITORY_ROLE.GP_CLINICAL); + }, + ); + }); }); const TestApp = (props: Omit) => { From 84cc6d86e46776775c58a5055a08ce7924f1f1f1 Mon Sep 17 00:00:00 2001 From: Rio Knightley Date: Wed, 15 Nov 2023 15:32:10 +0000 Subject: [PATCH 14/18] Add GP clinical denied test to delete document stage --- .../DeleteDocumentsStage.test.tsx | 61 ++++++++++++++----- .../DeleteDocumentsStage.tsx | 11 ++-- 2 files changed, 53 insertions(+), 19 deletions(-) diff --git a/app/src/components/blocks/deleteDocumentsStage/DeleteDocumentsStage.test.tsx b/app/src/components/blocks/deleteDocumentsStage/DeleteDocumentsStage.test.tsx index fb16bc73c..2135edca9 100644 --- a/app/src/components/blocks/deleteDocumentsStage/DeleteDocumentsStage.test.tsx +++ b/app/src/components/blocks/deleteDocumentsStage/DeleteDocumentsStage.test.tsx @@ -60,23 +60,20 @@ describe('DeleteDocumentsStage', () => { }, ); - 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); + it('renders DocumentSearchResults when No is selected and Continue clicked, when user role is GP Admin', async () => { + mockedUseRole.mockReturnValue(REPOSITORY_ROLE.GP_ADMIN); - renderComponent(DOCUMENT_TYPE.LLOYD_GEORGE); + renderComponent(DOCUMENT_TYPE.LLOYD_GEORGE); - act(() => { - userEvent.click(screen.getByRole('radio', { name: 'No' })); - userEvent.click(screen.getByRole('button', { name: 'Continue' })); - }); + act(() => { + userEvent.click(screen.getByRole('radio', { name: 'No' })); + userEvent.click(screen.getByRole('button', { name: 'Continue' })); + }); - await waitFor(() => { - expect(mockSetStage).toHaveBeenCalledWith(LG_RECORD_STAGE.RECORD); - }); - }, - ); + 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); @@ -93,7 +90,22 @@ describe('DeleteDocumentsStage', () => { }); }); - it.each([REPOSITORY_ROLE.GP_ADMIN, REPOSITORY_ROLE.GP_CLINICAL, REPOSITORY_ROLE.PCSE])( + it('does not render a view DocumentSearchResults when No is selected and Continue clicked, when user role is GP Clinical', async () => { + mockedUseRole.mockReturnValue(REPOSITORY_ROLE.GP_ADMIN); + + renderComponent(DOCUMENT_TYPE.LLOYD_GEORGE); + + act(() => { + userEvent.click(screen.getByRole('radio', { name: 'No' })); + userEvent.click(screen.getByRole('button', { name: 'Continue' })); + }); + + await waitFor(() => { + expect(mockSetStage).toHaveBeenCalledTimes(0); + }); + }); + + it.each([REPOSITORY_ROLE.GP_ADMIN, REPOSITORY_ROLE.PCSE])( "renders DeletionConfirmationStage when the Yes is selected and Continue clicked, when user role is '%s'", async (role) => { mockedAxios.delete.mockReturnValue( @@ -117,6 +129,25 @@ describe('DeleteDocumentsStage', () => { }, ); + it('does not render DeletionConfirmationStage when the Yes is selected, Continue clicked, and user role is GP Clinical', async () => { + mockedAxios.delete.mockReturnValue(Promise.resolve({ status: 200, data: 'Success' })); + mockedUseRole.mockReturnValue(REPOSITORY_ROLE.GP_CLINICAL); + + renderComponent(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.queryByText('Deletion complete')).not.toBeInTheDocument(); + }); + }); + it('renders a service error when service is down', async () => { const errorResponse = { response: { diff --git a/app/src/components/blocks/deleteDocumentsStage/DeleteDocumentsStage.tsx b/app/src/components/blocks/deleteDocumentsStage/DeleteDocumentsStage.tsx index 3ba14665f..152822957 100644 --- a/app/src/components/blocks/deleteDocumentsStage/DeleteDocumentsStage.tsx +++ b/app/src/components/blocks/deleteDocumentsStage/DeleteDocumentsStage.tsx @@ -108,10 +108,13 @@ function DeleteDocumentsStage({ }; const submit = async (fieldValues: FieldValues) => { - if (fieldValues.deleteDocs === DELETE_DOCUMENTS_OPTION.YES) { - await handleYesOption(); - } else if (fieldValues.deleteDocs === DELETE_DOCUMENTS_OPTION.NO) { - handleNoOption(); + const allowedRoles = [REPOSITORY_ROLE.GP_ADMIN, REPOSITORY_ROLE.PCSE]; + if (role && allowedRoles.includes(role)) { + if (fieldValues.deleteDocs === DELETE_DOCUMENTS_OPTION.YES) { + await handleYesOption(); + } else if (fieldValues.deleteDocs === DELETE_DOCUMENTS_OPTION.NO) { + handleNoOption(); + } } }; From 273887f025041631b0b495dc1c89e7c54818b6b3 Mon Sep 17 00:00:00 2001 From: Rio Knightley Date: Mon, 20 Nov 2023 09:28:33 +0000 Subject: [PATCH 15/18] Remove todo comments --- .../blocks/deleteDocumentsStage/DeleteDocumentsStage.tsx | 4 ---- .../deletionConfirmationStage/DeletionConfirmationStage.tsx | 4 ---- .../LloydGeorgeDownloadAllStage.tsx | 6 +----- 3 files changed, 1 insertion(+), 13 deletions(-) diff --git a/app/src/components/blocks/deleteDocumentsStage/DeleteDocumentsStage.tsx b/app/src/components/blocks/deleteDocumentsStage/DeleteDocumentsStage.tsx index 152822957..ef7140969 100644 --- a/app/src/components/blocks/deleteDocumentsStage/DeleteDocumentsStage.tsx +++ b/app/src/components/blocks/deleteDocumentsStage/DeleteDocumentsStage.tsx @@ -20,10 +20,6 @@ import useRole from '../../../helpers/hooks/useRole'; import { REPOSITORY_ROLE } from '../../../types/generic/authRole'; import { LG_RECORD_STAGE } from '../../../types/blocks/lloydGeorgeStages'; -/** - * TODO: REMOVE GP CLINICAL FROM COMPONENT & TESTS - */ - export type Props = { docType: DOCUMENT_TYPE; numberOfFiles: number; diff --git a/app/src/components/blocks/deletionConfirmationStage/DeletionConfirmationStage.tsx b/app/src/components/blocks/deletionConfirmationStage/DeletionConfirmationStage.tsx index 708909485..aec4bd13b 100644 --- a/app/src/components/blocks/deletionConfirmationStage/DeletionConfirmationStage.tsx +++ b/app/src/components/blocks/deletionConfirmationStage/DeletionConfirmationStage.tsx @@ -15,10 +15,6 @@ export type Props = { setStage?: Dispatch>; }; -/** - * TODO: REMOVE GP CLINICAL FROM COMPONENT & TESTS - */ - function DeletionConfirmationStage({ numberOfFiles, patientDetails, setStage }: Props) { const navigate = useNavigate(); const nhsNumber: string = patientDetails?.nhsNumber || ''; diff --git a/app/src/components/blocks/lloydGeorgeDownloadAllStage/LloydGeorgeDownloadAllStage.tsx b/app/src/components/blocks/lloydGeorgeDownloadAllStage/LloydGeorgeDownloadAllStage.tsx index 9444699f0..d5c0dbbae 100644 --- a/app/src/components/blocks/lloydGeorgeDownloadAllStage/LloydGeorgeDownloadAllStage.tsx +++ b/app/src/components/blocks/lloydGeorgeDownloadAllStage/LloydGeorgeDownloadAllStage.tsx @@ -9,19 +9,15 @@ import React, { } from 'react'; import { Card } from 'nhsuk-react-components'; import { Link } from 'react-router-dom'; -import { LG_RECORD_STAGE } from '../../../pages/lloydGeorgeRecordPage/LloydGeorgeRecordPage'; import { PatientDetails } from '../../../types/generic/patientDetails'; import { useBaseAPIUrl } from '../../../providers/configProvider/ConfigProvider'; import useBaseAPIHeaders from '../../../helpers/hooks/useBaseAPIHeaders'; import getPresignedUrlForZip from '../../../helpers/requests/getPresignedUrlForZip'; import { DOCUMENT_TYPE } from '../../../types/pages/UploadDocumentsPage/types'; import LgDownloadComplete from '../lloydGeorgeDownloadComplete/LloydGeorgeDownloadComplete'; +import { LG_RECORD_STAGE } from '../../../types/blocks/lloydGeorgeStages'; const FakeProgress = require('fake-progress'); -/** - * TODO: REMOVE GP CLINICAL FROM COMPONENT & TESTS - * - */ export type Props = { numberOfFiles: number; setStage: Dispatch>; From 05366e9083ac1e23a7fbe0f873708b372562a6e5 Mon Sep 17 00:00:00 2001 From: Rio Knightley Date: Mon, 20 Nov 2023 09:51:14 +0000 Subject: [PATCH 16/18] Add role guard test --- app/src/router/AppRouter.tsx | 11 ++-- .../guards/roleGuard/RoleGuard.test.tsx | 59 +++++++++++++++++++ app/src/router/guards/roleGuard/RoleGuard.tsx | 9 ++- 3 files changed, 70 insertions(+), 9 deletions(-) create mode 100644 app/src/router/guards/roleGuard/RoleGuard.test.tsx diff --git a/app/src/router/AppRouter.tsx b/app/src/router/AppRouter.tsx index 8886904a4..200c28608 100644 --- a/app/src/router/AppRouter.tsx +++ b/app/src/router/AppRouter.tsx @@ -70,34 +70,37 @@ export const routeMap: Routes = { [DOWNLOAD_SEARCH]: { page: , type: ROUTE_TYPE.PRIVATE, - unauthorized: [REPOSITORY_ROLE.PCSE], + unauthorized: [REPOSITORY_ROLE.GP_ADMIN, REPOSITORY_ROLE.GP_CLINICAL], }, [UPLOAD_SEARCH]: { page: , type: ROUTE_TYPE.PRIVATE, - unauthorized: [REPOSITORY_ROLE.GP_ADMIN, REPOSITORY_ROLE.GP_CLINICAL], + unauthorized: [REPOSITORY_ROLE.PCSE], }, [DOWNLOAD_VERIFY]: { page: , type: ROUTE_TYPE.PATIENT, - unauthorized: [REPOSITORY_ROLE.PCSE], + unauthorized: [REPOSITORY_ROLE.GP_ADMIN, REPOSITORY_ROLE.GP_CLINICAL], }, [UPLOAD_VERIFY]: { page: , type: ROUTE_TYPE.PATIENT, - unauthorized: [REPOSITORY_ROLE.GP_ADMIN, REPOSITORY_ROLE.GP_CLINICAL], + unauthorized: [REPOSITORY_ROLE.PCSE], }, [UPLOAD_DOCUMENTS]: { page: , type: ROUTE_TYPE.PATIENT, + unauthorized: [REPOSITORY_ROLE.PCSE], }, [DOWNLOAD_DOCUMENTS]: { page: , type: ROUTE_TYPE.PATIENT, + unauthorized: [REPOSITORY_ROLE.GP_ADMIN, REPOSITORY_ROLE.GP_CLINICAL], }, [LLOYD_GEORGE]: { page: , type: ROUTE_TYPE.PATIENT, + unauthorized: [REPOSITORY_ROLE.PCSE], }, }; diff --git a/app/src/router/guards/roleGuard/RoleGuard.test.tsx b/app/src/router/guards/roleGuard/RoleGuard.test.tsx new file mode 100644 index 000000000..805f25904 --- /dev/null +++ b/app/src/router/guards/roleGuard/RoleGuard.test.tsx @@ -0,0 +1,59 @@ +import { render, waitFor } from '@testing-library/react'; +import * as ReactRouter from 'react-router'; +import { History, createMemoryHistory } from 'history'; +import { routes } from '../../../types/generic/routes'; +import RoleGuard from './RoleGuard'; +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 guardPage = routes.LLOYD_GEORGE; +describe('RoleGuard', () => { + beforeEach(() => { + process.env.REACT_APP_ENVIRONMENT = 'jest'; + }); + afterEach(() => { + jest.clearAllMocks(); + }); + it('navigates user to unauthorised when role is not accepted', async () => { + const history = createMemoryHistory({ + initialEntries: [guardPage], + initialIndex: 0, + }); + + mockedUseRole.mockReturnValue(REPOSITORY_ROLE.PCSE); + expect(history.location.pathname).toBe(guardPage); + renderAuthGuard(history); + + await waitFor(async () => { + expect(history.location.pathname).toBe(routes.UNAUTHORISED); + }); + }); + + it('navigates user to correct page when role is accepted', async () => { + const history = createMemoryHistory({ + initialEntries: [guardPage], + initialIndex: 0, + }); + + mockedUseRole.mockReturnValue(REPOSITORY_ROLE.GP_ADMIN); + expect(history.location.pathname).toBe(guardPage); + renderAuthGuard(history); + + await waitFor(async () => { + expect(history.location.pathname).toBe(guardPage); + }); + }); +}); + +const renderAuthGuard = (history: History) => { + return render( + + +
+ + , + ); +}; diff --git a/app/src/router/guards/roleGuard/RoleGuard.tsx b/app/src/router/guards/roleGuard/RoleGuard.tsx index c57dc8521..21dcaf20d 100644 --- a/app/src/router/guards/roleGuard/RoleGuard.tsx +++ b/app/src/router/guards/roleGuard/RoleGuard.tsx @@ -1,24 +1,23 @@ import { useEffect, type ReactNode } from 'react'; -import { REPOSITORY_ROLE } from '../../../types/generic/authRole'; import { useNavigate } from 'react-router'; import { useLocation } from 'react-router-dom'; import { routes } from '../../../types/generic/routes'; import { routeMap } from '../../AppRouter'; +import useRole from '../../../helpers/hooks/useRole'; type Props = { children: ReactNode; }; function RoleGuard({ children }: Props) { - const role = REPOSITORY_ROLE.PCSE; + const role = useRole(); const navigate = useNavigate(); const location = useLocation(); useEffect(() => { const routeKey = location.pathname as keyof typeof routeMap; const { unauthorized } = routeMap[routeKey]; - const denyResource = Array.isArray(unauthorized) && unauthorized.includes(role); - console.log(unauthorized); - console.log('DENY RESOURCE?:', denyResource); + const denyResource = Array.isArray(unauthorized) && role && unauthorized.includes(role); + if (denyResource) { navigate(routes.UNAUTHORISED); } From 8c6f9f36ceb2f79c2de56c3890ce74015c130b5a Mon Sep 17 00:00:00 2001 From: Rio Knightley Date: Mon, 20 Nov 2023 09:57:53 +0000 Subject: [PATCH 17/18] Fix import bug --- .../blocks/lloydGeorgeRecordStage/LloydGeorgeRecordStage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/components/blocks/lloydGeorgeRecordStage/LloydGeorgeRecordStage.tsx b/app/src/components/blocks/lloydGeorgeRecordStage/LloydGeorgeRecordStage.tsx index 7b2416bca..a05ddfd4b 100644 --- a/app/src/components/blocks/lloydGeorgeRecordStage/LloydGeorgeRecordStage.tsx +++ b/app/src/components/blocks/lloydGeorgeRecordStage/LloydGeorgeRecordStage.tsx @@ -4,9 +4,9 @@ import { BackLink, Card, Details } from 'nhsuk-react-components'; import { getFormattedDate } from '../../../helpers/utils/formatDate'; import { DOWNLOAD_STAGE } from '../../../types/generic/downloadStage'; import PdfViewer from '../../generic/pdfViewer/PdfViewer'; -import { LG_RECORD_STAGE } from '../../../pages/lloydGeorgeRecordPage/LloydGeorgeRecordPage'; import LloydGeorgeRecordDetails from '../lloydGeorgeRecordDetails/LloydGeorgeRecordDetails'; import { formatNhsNumber } from '../../../helpers/utils/formatNhsNumber'; +import { LG_RECORD_STAGE } from '../../../types/blocks/lloydGeorgeStages'; export type Props = { patientDetails: PatientDetails; From b366930a0e3673e091e6af587aae54adb1e09a14 Mon Sep 17 00:00:00 2001 From: Rio Knightley Date: Mon, 20 Nov 2023 10:15:31 +0000 Subject: [PATCH 18/18] Remove unused package --- app/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/app/package.json b/app/package.json index ff7226d63..22af3450e 100644 --- a/app/package.json +++ b/app/package.json @@ -32,7 +32,6 @@ "react-hook-form": "^7.45.4", "react-router": "^6.14.2", "react-router-dom": "^6.14.2", - "react-router-to-array": "^0.1.3", "react-scripts": "^5.0.1", "sass": "^1.66.1", "serve": "^14.2.1",