Skip to content

Commit a7e6b86

Browse files
authored
New: [AEA-4655] - User has no access to CPTS (#282)
## Summary 🎫 [AEA-4655](https://nhsd-jira.digital.nhs.uk/browse/AEA-4655) User has no access to CPTS 🧪 Regression Tests: [PR-208](NHSDigital/electronic-prescription-service-api-regression-tests#208) 🆔 User UID: 555083343101 ✨ New Feature ### Details User has no access to CPTS
1 parent 18814e3 commit a7e6b86

File tree

9 files changed

+198
-112
lines changed

9 files changed

+198
-112
lines changed

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

Lines changed: 69 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,19 @@ import {render, screen, waitFor} from "@testing-library/react"
33
import {useRouter} from 'next/navigation'
44
import React from "react"
55
import SelectYourRolePage from "@/app/selectyourrole/page"
6+
import {AccessProvider} from "@/context/AccessProvider"
67
import {AuthContext} from "@/context/AuthProvider"
7-
8-
// Mock the card strings, so we have known text for the tests
8+
import {SELECT_YOUR_ROLE_PAGE_TEXT} from "@/constants/ui-strings/CardStrings"
99

1010
// Mock the module and directly reference the variable
1111
jest.mock("@/constants/ui-strings/CardStrings", () => {
1212
const SELECT_YOUR_ROLE_PAGE_TEXT = {
1313
title: "Select your role",
1414
caption: "Select the role you wish to use to access the service.",
15+
titleNoAccess: "No access to the clinical prescription tracking service",
16+
captionNoAccess:
17+
"None of the roles on your Smartcard or other authenticators allow you to access the clinical prescription tracking service. " +
18+
"Contact your Registration Authority representative to obtain the correct code.",
1519
insetText: {
1620
visuallyHidden: "Information: ",
1721
message:
@@ -58,27 +62,39 @@ global.fetch = mockFetch
5862

5963
// Default mock values for the `AuthContext` to simulate authentication state
6064
const defaultAuthContext = {
61-
error: null, // No errors by default
62-
user: null, // User is initially null (not logged in)
63-
isSignedIn: false, // Default state is "not signed in"
64-
idToken: null, // No ID token available
65-
accessToken: null, // No access token available
66-
cognitoSignIn: jest.fn(), // Mock Cognito sign-in function
67-
cognitoSignOut: jest.fn(), // Mock Cognito sign-out function
65+
error: null,
66+
user: null,
67+
isSignedIn: false,
68+
idToken: null,
69+
accessToken: null,
70+
cognitoSignIn: jest.fn(),
71+
cognitoSignOut: jest.fn(),
72+
}
73+
74+
const defaultAccessContext = {
75+
rolesWithAccess: [],
76+
rolesWithoutAccess: [],
77+
loading: false,
78+
error: null,
6879
}
6980

70-
// Utility function to render the component with custom AuthContext overrides
71-
const renderWithAuth = (authOverrides = {}) => {
81+
// Utility function to render with both AuthProvider and AccessProvider
82+
const renderWithAuthAndAccess = (
83+
authOverrides = {},
84+
accessOverrides = {}
85+
) => {
7286
const authValue = {...defaultAuthContext, ...authOverrides}
87+
const accessValue = {...defaultAccessContext, ...accessOverrides}
88+
7389
return render(
7490
<AuthContext.Provider value={authValue}>
75-
<SelectYourRolePage />
91+
<AccessProvider>
92+
<SelectYourRolePage />
93+
</AccessProvider>
7694
</AuthContext.Provider>
7795
)
7896
}
7997

80-
import {SELECT_YOUR_ROLE_PAGE_TEXT} from "@/constants/ui-strings/CardStrings"
81-
8298
describe("SelectYourRolePage", () => {
8399
// Clear all mock calls before each test to avoid state leaks
84100
beforeEach(() => {
@@ -90,10 +106,12 @@ describe("SelectYourRolePage", () => {
90106
mockFetch.mockImplementation(() => new Promise(() => {}))
91107

92108
// Render the page with user signed in
93-
renderWithAuth({isSignedIn: true, idToken: "mock-id-token"})
109+
renderWithAuthAndAccess({isSignedIn: true, idToken: "mock-id-token"}, {loading: true})
94110

95111
// Verify that the loading text appears
96-
const loadingText = screen.getByText(SELECT_YOUR_ROLE_PAGE_TEXT.loadingMessage)
112+
const loadingText = screen.getByText(
113+
SELECT_YOUR_ROLE_PAGE_TEXT.loadingMessage
114+
)
97115
expect(loadingText).toBeInTheDocument()
98116
})
99117

@@ -102,11 +120,10 @@ describe("SelectYourRolePage", () => {
102120
mockFetch.mockResolvedValue({status: 500})
103121

104122
// Render the page with user signed in
105-
renderWithAuth({isSignedIn: true, idToken: "mock-id-token"})
123+
renderWithAuthAndAccess({isSignedIn: true, idToken: "mock-id-token"})
106124

107125
// Wait for the error message to appear
108126
await waitFor(() => {
109-
// Check for error summary heading
110127
const errorHeading = screen.getByRole("heading", {
111128
name: SELECT_YOUR_ROLE_PAGE_TEXT.errorDuringRoleSelection,
112129
})
@@ -126,7 +143,7 @@ describe("SelectYourRolePage", () => {
126143
})
127144

128145
// Render the page with user signed in
129-
renderWithAuth({isSignedIn: true, idToken: "mock-id-token"})
146+
renderWithAuthAndAccess({isSignedIn: true, idToken: "mock-id-token"})
130147

131148
// Wait for the error message to appear
132149
await waitFor(() => {
@@ -170,45 +187,46 @@ describe("SelectYourRolePage", () => {
170187
})
171188

172189
// Render the page with user signed in
173-
renderWithAuth({isSignedIn: true, idToken: "mock-id-token"})
190+
renderWithAuthAndAccess({isSignedIn: true, idToken: "mock-id-token"})
174191

175192
// Wait for the main content to load
176193
await waitFor(() => {
177194
// Check for the page heading
178195
const heading = screen.getByRole("heading", {level: 1})
179196
expect(heading).toHaveTextContent(SELECT_YOUR_ROLE_PAGE_TEXT.title)
180197
})
198+
})
181199

182-
// Verify the page caption
183-
const caption = screen.getByText(SELECT_YOUR_ROLE_PAGE_TEXT.caption)
184-
expect(caption).toBeInTheDocument()
185-
186-
// Verify the "Roles without access" section (expander)
187-
const expanderText = SELECT_YOUR_ROLE_PAGE_TEXT.roles_without_access_table_title
188-
const expander = screen.getByText(expanderText)
189-
expect(expander).toBeInTheDocument()
200+
it("renders no access title and caption when no roles with access are available", async () => {
201+
// Mock user data with no roles with access
202+
const mockUserInfo = {
203+
roles_with_access: [], // No roles with access
204+
roles_without_access: [
205+
{
206+
role_name: "Technician",
207+
org_name: "Tech Org",
208+
org_code: "ORG456",
209+
site_address: "2 Fake Street",
210+
},
211+
],
212+
}
190213

191-
// Check for the table data in "Roles without access"
192-
const tableOrg = screen.getByText(/Tech Org \(ODS: ORG456\)/i)
193-
expect(tableOrg).toBeInTheDocument()
194-
const tableRole = screen.getByText("Technician")
195-
expect(tableRole).toBeInTheDocument()
196-
})
214+
// Mock fetch to return 200 OK with valid userInfo
215+
mockFetch.mockResolvedValue({
216+
status: 200,
217+
json: async () => ({userInfo: mockUserInfo}),
218+
})
197219

198-
it("renders error summary when not signed in", async () => {
199-
// Render the page with `isSignedIn` set to false
200-
renderWithAuth({isSignedIn: false, error: "Missing access or ID token"})
220+
// Render the page with user signed in
221+
renderWithAuthAndAccess({isSignedIn: true, idToken: "mock-id-token"})
201222

202-
// Wait for the error message to appear
223+
// Wait for the main content to load
203224
await waitFor(() => {
204-
// Check for error summary heading
205-
const errorHeading = screen.getByRole("heading", {
206-
name: SELECT_YOUR_ROLE_PAGE_TEXT.errorDuringRoleSelection,
207-
})
208-
expect(errorHeading).toBeInTheDocument()
209-
210-
const errorItem = screen.getByText("Missing access or ID token")
211-
expect(errorItem).toBeInTheDocument()
225+
// Check for the no-access title
226+
const heading = screen.getByRole("heading", {level: 1})
227+
expect(heading).toHaveTextContent(
228+
SELECT_YOUR_ROLE_PAGE_TEXT.titleNoAccess
229+
)
212230
})
213231
})
214232

@@ -225,49 +243,18 @@ describe("SelectYourRolePage", () => {
225243
roles_without_access: [],
226244
}
227245

228-
// Mock fetch response
229-
global.fetch = jest.fn(() =>
230-
Promise.resolve({
231-
ok: true,
232-
status: 200,
233-
statusText: "OK",
234-
headers: new Headers(),
235-
redirected: false,
236-
type: "basic",
237-
url: "",
238-
clone: jest.fn(),
239-
body: null,
240-
bodyUsed: false,
241-
text: jest.fn(),
242-
json: jest.fn(() => Promise.resolve({userInfo: mockUserInfo})), // Simulate JSON body
243-
})
244-
) as jest.Mock
246+
mockFetch.mockResolvedValue({
247+
status: 200,
248+
json: async () => ({userInfo: mockUserInfo}),
249+
})
245250

246-
// Mock useRouter's push function
247251
const mockPush = jest.fn();
248252
(useRouter as jest.Mock).mockReturnValue({
249253
push: mockPush,
250254
})
251255

252-
// Mock AuthContext
253-
const mockAuthContext = {
254-
isSignedIn: true,
255-
idToken: mockJWT,
256-
accessToken: mockJWT,
257-
error: null,
258-
user: null,
259-
cognitoSignIn: jest.fn(),
260-
cognitoSignOut: jest.fn(),
261-
}
262-
263-
// Render the component
264-
render(
265-
<AuthContext.Provider value={mockAuthContext}>
266-
<SelectYourRolePage />
267-
</AuthContext.Provider>
268-
)
256+
renderWithAuthAndAccess({isSignedIn: true, idToken: "mock-id-token"})
269257

270-
// Wait for redirection
271258
await waitFor(() => {
272259
expect(mockPush).toHaveBeenCalledWith("/searchforaprescription")
273260
})

packages/cpt-ui/app/layout.tsx

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

4-
import 'nhsuk-frontend/dist/nhsuk.css';
5-
import EpsHeader from '@/components/EpsHeader'
4+
import 'nhsuk-frontend/dist/nhsuk.css'
5+
import EpsHeaderLayout from '@/components/EpsHeaderLayout'
66
import EpsFooter from '@/components/EpsFooter'
7-
import { AuthProvider } from '@/context/AuthProvider'
7+
import {AuthProvider} from '@/context/AuthProvider'
8+
import {AccessProvider} from '@/context/AccessProvider'
89

910
export default function RootLayout({
1011
children,
@@ -15,12 +16,13 @@ export default function RootLayout({
1516
<html lang="en">
1617
<body>
1718
<AuthProvider>
18-
<EpsHeader />
19-
{children}
20-
<EpsFooter />
19+
<AccessProvider>
20+
<EpsHeaderLayout />
21+
{children}
22+
<EpsFooter />
23+
</AccessProvider>
2124
</AuthProvider>
2225
</body>
23-
2426
</html>
2527
)
2628
}

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

Lines changed: 39 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import React, {useState, useEffect, useContext, useCallback} from "react"
33
import {useRouter} from 'next/navigation'
44
import {Container, Col, Row, Details, Table, ErrorSummary, Button, InsetText} from "nhsuk-react-components"
55
import {AuthContext} from "@/context/AuthProvider"
6+
import {useAccess} from '@/context/AccessProvider'
67
import EpsCard, {EpsCardProps} from "@/components/EpsCard"
78
import {SELECT_YOUR_ROLE_PAGE_TEXT} from "@/constants/ui-strings/CardStrings"
89

@@ -39,6 +40,8 @@ const trackerUserInfoEndpoint = "/api/tracker-user-info"
3940
const {
4041
title,
4142
caption,
43+
titleNoAccess,
44+
captionNoAccess,
4245
insetText,
4346
confirmButton,
4447
alternativeMessage,
@@ -55,6 +58,7 @@ const {
5558
} = SELECT_YOUR_ROLE_PAGE_TEXT
5659

5760
export default function SelectYourRolePage() {
61+
const {setNoAccess} = useAccess()
5862
const [loading, setLoading] = useState<boolean>(true)
5963
const [error, setError] = useState<string | null>(null)
6064
const [redirecting, setRedirecting] = useState<boolean>(false)
@@ -125,6 +129,8 @@ export default function SelectYourRolePage() {
125129
}))
126130
)
127131

132+
setNoAccess(rolesWithAccess.length === 0)
133+
128134
// Redirect if conditions are met
129135
if (rolesWithAccess.length === 1 && rolesWithoutAccess.length === 0) {
130136
setRedirecting(true)
@@ -138,7 +144,8 @@ export default function SelectYourRolePage() {
138144
} finally {
139145
setLoading(false)
140146
}
141-
}, [auth, router])
147+
148+
}, [auth, router, setNoAccess])
142149

143150
useEffect(() => {
144151
if (auth?.isSignedIn === undefined) {
@@ -201,45 +208,56 @@ export default function SelectYourRolePage() {
201208
)
202209
}
203210

211+
const noAccess = rolesWithAccess.length === 0
212+
213+
console.log("Title for no access:", SELECT_YOUR_ROLE_PAGE_TEXT.titleNoAccess);
214+
console.log("No Access State:", noAccess);
215+
204216
return (
205217
<main id="main-content" className="nhsuk-main-wrapper">
206218
<Container role="contentinfo">
207219
{/* Title Section */}
208220
<Row>
209221
<Col width="two-thirds">
210-
<h1 className='nhsuk-heading-xl'>
222+
<h1 className="nhsuk-heading-xl">
211223
<span role="text" data-testid="eps_header_selectYourRole">
212-
<span className="nhsuk-title">{title}</span>
224+
<span className="nhsuk-title">{noAccess ? titleNoAccess : title}</span>
213225
<span className="nhsuk-caption-l nhsuk-caption--bottom">
214226
<span className="nhsuk-u-visually-hidden"> - </span>
215-
{caption}
227+
{!noAccess && caption}
216228
</span>
217229
</span>
218230
</h1>
231+
{/* Caption Section for No Access */}
232+
{noAccess && (<p>{captionNoAccess}</p>)}
219233
{/* Inset Text Section */}
220-
<section aria-label="Login Information">
221-
<InsetText>
222-
<span className="nhsuk-u-visually-hidden">{insetText.visuallyHidden}</span>
223-
<p>{insetText.message}</p>
224-
</InsetText>
225-
{/* Confirm Button */}
226-
<Button href={confirmButton.link}>{confirmButton.text}</Button>
227-
<p>{alternativeMessage}</p>
228-
</section>
234+
{!noAccess && (
235+
<section aria-label="Login Information">
236+
<InsetText>
237+
<span className="nhsuk-u-visually-hidden">{insetText.visuallyHidden}</span>
238+
<p>{insetText.message}</p>
239+
</InsetText>
240+
{/* Confirm Button */}
241+
<Button href={confirmButton.link}>{confirmButton.text}</Button>
242+
<p>{alternativeMessage}</p>
243+
</section>
244+
)}
229245
</Col>
230246

231247
{/* Roles with access Section */}
232-
<Col width="two-thirds">
233-
<div className="section">
234-
{rolesWithAccess.map((role: RolesWithAccessProps) => (
235-
<EpsCard {...role} key={role.uuid} />
236-
))}
237-
</div>
238-
</Col>
248+
{!noAccess && (
249+
<Col width="two-thirds">
250+
<div className="section" >
251+
{rolesWithAccess.map((role: RolesWithAccessProps) => (
252+
<EpsCard {...role} key={role.uuid} />
253+
))}
254+
</div>
255+
</Col>
256+
)}
239257

240258
{/* Roles without access Section */}
241259
<Col width="two-thirds">
242-
<h2>{rolesWithoutAccessHeader}</h2>
260+
<h3>{rolesWithoutAccessHeader}</h3>
243261
<Details expander>
244262
<Details.Summary>
245263
{roles_without_access_table_title}

0 commit comments

Comments
 (0)