Skip to content

Commit a68a216

Browse files
Fix: [AEA-4755] - PUT /selectedRole (#384)
## Summary 🎫 [AEA-4755](https://nhsd-jira.digital.nhs.uk/browse/AEA-4755) PUT /selectedRole 🧪 Regression Tests: NHSDigital/electronic-prescription-service-api-regression-tests#237 👤 Audrey Ricker: 555043304334 `CONFIDENTIAL: PERSONAL PATIENT DATA accessed by RICKER, Audrey - General Medical Practitioner - NO_ORG_NAME (ODS: N82668)` 👤 Curtis Rogers: 555043308597 `CONFIDENTIAL: PERSONAL PATIENT DATA accessed by ROGERS, Curtis - Registration Authority Manager - NO_ORG_NAME (ODS: A21293)` - Routine Change ### Details of the Fix - If the role with access matches `selectedRoleId`, it is added to the `currently_selected_role` but not to the `roles_with_access` array. Move the previously selected role back into rolesWithAccess, but only if it was set. - The RBAC (Role-Based Access Control) User Profile Banner follows these patterns: - Standard user: CONFIDENTIAL: PERSONAL PATIENT DATA accessed by LAST NAME, First Name - RBAC Role - Site Name (ODS: ODS) - Locum user: CONFIDENTIAL: PERSONAL PATIENT DATA accessed by LAST NAME, First Name - RBAC Role - Locum pharmacy (ODS: FFFFF) - Site Name (ODS: ODS Code) - The `confirmButtonText` contains the text: "Continue to find a prescription" --------- Co-authored-by: Sean Steberis <[email protected]>
1 parent 4a5308e commit a68a216

27 files changed

+1011
-366
lines changed

package-lock.json

Lines changed: 1 addition & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/cpt-ui/__tests__/ChangeRolePage.test.tsx

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ jest.mock("@/constants/ui-strings/CardStrings", () => {
1616
"You are currently logged in at GREENE'S PHARMACY (ODS: FG419) with Health Professional Access Role.",
1717
},
1818
confirmButton: {
19-
text: "Confirm and continue to find a prescription",
20-
link: "tracker-presc-no",
19+
text: "Continue to find a prescription",
20+
link: "searchforaprescription",
2121
},
2222
alternativeMessage: "Alternatively, you can choose a new role below.",
2323
organisation: "Organisation",
@@ -280,4 +280,58 @@ describe("ChangeRolePage", () => {
280280
expect(mockPush).toHaveBeenCalledWith("/searchforaprescription")
281281
})
282282
})
283+
284+
it("renders loading state when waiting for API response", async () => {
285+
mockFetch.mockImplementation(() => new Promise(() => {}))
286+
renderWithAuth()
287+
expect(screen.getByText("Loading...")).toBeInTheDocument()
288+
})
289+
290+
it("redirects when a single role is available", async () => {
291+
(useRouter as jest.Mock).mockReturnValue({
292+
push: jest.fn()
293+
})
294+
295+
const mockUserInfo = {
296+
roles_with_access: [{
297+
role_name: "Pharmacist",
298+
org_name: "Test Pharmacy",
299+
org_code: "ORG123",
300+
site_address: "123 Test St"
301+
}],
302+
roles_without_access: []
303+
}
304+
305+
mockFetch.mockResolvedValue({
306+
status: 200,
307+
json: async () => ({userInfo: mockUserInfo})
308+
})
309+
310+
renderWithAuth({isSignedIn: true, idToken: {toString: jest.fn().mockReturnValue("mock-id-token")}})
311+
312+
await waitFor(() => {
313+
expect(useRouter().push).toHaveBeenCalledWith("/searchforaprescription")
314+
})
315+
})
316+
317+
it("does not fetch user roles if user is not signed in", async () => {
318+
const mockFetch = jest.fn()
319+
global.fetch = mockFetch
320+
321+
renderWithAuth({isSignedIn: false}) // Simulating a user who is not signed in
322+
323+
expect(mockFetch).not.toHaveBeenCalled()
324+
})
325+
326+
it("displays an error when the API request fails", async () => {
327+
mockFetch.mockRejectedValue(new Error("Failed to fetch user roles"))
328+
329+
renderWithAuth({isSignedIn: true, idToken: {toString: jest.fn().mockReturnValue("mock-id-token")}})
330+
331+
await waitFor(() => {
332+
const errorSummary = screen.getByRole("heading", {name: "Error during role selection"})
333+
expect(errorSummary).toBeInTheDocument()
334+
expect(screen.getByText("Failed to fetch CPT user info")).toBeInTheDocument()
335+
})
336+
})
283337
})

packages/cpt-ui/__tests__/RBACBanner.test.tsx

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import "@testing-library/jest-dom"
2-
import { render, screen } from "@testing-library/react"
3-
import { useRouter } from "next/navigation"
2+
import {render, screen} from "@testing-library/react"
3+
import {useRouter} from "next/navigation"
44
import React from "react"
5-
import { JWT } from "aws-amplify/auth"
5+
import {JWT} from "aws-amplify/auth"
66

77
import RBACBanner from "@/components/RBACBanner"
88

@@ -19,10 +19,10 @@ jest.mock("@/constants/ui-strings/RBACBannerStrings", () => {
1919
LOCUM_NAME: "Locum pharmacy"
2020
}
2121

22-
return { RBAC_BANNER_STRINGS }
22+
return {RBAC_BANNER_STRINGS}
2323
})
2424

25-
const { RBAC_BANNER_STRINGS } = require("@/constants/ui-strings/RBACBannerStrings")
25+
const {RBAC_BANNER_STRINGS} = require("@/constants/ui-strings/RBACBannerStrings")
2626

2727
// Mock `next/navigation`
2828
jest.mock("next/navigation", () => ({
@@ -46,8 +46,8 @@ jest.mock("@/context/AccessProvider", () => {
4646
org_name: "org name"
4747
},
4848
userDetails: {
49-
given_name: "Jane",
50-
family_name: "Doe"
49+
given_name: "JaNe",
50+
family_name: "DoE"
5151
},
5252
setUserDetails: jest.fn(),
5353
setSelectedRole: jest.fn(),
@@ -57,7 +57,7 @@ jest.mock("@/context/AccessProvider", () => {
5757
const useAccess = () => React.useContext(MockAccessContext)
5858

5959
const __setMockContextValue = (newValue: any) => {
60-
mockContextValue = { ...mockContextValue, ...newValue }
60+
mockContextValue = {...mockContextValue, ...newValue}
6161
// Reassign the context’s defaultValue so subsequent consumers get new values
6262
MockAccessContext._currentValue = mockContextValue
6363
MockAccessContext._currentValue2 = mockContextValue
@@ -70,7 +70,7 @@ jest.mock("@/context/AccessProvider", () => {
7070
__setMockContextValue
7171
}
7272
})
73-
const { __setMockContextValue } = require("@/context/AccessProvider")
73+
const {__setMockContextValue} = require("@/context/AccessProvider")
7474

7575
// Mock an AuthContext
7676
const AuthContext = React.createContext<any>(null)
@@ -90,7 +90,7 @@ const defaultAuthContext = {
9090
}
9191

9292
export const renderWithAuth = (authOverrides = {}) => {
93-
const authValue = { ...defaultAuthContext, ...authOverrides }
93+
const authValue = {...defaultAuthContext, ...authOverrides}
9494

9595
return render(
9696
<AuthContext.Provider value={authValue}>
@@ -112,7 +112,7 @@ describe("RBACBanner", () => {
112112
org_name: "org name"
113113
},
114114
userDetails: {
115-
family_name: "Doe",
115+
family_name: "DoE",
116116
given_name: "Jane"
117117
}
118118
})
@@ -145,7 +145,7 @@ describe("RBACBanner", () => {
145145
expect(bannerText).toBeInTheDocument()
146146

147147
// Check that placeholders are properly replaced
148-
const expectedText = `CONFIDENTIAL: PERSONAL PATIENT DATA accessed by Doe, Jane - Role Name - org name (ODS: deadbeef)`
148+
const expectedText = `CONFIDENTIAL: PERSONAL PATIENT DATA accessed by DOE, Jane - Role Name - org name (ODS: deadbeef)`
149149
expect(bannerText).toHaveTextContent(expectedText)
150150
})
151151

@@ -165,7 +165,7 @@ describe("RBACBanner", () => {
165165
const bannerText = screen.getByTestId("rbac-banner-text")
166166
// Locum pharmacy name should appear
167167
expect(bannerText).toHaveTextContent(
168-
`CONFIDENTIAL: PERSONAL PATIENT DATA accessed by Doe, Jane - Role Name - Locum pharmacy (ODS: FFFFF)`
168+
`CONFIDENTIAL: PERSONAL PATIENT DATA accessed by DOE, Jane - Role Name - Locum pharmacy (ODS: FFFFF)`
169169
)
170170
})
171171

@@ -198,7 +198,25 @@ describe("RBACBanner", () => {
198198
const bannerText = screen.getByTestId("rbac-banner-text")
199199
// Notice fallback values: NO_ROLE_NAME, NO_ORG_NAME, NO_ODS_CODE
200200
expect(bannerText).toHaveTextContent(
201-
`CONFIDENTIAL: PERSONAL PATIENT DATA accessed by Doe, Jane - NO_ROLE_NAME - NO_ORG_NAME (ODS: NO_ODS_CODE)`
201+
`CONFIDENTIAL: PERSONAL PATIENT DATA accessed by DOE, Jane - NO_ROLE_NAME - NO_ORG_NAME (ODS: NO_ODS_CODE)`
202+
)
203+
})
204+
205+
it("should fallback to NO_ORG_NAME if org_name is missing", () => {
206+
__setMockContextValue({
207+
selectedRole: {
208+
role_name: "Role Name",
209+
role_id: "role-id",
210+
org_code: "deadbeef"
211+
// org_name is missing
212+
}
213+
})
214+
215+
renderWithAuth()
216+
217+
const bannerText = screen.getByTestId("rbac-banner-text")
218+
expect(bannerText).toHaveTextContent(
219+
`CONFIDENTIAL: PERSONAL PATIENT DATA accessed by DOE, Jane - Role Name - NO_ORG_NAME (ODS: deadbeef)`
202220
)
203221
})
204222

packages/cpt-ui/__tests__/SelectYourRolePage.test.tsx

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ jest.mock("@/constants/ui-strings/CardStrings", () => {
2020
"You are currently logged in at GREENE'S PHARMACY (ODS: FG419) with Health Professional Access Role.",
2121
},
2222
confirmButton: {
23-
text: "Confirm and continue to find a prescription",
24-
link: "tracker-presc-no",
23+
text: "Continue to find a prescription",
24+
link: "searchforaprescription",
2525
},
2626
alternativeMessage: "Alternatively, you can choose a new role below.",
2727
organisation: "Organisation",
@@ -321,4 +321,25 @@ describe("SelectYourRolePage", () => {
321321
expect(useRouter().push).toHaveBeenCalledWith("/searchforaprescription")
322322
})
323323
})
324+
325+
it("does not fetch user roles if user is not signed in", async () => {
326+
const mockFetch = jest.fn()
327+
global.fetch = mockFetch
328+
329+
renderWithAuth({isSignedIn: false}) // Simulating a user who is not signed in
330+
331+
expect(mockFetch).not.toHaveBeenCalled()
332+
})
333+
334+
it("displays an error when the API request fails", async () => {
335+
mockFetch.mockRejectedValue(new Error("Failed to fetch user roles"))
336+
337+
renderWithAuth({isSignedIn: true, idToken: {toString: jest.fn().mockReturnValue("mock-id-token")}})
338+
339+
await waitFor(() => {
340+
const errorSummary = screen.getByRole("heading", {name: "Error during role selection"})
341+
expect(errorSummary).toBeInTheDocument()
342+
expect(screen.getByText("Failed to fetch CPT user info")).toBeInTheDocument()
343+
})
344+
})
324345
})

packages/cpt-ui/__tests__/YourSelectedRole.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jest.mock("@/constants/ui-strings/YourSelectedRoleStrings", () => {
1515
roleLabel: "Role",
1616
orgLabel: "Organisation",
1717
changeLinkText: "Change",
18-
confirmButtonText: "Confirm and continue to find a prescription",
18+
confirmButtonText: "Continue to find a prescription",
1919
noODSCode: "NO ODS CODE",
2020
noRoleName: "NO ROLE NAME",
2121
noOrgName: "NO ORG NAME"

packages/cpt-ui/app/confirmrole/page.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client'
2-
import React from "react";
2+
import React from "react"
33

4-
import { Button, Card, Container, Col, InsetText, Row } from "nhsuk-react-components";
4+
import {Button, Card, Container, Col, InsetText, Row} from "nhsuk-react-components"
55
export default function Page() {
66
return (
77
<main className="nhsuk-main-wrapper">
@@ -26,7 +26,7 @@ export default function Page() {
2626
</p>
2727
</InsetText>
2828
<Button>
29-
Confirm and continue to find a prescription
29+
Continue to find a prescription
3030
</Button>
3131
<p>Alternatively, you can choose a new role below.</p>
3232

Lines changed: 26 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,53 @@
11
'use client'
2-
import React, { useContext, useEffect } from "react";
3-
import { Container } from "nhsuk-react-components"
4-
import Link from "next/link";
2+
import React, {useContext, useEffect} from "react"
3+
import {Container} from "nhsuk-react-components"
4+
import Link from "next/link"
55

6-
import { AuthContext } from "@/context/AuthProvider";
7-
import { useAccess } from "@/context/AccessProvider"
8-
import EpsSpinner from "@/components/EpsSpinner";
9-
import { EpsLogoutStrings } from "@/constants/ui-strings/EpsLogoutPageStrings";
6+
import {AuthContext} from "@/context/AuthProvider"
7+
import {useAccess} from "@/context/AccessProvider"
8+
import EpsSpinner from "@/components/EpsSpinner"
9+
import {EpsLogoutStrings} from "@/constants/ui-strings/EpsLogoutPageStrings"
1010

1111
export default function LogoutPage() {
12-
13-
const auth = useContext(AuthContext);
14-
const { clear } = useAccess();
12+
const auth = useContext(AuthContext)
13+
const {clear} = useAccess()
1514

1615
// Log out on page load
1716
useEffect(() => {
1817
const signOut = async () => {
19-
console.log("Signing out", auth);
18+
console.log("Signing out", auth)
19+
20+
await auth?.cognitoSignOut()
2021

21-
await auth?.cognitoSignOut();
22-
console.log("Signed out: ", auth);
22+
// Ensure user details & roles are cleared from local storage
23+
clear()
24+
console.log("Signed out and cleared session data")
2325
}
2426

2527
if (auth?.isSignedIn) {
26-
signOut();
27-
clear();
28+
signOut()
2829
} else {
29-
console.log("Cannot sign out - not signed in");
30+
console.log("Cannot sign out - not signed in")
31+
clear() // Clear data even if not signed in
3032
}
31-
}, [auth, clear]);
33+
}, [auth, clear])
3234

3335
return (
3436
<main id="main-content" className="nhsuk-main-wrapper">
3537
<Container>
36-
{auth?.isSignedIn ? (
38+
{!auth?.isSignedIn ? (
3739
<>
38-
<h1>{EpsLogoutStrings.loading}</h1>
39-
<EpsSpinner />
40+
<h1>{EpsLogoutStrings.title}</h1>
41+
<p>{EpsLogoutStrings.body}</p>
42+
<Link href="/login">{EpsLogoutStrings.login_link}</Link>
4043
</>
4144
) : (
4245
<>
43-
<h1>{EpsLogoutStrings.title}</h1>
44-
<div>{EpsLogoutStrings.body}</div>
45-
<p />
46-
<Link href="/login">
47-
{EpsLogoutStrings.login_link}
48-
</Link>
46+
<h1>{EpsLogoutStrings.loading}</h1>
47+
<EpsSpinner />
4948
</>
5049
)}
5150
</Container>
5251
</main>
53-
);
52+
)
5453
}

packages/cpt-ui/components/EpsRoleSelectionPage.tsx

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -133,12 +133,20 @@ export default function RoleSelectionPage({contentText}: RoleSelectionPageProps)
133133
const rolesWithAccess = userInfo.roles_with_access || []
134134
const rolesWithoutAccess = userInfo.roles_without_access || []
135135

136-
const selectedRole = userInfo?.currently_selected_role
137-
? {
138-
...userInfo?.currently_selected_role,
139-
uuid: `selected_role_0`
140-
}
141-
: undefined
136+
// Check if the user info object and currently_selected_role exist
137+
const selectedRole =
138+
userInfo?.currently_selected_role && Object.keys(userInfo.currently_selected_role).length > 0
139+
? {
140+
// If currently_selected_role is not empty, spread its properties
141+
...userInfo.currently_selected_role,
142+
// Add uuid only if the selected role is not an empty object
143+
uuid: `selected_role_0`
144+
}
145+
// If currently_selected_role is an empty object `{}`, set selectedRole to undefined
146+
: undefined
147+
148+
console.log("Selected role:", selectedRole)
149+
setSelectedRole(selectedRole)
142150

143151
// Populate the EPS card props
144152
setRolesWithAccess(
@@ -158,7 +166,6 @@ export default function RoleSelectionPage({contentText}: RoleSelectionPageProps)
158166
}))
159167
)
160168

161-
setSelectedRole(selectedRole)
162169
setNoAccess(rolesWithAccess.length === 0)
163170
setSingleAccess(rolesWithAccess.length === 1)
164171

0 commit comments

Comments
 (0)