diff --git a/package-lock.json b/package-lock.json
index ebcc1e0aee..f55a2ef27b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -26607,7 +26607,7 @@
"version": "2.0.0",
"license": "MIT"
},
- "node_modules/selected-role-lambda": {
+ "node_modules/selectedRoleLambda": {
"resolved": "packages/selectedRoleLambda",
"link": true
},
@@ -32647,7 +32647,6 @@
}
},
"packages/selectedRoleLambda": {
- "name": "selected-role-lambda",
"version": "1.0.0",
"license": "MIT",
"dependencies": {
diff --git a/packages/cpt-ui/__tests__/ChangeRolePage.test.tsx b/packages/cpt-ui/__tests__/ChangeRolePage.test.tsx
index c539b1989d..c25d7516e6 100644
--- a/packages/cpt-ui/__tests__/ChangeRolePage.test.tsx
+++ b/packages/cpt-ui/__tests__/ChangeRolePage.test.tsx
@@ -16,8 +16,8 @@ jest.mock("@/constants/ui-strings/CardStrings", () => {
"You are currently logged in at GREENE'S PHARMACY (ODS: FG419) with Health Professional Access Role.",
},
confirmButton: {
- text: "Confirm and continue to find a prescription",
- link: "tracker-presc-no",
+ text: "Continue to find a prescription",
+ link: "searchforaprescription",
},
alternativeMessage: "Alternatively, you can choose a new role below.",
organisation: "Organisation",
@@ -280,4 +280,58 @@ describe("ChangeRolePage", () => {
expect(mockPush).toHaveBeenCalledWith("/searchforaprescription")
})
})
+
+ it("renders loading state when waiting for API response", async () => {
+ mockFetch.mockImplementation(() => new Promise(() => {}))
+ renderWithAuth()
+ expect(screen.getByText("Loading...")).toBeInTheDocument()
+ })
+
+ it("redirects when a single role is available", async () => {
+ (useRouter as jest.Mock).mockReturnValue({
+ push: jest.fn()
+ })
+
+ const mockUserInfo = {
+ roles_with_access: [{
+ role_name: "Pharmacist",
+ org_name: "Test Pharmacy",
+ org_code: "ORG123",
+ site_address: "123 Test St"
+ }],
+ roles_without_access: []
+ }
+
+ mockFetch.mockResolvedValue({
+ status: 200,
+ json: async () => ({userInfo: mockUserInfo})
+ })
+
+ renderWithAuth({isSignedIn: true, idToken: {toString: jest.fn().mockReturnValue("mock-id-token")}})
+
+ await waitFor(() => {
+ expect(useRouter().push).toHaveBeenCalledWith("/searchforaprescription")
+ })
+ })
+
+ it("does not fetch user roles if user is not signed in", async () => {
+ const mockFetch = jest.fn()
+ global.fetch = mockFetch
+
+ renderWithAuth({isSignedIn: false}) // Simulating a user who is not signed in
+
+ expect(mockFetch).not.toHaveBeenCalled()
+ })
+
+ it("displays an error when the API request fails", async () => {
+ mockFetch.mockRejectedValue(new Error("Failed to fetch user roles"))
+
+ renderWithAuth({isSignedIn: true, idToken: {toString: jest.fn().mockReturnValue("mock-id-token")}})
+
+ await waitFor(() => {
+ const errorSummary = screen.getByRole("heading", {name: "Error during role selection"})
+ expect(errorSummary).toBeInTheDocument()
+ expect(screen.getByText("Failed to fetch CPT user info")).toBeInTheDocument()
+ })
+ })
})
diff --git a/packages/cpt-ui/__tests__/RBACBanner.test.tsx b/packages/cpt-ui/__tests__/RBACBanner.test.tsx
index 496ec22d50..6420e1a477 100644
--- a/packages/cpt-ui/__tests__/RBACBanner.test.tsx
+++ b/packages/cpt-ui/__tests__/RBACBanner.test.tsx
@@ -1,8 +1,8 @@
import "@testing-library/jest-dom"
-import { render, screen } from "@testing-library/react"
-import { useRouter } from "next/navigation"
+import {render, screen} from "@testing-library/react"
+import {useRouter} from "next/navigation"
import React from "react"
-import { JWT } from "aws-amplify/auth"
+import {JWT} from "aws-amplify/auth"
import RBACBanner from "@/components/RBACBanner"
@@ -19,10 +19,10 @@ jest.mock("@/constants/ui-strings/RBACBannerStrings", () => {
LOCUM_NAME: "Locum pharmacy"
}
- return { RBAC_BANNER_STRINGS }
+ return {RBAC_BANNER_STRINGS}
})
-const { RBAC_BANNER_STRINGS } = require("@/constants/ui-strings/RBACBannerStrings")
+const {RBAC_BANNER_STRINGS} = require("@/constants/ui-strings/RBACBannerStrings")
// Mock `next/navigation`
jest.mock("next/navigation", () => ({
@@ -46,8 +46,8 @@ jest.mock("@/context/AccessProvider", () => {
org_name: "org name"
},
userDetails: {
- given_name: "Jane",
- family_name: "Doe"
+ given_name: "JaNe",
+ family_name: "DoE"
},
setUserDetails: jest.fn(),
setSelectedRole: jest.fn(),
@@ -57,7 +57,7 @@ jest.mock("@/context/AccessProvider", () => {
const useAccess = () => React.useContext(MockAccessContext)
const __setMockContextValue = (newValue: any) => {
- mockContextValue = { ...mockContextValue, ...newValue }
+ mockContextValue = {...mockContextValue, ...newValue}
// Reassign the context’s defaultValue so subsequent consumers get new values
MockAccessContext._currentValue = mockContextValue
MockAccessContext._currentValue2 = mockContextValue
@@ -70,7 +70,7 @@ jest.mock("@/context/AccessProvider", () => {
__setMockContextValue
}
})
-const { __setMockContextValue } = require("@/context/AccessProvider")
+const {__setMockContextValue} = require("@/context/AccessProvider")
// Mock an AuthContext
const AuthContext = React.createContext(null)
@@ -90,7 +90,7 @@ const defaultAuthContext = {
}
export const renderWithAuth = (authOverrides = {}) => {
- const authValue = { ...defaultAuthContext, ...authOverrides }
+ const authValue = {...defaultAuthContext, ...authOverrides}
return render(
@@ -112,7 +112,7 @@ describe("RBACBanner", () => {
org_name: "org name"
},
userDetails: {
- family_name: "Doe",
+ family_name: "DoE",
given_name: "Jane"
}
})
@@ -145,7 +145,7 @@ describe("RBACBanner", () => {
expect(bannerText).toBeInTheDocument()
// Check that placeholders are properly replaced
- const expectedText = `CONFIDENTIAL: PERSONAL PATIENT DATA accessed by Doe, Jane - Role Name - org name (ODS: deadbeef)`
+ const expectedText = `CONFIDENTIAL: PERSONAL PATIENT DATA accessed by DOE, Jane - Role Name - org name (ODS: deadbeef)`
expect(bannerText).toHaveTextContent(expectedText)
})
@@ -165,7 +165,7 @@ describe("RBACBanner", () => {
const bannerText = screen.getByTestId("rbac-banner-text")
// Locum pharmacy name should appear
expect(bannerText).toHaveTextContent(
- `CONFIDENTIAL: PERSONAL PATIENT DATA accessed by Doe, Jane - Role Name - Locum pharmacy (ODS: FFFFF)`
+ `CONFIDENTIAL: PERSONAL PATIENT DATA accessed by DOE, Jane - Role Name - Locum pharmacy (ODS: FFFFF)`
)
})
@@ -198,7 +198,25 @@ describe("RBACBanner", () => {
const bannerText = screen.getByTestId("rbac-banner-text")
// Notice fallback values: NO_ROLE_NAME, NO_ORG_NAME, NO_ODS_CODE
expect(bannerText).toHaveTextContent(
- `CONFIDENTIAL: PERSONAL PATIENT DATA accessed by Doe, Jane - NO_ROLE_NAME - NO_ORG_NAME (ODS: NO_ODS_CODE)`
+ `CONFIDENTIAL: PERSONAL PATIENT DATA accessed by DOE, Jane - NO_ROLE_NAME - NO_ORG_NAME (ODS: NO_ODS_CODE)`
+ )
+ })
+
+ it("should fallback to NO_ORG_NAME if org_name is missing", () => {
+ __setMockContextValue({
+ selectedRole: {
+ role_name: "Role Name",
+ role_id: "role-id",
+ org_code: "deadbeef"
+ // org_name is missing
+ }
+ })
+
+ renderWithAuth()
+
+ const bannerText = screen.getByTestId("rbac-banner-text")
+ expect(bannerText).toHaveTextContent(
+ `CONFIDENTIAL: PERSONAL PATIENT DATA accessed by DOE, Jane - Role Name - NO_ORG_NAME (ODS: deadbeef)`
)
})
diff --git a/packages/cpt-ui/__tests__/SelectYourRolePage.test.tsx b/packages/cpt-ui/__tests__/SelectYourRolePage.test.tsx
index bcccad6d68..ee1f770ac1 100644
--- a/packages/cpt-ui/__tests__/SelectYourRolePage.test.tsx
+++ b/packages/cpt-ui/__tests__/SelectYourRolePage.test.tsx
@@ -20,8 +20,8 @@ jest.mock("@/constants/ui-strings/CardStrings", () => {
"You are currently logged in at GREENE'S PHARMACY (ODS: FG419) with Health Professional Access Role.",
},
confirmButton: {
- text: "Confirm and continue to find a prescription",
- link: "tracker-presc-no",
+ text: "Continue to find a prescription",
+ link: "searchforaprescription",
},
alternativeMessage: "Alternatively, you can choose a new role below.",
organisation: "Organisation",
@@ -321,4 +321,25 @@ describe("SelectYourRolePage", () => {
expect(useRouter().push).toHaveBeenCalledWith("/searchforaprescription")
})
})
+
+ it("does not fetch user roles if user is not signed in", async () => {
+ const mockFetch = jest.fn()
+ global.fetch = mockFetch
+
+ renderWithAuth({isSignedIn: false}) // Simulating a user who is not signed in
+
+ expect(mockFetch).not.toHaveBeenCalled()
+ })
+
+ it("displays an error when the API request fails", async () => {
+ mockFetch.mockRejectedValue(new Error("Failed to fetch user roles"))
+
+ renderWithAuth({isSignedIn: true, idToken: {toString: jest.fn().mockReturnValue("mock-id-token")}})
+
+ await waitFor(() => {
+ const errorSummary = screen.getByRole("heading", {name: "Error during role selection"})
+ expect(errorSummary).toBeInTheDocument()
+ expect(screen.getByText("Failed to fetch CPT user info")).toBeInTheDocument()
+ })
+ })
})
diff --git a/packages/cpt-ui/__tests__/YourSelectedRole.test.tsx b/packages/cpt-ui/__tests__/YourSelectedRole.test.tsx
index 1b699d44f9..57dc1c34c0 100644
--- a/packages/cpt-ui/__tests__/YourSelectedRole.test.tsx
+++ b/packages/cpt-ui/__tests__/YourSelectedRole.test.tsx
@@ -15,7 +15,7 @@ jest.mock("@/constants/ui-strings/YourSelectedRoleStrings", () => {
roleLabel: "Role",
orgLabel: "Organisation",
changeLinkText: "Change",
- confirmButtonText: "Confirm and continue to find a prescription",
+ confirmButtonText: "Continue to find a prescription",
noODSCode: "NO ODS CODE",
noRoleName: "NO ROLE NAME",
noOrgName: "NO ORG NAME"
diff --git a/packages/cpt-ui/app/confirmrole/page.tsx b/packages/cpt-ui/app/confirmrole/page.tsx
index 7534870e52..901335690f 100644
--- a/packages/cpt-ui/app/confirmrole/page.tsx
+++ b/packages/cpt-ui/app/confirmrole/page.tsx
@@ -1,7 +1,7 @@
'use client'
-import React from "react";
+import React from "react"
-import { Button, Card, Container, Col, InsetText, Row } from "nhsuk-react-components";
+import {Button, Card, Container, Col, InsetText, Row} from "nhsuk-react-components"
export default function Page() {
return (
@@ -26,7 +26,7 @@ export default function Page() {
Alternatively, you can choose a new role below.
diff --git a/packages/cpt-ui/app/logout/page.tsx b/packages/cpt-ui/app/logout/page.tsx
index d8b235a3a6..19e89ab0b7 100644
--- a/packages/cpt-ui/app/logout/page.tsx
+++ b/packages/cpt-ui/app/logout/page.tsx
@@ -1,54 +1,53 @@
'use client'
-import React, { useContext, useEffect } from "react";
-import { Container } from "nhsuk-react-components"
-import Link from "next/link";
+import React, {useContext, useEffect} from "react"
+import {Container} from "nhsuk-react-components"
+import Link from "next/link"
-import { AuthContext } from "@/context/AuthProvider";
-import { useAccess } from "@/context/AccessProvider"
-import EpsSpinner from "@/components/EpsSpinner";
-import { EpsLogoutStrings } from "@/constants/ui-strings/EpsLogoutPageStrings";
+import {AuthContext} from "@/context/AuthProvider"
+import {useAccess} from "@/context/AccessProvider"
+import EpsSpinner from "@/components/EpsSpinner"
+import {EpsLogoutStrings} from "@/constants/ui-strings/EpsLogoutPageStrings"
export default function LogoutPage() {
-
- const auth = useContext(AuthContext);
- const { clear } = useAccess();
+ const auth = useContext(AuthContext)
+ const {clear} = useAccess()
// Log out on page load
useEffect(() => {
const signOut = async () => {
- console.log("Signing out", auth);
+ console.log("Signing out", auth)
+
+ await auth?.cognitoSignOut()
- await auth?.cognitoSignOut();
- console.log("Signed out: ", auth);
+ // Ensure user details & roles are cleared from local storage
+ clear()
+ console.log("Signed out and cleared session data")
}
if (auth?.isSignedIn) {
- signOut();
- clear();
+ signOut()
} else {
- console.log("Cannot sign out - not signed in");
+ console.log("Cannot sign out - not signed in")
+ clear() // Clear data even if not signed in
}
- }, [auth, clear]);
+ }, [auth, clear])
return (
- {auth?.isSignedIn ? (
+ {!auth?.isSignedIn ? (
<>
-
{EpsLogoutStrings.loading}
-
+
{EpsLogoutStrings.title}
+
{EpsLogoutStrings.body}
+ {EpsLogoutStrings.login_link}
>
) : (
<>
-
{EpsLogoutStrings.title}
-
{EpsLogoutStrings.body}
-
-
- {EpsLogoutStrings.login_link}
-
+
{EpsLogoutStrings.loading}
+
>
)}
- );
+ )
}
diff --git a/packages/cpt-ui/components/EpsRoleSelectionPage.tsx b/packages/cpt-ui/components/EpsRoleSelectionPage.tsx
index e6373a9403..03dc919adf 100644
--- a/packages/cpt-ui/components/EpsRoleSelectionPage.tsx
+++ b/packages/cpt-ui/components/EpsRoleSelectionPage.tsx
@@ -133,12 +133,20 @@ export default function RoleSelectionPage({contentText}: RoleSelectionPageProps)
const rolesWithAccess = userInfo.roles_with_access || []
const rolesWithoutAccess = userInfo.roles_without_access || []
- const selectedRole = userInfo?.currently_selected_role
- ? {
- ...userInfo?.currently_selected_role,
- uuid: `selected_role_0`
- }
- : undefined
+ // Check if the user info object and currently_selected_role exist
+ const selectedRole =
+ userInfo?.currently_selected_role && Object.keys(userInfo.currently_selected_role).length > 0
+ ? {
+ // If currently_selected_role is not empty, spread its properties
+ ...userInfo.currently_selected_role,
+ // Add uuid only if the selected role is not an empty object
+ uuid: `selected_role_0`
+ }
+ // If currently_selected_role is an empty object `{}`, set selectedRole to undefined
+ : undefined
+
+ console.log("Selected role:", selectedRole)
+ setSelectedRole(selectedRole)
// Populate the EPS card props
setRolesWithAccess(
@@ -158,7 +166,6 @@ export default function RoleSelectionPage({contentText}: RoleSelectionPageProps)
}))
)
- setSelectedRole(selectedRole)
setNoAccess(rolesWithAccess.length === 0)
setSingleAccess(rolesWithAccess.length === 1)
diff --git a/packages/cpt-ui/components/RBACBanner.tsx b/packages/cpt-ui/components/RBACBanner.tsx
index 899f85b7fe..37286ff21b 100644
--- a/packages/cpt-ui/components/RBACBanner.tsx
+++ b/packages/cpt-ui/components/RBACBanner.tsx
@@ -1,62 +1,67 @@
-"use client";
-import React, { useEffect, useState } from "react";
+"use client"
+import React, {useEffect, useState} from "react"
-import { Row } from "nhsuk-react-components";
+import {Row} from "nhsuk-react-components"
import "@/assets/styles/rbacBanner.scss"
-import { RBAC_BANNER_STRINGS } from "@/constants/ui-strings/RBACBannerStrings";
-import { useAccess } from "@/context/AccessProvider";
+import {RBAC_BANNER_STRINGS} from "@/constants/ui-strings/RBACBannerStrings"
+import {useAccess} from "@/context/AccessProvider"
export default function RBACBanner() {
const [bannerText, setBannerText] = useState("")
-
- const { selectedRole, userDetails } = useAccess();
+ const {selectedRole, userDetails} = useAccess()
useEffect(() => {
- let orgName
- if (selectedRole) {
- // Locums have a specific ODS code, and need to be shouted out
- if (selectedRole.org_code === "FFFFF") {
- orgName = RBAC_BANNER_STRINGS.LOCUM_NAME
- } else if (selectedRole.org_name) {
- orgName = selectedRole.org_name
- }
+ if (!selectedRole || !userDetails) {
+ console.log("No selected role or user details - hiding RBAC banner.")
+ return
}
- setBannerText(RBAC_BANNER_STRINGS.CONFIDENTIAL_DATA
- .replace("{lastName}", userDetails?.family_name ?? RBAC_BANNER_STRINGS.NO_FAMILY_NAME)
- .replace("{firstName}", userDetails?.given_name ?? RBAC_BANNER_STRINGS.NO_GIVEN_NAME)
- .replace("{roleName}", selectedRole?.role_name ?? RBAC_BANNER_STRINGS.NO_ROLE_NAME)
- .replace("{orgName}", orgName ?? RBAC_BANNER_STRINGS.NO_ORG_NAME)
- .replace("{odsCode}", selectedRole?.org_code ?? RBAC_BANNER_STRINGS.NO_ODS_CODE)
+ /**
+ * 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 (org_code === 'FFFFF'):
+ * CONFIDENTIAL: PERSONAL PATIENT DATA accessed by LAST NAME, First Name - RBAC Role - Locum pharmacy (ODS: FFFFF) - Site Name (ODS: ODS Code)
+ */
+
+ // Determine the organization name (use "Locum pharmacy" for locum users)
+ const orgName = selectedRole.org_code === "FFFFF"
+ ? RBAC_BANNER_STRINGS.LOCUM_NAME
+ : selectedRole.org_name || RBAC_BANNER_STRINGS.NO_ORG_NAME
+
+ // Format the last name in uppercase
+ const lastName = userDetails.family_name?.toUpperCase() || RBAC_BANNER_STRINGS.NO_FAMILY_NAME
+
+ // Construct the final banner text using template replacement
+ setBannerText(
+ RBAC_BANNER_STRINGS.CONFIDENTIAL_DATA
+ .replace("{lastName}", lastName)
+ .replace("{firstName}", userDetails.given_name || RBAC_BANNER_STRINGS.NO_GIVEN_NAME)
+ .replace("{roleName}", selectedRole.role_name || RBAC_BANNER_STRINGS.NO_ROLE_NAME)
+ .replace("{orgName}", orgName)
+ .replace("{odsCode}", selectedRole.org_code || RBAC_BANNER_STRINGS.NO_ODS_CODE)
)
- }, [
- selectedRole,
- userDetails
- ])
+ }, [selectedRole, userDetails])
- // Render only after the user selects a role.
- // This has to come after all the logic, or we get a hydration error!
+ /**
+ * Hide the banner if the user session is missing or incomplete.
+ * The component should render only after a role is selected.
+ * This check must come after the effect logic to prevent hydration errors in SSR.
+ */
if (!selectedRole) {
- return (null)
+ return null
}
return (
- <>
- {/* I can't find a component for a banner, so I've cribbed the CSS from the prototype here. */}
-
-
-
- {bannerText}
-
-
-
- >
- );
+
+
+
+ {bannerText}
+
+
+
+ )
}
diff --git a/packages/cpt-ui/constants/ui-strings/CardStrings.ts b/packages/cpt-ui/constants/ui-strings/CardStrings.ts
index 7e6978deee..919ca57673 100644
--- a/packages/cpt-ui/constants/ui-strings/CardStrings.ts
+++ b/packages/cpt-ui/constants/ui-strings/CardStrings.ts
@@ -12,8 +12,8 @@ export const SELECT_YOUR_ROLE_PAGE_TEXT = {
"You are currently logged in at GREENE'S PHARMACY (ODS: FG419) with Health Professional Access Role."
},
confirmButton: {
- text: "Confirm and continue to find a prescription",
- link: "tracker-presc-no"
+ text: "Continue to find a prescription",
+ link: "searchforaprescription"
},
alternativeMessage: "Alternatively, you can choose a new role below.",
organisation: "Organisation",
diff --git a/packages/cpt-ui/constants/ui-strings/ChangeRolePageStrings.ts b/packages/cpt-ui/constants/ui-strings/ChangeRolePageStrings.ts
index c31c930950..4133810198 100644
--- a/packages/cpt-ui/constants/ui-strings/ChangeRolePageStrings.ts
+++ b/packages/cpt-ui/constants/ui-strings/ChangeRolePageStrings.ts
@@ -8,11 +8,11 @@ export const CHANGE_YOUR_ROLE_PAGE_TEXT = {
insetText: {
visuallyHidden: "Information: ",
message:
- "You are currently logged in at GREENE'S PHARMACY (ODS: FG419) with Health Professional Access Role."
+ "You are currently logged in at GREENE'S PHARMACY (ODS: FG419) with Health Professional Access Role."
},
confirmButton: {
- text: "Confirm and continue to find a prescription",
- link: "tracker-presc-no"
+ text: "Continue to find a prescription",
+ link: "searchforaprescription"
},
alternativeMessage: "Alternatively, you can choose a new role below.",
organisation: "Organisation",
diff --git a/packages/cpt-ui/constants/ui-strings/YourSelectedRoleStrings.ts b/packages/cpt-ui/constants/ui-strings/YourSelectedRoleStrings.ts
index 7502d89e5e..6078861c4d 100644
--- a/packages/cpt-ui/constants/ui-strings/YourSelectedRoleStrings.ts
+++ b/packages/cpt-ui/constants/ui-strings/YourSelectedRoleStrings.ts
@@ -2,10 +2,10 @@ export const YOUR_SELECTED_ROLE_STRINGS = {
heading: "Your selected role",
subheading: "The following role is now active.",
tableTitle: "Current role Details",
- roleLabel:"Role",
+ roleLabel: "Role",
orgLabel: "Organisation",
changeLinkText: "Change",
- confirmButtonText: "Confirm and continue to find a prescription",
+ confirmButtonText: "Continue to find a prescription",
noODSCode: "NO ODS CODE",
noRoleName: "NO ROLE NAME",
noOrgName: "NO ORG NAME"
diff --git a/packages/cpt-ui/context/AccessProvider.tsx b/packages/cpt-ui/context/AccessProvider.tsx
index 02e24d4ee7..dea7b6f1db 100644
--- a/packages/cpt-ui/context/AccessProvider.tsx
+++ b/packages/cpt-ui/context/AccessProvider.tsx
@@ -31,11 +31,17 @@ export const AccessProvider = ({children}: {children: ReactNode}) => {
const auth = useContext(AuthContext)
const clear = () => {
- console.warn("Clearing access context.")
+ console.log("Clearing access context and local storage...")
setNoAccess(false)
setSingleAccess(false)
setSelectedRole(undefined)
setUserDetails(undefined)
+
+ // Clear from localStorage to ensure RBAC Banner is removed
+ localStorage.removeItem("access")
+ localStorage.removeItem("selectedRole")
+ localStorage.removeItem("userDetails")
+ console.log("Local storage cleared.")
}
diff --git a/packages/selectedRoleLambda/package.json b/packages/selectedRoleLambda/package.json
index d6d65583de..c19490d451 100644
--- a/packages/selectedRoleLambda/package.json
+++ b/packages/selectedRoleLambda/package.json
@@ -1,7 +1,7 @@
{
- "name": "selected-role-lambda",
+ "name": "selectedRoleLambda",
"version": "1.0.0",
- "description": "Lambda to handle selectedRole endpoint",
+ "description": "Lambda function handling user role selection in the Clinical Prescription Tracking Service",
"main": "handler.ts",
"author": "NHS Digital",
"license": "MIT",
diff --git a/packages/selectedRoleLambda/src/handler.ts b/packages/selectedRoleLambda/src/handler.ts
index a4429cb9b9..7dd7bef678 100644
--- a/packages/selectedRoleLambda/src/handler.ts
+++ b/packages/selectedRoleLambda/src/handler.ts
@@ -7,9 +7,9 @@ import middy from "@middy/core"
import inputOutputLogger from "@middy/input-output-logger"
import {MiddyErrorHandler} from "@cpt-ui-common/middyErrorHandler"
import {getUsernameFromEvent} from "@cpt-ui-common/authFunctions"
-import {updateDynamoTable} from "./selectedRoleHelpers"
+import {updateDynamoTable, fetchUserRolesFromDynamoDB} from "./selectedRoleHelpers"
-/*
+/**
* Lambda function for updating the selected role in the DynamoDB table.
* This function handles incoming API Gateway requests, extracts the username,
* parses the request body, and updates the user's role in the database.
@@ -26,16 +26,13 @@ const documentClient = DynamoDBDocumentClient.from(dynamoClient)
const tokenMappingTableName = process.env["TokenMappingTableName"] ?? ""
// Default error response body for internal system errors
-const errorResponseBody = {
- message: "A system error has occurred"
-}
+const errorResponseBody = {message: "A system error has occurred"}
// Custom error handler for handling unexpected errors in the Lambda function
const middyErrorHandler = new MiddyErrorHandler(errorResponseBody)
/**
- * The main handler function for processing API Gateway events.
- * Handles parsing, validation, and updates the selected role in the DynamoDB table.
+ * Lambda function handler for updating a user's selected role.
*/
const lambdaHandler = async (event: APIGatewayProxyEvent): Promise => {
logger.appendKeys({"apigw-request-id": event.requestContext?.requestId})
@@ -46,7 +43,7 @@ const lambdaHandler = async (event: APIGatewayProxyEvent): Promise role.role_id === userSelectedRoleId)
+
+ // Log extracted role details
+ logger.info("Extracted role data", {
+ username,
+ rolesWithAccessCount: rolesWithAccess.length,
+ rolesWithAccess: rolesWithAccess.map(role => ({
+ role_id: role.role_id,
+ role_name: role.role_name,
+ org_code: role.org_code
+ })),
+ previousSelectedRole: currentSelectedRole
+ ? {
+ role_id: currentSelectedRole.role_id,
+ role_name: currentSelectedRole.role_name,
+ org_code: currentSelectedRole.org_code
+ }
+ : "No previous role selected",
+ newSelectedRole: newSelectedRole
+ ? {
+ role_id: newSelectedRole.role_id,
+ role_name: newSelectedRole.role_name,
+ org_code: newSelectedRole.org_code
+ }
+ : "Role not found in rolesWithAccess"
+ })
- // Call helper function to update the selected role in the database
- await updateDynamoTable(username, userInfoSelectedRole, documentClient, logger, tokenMappingTableName)
+ // Construct updated roles list
+ const updatedRolesWithAccess = [
+ ...rolesWithAccess.filter(role => role.role_id !== userSelectedRoleId), // Remove the new selected role
+
+ // Move the previously selected role back into rolesWithAccess, but only if it was set
+ ...(currentSelectedRole && Object.keys(currentSelectedRole).length > 0
+ ? [currentSelectedRole]
+ : [])
+ ]
+
+ logger.info("Updated roles list before database update", {
+ username,
+ newSelectedRole: newSelectedRole
+ ? {
+ role_id: newSelectedRole.role_id,
+ role_name: newSelectedRole.role_name,
+ org_code: newSelectedRole.org_code
+ }
+ : "No role selected",
+ returningRoleToAccessList: currentSelectedRole
+ ? {
+ role_id: currentSelectedRole.role_id,
+ role_name: currentSelectedRole.role_name,
+ org_code: currentSelectedRole.org_code
+ }
+ : "No previous role to return",
+ updatedRolesWithAccessCount: updatedRolesWithAccess.length,
+ updatedRolesWithAccess: updatedRolesWithAccess.map(role => ({
+ role_id: role.role_id,
+ role_name: role.role_name,
+ org_code: role.org_code
+ }))
+ })
+
+ // Prepare the updated user info to be stored in DynamoDB
+ const updatedUserInfo = {
+ currentlySelectedRole: newSelectedRole || undefined, // If no role is found, store `undefined`
+ rolesWithAccess: updatedRolesWithAccess,
+ selectedRoleId: userSelectedRoleId
+ }
+
+ logger.info("Updating user role in DynamoDB", {
+ username,
+ updatedUserInfo
+ })
+
+ // Persist changes to DynamoDB
+ await updateDynamoTable(username, updatedUserInfo, documentClient, logger, tokenMappingTableName)
return {
statusCode: 200,
body: JSON.stringify({
message: "Selected role data has been updated successfully",
- userInfo: userInfoSelectedRole
+ userInfo: updatedUserInfo
})
}
-
}
export const handler = middy(lambdaHandler)
diff --git a/packages/selectedRoleLambda/src/selectedRoleHelpers.ts b/packages/selectedRoleLambda/src/selectedRoleHelpers.ts
index df8cf2c2c6..d5e40ec7cf 100644
--- a/packages/selectedRoleLambda/src/selectedRoleHelpers.ts
+++ b/packages/selectedRoleLambda/src/selectedRoleHelpers.ts
@@ -1,57 +1,60 @@
import {Logger} from "@aws-lambda-powertools/logger"
-import {DynamoDBDocumentClient, UpdateCommand} from "@aws-sdk/lib-dynamodb"
-import {RoleDetails, TrackerUserInfo} from "./selectedRoleTypes"
+import {DynamoDBDocumentClient, GetCommand, UpdateCommand} from "@aws-sdk/lib-dynamodb"
+import {RoleDetails, SelectedRole} from "./selectedRoleTypes"
/**
- * Update the user currentlySelectedRole and selectedRoleId in the DynamoDB table.
- * @param username - The username of the user.
- * @param data - The TrackerUserInfo object containing user role information.
- * @param documentClient - The DynamoDBDocumentClient instance.
- * @param logger - The Logger instance for logging.
- * @param tokenMappingTableName - The name of the DynamoDB table.
+ * **Updates the user's selected role in DynamoDB**
+ *
+ * - Stores the `currentlySelectedRole` and `selectedRoleId` in the table.
+ * - Removes the selected role from `rolesWithAccess` to avoid duplication.
*/
export const updateDynamoTable = async (
username: string,
- data: TrackerUserInfo,
+ updatedUserInfo: SelectedRole,
documentClient: DynamoDBDocumentClient,
logger: Logger,
tokenMappingTableName: string
) => {
- // Check if the token mapping table name is provided
+ // Validate if the table name is provided in environment variables
if (!tokenMappingTableName) {
- logger.error("Token mapping table name not set")
+ logger.error("Token mapping table name is not set.")
throw new Error("Token mapping table name not set")
}
logger.debug("Starting DynamoDB update process", {
username,
table: tokenMappingTableName,
- receivedData: data
+ receivedData: updatedUserInfo
})
- // DyanamoDB cannot allow undefined values. We need to scrub any undefined values from the data objects
- const currentlySelectedRole: RoleDetails = data.currently_selected_role ? data.currently_selected_role : {}
+ // Ensure no undefined values are stored in DynamoDB
+ const currentlySelectedRole: RoleDetails = updatedUserInfo.currentlySelectedRole || {}
+ const rolesWithAccess: Array = updatedUserInfo.rolesWithAccess || []
+ const selectedRoleId: string = updatedUserInfo.selectedRoleId || ""
- // Ensure selectedRoleId is never undefined by providing a fallback value
- const selectedRoleId: string = currentlySelectedRole.role_id || "UNSPECIFIED_ROLE_ID"
-
- // Since RoleDetails has a bunch of possibly undefined fields, we need to scrub those out.
- // Convert everything to strings, then convert back to a generic object.
+ // Remove `undefined` properties from the objects before updating
const scrubbedCurrentlySelectedRole = JSON.parse(JSON.stringify(currentlySelectedRole))
+ const scrubbedRolesWithAccess = rolesWithAccess.map((role) => JSON.parse(JSON.stringify(role)))
- logger.info("Prepared data for DynamoDB update", {
+ logger.info("Prepared role data for DynamoDB update", {
currentlySelectedRole: scrubbedCurrentlySelectedRole,
+ rolesWithAccess: scrubbedRolesWithAccess,
selectedRoleId
})
try {
- // Create the update command for DynamoDB
+ // Construct the UpdateCommand to modify user role data in DynamoDB
const updateCommand = new UpdateCommand({
TableName: tokenMappingTableName,
Key: {username},
- UpdateExpression: "SET currentlySelectedRole = :currentlySelectedRole, selectedRoleId = :selectedRoleId",
+ UpdateExpression: `
+ SET currentlySelectedRole = :currentlySelectedRole,
+ selectedRoleId = :selectedRoleId,
+ rolesWithAccess = :rolesWithAccess
+ `,
ExpressionAttributeValues: {
":currentlySelectedRole": scrubbedCurrentlySelectedRole,
+ ":rolesWithAccess": scrubbedRolesWithAccess,
":selectedRoleId": selectedRoleId
},
ReturnValues: "ALL_NEW"
@@ -59,9 +62,8 @@ export const updateDynamoTable = async (
logger.debug("Executing DynamoDB update command", {updateCommand})
- // Send the update command to DynamoDB
+ // Execute the update operation
const response = await documentClient.send(updateCommand)
-
logger.info("DynamoDB update successful", {response})
} catch (error) {
@@ -71,12 +73,59 @@ export const updateDynamoTable = async (
errorMessage: error.message,
errorStack: error.stack
})
+ throw error // Ensure the function actually throws the error
} else {
logger.error("Unknown error type while updating user's selected role", {
username,
- error
+ error: JSON.stringify(error) // Ensure the error is stringified
})
+ throw new Error("Unexpected error occurred while updating user's selected role")
}
- throw error
+ }
+}
+
+/**
+ * **Fetches user roles from DynamoDB**
+ *
+ * - Retrieves `rolesWithAccess` (available roles for the user).
+ * - Retrieves `currentlySelectedRole` (the role currently chosen by the user).
+ * - Ensures both fields are always returned, even if no data exists.
+ */
+export const fetchUserRolesFromDynamoDB = async (
+ username: string,
+ documentClient: DynamoDBDocumentClient,
+ logger: Logger,
+ tokenMappingTableName: string
+): Promise => {
+ logger.info("Fetching user role information from DynamoDB", {username})
+
+ try {
+ // Execute a GetCommand to fetch user role details
+ const response = await documentClient.send(
+ new GetCommand({
+ TableName: tokenMappingTableName,
+ Key: {username}
+ })
+ )
+
+ // Handle case where no user data is found
+ if (!response.Item) {
+ logger.warn("No user info found in DynamoDB", {username})
+ return {rolesWithAccess: [], currentlySelectedRole: undefined} // Ensure both fields are always returned
+ }
+
+ // Extract rolesWithAccess and currentlySelectedRole safely
+ const retrievedRolesWithAccess: SelectedRole = {
+ rolesWithAccess: response.Item.rolesWithAccess || [],
+ currentlySelectedRole: response.Item.currentlySelectedRole || undefined // Retrieve currentlySelectedRole
+ }
+
+ logger.info("Roles and selected role successfully retrieved from DynamoDB", {
+ data: retrievedRolesWithAccess
+ })
+ return retrievedRolesWithAccess
+ } catch (error) {
+ logger.error("Error fetching user info from DynamoDB", {error})
+ throw new Error("Failed to retrieve user info from cache")
}
}
diff --git a/packages/selectedRoleLambda/src/selectedRoleTypes.ts b/packages/selectedRoleLambda/src/selectedRoleTypes.ts
index 7abafb4729..392bc8f9bc 100644
--- a/packages/selectedRoleLambda/src/selectedRoleTypes.ts
+++ b/packages/selectedRoleLambda/src/selectedRoleTypes.ts
@@ -8,8 +8,8 @@ export type RoleDetails = {
uuid?: string
}
-export type TrackerUserInfo = {
- roles_with_access: Array
- roles_without_access: Array
- currently_selected_role?: RoleDetails
+export type SelectedRole = {
+ rolesWithAccess: Array
+ currentlySelectedRole?: RoleDetails
+ selectedRoleId?: string
}
diff --git a/packages/selectedRoleLambda/tests/test_handler.mockDisabled.test.ts b/packages/selectedRoleLambda/tests/test_handler.mockDisabled.test.ts
deleted file mode 100644
index 2df4ebc03d..0000000000
--- a/packages/selectedRoleLambda/tests/test_handler.mockDisabled.test.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-import {jest} from "@jest/globals"
-
-// Mock environment variables
-process.env.MOCK_MODE_ENABLED = "false"
-
-// Mocked functions
-const mockGetUsernameFromEvent = jest.fn()
-const mockUpdateDynamoTable = jest.fn()
-
-jest.unstable_mockModule("@cpt-ui-common/authFunctions", () => {
- return {
- getUsernameFromEvent: mockGetUsernameFromEvent.mockImplementation(() => "Primary_JoeBloggs")
- }
-})
-
-jest.unstable_mockModule("@/selectedRoleHelpers", () => {
- return {
- updateDynamoTable: mockUpdateDynamoTable.mockImplementation(() => Promise.resolve())
- }
-})
-
-const {handler} = await import("@/handler")
-import {mockContext, mockAPIGatewayProxyEvent} from "./mockObjects"
-
-describe("Lambda Handler Tests with mock disabled", () => {
- let event = {...mockAPIGatewayProxyEvent, body: JSON.stringify({role_id: "123", org_code: "XYZ"})}
- let context = {...mockContext}
-
- beforeEach(() => {
- jest.clearAllMocks()
- })
-
- it("should return a successful response when called normally", async () => {
- const response = await handler(event, context)
-
- expect(mockGetUsernameFromEvent).toHaveBeenCalled()
- expect(mockUpdateDynamoTable).toHaveBeenCalledWith(
- "Primary_JoeBloggs",
- {role_id: "123", org_code: "XYZ"},
- expect.any(Object),
- expect.any(Object),
- expect.any(String)
- )
- expect(response).toEqual({
- statusCode: 200,
- body: JSON.stringify({
- message: "Selected role data has been updated successfully",
- userInfo: JSON.parse(event.body)
- })
- })
- })
-
- it("should return 400 when request body is missing", async () => {
- event.body = "" as unknown as string
- const response = await handler(event, context)
-
- expect(response.statusCode).toBe(400)
- expect(response.body).toContain("Request body is required")
- })
-
- it("should return 400 when request body is invalid JSON", async () => {
- event.body = "Invalid JSON"
- const response = await handler(event, context)
-
- expect(response.statusCode).toBe(400)
- expect(response.body).toContain("Invalid JSON format in request body")
- })
-})
diff --git a/packages/selectedRoleLambda/tests/test_handler.mockEnabled.test.ts b/packages/selectedRoleLambda/tests/test_handler.mockEnabled.test.ts
deleted file mode 100644
index e1891e950e..0000000000
--- a/packages/selectedRoleLambda/tests/test_handler.mockEnabled.test.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-import {jest} from "@jest/globals"
-
-// Mocked functions
-const mockGetUsernameFromEvent = jest.fn()
-const mockUpdateDynamoTable = jest.fn()
-
-jest.unstable_mockModule("@cpt-ui-common/authFunctions", () => {
- return {
- getUsernameFromEvent: mockGetUsernameFromEvent.mockImplementation(() => "Mock_JoeBloggs")
- }
-})
-
-jest.unstable_mockModule("@/selectedRoleHelpers", () => {
- return {
- updateDynamoTable: mockUpdateDynamoTable.mockImplementation(() => Promise.resolve())
- }
-})
-
-const {handler} = await import("@/handler")
-import {mockContext, mockAPIGatewayProxyEvent} from "./mockObjects"
-
-describe("Lambda Handler Tests with mock enabled", () => {
- let event = {...mockAPIGatewayProxyEvent, body: JSON.stringify({role_id: "123", org_code: "XYZ"})}
- let context = {...mockContext}
-
- beforeEach(() => {
- jest.clearAllMocks()
- })
-
- it("should return a successful response with mock username", async () => {
- const response = await handler(event, context)
-
- expect(mockGetUsernameFromEvent).toHaveBeenCalled()
- expect(mockUpdateDynamoTable).toHaveBeenCalledWith(
- "Mock_JoeBloggs",
- {role_id: "123", org_code: "XYZ"},
- expect.any(Object),
- expect.any(Object),
- expect.any(String)
- )
- expect(response).toEqual({
- statusCode: 200,
- body: JSON.stringify({
- message: "Selected role data has been updated successfully",
- userInfo: JSON.parse(event.body)
- })
- })
- })
-
- it("should return 400 when request body is missing", async () => {
- event.body = "" as unknown as string
- const response = await handler(event, context)
-
- expect(response.statusCode).toBe(400)
- expect(response.body).toContain("Request body is required")
- })
-
- it("should return 400 when request body is invalid JSON", async () => {
- event.body = "Invalid JSON"
- const response = await handler(event, context)
-
- expect(response.statusCode).toBe(400)
- expect(response.body).toContain("Invalid JSON format in request body")
- })
-})
diff --git a/packages/selectedRoleLambda/tests/test_handler.test.ts b/packages/selectedRoleLambda/tests/test_handler.test.ts
new file mode 100644
index 0000000000..05fc9b02a6
--- /dev/null
+++ b/packages/selectedRoleLambda/tests/test_handler.test.ts
@@ -0,0 +1,348 @@
+import {jest} from "@jest/globals"
+
+// Mocked functions from authFunctions
+const mockGetUsernameFromEvent = jest.fn()
+
+jest.unstable_mockModule("@cpt-ui-common/authFunctions", () => {
+ const getUsernameFromEvent = mockGetUsernameFromEvent.mockImplementation(() => "Mock_JoeBloggs")
+
+ return {
+ getUsernameFromEvent
+ }
+})
+
+// Mocked functions from selectedRoleHelpers
+const mockFetchUserRolesFromDynamoDB = jest.fn()
+const mockUpdateDynamoTable = jest.fn()
+
+jest.unstable_mockModule("@/selectedRoleHelpers", () => {
+ const fetchUserRolesFromDynamoDB = mockFetchUserRolesFromDynamoDB.mockImplementation(() => {
+ return {
+ rolesWithAccess: [
+ {role_id: "123", org_code: "XYZ", role_name: "MockRole_1"},
+ {role_id: "456", org_code: "ABC", role_name: "MockRole_2"}
+ ],
+ currentlySelectedRole: undefined // Initially no role is selected
+ }
+ })
+
+ const updateDynamoTable = mockUpdateDynamoTable.mockImplementation(() => {})
+
+ return {
+ fetchUserRolesFromDynamoDB,
+ updateDynamoTable
+ }
+})
+
+const {handler} = await import("@/handler")
+import {mockContext, mockAPIGatewayProxyEvent} from "./mockObjects"
+import {Logger} from "@aws-lambda-powertools/logger"
+
+describe("Lambda Handler Tests", () => {
+ let event = {
+ ...mockAPIGatewayProxyEvent,
+ body: JSON.stringify({
+ currently_selected_role: {
+ role_id: "123",
+ org_code: "XYZ",
+ role_name: "MockRole_1"
+ }
+ })
+ }
+ let context = {...mockContext}
+
+ beforeAll(() => {
+ jest.clearAllMocks()
+ })
+
+ it("should return a successful response when called", async () => {
+ const response = await handler(event, context)
+ expect(mockUpdateDynamoTable).toHaveBeenCalled()
+
+ expect(response).toBeDefined()
+ expect(response).toHaveProperty("statusCode", 200)
+ expect(response).toHaveProperty("body")
+
+ const body = JSON.parse(response.body)
+ expect(body).toHaveProperty("message", "Selected role data has been updated successfully")
+ expect(body).toHaveProperty("userInfo")
+ })
+
+ it(
+ "should call updateDynamoTable and move the selected role from rolesWithAccess to currentlySelectedRole",
+ async () => {
+ const testUsername = "Mock_JoeBloggs"
+ const updatedUserInfo = {
+ // The selected role has been moved from rolesWithAccess to currentlySelectedRole
+ currentlySelectedRole: {
+ role_id: "123",
+ org_code: "XYZ",
+ role_name: "MockRole_1"
+ },
+ rolesWithAccess: [
+ {
+ role_id: "456",
+ org_code: "ABC",
+ role_name: "MockRole_2"
+ }
+ ],
+ selectedRoleId: "123"
+ }
+
+ const response = await handler(event, context)
+
+ expect(mockGetUsernameFromEvent).toHaveBeenCalled()
+ expect(mockUpdateDynamoTable).toHaveBeenCalledWith(
+ testUsername,
+ updatedUserInfo,
+ expect.any(Object),
+ expect.any(Object),
+ expect.any(String)
+ )
+ expect(response).toEqual({
+ statusCode: 200,
+ body: JSON.stringify({
+ message: "Selected role data has been updated successfully",
+ userInfo: {
+ currentlySelectedRole: {
+ role_id: "123",
+ org_code: "XYZ",
+ role_name: "MockRole_1"
+ },
+ rolesWithAccess: [
+ {
+ role_id: "456",
+ org_code: "ABC",
+ role_name: "MockRole_2"
+ }
+ ],
+ selectedRoleId: "123"
+ }
+ })
+ })
+ })
+
+ it(
+ "should swap currentlySelectedRole with the new selected role and move the old one back to rolesWithAccess",
+ async () => {
+ // Initial rolesWithAccess contains multiple roles, currentlySelectedRole is undefined
+ mockFetchUserRolesFromDynamoDB.mockImplementation(() => {
+ return {
+ rolesWithAccess: [
+ {role_id: "123", org_code: "XYZ", role_name: "MockRole_1"},
+ {role_id: "456", org_code: "ABC", role_name: "MockRole_2"},
+ {role_id: "789", org_code: "DEF", role_name: "MockRole_3"}
+ ],
+ currentlySelectedRole: undefined // Initially no role is selected
+ }
+ })
+
+ // User selects "MockRole_1" (role_id: 123)
+ let event = {
+ ...mockAPIGatewayProxyEvent,
+ body: JSON.stringify({
+ currently_selected_role: {
+ role_id: "123",
+ org_code: "XYZ",
+ role_name: "MockRole_1"
+ }
+ })
+ }
+
+ let response = await handler(event, mockContext)
+ let firstResponseBody = JSON.parse(response.body)
+
+ expect(firstResponseBody.userInfo).toEqual({
+ currentlySelectedRole: {role_id: "123", org_code: "XYZ", role_name: "MockRole_1"},
+ rolesWithAccess: [
+ {role_id: "456", org_code: "ABC", role_name: "MockRole_2"},
+ {role_id: "789", org_code: "DEF", role_name: "MockRole_3"}
+ ],
+ selectedRoleId: "123"
+ })
+
+ // User selects "MockRole_2" (role_id: 456)
+ event = {
+ ...mockAPIGatewayProxyEvent,
+ body: JSON.stringify({
+ currently_selected_role: {
+ role_id: "456",
+ org_code: "ABC",
+ role_name: "MockRole_2"
+ }
+ })
+ }
+
+ response = await handler(event, mockContext)
+ let secondResponseBody = JSON.parse(response.body)
+
+ expect(secondResponseBody.userInfo).toEqual({
+ currentlySelectedRole: {role_id: "456", org_code: "ABC", role_name: "MockRole_2"},
+ rolesWithAccess: [
+ {role_id: "123", org_code: "XYZ", role_name: "MockRole_1"}, // Previously selected role moved back
+ {role_id: "789", org_code: "DEF", role_name: "MockRole_3"}
+ ],
+ selectedRoleId: "456"
+ })
+
+ // User selects "MockRole_3" (role_id: 789)
+ event = {
+ ...mockAPIGatewayProxyEvent,
+ body: JSON.stringify({
+ currently_selected_role: {
+ role_id: "789",
+ org_code: "DEF",
+ role_name: "MockRole_3"
+ }
+ })
+ }
+
+ response = await handler(event, mockContext)
+ let thirdResponseBody = JSON.parse(response.body)
+
+ expect(thirdResponseBody.userInfo).toEqual({
+ currentlySelectedRole: {role_id: "789", org_code: "DEF", role_name: "MockRole_3"},
+ rolesWithAccess: [
+ {role_id: "123", org_code: "XYZ", role_name: "MockRole_1"},
+ {role_id: "456", org_code: "ABC", role_name: "MockRole_2"} // Previously selected role moved back
+ ],
+ selectedRoleId: "789"
+ })
+ }
+ )
+
+ it(
+ "should swap initially selected role with the new selected role and move the old one back to rolesWithAccess",
+ async () => {
+ // Initial database state with a previously selected role
+ mockFetchUserRolesFromDynamoDB.mockImplementation(() => {
+ return {
+ rolesWithAccess: [
+ {role_id: "123", org_code: "XYZ", role_name: "MockRole_1"},
+ {role_id: "456", org_code: "ABC", role_name: "MockRole_2"},
+ {role_id: "789", org_code: "DEF", role_name: "MockRole_3"}
+ ],
+ currentlySelectedRole: {role_id: "555", org_code: "GHI", role_name: "MockRole_4"} // Initially selected role
+ }
+ })
+
+ // First role selection: Change to "MockRole_1"
+ const firstEvent = {
+ ...mockAPIGatewayProxyEvent,
+ body: JSON.stringify({
+ currently_selected_role: {
+ role_id: "123",
+ org_code: "XYZ",
+ role_name: "MockRole_1"
+ }
+ })
+ }
+
+ const firstResponse = await handler(firstEvent, mockContext)
+ const firstResponseBody = JSON.parse(firstResponse.body)
+
+ expect(mockUpdateDynamoTable).toHaveBeenCalledWith(
+ "Mock_JoeBloggs",
+ {
+ currentlySelectedRole: {
+ role_id: "123",
+ org_code: "XYZ",
+ role_name: "MockRole_1"
+ },
+ rolesWithAccess: [
+ {role_id: "456", org_code: "ABC", role_name: "MockRole_2"},
+ {role_id: "789", org_code: "DEF", role_name: "MockRole_3"},
+ {role_id: "555", org_code: "GHI", role_name: "MockRole_4"} // Old role moved back
+ ],
+ selectedRoleId: "123"
+ },
+ expect.any(Object),
+ expect.any(Object),
+ expect.any(String)
+ )
+
+ // Now simulate switching to another role
+ mockFetchUserRolesFromDynamoDB.mockImplementation(() => firstResponseBody.userInfo)
+
+ const secondEvent = {
+ ...mockAPIGatewayProxyEvent,
+ body: JSON.stringify({
+ currently_selected_role: {
+ role_id: "789",
+ org_code: "DEF",
+ role_name: "MockRole_3"
+ }
+ })
+ }
+
+ const secondResponse = await handler(secondEvent, mockContext)
+ const secondResponseBody = JSON.parse(secondResponse.body)
+
+ expect(secondResponseBody).toEqual({
+ message: "Selected role data has been updated successfully",
+ userInfo: {
+ currentlySelectedRole: {
+ role_id: "789",
+ org_code: "DEF",
+ role_name: "MockRole_3"
+ },
+ rolesWithAccess: [
+ {role_id: "456", org_code: "ABC", role_name: "MockRole_2"},
+ {role_id: "555", org_code: "GHI", role_name: "MockRole_4"},
+ {role_id: "123", org_code: "XYZ", role_name: "MockRole_1"} // Previously selected role moved back
+ ],
+ selectedRoleId: "789"
+ }
+ })
+ })
+
+ it("should return 500 and log error when updateDynamoTable throws an error", async () => {
+ const error = new Error("Dynamo update failed")
+ const loggerSpy = jest.spyOn(Logger.prototype, "error")
+ mockUpdateDynamoTable.mockImplementation(() => {
+ throw error
+ })
+
+ const response = await handler(event, context)
+ expect(response).toMatchObject({
+ message: "A system error has occurred"
+ })
+ expect(loggerSpy).toHaveBeenCalledWith(
+ expect.any(Object),
+ "Error: Dynamo update failed"
+ )
+ })
+
+ it("should handle unexpected error types gracefully", async () => {
+ mockUpdateDynamoTable.mockImplementation(() => {
+ throw new Error("Unexpected error string")
+ })
+ const loggerSpy = jest.spyOn(Logger.prototype, "error")
+
+ const response = await handler(event, context)
+ expect(response).toMatchObject({
+ message: "A system error has occurred"
+ })
+ expect(loggerSpy).toHaveBeenCalledWith(
+ expect.any(Object),
+ "Error: Unexpected error string"
+ )
+ })
+
+ it("should return 400 when request body is missing", async () => {
+ event.body = "" as unknown as string
+ const response = await handler(event, context)
+
+ expect(response.statusCode).toBe(400)
+ expect(response.body).toContain("Request body is required")
+ })
+
+ it("should return 400 when request body is invalid JSON", async () => {
+ event.body = "Invalid JSON"
+ const response = await handler(event, context)
+
+ expect(response.statusCode).toBe(400)
+ expect(response.body).toContain("Invalid JSON format in request body")
+ })
+
+})
diff --git a/packages/selectedRoleLambda/tests/test_selectedRoleHelpers.test.ts b/packages/selectedRoleLambda/tests/test_selectedRoleHelpers.test.ts
index 055a6b4df8..c88fb9ac3c 100644
--- a/packages/selectedRoleLambda/tests/test_selectedRoleHelpers.test.ts
+++ b/packages/selectedRoleLambda/tests/test_selectedRoleHelpers.test.ts
@@ -1,13 +1,14 @@
import {jest} from "@jest/globals"
import {Logger} from "@aws-lambda-powertools/logger"
import {DynamoDBClient} from "@aws-sdk/client-dynamodb"
-import {DynamoDBDocumentClient} from "@aws-sdk/lib-dynamodb"
-import {updateDynamoTable} from "@/selectedRoleHelpers"
-import {TrackerUserInfo} from "@/selectedRoleTypes"
+import {DynamoDBDocumentClient, GetCommand, UpdateCommand} from "@aws-sdk/lib-dynamodb"
+import {updateDynamoTable, fetchUserRolesFromDynamoDB} from "@/selectedRoleHelpers"
+import {SelectedRole} from "@/selectedRoleTypes"
// Mock Logger
const logger = new Logger()
jest.spyOn(logger, "info")
+jest.spyOn(logger, "warn")
jest.spyOn(logger, "error")
// Properly initializing the DynamoDB client
@@ -18,8 +19,8 @@ jest.spyOn(dynamoDBClient, "send").mockImplementation(() => Promise.resolve({}))
const username = "testUser"
const tokenMappingTableName = "dummyTable"
-const mockRoleData: TrackerUserInfo = {
- roles_with_access: [
+const mockRoleData: SelectedRole = {
+ rolesWithAccess: [
{
role_name: "Doctor",
role_id: "123",
@@ -27,31 +28,15 @@ const mockRoleData: TrackerUserInfo = {
org_name: "Test Hospital"
}
],
- roles_without_access: [],
- currently_selected_role: {
+ currentlySelectedRole: {
role_name: "Doctor",
role_id: "123",
org_code: "ABC",
org_name: "Test Hospital"
- }
+ },
+ selectedRoleId: "123"
}
-// Mock OIDC Token Verification
-const mockVerifyIdToken = jest.fn()
-
-jest.unstable_mockModule("@cpt-ui-common/authFunctions", async () => {
- const verifyIdToken = mockVerifyIdToken.mockImplementation(async () => {
- return {
- selected_roleid: "123"
- }
- })
-
- return {
- __esModule: true,
- verifyIdToken: verifyIdToken
- }
-})
-
describe("updateDynamoTable", () => {
beforeEach(() => {
jest.clearAllMocks()
@@ -59,6 +44,38 @@ describe("updateDynamoTable", () => {
it("should update DynamoDB with user selected role successfully", async () => {
await updateDynamoTable(username, mockRoleData, dynamoDBClient, logger, tokenMappingTableName)
+
+ expect(dynamoDBClient.send).toHaveBeenCalledWith(expect.any(UpdateCommand))
+ expect(logger.info).toHaveBeenCalledWith(
+ expect.stringContaining("DynamoDB update successful"),
+ expect.any(Object)
+ )
+ })
+
+ it("should ensure no undefined values are stored in DynamoDB", async () => {
+ const incompleteRoleData: SelectedRole = {
+ // Ensuring an empty array instead of undefined
+ rolesWithAccess: undefined as unknown as [],
+ // Ensuring an empty object instead of undefined
+ currentlySelectedRole: undefined as unknown as Record,
+ // Ensuring an empty string instead of undefined
+ selectedRoleId: undefined as unknown as string
+ }
+
+ // Call the function with incomplete role data
+ await updateDynamoTable(username, incompleteRoleData, dynamoDBClient, logger, tokenMappingTableName)
+
+ // Verify that the logger captured the correct transformation of data
+ expect(logger.info).toHaveBeenCalledWith(
+ expect.stringContaining("Prepared role data for DynamoDB update"),
+ expect.objectContaining({
+ currentlySelectedRole: {}, // Should be set to an empty object
+ rolesWithAccess: [], // Should be set to an empty array
+ selectedRoleId: "" // Should be set to an empty string
+ })
+ )
+
+ // Verify that the function actually sent the update command to DynamoDB
expect(dynamoDBClient.send).toHaveBeenCalled()
})
@@ -86,4 +103,79 @@ describe("updateDynamoTable", () => {
})
)
})
+
+ it("should handle unknown errors gracefully", async () => {
+ const unexpectedError = {customMessage: "Unexpected error"} // Simulating a non-Error object
+ jest.spyOn(dynamoDBClient, "send").mockRejectedValueOnce(unexpectedError as never)
+
+ await expect(
+ updateDynamoTable(username, mockRoleData, dynamoDBClient, logger, tokenMappingTableName)
+ ).rejects.toThrow("Unexpected error occurred while updating user's selected role") // Expect our new custom error
+
+ expect(logger.error).toHaveBeenCalledWith(
+ "Unknown error type while updating user's selected role",
+ expect.objectContaining({
+ username,
+ error: JSON.stringify(unexpectedError)
+ })
+ )
+ })
+})
+
+describe("fetchUserRolesFromDynamoDB", () => {
+ beforeEach(() => {
+ jest.clearAllMocks()
+ jest.spyOn(logger, "info").mockImplementation(() => {})
+ jest.spyOn(logger, "warn").mockImplementation(() => {})
+ jest.spyOn(logger, "error").mockImplementation(() => {})
+ })
+
+ it("should successfully fetch roles with access from DynamoDB", async () => {
+ // Mock a successful DynamoDB response
+ const mockDynamoResponse = {
+ Item: {
+ rolesWithAccess: [
+ {role_name: "Nurse", role_id: "456", org_code: "XYZ", org_name: "Clinic A"},
+ {role_name: "Surgeon", role_id: "789", org_code: "DEF", org_name: "Hospital B"}
+ ]
+ }
+ }
+
+ jest.spyOn(dynamoDBClient, "send").mockResolvedValueOnce(mockDynamoResponse as never)
+
+ const result = await fetchUserRolesFromDynamoDB(username, dynamoDBClient, logger, tokenMappingTableName)
+
+ expect(dynamoDBClient.send).toHaveBeenCalledWith(expect.any(GetCommand))
+ expect(logger.info).toHaveBeenCalledWith(
+ "Roles and selected role successfully retrieved from DynamoDB",
+ expect.objectContaining({data: expect.any(Object)})
+ )
+
+ expect(result).toEqual({
+ rolesWithAccess: mockDynamoResponse.Item.rolesWithAccess
+ })
+ })
+
+ it("should return an empty array if no user data is found in DynamoDB", async () => {
+ const mockEmptyResponse: {Item: undefined} = {Item: undefined}
+ jest.spyOn(dynamoDBClient, "send").mockResolvedValueOnce(mockEmptyResponse as never)
+
+ const result = await fetchUserRolesFromDynamoDB(username, dynamoDBClient, logger, tokenMappingTableName)
+
+ expect(logger.warn).toHaveBeenCalledWith("No user info found in DynamoDB", {username})
+ expect(result).toEqual({rolesWithAccess: []})
+ })
+
+ it("should throw an error if DynamoDB request fails", async () => {
+ jest.spyOn(dynamoDBClient, "send").mockRejectedValueOnce(new Error("DynamoDB fetch error") as never)
+
+ await expect(
+ fetchUserRolesFromDynamoDB(username, dynamoDBClient, logger, tokenMappingTableName)
+ ).rejects.toThrow("Failed to retrieve user info from cache")
+
+ expect(logger.error).toHaveBeenCalledWith(
+ "Error fetching user info from DynamoDB",
+ expect.objectContaining({error: expect.any(Error)})
+ )
+ })
})
diff --git a/packages/trackerUserInfoLambda/src/handler.ts b/packages/trackerUserInfoLambda/src/handler.ts
index b7ce468fbd..ebabf295f0 100644
--- a/packages/trackerUserInfoLambda/src/handler.ts
+++ b/packages/trackerUserInfoLambda/src/handler.ts
@@ -66,7 +66,7 @@ const lambdaHandler = async (event: APIGatewayProxyEvent): Promise {
if (!roleString) {
return undefined
@@ -19,13 +23,16 @@ export const removeRoleCategories = (roleString: string | undefined) => {
return chunk.replace(/"/g, "")
}
-// Fetch user info from the OIDC UserInfo endpoint
-// The access token is used to identify the user, and fetch their roles.
-// This populates three lists:
-// - rolesWithAccess: roles that have access to the CPT
-// - rolesWithoutAccess: roles that don't have access to the CPT
-// - [OPTIONAL] currentlySelectedRole: the role that is currently selected by the user
-// Each list contains information on the roles, such as the role name, role ID, ODS code, and organization name.
+/**
+ * **Fetches user information from the OIDC UserInfo endpoint.**
+ *
+ * - Uses the provided access token to retrieve user roles and details.
+ * - Organizes roles into three categories:
+ * - `rolesWithAccess`: Roles that have CPT access.
+ * - `rolesWithoutAccess`: Roles that do not have CPT access.
+ * - `currentlySelectedRole`: The user's currently selected role, if applicable.
+ * - Extracts basic user details (`family_name` and `given_name`).
+ */
export const fetchUserInfo = async (
cis2AccessToken: string,
cis2IdToken: string,
@@ -64,6 +71,7 @@ export const fetchUserInfo = async (
const rolesWithoutAccess: Array = []
let currentlySelectedRole: RoleDetails | undefined = undefined
+ // Extract user details
const userDetails: UserDetails = {
family_name: data.family_name,
given_name: data.given_name
@@ -72,10 +80,19 @@ export const fetchUserInfo = async (
// Get roles from the user info response
const roles = data.nhsid_nrbac_roles || []
+ /**
+ * Processes user roles to determine their access level and selection status.
+ *
+ * - If a role has access (`hasAccess`), it is added to `rolesWithAccess`.
+ * - If a role does not have access, it is added to `rolesWithoutAccess`.
+ * - If a role has access and matches the `selectedRoleId`, it is set as `currentlySelectedRole`.
+ *
+ */
roles.forEach((role) => {
logger.debug("Processing role", {role})
- const activityCodes = role.activity_codes || []
+ // Extract activity codes and check if any match the accepted access codes
+ const activityCodes = role.activity_codes || []
const hasAccess = activityCodes.some((code: string) => accepted_access_codes.includes(code))
logger.debug("Role CPT access?", {hasAccess})
@@ -86,7 +103,7 @@ export const fetchUserInfo = async (
org_name: getOrgNameFromOrgCode(data, role.org_code, logger)
}
- // Ensure the role has at least one of the required fields
+ // Ensure the role has at least one of the required fields to be processed
if (!(roleInfo.role_name || roleInfo.role_id || roleInfo.org_code || roleInfo.org_name)) {
// Skip roles that don't meet the minimum field requirements
logger.warn("Role does not meet minimum field requirements", {roleInfo})
@@ -94,24 +111,19 @@ export const fetchUserInfo = async (
}
if (hasAccess) {
- rolesWithAccess.push(roleInfo)
- logger.debug("Role has access; adding to rolesWithAccess", {roleInfo})
- } else {
- rolesWithoutAccess.push(roleInfo)
- logger.debug("Role does not have access; adding to rolesWithoutAccess", {roleInfo})
- }
-
- // Determine the currently selected role
- logger.debug("Checking if role is currently selected", {selectedRoleId, role_id: role.person_roleid, roleInfo})
- if (selectedRoleId && role.person_roleid === selectedRoleId) {
- logger.debug("Role is currently selected", {role_id: role.person_roleid, roleInfo})
- if (hasAccess) {
- logger.debug("Role has access; setting as currently selected", {roleInfo})
+ if (selectedRoleId && role.person_roleid === selectedRoleId) {
+ // If the role has access and matches the selectedRoleId, set it as currently selected
+ logger.debug("Role has access and matches selectedRoleId; setting as currently selected", {roleInfo})
currentlySelectedRole = roleInfo
} else {
- logger.debug("Role does not have access; unsetting currently selected role", {roleInfo})
- currentlySelectedRole = undefined
+ // Add the role to rolesWithAccess array only if it is NOT the selectedRoleId
+ rolesWithAccess.push(roleInfo)
+ logger.debug("Role has access; adding to rolesWithAccess", {roleInfo})
}
+ } else {
+ // If role lacks access, add it to rolesWithoutAccess array
+ rolesWithoutAccess.push(roleInfo)
+ logger.debug("Role does not have access; adding to rolesWithoutAccess", {roleInfo})
}
})
@@ -130,7 +142,9 @@ export const fetchUserInfo = async (
}
}
-// Helper function to get organization name from org_code
+/**
+ * **Fetches the organization name for a given organization code.**
+ */
function getOrgNameFromOrgCode(data: UserInfoResponse, org_code: string, logger: Logger): string | undefined {
logger.info("Getting org name from org code", {org_code})
const orgs = data.nhsid_user_orgs || []
@@ -139,8 +153,12 @@ function getOrgNameFromOrgCode(data: UserInfoResponse, org_code: string, logger:
return org ? org.org_name : undefined
}
-// Add the user currentlySelectedRole, rolesWithAccess and rolesWithoutAccess to the dynamoDB document,
-// keyed by this user's username.
+/**
+ * **Updates the user information in DynamoDB.**
+ *
+ * - Stores `currentlySelectedRole`, `rolesWithAccess`, `rolesWithoutAccess`, and `userDetails`.
+ * - Ensures no undefined values are stored in DynamoDB.
+ */
export const updateDynamoTable = async (
username: string,
data: TrackerUserInfo,
@@ -154,31 +172,35 @@ export const updateDynamoTable = async (
logger.debug("Adding user roles to DynamoDB", {username, data})
- // Add the user roles to the DynamoDB table
-
- // Dyanamo cannot allow undefined values. We need to scrub any undefined values from the data objects
+ // Dynamo cannot allow undefined values. We need to scrub any undefined values from the data objects
const currentlySelectedRole: RoleDetails = data.currently_selected_role ? data.currently_selected_role : {}
const rolesWithAccess: Array = data.roles_with_access ? data.roles_with_access : []
const rolesWithoutAccess: Array = data.roles_without_access ? data.roles_without_access : []
+ const userDetails: UserDetails = data.user_details || {family_name: "", given_name: ""}
// Since RoleDetails has a bunch of possibly undefined fields, we need to scrub those out.
// Convert everything to strings, then convert back to a generic object.
const scrubbedCurrentlySelectedRole = JSON.parse(JSON.stringify(currentlySelectedRole))
const scrubbedRolesWithAccess = rolesWithAccess.map((role) => JSON.parse(JSON.stringify(role)))
const scrubbedRolesWithoutAccess = rolesWithoutAccess.map((role) => JSON.parse(JSON.stringify(role)))
+ const scrubbedUserDetails = JSON.parse(JSON.stringify(userDetails))
try {
await documentClient.send(
new UpdateCommand({
TableName: tokenMappingTableName,
Key: {username},
- UpdateExpression:
- // eslint-disable-next-line max-len
- "SET rolesWithAccess = :rolesWithAccess, rolesWithoutAccess = :rolesWithoutAccess, currentlySelectedRole = :currentlySelectedRole",
+ UpdateExpression: `
+ SET rolesWithAccess = :rolesWithAccess,
+ rolesWithoutAccess = :rolesWithoutAccess,
+ currentlySelectedRole = :currentlySelectedRole,
+ userDetails = :userDetails
+ `,
ExpressionAttributeValues: {
":rolesWithAccess": scrubbedRolesWithAccess,
":rolesWithoutAccess": scrubbedRolesWithoutAccess,
- ":currentlySelectedRole": scrubbedCurrentlySelectedRole
+ ":currentlySelectedRole": scrubbedCurrentlySelectedRole,
+ ":userDetails": scrubbedUserDetails
}
})
)
@@ -189,7 +211,12 @@ export const updateDynamoTable = async (
}
}
-// Fetch user info from DynamoDB
+/**
+ * **Fetches user information from DynamoDB.**
+ *
+ * - Retrieves `rolesWithAccess`, `rolesWithoutAccess`, `currentlySelectedRole`, and `userDetails`.
+ * - Returns default values for missing attributes.
+ */
export const fetchDynamoTable = async (
username: string,
documentClient: DynamoDBDocumentClient,
@@ -216,12 +243,10 @@ export const fetchDynamoTable = async (
roles_with_access: response.Item.rolesWithAccess || [],
roles_without_access: response.Item.rolesWithoutAccess || [],
currently_selected_role: response.Item.currentlySelectedRole || undefined,
- user_details: response.Item.userDetails || {family_name: "", given_name: ""} // Default empty user details
+ user_details: response.Item.userDetails || {family_name: "", given_name: ""}
}
- logger.info("User info successfully retrieved from DynamoDB", {
- data: mappedUserInfo
- })
+ logger.info("User info successfully retrieved from DynamoDB", {data: mappedUserInfo})
return mappedUserInfo
} catch (error) {
logger.error("Error fetching user info from DynamoDB", {error})
diff --git a/packages/trackerUserInfoLambda/tests/test_handler.mockDisabled.test.ts b/packages/trackerUserInfoLambda/tests/test_handler.mockDisabled.test.ts
index a3ec4c9907..df778f82c6 100644
--- a/packages/trackerUserInfoLambda/tests/test_handler.mockDisabled.test.ts
+++ b/packages/trackerUserInfoLambda/tests/test_handler.mockDisabled.test.ts
@@ -133,7 +133,7 @@ describe("Lambda Handler Tests with mock disabled", () => {
expect(response).toHaveProperty("body")
const body = JSON.parse(response.body)
- expect(body).toHaveProperty("message", "UserInfo fetched successfully")
+ expect(body).toHaveProperty("message", "UserInfo fetched successfully from the OIDC endpoint")
expect(body).toHaveProperty("userInfo")
})
diff --git a/packages/trackerUserInfoLambda/tests/test_handler.mockEnabled.test.ts b/packages/trackerUserInfoLambda/tests/test_handler.mockEnabled.test.ts
index 98bbb944eb..3f616679f3 100644
--- a/packages/trackerUserInfoLambda/tests/test_handler.mockEnabled.test.ts
+++ b/packages/trackerUserInfoLambda/tests/test_handler.mockEnabled.test.ts
@@ -160,7 +160,7 @@ describe("Lambda Handler Tests with mock enabled", () => {
expect(response).toHaveProperty("body")
const body = JSON.parse(response.body)
- expect(body).toHaveProperty("message", "UserInfo fetched successfully")
+ expect(body).toHaveProperty("message", "UserInfo fetched successfully from the OIDC endpoint")
expect(body).toHaveProperty("userInfo")
})
@@ -362,7 +362,7 @@ describe("Lambda Handler Tests with mock enabled", () => {
)
expect(response.statusCode).toBe(200)
- expect(response.body).toContain("UserInfo fetched successfully from cache")
+ expect(response.body).toContain("UserInfo fetched successfully from DynamoDB")
const responseBody = JSON.parse(response.body)
expect(responseBody.userInfo).toEqual(userInfoMock)
@@ -398,7 +398,7 @@ describe("Lambda Handler Tests with mock enabled", () => {
)
expect(response.statusCode).toBe(200)
- expect(response.body).toContain("UserInfo fetched successfully from cache")
+ expect(response.body).toContain("UserInfo fetched successfully from DynamoDB")
const responseBody = JSON.parse(response.body)
expect(responseBody.userInfo).toEqual(userInfoMock)
@@ -434,7 +434,7 @@ describe("Lambda Handler Tests with mock enabled", () => {
)
expect(response.statusCode).toBe(200)
- expect(response.body).toContain("UserInfo fetched successfully from cache")
+ expect(response.body).toContain("UserInfo fetched successfully from DynamoDB")
const responseBody = JSON.parse(response.body)
expect(responseBody.userInfo).toEqual(userInfoMock)
diff --git a/packages/trackerUserInfoLambda/tests/test_userInfoHelpers.test.ts b/packages/trackerUserInfoLambda/tests/test_userInfoHelpers.test.ts
index aa555f4e9b..ab944eb6f5 100644
--- a/packages/trackerUserInfoLambda/tests/test_userInfoHelpers.test.ts
+++ b/packages/trackerUserInfoLambda/tests/test_userInfoHelpers.test.ts
@@ -92,6 +92,14 @@ describe("fetchUserInfo", () => {
activity_codes: ["OTHER_CODE"],
person_orgid: "org-id-2",
role_code: "role-code-2"
+ },
+ {
+ role_name: "Doctor",
+ person_roleid: "role-id-3",
+ org_code: "ORG3",
+ activity_codes: ["CPT_CODE"],
+ person_orgid: "org-id-3",
+ role_code: "role-code-3"
}
],
nhsid_user_orgs: [
@@ -102,6 +110,10 @@ describe("fetchUserInfo", () => {
{
org_code: "ORG2",
org_name: "Organization Two"
+ },
+ {
+ org_code: "ORG3",
+ org_name: "Organization Three"
}
]
}
@@ -126,9 +138,9 @@ describe("fetchUserInfo", () => {
roles_with_access: [
{
role_name: "Doctor",
- role_id: "role-id-1",
- org_code: "ORG1",
- org_name: "Organization One"
+ role_id: "role-id-3",
+ org_code: "ORG3",
+ org_name: "Organization Three"
}
],
roles_without_access: [
@@ -338,6 +350,34 @@ describe("fetchDynamoTable", () => {
jest.clearAllMocks()
})
+ it("should fetch user roles and user details from DynamoDB", async () => {
+ const mockResponse = {
+ Item: {
+ rolesWithAccess: [
+ {role_name: "Doctor", role_id: "123", org_code: "ABC", org_name: "Test Hospital"}
+ ],
+ rolesWithoutAccess: [],
+ currentlySelectedRole: undefined,
+ userDetails: {family_name: "Doe", given_name: "John"}
+ }
+ }
+
+ mockSend.mockResolvedValueOnce(mockResponse as never)
+
+ const result = await fetchDynamoTable(username, documentClient, logger, tokenMappingTableName)
+
+ expect(result).toEqual({
+ roles_with_access: [
+ {role_name: "Doctor", role_id: "123", org_code: "ABC", org_name: "Test Hospital"}
+ ],
+ roles_without_access: [],
+ currently_selected_role: undefined,
+ user_details: {family_name: "Doe", given_name: "John"}
+ })
+
+ expect(mockSend).toHaveBeenCalled()
+ })
+
it("should throw an error when DynamoDB retrieval fails", async () => {
mockSend.mockRejectedValue(new Error("DynamoDB error"))
@@ -373,4 +413,28 @@ describe("fetchDynamoTable", () => {
user_details: {family_name: "Doe", given_name: "John"}
})
})
+
+ it("should return default user details if missing in DynamoDB", async () => {
+ const mockResponse = {
+ Item: {
+ rolesWithAccess: [],
+ rolesWithoutAccess: [],
+ currentlySelectedRole: undefined
+ // userDetails is missing
+ }
+ }
+
+ mockSend.mockResolvedValueOnce(mockResponse as never)
+
+ const result = await fetchDynamoTable(username, documentClient, logger, tokenMappingTableName)
+
+ expect(result).toEqual({
+ roles_with_access: [],
+ roles_without_access: [],
+ currently_selected_role: undefined,
+ user_details: {family_name: "", given_name: ""}
+ })
+
+ expect(mockSend).toHaveBeenCalled()
+ })
})
diff --git a/scripts/run_regression_tests.py b/scripts/run_regression_tests.py
index f377312b09..9506d712f4 100644
--- a/scripts/run_regression_tests.py
+++ b/scripts/run_regression_tests.py
@@ -13,7 +13,7 @@
from requests.auth import HTTPBasicAuth
# This should be set to a known good version of regression test repo
-REGRESSION_TESTS_REPO_TAG = "v2.11.0"
+REGRESSION_TESTS_REPO_TAG = "v2.11.4"
GITHUB_API_URL = "https://api.github.com/repos/NHSDigital/electronic-prescription-service-api-regression-tests/actions"
GITHUB_RUN_URL = "https://github.com/NHSDigital/electronic-prescription-service-api-regression-tests/actions/runs"