diff --git a/README.md b/README.md index a113dada..2198b9ec 100644 --- a/README.md +++ b/README.md @@ -63,18 +63,19 @@ git clone https://github.com/ONSdigital/blaise-deploy-questionnaire-service.git Create a new .env file and add the following variables. -| Variable | Description | Var Example | -|----------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------| -| PORT | **Optional variable**, specify the Port for express server to run on. If not passed in this is set as 5000 by default.

It's best not to set this as the react project will try and use the variable as well and conflict. By default, React project locally runs on port 3000 | 5009 | -| BLAISE_API_URL | URL that the [Blaise Rest API](https://github.com/ONSdigital/blaise-api-rest) is running on to send calls to | localhost:90 | -| PROJECT_ID | GCP Project ID | ons-blaise-dev-matt55 | -| BUCKET_NAME | GCP Bucket name for the Questionnaire file to be put in | ons-blaise-dev-matt55-dqs | -| SERVER_PARK | Name of Blaise Server Park | gusty | -| BIMS_API_URL | URL that the [BIMS Service](https://github.com/ONSdigital/blaise-instrument-metadata-service) is running on to send calls to set and get the live date | localhost:5011 | -| BIMS_CLIENT_ID | GCP IAP ID for the [BIMS Service](https://github.com/ONSdigital/blaise-instrument-metadata-service) | randomKey0112 | -| BUS_API_CLIENT | Not needed for local development but the config will look for this variables in the .env file and throw an error if it is not found. Hence, give them a random string | FOO | -| BUS_CLIENT_ID | Not needed for local development but the config will look for this variables in the .env file and throw an error if it is not found. Hence, give them a random string | FOO | -| CREATE_DONOR_CASES_CLOUD_FUNCTION_URL | URL to trigger the Create Donor Cases Cloud Function in GCP. This is needed when you want to deploy donor cases for an IPS questionnaire. **The Cloud Function needs to be deployed in GCP**. | https://example-cloud-function-url.com | +| Variable | Description | Var Example | +|-------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------| +| PORT | **Optional variable**, specify the Port for express server to run on. If not passed in this is set as 5000 by default.

It's best not to set this as the react project will try and use the variable as well and conflict. By default, React project locally runs on port 3000 | 5009 | +| BLAISE_API_URL | URL that the [Blaise Rest API](https://github.com/ONSdigital/blaise-api-rest) is running on to send calls to | localhost:90 | +| PROJECT_ID | GCP Project ID | ons-blaise-dev-matt55 | +| BUCKET_NAME | GCP Bucket name for the Questionnaire file to be put in | ons-blaise-dev-matt55-dqs | +| SERVER_PARK | Name of Blaise Server Park | gusty | +| BIMS_API_URL | URL that the [BIMS Service](https://github.com/ONSdigital/blaise-instrument-metadata-service) is running on to send calls to set and get the live date | localhost:5011 | +| BIMS_CLIENT_ID | GCP IAP ID for the [BIMS Service](https://github.com/ONSdigital/blaise-instrument-metadata-service) | randomKey0112 | +| BUS_API_CLIENT | Not needed for local development but the config will look for this variables in the .env file and throw an error if it is not found. Hence, give them a random string | FOO | +| BUS_CLIENT_ID | Not needed for local development but the config will look for this variables in the .env file and throw an error if it is not found. Hence, give them a random string | FOO | +| CREATE_DONOR_CASES_CLOUD_FUNCTION_URL | URL to trigger the Create Donor Cases Cloud Function in GCP. This is needed when you want to deploy donor cases for an IPS questionnaire. **The Cloud Function needs to be deployed in GCP**. | https://example-cloud-function-url.com | +| REISSUE_NEW_DONOR_CASE_CLOUD_FUNCTION_URL | URL to trigger the Reissue New Donor Case Cloud Function in GCP. This is needed when you want to reissue a new donor case for an IPS questionnaire. **The Cloud Function needs to be deployed in GCP**. | https://example-cloud-function-url.com | To find the `X_CLIENT_ID`, navigate to the GCP console, search for `IAP`, click the three dots on right of the service and select `OAuth`. `Client Id` will be on the right. @@ -83,7 +84,7 @@ The .env file should be setup as below Example .env file: ```.env -BLAISE_API_URL=localhost:5011 +BLAISE_API_URL=localhost:8011 PROJECT_ID=ons-blaise-v2-dev- BUCKET_NAME=ons-blaise-v2-dev--dqs SERVER_PARK=gusty @@ -92,6 +93,7 @@ BIMS_CLIENT_ID=randomKey0778 BUS_API_URL=FOO BUS_CLIENT_ID=FOO CREATE_DONOR_CASES_CLOUD_FUNCTION_URL= +REISSUE_NEW_DONOR_CASE_CLOUD_FUNCTION_URL= ``` **NB** DQS environment variables for sandboxes can be found within GCP > App Engine > Versions > DQS service > Config diff --git a/appengine_templates/app.yaml.tpl b/appengine_templates/app.yaml.tpl index caa5875f..634a4ad2 100644 --- a/appengine_templates/app.yaml.tpl +++ b/appengine_templates/app.yaml.tpl @@ -17,6 +17,7 @@ env_variables: SESSION_SECRET: _SESSION_SECRET ROLES: _ROLES CREATE_DONOR_CASES_CLOUD_FUNCTION_URL: _CREATE_DONOR_CASES_CLOUD_FUNCTION_URL + REISSUE_NEW_DONOR_CASE_CLOUD_FUNCTION_URL: _REISSUE_NEW_DONOR_CASE_CLOUD_FUNCTION_URL automatic_scaling: min_instances: _MIN_INSTANCES diff --git a/jest.config.js b/jest.config.js index 97385be4..05837f2b 100644 --- a/jest.config.js +++ b/jest.config.js @@ -17,5 +17,6 @@ process.env = Object.assign(process.env, { BIMS_API_URL: "bims-mock-api", BIMS_CLIENT_ID: "mock-client-id", CREATE_DONOR_CASES_CLOUD_FUNCTION_URL: "mock-cloud-function-url", + REISSUE_NEW_DONOR_CASE_CLOUD_FUNCTION_URL: "mock-cloud-function-url", AUTH_TOKEN: "mock-dummy-token" }); diff --git a/src/app.tsx b/src/app.tsx index a695a98e..8a392663 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -21,6 +21,7 @@ import QuestionnaireDetailsPage from "./components/questionnaireDetailsPage/ques import ChangeTOStartDate from "./components/questionnaireDetailsPage/changeTOStartDate"; import ChangeTMReleaseDate from "./components/questionnaireDetailsPage/changeTmReleaseDate"; import CreateDonorCasesConfirmation from "./components/createDonorCasePage/createDonorCasesConfirmation"; +import ReissueNewDonorCaseConfirmation from "./components/reissueNewDonorCasePage/reissueNewDonorCaseConfirmation"; import "./style.css"; import { isProduction } from "./client/env"; import { Authenticate } from "blaise-login-react/blaise-login-react-client"; @@ -101,6 +102,10 @@ function App(): ReactElement { path="/createDonorCasesConfirmation" element={}> + }> + @@ -122,7 +127,7 @@ function App(): ReactElement { {(_user, loggedIn, logOutFunction) => ( <> - Skip to content + Skip to content { isProduction(window.location.hostname) ? <> : } @@ -154,4 +159,4 @@ function App(): ReactElement { ); } -export default App; \ No newline at end of file +export default App; diff --git a/src/components/createDonorCasePage/createDonorCasesConfirmation.test.tsx b/src/components/createDonorCasePage/createDonorCasesConfirmation.test.tsx index 230cd838..1ea6977b 100644 --- a/src/components/createDonorCasePage/createDonorCasesConfirmation.test.tsx +++ b/src/components/createDonorCasePage/createDonorCasesConfirmation.test.tsx @@ -8,7 +8,12 @@ import { MemoryRouter, RouterProvider, createMemoryRouter, useNavigate, useParam import CreateDonorCasesConfirmation from "./createDonorCasesConfirmation"; import "@testing-library/jest-dom"; import axios from "axios"; -import { cloudFunctionAxiosError, ipsQuestionnaire, mockSuccessResponseForDonorCasesCreation } from "../../features/step_definitions/helpers/apiMockObjects"; +import { + cloudFunctionAxiosError, + ipsQuestionnaire, + mockSectionForDonorCasesCreation, + mockSuccessResponseForDonorCasesCreation +} from "../../features/step_definitions/helpers/apiMockObjects"; jest.mock("axios"); @@ -81,15 +86,16 @@ describe("CreateDonorCasesConfirmation navigation", () => { }); it("should redirect back to the questionnaire details page if user clicks Cancel", async () => { - + act(() => { fireEvent.click(screen.getByRole("button", { name: "Cancel" })); }); expect(navigate).toHaveBeenCalledWith("/questionnaire/IPS1337a", { state: { - donorCasesResponseMessage: "", - donorCasesStatusCode: 0, + section: "createDonorCases", + responseMessage: "", + statusCode: 0, role: "", questionnaire: ipsQuestionnaire, }, @@ -113,8 +119,9 @@ describe("CreateDonorCasesConfirmation navigation", () => { expect(navigate).toHaveBeenCalledWith("/questionnaire/IPS1337a", { state: { - donorCasesResponseMessage: mockSuccessResponseForDonorCasesCreation.data, - donorCasesStatusCode: mockSuccessResponseForDonorCasesCreation.status, + section: mockSectionForDonorCasesCreation.data, + responseMessage: mockSuccessResponseForDonorCasesCreation.data, + statusCode: mockSuccessResponseForDonorCasesCreation.status, questionnaire: ipsQuestionnaire, role: "IPS Manager" } @@ -141,8 +148,9 @@ describe("CreateDonorCasesConfirmation navigation", () => { expect(navigate).toHaveBeenCalledWith("/questionnaire/IPS1337a", { state: { - donorCasesResponseMessage: (cloudFunctionAxiosError as any).response.data.message, - donorCasesStatusCode: 500, + section: "createDonorCases", + responseMessage: (cloudFunctionAxiosError as any).response.data.message, + statusCode: 500, questionnaire: ipsQuestionnaire, role: "IPS Manager" } @@ -150,4 +158,4 @@ describe("CreateDonorCasesConfirmation navigation", () => { }); }); -}); \ No newline at end of file +}); diff --git a/src/components/createDonorCasePage/createDonorCasesConfirmation.tsx b/src/components/createDonorCasePage/createDonorCasesConfirmation.tsx index a003825f..e7f6b9dc 100644 --- a/src/components/createDonorCasePage/createDonorCasesConfirmation.tsx +++ b/src/components/createDonorCasePage/createDonorCasesConfirmation.tsx @@ -33,7 +33,7 @@ function CreateDonorCasesConfirmation(): ReactElement { }; } isLoading(false); - navigate(`/questionnaire/${questionnaire.name}`, { state: { donorCasesResponseMessage: res.data, donorCasesStatusCode: res.status, questionnaire: questionnaire, role: role } }); + navigate(`/questionnaire/${questionnaire.name}`, { state: { section: "createDonorCases", responseMessage: res.data, statusCode: res.status, questionnaire: questionnaire, role: role } }); } if (loading) { return ; @@ -61,7 +61,7 @@ function CreateDonorCasesConfirmation(): ReactElement { /> navigate(`/questionnaire/${questionnaire.name}`, { state: { donorCasesResponseMessage: "", donorCasesStatusCode: 0, questionnaire: questionnaire, role: "" } })} primary={false} /> + onClick={() => navigate(`/questionnaire/${questionnaire.name}`, { state: { section: "createDonorCases", responseMessage: "", statusCode: 0, questionnaire: questionnaire, role: "" } })} primary={false} /> ) } @@ -70,4 +70,4 @@ function CreateDonorCasesConfirmation(): ReactElement { ); } -export default CreateDonorCasesConfirmation; \ No newline at end of file +export default CreateDonorCasesConfirmation; diff --git a/src/components/questionnaireDetailsPage/questionnaireDetailsPage.tsx b/src/components/questionnaireDetailsPage/questionnaireDetailsPage.tsx index c5082943..82fbba7d 100644 --- a/src/components/questionnaireDetailsPage/questionnaireDetailsPage.tsx +++ b/src/components/questionnaireDetailsPage/questionnaireDetailsPage.tsx @@ -13,11 +13,14 @@ import { ONSButton, ONSLoadingPanel, ONSPanel } from "blaise-design-system-react import QuestionnaireDetails from "./sections/questionnaireDetails"; import CreateDonorCases from "./sections/createDonorCases"; import CreateDonorCasesSummary from "../createDonorCasePage/createDonorCasesSummary"; +import ReissueNewDonorCase from "./sections/reissueNewDonorCase"; +import ReissueNewDonorCaseSummary from "../reissueNewDonorCasePage/reissueNewDonorCaseSummary"; interface State { + section?: string; questionnaire: Questionnaire | null; - donorCasesResponseMessage?: string; - donorCasesStatusCode?: number; + responseMessage?: string; + statusCode?: number; role?: string; } @@ -31,7 +34,7 @@ function QuestionnaireDetailsPage(): ReactElement { const [loaded, setLoaded] = useState(false); const initialState = location || { questionnaire: null }; const { questionnaireName } = useParams(); - const { donorCasesResponseMessage, donorCasesStatusCode, role } = location || { donorCasesResponseMessage: "", donorCasesStatusCode: 0, role: "" }; + const { section, responseMessage, statusCode, role } = location || { section: "", responseMessage: "", statusCode: 0, role: "" }; useEffect(() => { if (initialState.questionnaire === null) { @@ -115,9 +118,11 @@ function QuestionnaireDetailsPage(): ReactElement { {questionnaire.name} - {donorCasesResponseMessage && donorCasesStatusCode && role && } + {section === "createDonorCases" && responseMessage && statusCode && role && } + {section === "reissueNewDonorCase" && responseMessage && statusCode && role && } {questionnaire.name.includes("IPS") && } + {questionnaire.name.includes("IPS") && } diff --git a/src/components/questionnaireDetailsPage/sections/createDonorCases.tsx b/src/components/questionnaireDetailsPage/sections/createDonorCases.tsx index 2bf4219e..0cf548b6 100644 --- a/src/components/questionnaireDetailsPage/sections/createDonorCases.tsx +++ b/src/components/questionnaireDetailsPage/sections/createDonorCases.tsx @@ -38,7 +38,7 @@ function CreateDonorCases({ questionnaire }: Props): ReactElement { Create cases diff --git a/src/components/questionnaireDetailsPage/sections/reissueNewDonorCase.tsx b/src/components/questionnaireDetailsPage/sections/reissueNewDonorCase.tsx new file mode 100644 index 00000000..67063d59 --- /dev/null +++ b/src/components/questionnaireDetailsPage/sections/reissueNewDonorCase.tsx @@ -0,0 +1,77 @@ +import React, { ReactElement, useState } from "react"; +import { Questionnaire } from "blaise-api-node-client"; +import { useNavigate } from "react-router-dom"; +import { ONSButton, ONSPanel } from "blaise-design-system-react-components"; + +interface Props { + questionnaire: Questionnaire; +} + +function ReissueNewDonorCase({ questionnaire }: Props): ReactElement { + const [user, setUser] = useState(""); + const [error, setError] = useState(false); + const [errorMessage, setErrorMessage] = useState(""); + + const navigate = useNavigate(); + + function reissueNewDonorCaseButtonClicked() { + const trimmedUser = user.trim(); + setUser(trimmedUser); + + // Check if input is empty after trimming + if (trimmedUser === "") { + setErrorMessage("User input cannot be empty or contain only spaces"); + setError(true); + } else { + setError(false); + setErrorMessage(""); + navigate("/reissueNewDonorCaseConfirmation", { state: { section: "reissueNewDonorCase", questionnaire: questionnaire, user: trimmedUser } }); + } + } + + return ( + <> +
+
+

Reissue New Donor Case

+ + + + + + + +
+
+
+ + setUser(e.target.value)}/> +
+
+ {error && + +

+ {errorMessage} +

+
} +
+
+
+
+ +
+
+
+ + ); +} + +export default ReissueNewDonorCase; diff --git a/src/components/reissueNewDonorCasePage/reissueNewDonorCaseConfirmation.test.tsx b/src/components/reissueNewDonorCasePage/reissueNewDonorCaseConfirmation.test.tsx new file mode 100644 index 00000000..7a0d9b7c --- /dev/null +++ b/src/components/reissueNewDonorCasePage/reissueNewDonorCaseConfirmation.test.tsx @@ -0,0 +1,161 @@ +/** + * @jest-environment jsdom + */ + +import React from "react"; +import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { MemoryRouter, RouterProvider, createMemoryRouter, useNavigate } from "react-router-dom"; +import ReissueNewDonorCaseConfirmation from "./reissueNewDonorCaseConfirmation"; +import "@testing-library/jest-dom"; +import axios from "axios"; +import { + cloudFunctionAxiosError, + ipsQuestionnaire, + mockSectionForReissueNewDonorCase, + mockSuccessResponseForReissueNewDonorCase +} from "../../features/step_definitions/helpers/apiMockObjects"; + +jest.mock("axios"); + +jest.mock("react-router-dom", () => ({ + ...jest.requireActual("react-router-dom"), + useNavigate: jest.fn() +})); + +const mockedAxios = axios as jest.Mocked; + +describe("ReissueNewDonorCaseConfirmation rendering", () => { + beforeEach(() => { + render( + + + + ); + }); + + it("displays correct prompt to reissue new donor case", () => { + expect(screen.getByText("Reissue a new donor case for on behalf of ?")).toBeInTheDocument(); + }); + + it("displays the correct number of breadcrumbs", () => { + expect.assertions(2); + + expect(screen.getByTestId("breadcrumb-0")).toBeInTheDocument(); + expect(screen.getByTestId("breadcrumb-1")).toBeInTheDocument(); + }); + + it("displays a button continue to reissue new donor case", () => { + expect(screen.getByRole("button", { name: "Continue" })).toBeInTheDocument(); + }); + + it("displays a button to navigate back to reissue new donor case page", () => { + expect(screen.getByRole("button", { name: "Cancel" })).toBeInTheDocument(); + }); + +}); + +describe("ReissueNewDonorCaseConfirmation navigation", () => { + let navigate: jest.Mock; + const routes = [ + { + path: "/reissueNewDonorCaseConfirmation", + element: , + }, + ]; + + const initialEntries = [ + { + pathname: "/reissueNewDonorCaseConfirmation", + state: { questionnaire: ipsQuestionnaire, user: "testuser" }, + }, + ]; + beforeEach(() => { + navigate = jest.fn(); + (useNavigate as jest.Mock).mockReturnValue(navigate); + + const router = createMemoryRouter(routes, { + initialEntries, + initialIndex: 0, + }); + + render(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it("should redirect back to the questionnaire details page if user clicks Cancel", async () => { + + act(() => { + fireEvent.click(screen.getByRole("button", { name: "Cancel" })); + }); + + expect(navigate).toHaveBeenCalledWith("/questionnaire/IPS1337a", { + state: { + section: "reissueNewDonorCase", + responseMessage: "", + statusCode: 0, + role: "", + questionnaire: ipsQuestionnaire, + }, + }); + }); + + it("calls the API endpoint correctly when the continue button is clicked", async () => { + + mockedAxios.post.mockResolvedValueOnce(mockSuccessResponseForReissueNewDonorCase); + act(() => { + fireEvent.click(screen.getByRole("button", { name: /Continue/i })); + }); + await waitFor(() => { + + expect(mockedAxios.post).toHaveBeenCalledWith( + "/api/cloudFunction/reissueNewDonorCase", + { questionnaire_name: ipsQuestionnaire.name, user: "testuser" }, + { headers: { "Content-Type": "application/json" } } + ); + + expect(navigate).toHaveBeenCalledWith("/questionnaire/IPS1337a", + { + state: { + section: mockSectionForReissueNewDonorCase.data, + responseMessage: mockSuccessResponseForReissueNewDonorCase.data, + statusCode: mockSuccessResponseForReissueNewDonorCase.status, + questionnaire: ipsQuestionnaire, + role: "testuser" + } + }); + + }); + + }); + + it("should go back to the questionnaire details page if user clicks Continue and error panel is shown", async () => { + + mockedAxios.post.mockRejectedValue(cloudFunctionAxiosError); + act(() => { + fireEvent.click(screen.getByRole("button", { name: /Continue/i })); + }); + await waitFor(() => { + + expect(mockedAxios.post).toHaveBeenCalledWith( + "/api/cloudFunction/reissueNewDonorCase", + { questionnaire_name: ipsQuestionnaire.name, user: "testuser" }, + { headers: { "Content-Type": "application/json" } } + ); + + expect(navigate).toHaveBeenCalledWith("/questionnaire/IPS1337a", + { + state: { + section: "reissueNewDonorCase", + responseMessage: (cloudFunctionAxiosError as any).response.data.message, + statusCode: 500, + questionnaire: ipsQuestionnaire, + role: "testuser" + } + }); + }); + + }); +}); diff --git a/src/components/reissueNewDonorCasePage/reissueNewDonorCaseConfirmation.tsx b/src/components/reissueNewDonorCasePage/reissueNewDonorCaseConfirmation.tsx new file mode 100644 index 00000000..b3736236 --- /dev/null +++ b/src/components/reissueNewDonorCasePage/reissueNewDonorCaseConfirmation.tsx @@ -0,0 +1,74 @@ +import React, { ReactElement } from "react"; +import { useLocation, useNavigate } from "react-router-dom"; +import Breadcrumbs from "../breadcrumbs"; +import { ONSButton, ONSLoadingPanel } from "blaise-design-system-react-components"; +import axios from "axios"; +import { Questionnaire } from "blaise-api-node-client"; +import axiosConfig from "../../client/axiosConfig"; + +interface Location { + questionnaire: Questionnaire; + user: string; +} + +function ReissueNewDonorCaseConfirmation(): ReactElement { + const location = useLocation().state as Location; + const { questionnaire, user } = location || { questionnaire: "", user: "" }; + + const navigate = useNavigate(); + + const [loading, isLoading] = React.useState(false); + + async function callReissueNewDonorCaseCloudFunction() { + isLoading(true); + console.log(questionnaire.name, user); + const payload = { questionnaire_name: questionnaire.name, user: user }; + let res; + try { + res = await axios.post("/api/cloudFunction/reissueNewDonorCase", payload, axiosConfig()); + } catch (error) { + const errorMessage = JSON.stringify((error as any).response.data.message); + res = { + data: errorMessage, + status: 500 + }; + } + isLoading(false); + navigate(`/questionnaire/${questionnaire.name}`, { state: { section: "reissueNewDonorCase", responseMessage: res.data, statusCode: res.status, questionnaire: questionnaire, role: user } }); + } + if (loading) { + return ; + } + return ( + <> + + +
+ { + ( + <> +

+ Reissue a new donor case for {questionnaire.name} on behalf of {user}? +

+ + navigate(`/questionnaire/${questionnaire.name}`, { state: { section: "reissueNewDonorCase", responseMessage: "", statusCode: 0, questionnaire: questionnaire, role: "" } })} primary={false} /> + + ) + } +
+ + ); +} + +export default ReissueNewDonorCaseConfirmation; diff --git a/src/components/reissueNewDonorCasePage/reissueNewDonorCaseSummary.test.tsx b/src/components/reissueNewDonorCasePage/reissueNewDonorCaseSummary.test.tsx new file mode 100644 index 00000000..54db8153 --- /dev/null +++ b/src/components/reissueNewDonorCasePage/reissueNewDonorCaseSummary.test.tsx @@ -0,0 +1,38 @@ +/** + * @jest-environment jsdom + */ + +import React from "react"; +import { render } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import ReissueNewDonorCaseSummary from "./reissueNewDonorCaseSummary"; + +describe("ReissueNewDonorCaseSummary", () => { + it("displays a success message when receiving a successful response from the cloud function", () => { + const props = { + responseMessage: "Success", + statusCode: 200, + role: "testuser1", + }; + + const { getByText } = render(); + expect( + getByText(/Reissued donor case created successfully for testuser1/i) + ).toBeInTheDocument(); + expect(getByText(/Success/i)).toBeInTheDocument(); + }); + + it("displays an error message when receiving a failed response from the cloud function", () => { + const props = { + responseMessage: "Internal Server Error", + statusCode: 500, + role: "testuser1", + }; + + const { getByText } = render(); + expect( + getByText(/Error reissuing new donor case for testuser1/i) + ).toBeInTheDocument(); + expect(getByText(/When reporting this issue to the Service Desk, please provide the questionnaire name, user, time and date of the failure./i)).toBeInTheDocument(); + }); +}); diff --git a/src/components/reissueNewDonorCasePage/reissueNewDonorCaseSummary.tsx b/src/components/reissueNewDonorCasePage/reissueNewDonorCaseSummary.tsx new file mode 100644 index 00000000..d5d5aaa1 --- /dev/null +++ b/src/components/reissueNewDonorCasePage/reissueNewDonorCaseSummary.tsx @@ -0,0 +1,38 @@ +import React, { ReactElement } from "react"; +import { ONSPanel } from "blaise-design-system-react-components"; + +interface Props { + responseMessage: string; + statusCode: number; + role: string; +} + +function ReissueNewDonorCaseSummary({ statusCode, role }: Props): ReactElement { + + return ( + <> +
+ { + (statusCode === 200 ? + +

+ Reissued donor case created successfully for {role} +

+
+ : + +

+ Error reissuing new donor case for {role} +

+

+ When reporting this issue to the Service Desk, please provide the questionnaire name, user, time and date of the failure. +

+
+ ) + } +
+ + ); +} + +export default ReissueNewDonorCaseSummary; diff --git a/src/features/step_definitions/helpers/apiMockObjects.ts b/src/features/step_definitions/helpers/apiMockObjects.ts index f56b3627..8db7590d 100644 --- a/src/features/step_definitions/helpers/apiMockObjects.ts +++ b/src/features/step_definitions/helpers/apiMockObjects.ts @@ -84,7 +84,20 @@ cloudFunctionAxiosError.response = { }, }; +export const mockSectionForDonorCasesCreation = { + data: "createDonorCases" +}; + +export const mockSectionForReissueNewDonorCase = { + data: "reissueNewDonorCase" +}; + export const mockSuccessResponseForDonorCasesCreation = { data: "Success", status: 200, -}; \ No newline at end of file +}; + +export const mockSuccessResponseForReissueNewDonorCase = { + data: "Success", + status: 200, +}; diff --git a/src/server/config.ts b/src/server/config.ts index a18d1947..4152c0b2 100644 --- a/src/server/config.ts +++ b/src/server/config.ts @@ -12,6 +12,7 @@ export interface Config extends AuthConfig { BusApiUrl: string; BusClientId: string; CreateDonorCasesCloudFunctionUrl: string; + ReissueNewDonorCaseCloudFunctionUrl: string; } export function getConfigFromEnv(): Config { @@ -25,7 +26,8 @@ export function getConfigFromEnv(): Config { BUS_API_URL, BUS_CLIENT_ID, SESSION_TIMEOUT, - CREATE_DONOR_CASES_CLOUD_FUNCTION_URL + CREATE_DONOR_CASES_CLOUD_FUNCTION_URL, + REISSUE_NEW_DONOR_CASE_CLOUD_FUNCTION_URL } = process.env; const { @@ -80,9 +82,15 @@ export function getConfigFromEnv(): Config { } if (CREATE_DONOR_CASES_CLOUD_FUNCTION_URL === undefined) { - console.error("CLOUD_FUNCTION_URL environment variable has not been set"); + console.error("CREATE_DONOR_CASES_CLOUD_FUNCTION_URL environment variable has not been set"); CREATE_DONOR_CASES_CLOUD_FUNCTION_URL = "ENV_VAR_NOT_SET"; } + + if (REISSUE_NEW_DONOR_CASE_CLOUD_FUNCTION_URL === undefined) { + console.error("REISSUE_NEW_DONOR_CASE_CLOUD_FUNCTION_URL environment variable has not been set"); + REISSUE_NEW_DONOR_CASE_CLOUD_FUNCTION_URL = "ENV_VAR_NOT_SET"; + } + let port = 5000; if (PORT !== undefined) { port = +PORT; @@ -101,7 +109,8 @@ export function getConfigFromEnv(): Config { SessionTimeout: SESSION_TIMEOUT, SessionSecret: sessionSecret(SESSION_SECRET), Roles: loadRoles(ROLES), - CreateDonorCasesCloudFunctionUrl: CREATE_DONOR_CASES_CLOUD_FUNCTION_URL + CreateDonorCasesCloudFunctionUrl: CREATE_DONOR_CASES_CLOUD_FUNCTION_URL, + ReissueNewDonorCaseCloudFunctionUrl: REISSUE_NEW_DONOR_CASE_CLOUD_FUNCTION_URL }; } diff --git a/src/server/handlers/cloudFunctionHandler.test.ts b/src/server/handlers/cloudFunctionHandler.test.ts index 15467f80..e6335cd8 100644 --- a/src/server/handlers/cloudFunctionHandler.test.ts +++ b/src/server/handlers/cloudFunctionHandler.test.ts @@ -4,7 +4,7 @@ import { newServer } from "../server"; import supertest from "supertest"; import { getConfigFromEnv } from "../config"; import createLogger from "../pino"; -import { callCloudFunctionToCreateDonorCases } from "../helpers/cloudFunctionCallerHelper"; +import { callCloudFunction } from "../helpers/cloudFunctionCallerHelper"; import { cloudFunctionAxiosError } from "../../features/step_definitions/helpers/apiMockObjects"; jest.mock("../helpers/cloudFunctionCallerHelper"); @@ -14,33 +14,33 @@ const successResponse = { }; const config = getConfigFromEnv(); -const callCloudFunctionToCreateDonorCasesMock = callCloudFunctionToCreateDonorCases as jest.Mock>; +const callCloudFunctionToCreateDonorCasesMock = callCloudFunction as jest.Mock>; describe("Call Cloud Function to create donor cases and return responses", () => { let request: supertest.SuperTest; - + beforeEach(() => { request = supertest(newServer(config, createLogger())); }); - + afterEach(() => { - jest.clearAllMocks(); + jest.clearAllMocks(); }); - + it("should return a 200 status and a json object with message and status if successfully created donor cases", async () => { callCloudFunctionToCreateDonorCasesMock.mockResolvedValue(successResponse); - + const response = await request.post("/api/cloudFunction/createDonorCases"); - + expect(response.status).toEqual(200); expect(response.body).toEqual(successResponse); }); - + it("should return a 500 status and a json object with message and status if cloud function failed creating donor cases", async () => { callCloudFunctionToCreateDonorCasesMock.mockRejectedValue(cloudFunctionAxiosError); - + const response = await request.post("/api/cloudFunction/createDonorCases"); - + expect(response.status).toEqual(500); expect(response.body.message).toEqual((cloudFunctionAxiosError as any).response.data); }); diff --git a/src/server/handlers/cloudFunctionHandler.ts b/src/server/handlers/cloudFunctionHandler.ts index 1f6ca9b1..2fb9376c 100644 --- a/src/server/handlers/cloudFunctionHandler.ts +++ b/src/server/handlers/cloudFunctionHandler.ts @@ -1,32 +1,44 @@ import express, { Request, Response, Router } from "express"; -import { callCloudFunctionToCreateDonorCases } from "../helpers/cloudFunctionCallerHelper"; +import { callCloudFunction } from "../helpers/cloudFunctionCallerHelper"; -export default function newCloudFunctionHandler( - CreateDonorCasesCloudFunctionUrl: string +export default function createDonorCasesCloudFunctionHandler( + CloudFunctionUrl: string ): Router { const router = express.Router(); const cloudFunctionHandler = new CloudFunctionHandler( - CreateDonorCasesCloudFunctionUrl + CloudFunctionUrl ); router.post("/api/cloudFunction/createDonorCases", cloudFunctionHandler.CallCloudFunction); return router; } +export function reissueNewDonorCaseCloudFunctionHandler( + CloudFunctionUrl: string +): Router { + const router = express.Router(); + const cloudFunctionHandler = new CloudFunctionHandler( + CloudFunctionUrl + ); + router.post("/api/cloudFunction/reissueNewDonorCase", cloudFunctionHandler.CallCloudFunction); + + return router; +} + export class CloudFunctionHandler { - CreateDonorCasesCloudFunctionUrl: string; + CloudFunctionUrl: string; - constructor(CreateDonorCasesCloudFunctionUrl: string) { - this.CreateDonorCasesCloudFunctionUrl = CreateDonorCasesCloudFunctionUrl; + constructor(CloudFunctionUrl: string) { + this.CloudFunctionUrl = CloudFunctionUrl; this.CallCloudFunction = this.CallCloudFunction.bind(this); } async CallCloudFunction(req: Request, res: Response): Promise { const reqData = req.body; - req.log.info(`${this.CreateDonorCasesCloudFunctionUrl} URL to invoke for Creating Donor Cases.`); + req.log.info(`${this.CloudFunctionUrl} URL to invoke for Cloud Function.`); try { - const cloudfunctionResponse = await callCloudFunctionToCreateDonorCases(this.CreateDonorCasesCloudFunctionUrl, reqData); - return res.status(cloudfunctionResponse.status).json(cloudfunctionResponse); + const cloudFunctionResponse = await callCloudFunction(this.CloudFunctionUrl, reqData); + return res.status(cloudFunctionResponse.status).json(cloudFunctionResponse); } catch (error) { console.error("Error:", error); diff --git a/src/server/helpers/cloudFunctionCallerHelper.test.ts b/src/server/helpers/cloudFunctionCallerHelper.test.ts index 8c4101e6..15f73781 100644 --- a/src/server/helpers/cloudFunctionCallerHelper.test.ts +++ b/src/server/helpers/cloudFunctionCallerHelper.test.ts @@ -1,5 +1,5 @@ import { getConfigFromEnv } from "../config"; -import { callCloudFunctionToCreateDonorCases } from "./cloudFunctionCallerHelper"; +import { callCloudFunction } from "./cloudFunctionCallerHelper"; import axios from "axios"; import { ipsQuestionnaire } from "../../features/step_definitions/helpers/apiMockObjects"; import { GoogleAuth } from "google-auth-library"; @@ -47,7 +47,7 @@ describe("Call Cloud Function to create donor cases and return responses", () => status: mockSuccessResponse.status, }); - const result = await callCloudFunctionToCreateDonorCases( + const result = await callCloudFunction( dummyUrl, payload ); @@ -84,7 +84,7 @@ describe("Call Cloud Function to create donor cases and return responses", () => status: mockErrorResponse.status, }); - const result = await callCloudFunctionToCreateDonorCases( + const result = await callCloudFunction( dummyUrl, payload ); diff --git a/src/server/helpers/cloudFunctionCallerHelper.ts b/src/server/helpers/cloudFunctionCallerHelper.ts index 30e7ad64..42d3f774 100644 --- a/src/server/helpers/cloudFunctionCallerHelper.ts +++ b/src/server/helpers/cloudFunctionCallerHelper.ts @@ -6,11 +6,10 @@ export async function getIdTokenFromMetadataServer(targetAudience: string) { const client = await googleAuth.getIdTokenClient(targetAudience); - const token = await client.idTokenProvider.fetchIdToken(targetAudience); - return token; + return await client.idTokenProvider.fetchIdToken(targetAudience); } -export async function callCloudFunctionToCreateDonorCases(url: string, payload: any): Promise<{ message: string, status: number }> { +export async function callCloudFunction(url: string, payload: any): Promise<{ message: string, status: number }> { const token = await getIdTokenFromMetadataServer(url); @@ -33,5 +32,4 @@ export async function callCloudFunctionToCreateDonorCases(url: string, payload: status: 500 }; } - -} \ No newline at end of file +} diff --git a/src/server/server.ts b/src/server/server.ts index 63ac2917..066a6936 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -17,7 +17,8 @@ import createLogger from "./pino"; import { HttpLogger } from "pino-http"; import AuditLogger from "./auditLogging/logger"; import newAuditHandler from "./handlers/auditHandler"; -import newCloudFunctionHandler from "./handlers/cloudFunctionHandler"; +import createDonorCasesCloudFunctionHandler from "./handlers/cloudFunctionHandler"; +import { reissueNewDonorCaseCloudFunctionHandler } from "./handlers/cloudFunctionHandler"; if (process.env.NODE_ENV === "production") { import("@google-cloud/profiler").then((profiler) => { @@ -44,7 +45,8 @@ export function newServer(config: Config, logger: HttpLogger = createLogger()): const busHandler = newBusHandler(busApiClient, auth); const uploadHandler = newUploadHandler(storageManager, auth, auditLogger); const auditHandler = newAuditHandler(auditLogger); - const cloudFunctionHandler = newCloudFunctionHandler(config.CreateDonorCasesCloudFunctionUrl); + const createDonorCasesHandler = createDonorCasesCloudFunctionHandler(config.CreateDonorCasesCloudFunctionUrl); + const reissueNewDonorCaseHandler = reissueNewDonorCaseCloudFunctionHandler(config.ReissueNewDonorCaseCloudFunctionUrl); const server = express(); @@ -70,7 +72,8 @@ export function newServer(config: Config, logger: HttpLogger = createLogger()): server.use("/", bimsHandler); server.use("/", busHandler); server.use("/", auditHandler); - server.use("/", cloudFunctionHandler); + server.use("/", createDonorCasesHandler); + server.use("/", reissueNewDonorCaseHandler); server.use("/", HealthCheckHandler()); server.get("*", function (req: Request, res: Response) {