diff --git a/package-lock.json b/package-lock.json index e908d1afa..712b12298 100644 --- a/package-lock.json +++ b/package-lock.json @@ -747,14 +747,13 @@ } }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.972.21", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.21.tgz", - "integrity": "sha512-qxNiHUtlrsjTeSlrPWiFkWps7uD6YB4eKzg7eLAFH8jbiHTlt0ePNlo2Xu+WlftP38JIcMaIX4jTUjOlE2ySWw==", + "version": "3.972.17", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.17.tgz", + "integrity": "sha512-Ra7hjqAZf1OXRRMueB13qex7mFJRDK/pgCvdSFemXBT8KCGnQDPoKzHY1SjN+TjJVmnpSF14W5tJ1vDamFu+Gg==", "license": "Apache-2.0", "dependencies": { - "@nodable/entities": "2.1.0", - "@smithy/types": "^4.14.1", - "fast-xml-parser": "5.7.2", + "@smithy/types": "^4.14.0", + "fast-xml-parser": "5.5.8", "tslib": "^2.6.2" }, "engines": { @@ -4159,9 +4158,9 @@ } }, "node_modules/@defra/forms-model": { - "version": "3.0.651", - "resolved": "https://registry.npmjs.org/@defra/forms-model/-/forms-model-3.0.651.tgz", - "integrity": "sha512-fEf7cldtT+4gthN1Na2l57S6H4oL9CdOfAus3W1Q8KXP9PbU5kuc8NY8jHCsa87PlwJL86q2v93FS14pFQZ2Bg==", + "version": "3.0.649", + "resolved": "https://registry.npmjs.org/@defra/forms-model/-/forms-model-3.0.649.tgz", + "integrity": "sha512-zmJlDrPeBFjNaF26Zv1Gxm79J4Kgqu90mns3N59LCNQnEYFwb0zgmSEam9xbMltHDjbAtbdm5R7/RnxMbzPZMg==", "license": "OGL-UK-3.0", "dependencies": { "@joi/date": "^2.1.1", @@ -8164,18 +8163,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/@nodable/entities": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", - "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/nodable" - } - ], - "license": "MIT" - }, "node_modules/@node-rs/jieba": { "version": "1.10.4", "resolved": "https://registry.npmjs.org/@node-rs/jieba/-/jieba-1.10.4.tgz", @@ -9478,9 +9465,9 @@ } }, "node_modules/@smithy/types": { - "version": "4.14.1", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.1.tgz", - "integrity": "sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg==", + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.0.tgz", + "integrity": "sha512-OWgntFLW88kx2qvf/c/67Vno1yuXm/f9M7QFAtVkkO29IJXGBIg0ycEaBTH0kvCtwmvZxRujrgP5a86RvsXJAQ==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -12928,9 +12915,9 @@ } }, "node_modules/basic-ftp": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", - "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.2.tgz", + "integrity": "sha512-1tDrzKsdCg70WGvbFss/ulVAxupNauGnOlgpyjKzeQxzyllBLS0CGLV7tjIXTK3ZQA9/FBEm9qyFFN1bciA6pw==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -18042,9 +18029,9 @@ "license": "BSD-3-Clause" }, "node_modules/fast-xml-builder": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.5.tgz", - "integrity": "sha512-4TJn/8FKLeslLAH3dnohXqE3QSoxkhvaMzepOIZytwJXZO69Bfz0HBdDHzOTOon6G59Zrk6VQ2bEiv1t61rfkA==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz", + "integrity": "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==", "funding": [ { "type": "github", @@ -18057,9 +18044,9 @@ } }, "node_modules/fast-xml-parser": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.2.tgz", - "integrity": "sha512-P7oW7tLbYnhOLQk/Gv7cZgzgMPP/XN03K02/Jy6Y/NHzyIAIpxuZIM/YqAkfiXFPxA2CTm7NtCijK9EDu09u2w==", + "version": "5.5.11", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.11.tgz", + "integrity": "sha512-QL0eb0YbSTVWF6tTf1+LEMSgtCEjBYPpnAjoLC8SscESlAjXEIRJ7cHtLG0pLeDFaZLa4VKZLArtA/60ZS7vyA==", "funding": [ { "type": "github", @@ -18068,9 +18055,8 @@ ], "license": "MIT", "dependencies": { - "@nodable/entities": "^2.1.0", - "fast-xml-builder": "^1.1.5", - "path-expression-matcher": "^1.5.0", + "fast-xml-builder": "^1.1.4", + "path-expression-matcher": "^1.4.0", "strnum": "^2.2.3" }, "bin": { @@ -22519,9 +22505,9 @@ } }, "node_modules/liquidjs": { - "version": "10.25.7", - "resolved": "https://registry.npmjs.org/liquidjs/-/liquidjs-10.25.7.tgz", - "integrity": "sha512-rPCjJLiD4eDhQjvv964AeXFC+HbeYBbZrd7Z82Q6hqv1lX7G+5w4SJcKLn9CAAAwHI4aS3dTdo083UB79K3pDA==", + "version": "10.25.5", + "resolved": "https://registry.npmjs.org/liquidjs/-/liquidjs-10.25.5.tgz", + "integrity": "sha512-GKiKeZjJDdVoQAu+S9rzkYsYnYhcep5W3WwZXgb5f+yq484P/k9JqamBbGYu+LBEixcUAXZr2jogdAIjB3ki1w==", "license": "MIT", "dependencies": { "commander": "^10.0.0" @@ -26919,9 +26905,9 @@ } }, "node_modules/postcss": { - "version": "8.5.12", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz", - "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "funding": [ { "type": "opencollective", diff --git a/src/server/plugins/SummaryPageWithConfirmationEmailController.test.ts b/src/server/plugins/SummaryPageWithConfirmationEmailController.test.ts index c6bc44e50..0ef5cd695 100644 --- a/src/server/plugins/SummaryPageWithConfirmationEmailController.test.ts +++ b/src/server/plugins/SummaryPageWithConfirmationEmailController.test.ts @@ -12,6 +12,7 @@ import { ControllerType, type PageSummaryWithConfirmationEmail } from '@defra/forms-model' +import { type ResponseObject } from '@hapi/hapi' import { SummaryPageWithConfirmationEmailController, @@ -129,6 +130,125 @@ describe('SummaryPageWithConfirmationEmailController', () => { }) }) + describe('persists userConfirmationEmailAddress to state', () => { + const responseStub = {} as ResponseObject + + it('should mergeState with the email when payload provides it', async () => { + const state: FormSubmissionState = { + $$__referenceNumber: 'foobar', + licenceLength: 365, + fullName: 'John Smith' + } + const mergedState: FormSubmissionState = { + $$__referenceNumber: 'foobar', + licenceLength: 365, + fullName: 'John Smith', + userConfirmationEmailAddress: 'cya@example.com' + } + const request = { + ...requestPage, + method: 'post', + payload: { + action: 'send', + userConfirmationEmailAddress: 'cya@example.com' + } + } as unknown as FormRequestPayload + + const context = model.getFormContext(request, state) + + const mergeStateSpy = jest + .spyOn(controller, 'mergeState') + .mockResolvedValue(mergedState) + jest.spyOn(controller, 'handleFormSubmit').mockResolvedValue(responseStub) + + const postHandler = controller.makePostRouteHandler() + await postHandler(request, context, h) + + expect(mergeStateSpy).toHaveBeenCalledWith(request, expect.any(Object), { + userConfirmationEmailAddress: 'cya@example.com' + }) + expect(context.state.userConfirmationEmailAddress).toBe('cya@example.com') + }) + + it('should not mergeState when payload omits the email', async () => { + const state: FormSubmissionState = { + $$__referenceNumber: 'foobar', + licenceLength: 365, + fullName: 'John Smith' + } + const request = { + ...requestPage, + method: 'post', + payload: { action: 'send' } + } as unknown as FormRequestPayload + + const context = model.getFormContext(request, state) + + const mergeStateSpy = jest + .spyOn(controller, 'mergeState') + .mockResolvedValue(state) + jest.spyOn(controller, 'handleFormSubmit').mockResolvedValue(responseStub) + + const postHandler = controller.makePostRouteHandler() + await postHandler(request, context, h) + + expect(mergeStateSpy).not.toHaveBeenCalled() + }) + + it('should not mergeState when payload provides an empty email', async () => { + const state: FormSubmissionState = { + $$__referenceNumber: 'foobar', + licenceLength: 365, + fullName: 'John Smith' + } + const request = { + ...requestPage, + method: 'post', + payload: { action: 'send', userConfirmationEmailAddress: '' } + } as unknown as FormRequestPayload + + const context = model.getFormContext(request, state) + + const mergeStateSpy = jest + .spyOn(controller, 'mergeState') + .mockResolvedValue(state) + jest.spyOn(controller, 'handleFormSubmit').mockResolvedValue(responseStub) + + const postHandler = controller.makePostRouteHandler() + await postHandler(request, context, h) + + expect(mergeStateSpy).not.toHaveBeenCalled() + }) + + it('should propagate the error when mergeState rejects', async () => { + const state: FormSubmissionState = { + $$__referenceNumber: 'foobar', + licenceLength: 365, + fullName: 'John Smith' + } + const request = { + ...requestPage, + method: 'post', + payload: { + action: 'send', + userConfirmationEmailAddress: 'cya@example.com' + } + } as unknown as FormRequestPayload + + const context = model.getFormContext(request, state) + + const cacheError = new Error('cache unavailable') + jest.spyOn(controller, 'mergeState').mockRejectedValue(cacheError) + const handleFormSubmitSpy = jest + .spyOn(controller, 'handleFormSubmit') + .mockResolvedValue(responseStub) + + const postHandler = controller.makePostRouteHandler() + await expect(postHandler(request, context, h)).rejects.toBe(cacheError) + expect(handleFormSubmitSpy).not.toHaveBeenCalled() + }) + }) + describe('getUserConfirmationEmailAddress', () => { test('should get confirmation email', () => { const field = getUserConfirmationEmailAddress() diff --git a/src/server/plugins/SummaryPageWithConfirmationEmailController.ts b/src/server/plugins/SummaryPageWithConfirmationEmailController.ts index f80caf6fe..57963e851 100644 --- a/src/server/plugins/SummaryPageWithConfirmationEmailController.ts +++ b/src/server/plugins/SummaryPageWithConfirmationEmailController.ts @@ -102,6 +102,19 @@ export class SummaryPageWithConfirmationEmailController extends SummaryPageContr return h.view(viewName, viewModel) } + const userConfirmationEmailAddress = + request.payload[CONFIRMATION_EMAIL_FIELD_NAME] + if ( + typeof userConfirmationEmailAddress === 'string' && + userConfirmationEmailAddress + ) { + context.state = await ( + this as unknown as QuestionPageController + ).mergeState(request, context.state, { + [CONFIRMATION_EMAIL_FIELD_NAME]: userConfirmationEmailAddress + }) + } + // Should not have to coerce the type - ticket to resolve later https://eaflood.atlassian.net/browse/DF-555 return (this as unknown as SummaryPageController).handleFormSubmit( request, diff --git a/src/server/services/outputService.test.js b/src/server/services/outputService.test.js index 3099a9db3..4c946b012 100644 --- a/src/server/services/outputService.test.js +++ b/src/server/services/outputService.test.js @@ -177,20 +177,16 @@ describe('OutputService', () => { data: mockItems } - const mockRequestWithEmail = /** @type {FormRequestPayload} */ ( - /** @type {unknown} */ ({ - ...mockRequest, - payload: { - userConfirmationEmailAddress: 'my-email@test123.com' - } - }) - ) + const mockContextWithEmail = { + ...mockContext, + state: { userConfirmationEmailAddress: 'my-email@test123.com' } + } mockFormatter.mockReturnValue(JSON.stringify(mockPayload)) await outputService.submit( - mockContext, - mockRequestWithEmail, + mockContextWithEmail, + mockRequest, mockModel, 'test@example.com', mockItems, @@ -201,7 +197,7 @@ describe('OutputService', () => { expect(checkFormStatus).toHaveBeenCalledWith(mockRequest.params) expect(getFormatter).toHaveBeenCalledWith('adapter', '1') expect(mockFormatter).toHaveBeenCalledWith( - mockContext, + mockContextWithEmail, mockItems, mockModel, mockSubmitResponse, diff --git a/src/server/services/outputService.ts b/src/server/services/outputService.ts index 95984d717..c2cdce35f 100644 --- a/src/server/services/outputService.ts +++ b/src/server/services/outputService.ts @@ -100,7 +100,7 @@ export class OutputService implements IOutputService { // Add user confirmation email if supplied const userConfirmationEmailAddress = - request.payload.userConfirmationEmailAddress + context.state.userConfirmationEmailAddress if (typeof userConfirmationEmailAddress === 'string') { customMeta.userConfirmationEmail = userConfirmationEmailAddress }