From fcba613c1a86d18f817815bac25376666dce41d6 Mon Sep 17 00:00:00 2001 From: NogaNHS Date: Thu, 25 Jan 2024 13:03:55 +0000 Subject: [PATCH 1/3] [PRMDR-600] add feedback endpoint to UI --- .../e2e/0-ndr-core-tests/feedback_page.cy.js | 7 +- .../blocks/feedbackForm/FeedbackForm.test.tsx | 84 ++++++++++++++++--- .../blocks/feedbackForm/FeedbackForm.tsx | 8 +- app/src/helpers/requests/sendEmail.ts | 29 ++++--- app/src/helpers/utils/errorCodes.ts | 2 + .../pages/feedbackPage/FeedbackPage.test.tsx | 1 + app/src/types/generic/endpoints.ts | 1 + 7 files changed, 108 insertions(+), 24 deletions(-) diff --git a/app/cypress/e2e/0-ndr-core-tests/feedback_page.cy.js b/app/cypress/e2e/0-ndr-core-tests/feedback_page.cy.js index 7f98d5515..d315ba3da 100644 --- a/app/cypress/e2e/0-ndr-core-tests/feedback_page.cy.js +++ b/app/cypress/e2e/0-ndr-core-tests/feedback_page.cy.js @@ -80,6 +80,9 @@ describe('Feedback Page', () => { fillInForm(mockInputData); cy.get('#submit-feedback').click(); + cy.intercept('POST', '/Feedback*', { + statusCode: 200, + }).as('feedback'); // TODO: when backend call for sending email is implemented, // intercept the call and check that payload data is the same as mockInputData @@ -104,7 +107,9 @@ describe('Feedback Page', () => { fillInForm(mockInputData); cy.get('#submit-feedback').click(); - + cy.intercept('POST', '/Feedback*', { + statusCode: 200, + }).as('feedback'); cy.get('.app-homepage-content h1', { timeout: 5000 }).should( 'have.text', 'We’ve received your feedback', diff --git a/app/src/components/blocks/feedbackForm/FeedbackForm.test.tsx b/app/src/components/blocks/feedbackForm/FeedbackForm.test.tsx index 44734a1ce..441f570a6 100644 --- a/app/src/components/blocks/feedbackForm/FeedbackForm.test.tsx +++ b/app/src/components/blocks/feedbackForm/FeedbackForm.test.tsx @@ -1,17 +1,25 @@ import { act, render, screen, waitFor } from '@testing-library/react'; import { SATISFACTION_CHOICES, SUBMISSION_STAGE } from '../../../types/pages/feedbackPage/types'; import userEvent from '@testing-library/user-event'; -import sendEmail from '../../../helpers/requests/sendEmail'; import FeedbackForm, { Props } from './FeedbackForm'; import { fillInForm } from '../../../helpers/test/formUtils'; +import axios from 'axios'; +import useBaseAPIUrl from '../../../helpers/hooks/useBaseAPIUrl'; +import { routes } from '../../../types/generic/routes'; -jest.mock('../../../helpers/requests/sendEmail'); +jest.mock('axios'); +jest.mock('../../../helpers/hooks/useBaseAPIUrl'); +jest.mock('../../../helpers/hooks/useBaseAPIHeaders'); const mockedUseNavigate = jest.fn(); -const mockSendEmail = sendEmail as jest.Mock; +const mockedAxios = axios as jest.Mocked; +const mockedBaseURL = useBaseAPIUrl as jest.Mock; +const baseURL = 'http://test'; + const mockSetStage = jest.fn(); jest.mock('react-router', () => ({ useNavigate: () => mockedUseNavigate, })); + const clickSubmitButton = () => { userEvent.click(screen.getByRole('button', { name: 'Send feedback' })); }; @@ -29,7 +37,7 @@ const renderComponent = (override: Partial = {}) => { describe('', () => { beforeEach(() => { process.env.REACT_APP_ENVIRONMENT = 'jest'; - mockSendEmail.mockReturnValueOnce(Promise.resolve()); + mockedBaseURL.mockReturnValue(baseURL); }); afterEach(() => { jest.clearAllMocks(); @@ -84,6 +92,10 @@ describe('', () => { describe('User interactions', () => { it('on submit, call sendEmail() with the data that user had filled in', async () => { + mockedAxios.post.mockImplementation(() => + Promise.resolve({ status: 200, data: 'Success' }), + ); + const mockInputData = { feedbackContent: 'Mock feedback content', howSatisfied: SATISFACTION_CHOICES.VerySatisfied, @@ -98,7 +110,11 @@ describe('', () => { clickSubmitButton(); }); - await waitFor(() => expect(mockSendEmail).toBeCalledWith(mockInputData)); + await waitFor(() => + expect(mockedAxios.post).toBeCalledWith(baseURL + '/Feedback', mockInputData, { + headers: {}, + }), + ); expect(mockSetStage).toBeCalledWith(SUBMISSION_STAGE.Submitting); }); @@ -119,7 +135,7 @@ describe('', () => { await waitFor(() => { expect(screen.getByText('Please enter your feedback')).toBeInTheDocument(); }); - expect(mockSendEmail).not.toBeCalled(); + expect(mockedAxios).not.toBeCalled(); expect(mockSetStage).not.toBeCalled(); }); @@ -140,11 +156,11 @@ describe('', () => { await waitFor(() => { expect(screen.getByText('Please select an option')).toBeInTheDocument(); }); - expect(mockSendEmail).not.toBeCalled(); + expect(mockedAxios).not.toBeCalled(); expect(mockSetStage).not.toBeCalled(); }); - it("on submit, if user filled in an invalid email address, display an error message and don't send email", async () => { + it("on submit, if user filled in an invalid email address, display an error message and don't send email", async () => { const mockInputData = { feedbackContent: 'Mock feedback content', howSatisfied: SATISFACTION_CHOICES.VerySatisfied, @@ -162,11 +178,14 @@ describe('', () => { await waitFor(() => { expect(screen.getByText('Enter a valid email address')).toBeInTheDocument(); }); - expect(mockSendEmail).not.toBeCalled(); + expect(mockedAxios).not.toBeCalled(); expect(mockSetStage).not.toBeCalled(); }); it('on submit, allows the respondent name and email to be blank', async () => { + mockedAxios.post.mockImplementation(() => + Promise.resolve({ status: 200, data: 'Success' }), + ); const mockInputData = { feedbackContent: 'Mock feedback content', howSatisfied: SATISFACTION_CHOICES.VeryDissatisfied, @@ -184,7 +203,15 @@ describe('', () => { clickSubmitButton(); }); - await waitFor(() => expect(mockSendEmail).toBeCalledWith(expectedEmailContent)); + await waitFor(() => + expect(mockedAxios.post).toBeCalledWith( + baseURL + '/Feedback', + expectedEmailContent, + { + headers: {}, + }, + ), + ); expect(mockSetStage).toBeCalledWith(SUBMISSION_STAGE.Submitting); }); @@ -195,4 +222,41 @@ describe('', () => { expect(screen.getByRole('SpinnerButton')).toHaveAttribute('disabled'); }); }); + describe('Navigation', () => { + it('navigates to Error page when call to feedback endpoint return 500', async () => { + const errorResponse = { + response: { + status: 500, + data: { message: 'An error occurred', err_code: 'SP_1001' }, + }, + }; + mockedAxios.post.mockImplementation(() => Promise.reject(errorResponse)); + const mockInputData = { + feedbackContent: 'Mock feedback content', + howSatisfied: SATISFACTION_CHOICES.VerySatisfied, + respondentName: 'Jane Smith', + respondentEmail: 'jane_smith@testing.com', + }; + + renderComponent(); + + act(() => { + fillInForm(mockInputData); + clickSubmitButton(); + }); + + await waitFor(() => + expect(mockedAxios.post).toBeCalledWith(baseURL + '/Feedback', mockInputData, { + headers: {}, + }), + ); + expect(mockSetStage).toBeCalledWith(SUBMISSION_STAGE.Submitting); + + await waitFor(() => { + expect(mockedUseNavigate).toHaveBeenCalledWith( + routes.SERVER_ERROR + '?errorCode=SP_1001', + ); + }); + }); + }); }); diff --git a/app/src/components/blocks/feedbackForm/FeedbackForm.tsx b/app/src/components/blocks/feedbackForm/FeedbackForm.tsx index 8e189abe1..7935c269e 100644 --- a/app/src/components/blocks/feedbackForm/FeedbackForm.tsx +++ b/app/src/components/blocks/feedbackForm/FeedbackForm.tsx @@ -14,7 +14,9 @@ import SpinnerButton from '../../generic/spinnerButton/SpinnerButton'; import { routes } from '../../../types/generic/routes'; import { useNavigate } from 'react-router-dom'; import { errorToParams } from '../../../helpers/utils/errorToParams'; -import { AxiosError } from 'axios/index'; +import { AxiosError } from 'axios'; +import useBaseAPIUrl from '../../../helpers/hooks/useBaseAPIUrl'; +import useBaseAPIHeaders from '../../../helpers/hooks/useBaseAPIHeaders'; export type Props = { stage: SUBMISSION_STAGE; @@ -22,6 +24,8 @@ export type Props = { }; function FeedbackForm({ stage, setStage }: Props) { + const baseUrl = useBaseAPIUrl(); + const baseHeaders = useBaseAPIHeaders(); const { handleSubmit, register, @@ -35,7 +39,7 @@ function FeedbackForm({ stage, setStage }: Props) { setStage(SUBMISSION_STAGE.Submitting); // add tests for failing and passing cases when real email service is implemented try { - await sendEmail(formData); + await sendEmail({ formData, baseUrl, baseHeaders }); setStage(SUBMISSION_STAGE.Successful); } catch (e) { const error = e as AxiosError; diff --git a/app/src/helpers/requests/sendEmail.ts b/app/src/helpers/requests/sendEmail.ts index 1b030a8f7..d7b09e6e5 100644 --- a/app/src/helpers/requests/sendEmail.ts +++ b/app/src/helpers/requests/sendEmail.ts @@ -1,18 +1,25 @@ import { FormData } from '../../types/pages/feedbackPage/types'; +import axios, { AxiosError } from 'axios'; +import { AuthHeaders } from '../../types/blocks/authHeaders'; +import { endpoints } from '../../types/generic/endpoints'; +type Args = { + formData: FormData; + baseUrl: string; + baseHeaders: AuthHeaders; +}; +const sendEmail = async ({ formData, baseUrl, baseHeaders }: Args) => { + const gatewayUrl = baseUrl + endpoints.FEEDBACK; -const sendEmail = async (formData: FormData) => { - // using console.log as a placeholder until we got the send email solution in place - /* eslint-disable-next-line no-console */ - console.log(`sending feedback from user by email: ${JSON.stringify(formData)}}`); try { - await new Promise((resolve, reject) => - setTimeout(() => { - resolve({}); - }, 1000), - ); - return { status: 200 }; + const { data } = await axios.post(gatewayUrl, formData, { + headers: { + ...baseHeaders, + }, + }); + return data; } catch (e) { - throw e; + const error = e as AxiosError; + throw error; } }; diff --git a/app/src/helpers/utils/errorCodes.ts b/app/src/helpers/utils/errorCodes.ts index f58e1f501..ab88559a4 100644 --- a/app/src/helpers/utils/errorCodes.ts +++ b/app/src/helpers/utils/errorCodes.ts @@ -25,6 +25,8 @@ const errorCodes: { [key: string]: string } = { OUT_5001: 'Error logging user out', ENV_5001: 'An error occurred due to missing environment variable', GWY_5001: 'Failed to utilise AWS client/resource', + SFB_5001: 'Error occur when sending email by SES', + SFB_5002: 'Failed to fetch parameters for sending email from SSM param store', }; export default errorCodes; diff --git a/app/src/pages/feedbackPage/FeedbackPage.test.tsx b/app/src/pages/feedbackPage/FeedbackPage.test.tsx index 1e78ed0c0..68cfb5c82 100644 --- a/app/src/pages/feedbackPage/FeedbackPage.test.tsx +++ b/app/src/pages/feedbackPage/FeedbackPage.test.tsx @@ -5,6 +5,7 @@ import { SATISFACTION_CHOICES } from '../../types/pages/feedbackPage/types'; import FeedbackPage from './FeedbackPage'; import sendEmail from '../../helpers/requests/sendEmail'; import { fillInForm } from '../../helpers/test/formUtils'; +jest.mock('../../helpers/hooks/useBaseAPIHeaders'); jest.mock('../../helpers/requests/sendEmail'); const mockSendEmail = sendEmail as jest.Mock; diff --git a/app/src/types/generic/endpoints.ts b/app/src/types/generic/endpoints.ts index 24cb47530..f3f386db0 100644 --- a/app/src/types/generic/endpoints.ts +++ b/app/src/types/generic/endpoints.ts @@ -10,4 +10,5 @@ export enum endpoints { DOCUMENT_PRESIGN = '/DocumentManifest', LLOYDGEORGE_STITCH = '/LloydGeorgeStitch', + FEEDBACK = '/Feedback', } From 6044c2cd3e85f775ff0d31bc984d8c745160a1c4 Mon Sep 17 00:00:00 2001 From: NogaNHS Date: Thu, 25 Jan 2024 13:08:50 +0000 Subject: [PATCH 2/3] error page test fix --- .../components/blocks/feedbackForm/FeedbackForm.test.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/components/blocks/feedbackForm/FeedbackForm.test.tsx b/app/src/components/blocks/feedbackForm/FeedbackForm.test.tsx index 441f570a6..42f6ab06f 100644 --- a/app/src/components/blocks/feedbackForm/FeedbackForm.test.tsx +++ b/app/src/components/blocks/feedbackForm/FeedbackForm.test.tsx @@ -14,7 +14,9 @@ const mockedUseNavigate = jest.fn(); const mockedAxios = axios as jest.Mocked; const mockedBaseURL = useBaseAPIUrl as jest.Mock; const baseURL = 'http://test'; - +jest.mock('moment', () => { + return () => jest.requireActual('moment')('2020-01-01T00:00:00.000Z'); +}); const mockSetStage = jest.fn(); jest.mock('react-router', () => ({ useNavigate: () => mockedUseNavigate, @@ -254,7 +256,7 @@ describe('', () => { await waitFor(() => { expect(mockedUseNavigate).toHaveBeenCalledWith( - routes.SERVER_ERROR + '?errorCode=SP_1001', + routes.SERVER_ERROR + '?encodedError=WyJTUF8xMDAxIiwiMTU3NzgzNjgwMCJd', ); }); }); From 6bede82aa39494d4a4257199cebf221b75b1c3e6 Mon Sep 17 00:00:00 2001 From: NogaNHS Date: Thu, 25 Jan 2024 15:06:57 +0000 Subject: [PATCH 3/3] remove comment --- app/cypress/e2e/0-ndr-core-tests/feedback_page.cy.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/cypress/e2e/0-ndr-core-tests/feedback_page.cy.js b/app/cypress/e2e/0-ndr-core-tests/feedback_page.cy.js index d315ba3da..d11b85d82 100644 --- a/app/cypress/e2e/0-ndr-core-tests/feedback_page.cy.js +++ b/app/cypress/e2e/0-ndr-core-tests/feedback_page.cy.js @@ -84,9 +84,6 @@ describe('Feedback Page', () => { statusCode: 200, }).as('feedback'); - // TODO: when backend call for sending email is implemented, - // intercept the call and check that payload data is the same as mockInputData - cy.get('.app-homepage-content h1', { timeout: 5000 }).should( 'have.text', 'We’ve received your feedback',