From 600df970f71288874bc2ffa553ad93319a573384 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Jun 2021 02:39:03 +0000 Subject: [PATCH 01/21] chore(deps-dev): bump core-js from 3.14.0 to 3.15.0 (#2225) Bumps [core-js](https://github.com/zloirock/core-js/tree/HEAD/packages/core-js) from 3.14.0 to 3.15.0. - [Release notes](https://github.com/zloirock/core-js/releases) - [Changelog](https://github.com/zloirock/core-js/blob/master/CHANGELOG.md) - [Commits](https://github.com/zloirock/core-js/commits/v3.15.0/packages/core-js) --- updated-dependencies: - dependency-name: core-js dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5bad2b2d4c..ca04f0329f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10373,9 +10373,9 @@ } }, "core-js": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.14.0.tgz", - "integrity": "sha512-3s+ed8er9ahK+zJpp9ZtuVcDoFzHNiZsPbNAAE4KXgrRHbjSqqNN6xGSXq6bq7TZIbKj4NLrLb6bJ5i+vSVjHA==", + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.15.0.tgz", + "integrity": "sha512-GUbtPllXMYRzIgHNZ4dTYTcUemls2cni83Q4Q/TrFONHfhcg9oEGOtaGHfb0cpzec60P96UKPvMkjX1jET8rUw==", "dev": true }, "core-js-compat": { diff --git a/package.json b/package.json index c02dbb42a7..0b3b2ce736 100644 --- a/package.json +++ b/package.json @@ -198,7 +198,7 @@ "babel-loader": "^8.2.2", "concurrently": "^6.2.0", "copy-webpack-plugin": "^6.0.2", - "core-js": "^3.14.0", + "core-js": "^3.15.0", "coveralls": "^3.1.0", "css-loader": "^2.1.1", "csv-parse": "^4.16.0", From 52470f09da1a23e107a7ca4016819845bb29dd2e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Jun 2021 03:55:05 +0000 Subject: [PATCH 02/21] chore(deps-dev): bump @typescript-eslint/parser from 4.27.0 to 4.28.0 (#2226) Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 4.27.0 to 4.28.0. - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/parser/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v4.28.0/packages/parser) --- updated-dependencies: - dependency-name: "@typescript-eslint/parser" dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 52 +++++++++++++++++++++++------------------------ package.json | 2 +- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/package-lock.json b/package-lock.json index ca04f0329f..5683cec7aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6870,41 +6870,41 @@ } }, "@typescript-eslint/parser": { - "version": "4.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.27.0.tgz", - "integrity": "sha512-XpbxL+M+gClmJcJ5kHnUpBGmlGdgNvy6cehgR6ufyxkEJMGP25tZKCaKyC0W/JVpuhU3VU1RBn7SYUPKSMqQvQ==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.28.0.tgz", + "integrity": "sha512-7x4D22oPY8fDaOCvkuXtYYTQ6mTMmkivwEzS+7iml9F9VkHGbbZ3x4fHRwxAb5KeuSkLqfnYjs46tGx2Nour4A==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "4.27.0", - "@typescript-eslint/types": "4.27.0", - "@typescript-eslint/typescript-estree": "4.27.0", + "@typescript-eslint/scope-manager": "4.28.0", + "@typescript-eslint/types": "4.28.0", + "@typescript-eslint/typescript-estree": "4.28.0", "debug": "^4.3.1" }, "dependencies": { "@typescript-eslint/scope-manager": { - "version": "4.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.27.0.tgz", - "integrity": "sha512-DY73jK6SEH6UDdzc6maF19AHQJBFVRf6fgAXHPXCGEmpqD4vYgPEzqpFz1lf/daSbOcMpPPj9tyXXDPW2XReAw==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.28.0.tgz", + "integrity": "sha512-eCALCeScs5P/EYjwo6se9bdjtrh8ByWjtHzOkC4Tia6QQWtQr3PHovxh3TdYTuFcurkYI4rmFsRFpucADIkseg==", "dev": true, "requires": { - "@typescript-eslint/types": "4.27.0", - "@typescript-eslint/visitor-keys": "4.27.0" + "@typescript-eslint/types": "4.28.0", + "@typescript-eslint/visitor-keys": "4.28.0" } }, "@typescript-eslint/types": { - "version": "4.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.27.0.tgz", - "integrity": "sha512-I4ps3SCPFCKclRcvnsVA/7sWzh7naaM/b4pBO2hVxnM3wrU51Lveybdw5WoIktU/V4KfXrTt94V9b065b/0+wA==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.28.0.tgz", + "integrity": "sha512-p16xMNKKoiJCVZY5PW/AfILw2xe1LfruTcfAKBj3a+wgNYP5I9ZEKNDOItoRt53p4EiPV6iRSICy8EPanG9ZVA==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "4.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.27.0.tgz", - "integrity": "sha512-KH03GUsUj41sRLLEy2JHstnezgpS5VNhrJouRdmh6yNdQ+yl8w5LrSwBkExM+jWwCJa7Ct2c8yl8NdtNRyQO6g==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.0.tgz", + "integrity": "sha512-m19UQTRtxMzKAm8QxfKpvh6OwQSXaW1CdZPoCaQuLwAq7VZMNuhJmZR4g5281s2ECt658sldnJfdpSZZaxUGMQ==", "dev": true, "requires": { - "@typescript-eslint/types": "4.27.0", - "@typescript-eslint/visitor-keys": "4.27.0", + "@typescript-eslint/types": "4.28.0", + "@typescript-eslint/visitor-keys": "4.28.0", "debug": "^4.3.1", "globby": "^11.0.3", "is-glob": "^4.0.1", @@ -6913,12 +6913,12 @@ } }, "@typescript-eslint/visitor-keys": { - "version": "4.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.27.0.tgz", - "integrity": "sha512-es0GRYNZp0ieckZ938cEANfEhsfHrzuLrePukLKtY3/KPXcq1Xd555Mno9/GOgXhKzn0QfkDLVgqWO3dGY80bg==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.0.tgz", + "integrity": "sha512-PjJyTWwrlrvM5jazxYF5ZPs/nl0kHDZMVbuIcbpawVXaDPelp3+S9zpOz5RmVUfS/fD5l5+ZXNKnWhNYjPzCvw==", "dev": true, "requires": { - "@typescript-eslint/types": "4.27.0", + "@typescript-eslint/types": "4.28.0", "eslint-visitor-keys": "^2.0.0" } }, @@ -6938,9 +6938,9 @@ "dev": true }, "globby": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.3.tgz", - "integrity": "sha512-ffdmosjA807y7+lA1NM0jELARVmYul/715xiILEjo3hBLPTcirgQNnXECn5g3mtR8TOLCVbkfua1Hpen25/Xcg==", + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.4.tgz", + "integrity": "sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==", "dev": true, "requires": { "array-union": "^2.1.0", diff --git a/package.json b/package.json index 0b3b2ce736..fa2baff13b 100644 --- a/package.json +++ b/package.json @@ -192,7 +192,7 @@ "@types/uuid": "^8.3.0", "@types/validator": "^13.1.4", "@typescript-eslint/eslint-plugin": "^4.27.0", - "@typescript-eslint/parser": "^4.27.0", + "@typescript-eslint/parser": "^4.28.0", "auto-changelog": "^2.3.0", "axios-mock-adapter": "^1.19.0", "babel-loader": "^8.2.2", From 6512a84c6456665e6f12a8fad5a7e8ccd9860d28 Mon Sep 17 00:00:00 2001 From: Foo Yong Jie Date: Tue, 22 Jun 2021 13:56:53 +0800 Subject: [PATCH 03/21] refactor(email-submission): encapsulate parsedResponses (#2206) --- .../__tests__/admin-form.controller.spec.ts | 49 +-- .../form/admin-form/admin-form.controller.ts | 38 +-- .../submission/submission.service.spec.ts | 295 +----------------- .../ParsedResponsesObject.class.ts | 153 +++++++++ .../ParsedResponsesObject.class.spec.ts | 124 ++++++++ .../email-submission.controller.ts | 46 +-- .../email-submission.types.ts | 6 +- .../modules/submission/submission.service.ts | 112 +------ 8 files changed, 355 insertions(+), 468 deletions(-) create mode 100644 src/app/modules/submission/email-submission/ParsedResponsesObject.class.ts create mode 100644 src/app/modules/submission/email-submission/__tests__/ParsedResponsesObject.class.spec.ts diff --git a/src/app/modules/form/admin-form/__tests__/admin-form.controller.spec.ts b/src/app/modules/form/admin-form/__tests__/admin-form.controller.spec.ts index 05a20e680d..3fefae6160 100644 --- a/src/app/modules/form/admin-form/__tests__/admin-form.controller.spec.ts +++ b/src/app/modules/form/admin-form/__tests__/admin-form.controller.spec.ts @@ -77,6 +77,7 @@ import { } from 'tests/unit/backend/helpers/generate-form-data' import expressHandler from 'tests/unit/backend/helpers/jest-express' +import ParsedResponsesObject from '../../../submission/email-submission/ParsedResponsesObject.class' import * as UserService from '../../../user/user.service' import { ForbiddenFormError, @@ -127,6 +128,8 @@ jest.mock('src/app/modules/submission/submission.utils') const MockSubmissionUtils = mocked(SubmissionUtils) jest.mock('../admin-form.service') const MockAdminFormService = mocked(AdminFormService) +jest.mock('../../../submission/email-submission/ParsedResponsesObject.class') +const MockParsedResponsesObject = mocked(ParsedResponsesObject) jest.mock('../../form.service') const MockFormService = mocked(FormService) jest.mock('../../../user/user.service') @@ -5130,8 +5133,12 @@ describe('admin-form.controller', () => { MockEmailSubmissionService.validateAttachments.mockReturnValue( okAsync(true), ) - MockSubmissionService.getProcessedResponses.mockReturnValue( - ok(MOCK_PARSED_RESPONSES), + MockParsedResponsesObject.mockClear() + MockParsedResponsesObject.parseResponses.mockReturnValue( + ok(new ParsedResponsesObject(MOCK_PARSED_RESPONSES)), + ) + MockParsedResponsesObject.mock.instances[0].getAllResponses.mockReturnValue( + MOCK_PARSED_RESPONSES, ) MockEmailSubmissionService.createEmailSubmissionWithoutSave.mockReturnValue( MOCK_SUBMISSION, @@ -5186,7 +5193,7 @@ describe('admin-form.controller', () => { expect( MockEmailSubmissionService.validateAttachments, ).toHaveBeenCalledWith(MOCK_RESPONSES) - expect(MockSubmissionService.getProcessedResponses).toHaveBeenCalledWith( + expect(MockParsedResponsesObject.parseResponses).toHaveBeenCalledWith( MOCK_FORM, MOCK_RESPONSES, ) @@ -5253,7 +5260,7 @@ describe('admin-form.controller', () => { expect( MockEmailSubmissionService.validateAttachments, ).not.toHaveBeenCalled() - expect(MockSubmissionService.getProcessedResponses).not.toHaveBeenCalled() + expect(MockParsedResponsesObject.parseResponses).not.toHaveBeenCalled() expect(MockAdminFormService.extractMyInfoFieldIds).not.toHaveBeenCalled() expect( MockEmailSubmissionService.createEmailSubmissionWithoutSave, @@ -5302,7 +5309,7 @@ describe('admin-form.controller', () => { expect( MockEmailSubmissionService.validateAttachments, ).not.toHaveBeenCalled() - expect(MockSubmissionService.getProcessedResponses).not.toHaveBeenCalled() + expect(MockParsedResponsesObject.parseResponses).not.toHaveBeenCalled() expect(MockAdminFormService.extractMyInfoFieldIds).not.toHaveBeenCalled() expect( MockEmailSubmissionService.createEmailSubmissionWithoutSave, @@ -5355,7 +5362,7 @@ describe('admin-form.controller', () => { expect( MockEmailSubmissionService.validateAttachments, ).not.toHaveBeenCalled() - expect(MockSubmissionService.getProcessedResponses).not.toHaveBeenCalled() + expect(MockParsedResponsesObject.parseResponses).not.toHaveBeenCalled() expect(MockAdminFormService.extractMyInfoFieldIds).not.toHaveBeenCalled() expect( MockEmailSubmissionService.createEmailSubmissionWithoutSave, @@ -5408,7 +5415,7 @@ describe('admin-form.controller', () => { expect( MockEmailSubmissionService.validateAttachments, ).not.toHaveBeenCalled() - expect(MockSubmissionService.getProcessedResponses).not.toHaveBeenCalled() + expect(MockParsedResponsesObject.parseResponses).not.toHaveBeenCalled() expect(MockAdminFormService.extractMyInfoFieldIds).not.toHaveBeenCalled() expect( MockEmailSubmissionService.createEmailSubmissionWithoutSave, @@ -5461,7 +5468,7 @@ describe('admin-form.controller', () => { expect( MockEmailSubmissionService.validateAttachments, ).not.toHaveBeenCalled() - expect(MockSubmissionService.getProcessedResponses).not.toHaveBeenCalled() + expect(MockParsedResponsesObject.parseResponses).not.toHaveBeenCalled() expect(MockAdminFormService.extractMyInfoFieldIds).not.toHaveBeenCalled() expect( MockEmailSubmissionService.createEmailSubmissionWithoutSave, @@ -5514,7 +5521,7 @@ describe('admin-form.controller', () => { expect( MockEmailSubmissionService.validateAttachments, ).not.toHaveBeenCalled() - expect(MockSubmissionService.getProcessedResponses).not.toHaveBeenCalled() + expect(MockParsedResponsesObject.parseResponses).not.toHaveBeenCalled() expect(MockAdminFormService.extractMyInfoFieldIds).not.toHaveBeenCalled() expect( MockEmailSubmissionService.createEmailSubmissionWithoutSave, @@ -5567,7 +5574,7 @@ describe('admin-form.controller', () => { expect( MockEmailSubmissionService.validateAttachments, ).not.toHaveBeenCalled() - expect(MockSubmissionService.getProcessedResponses).not.toHaveBeenCalled() + expect(MockParsedResponsesObject.parseResponses).not.toHaveBeenCalled() expect(MockAdminFormService.extractMyInfoFieldIds).not.toHaveBeenCalled() expect( MockEmailSubmissionService.createEmailSubmissionWithoutSave, @@ -5620,7 +5627,7 @@ describe('admin-form.controller', () => { expect( MockEmailSubmissionService.validateAttachments, ).toHaveBeenCalledWith(MOCK_RESPONSES) - expect(MockSubmissionService.getProcessedResponses).not.toHaveBeenCalled() + expect(MockParsedResponsesObject.parseResponses).not.toHaveBeenCalled() expect(MockAdminFormService.extractMyInfoFieldIds).not.toHaveBeenCalled() expect( MockEmailSubmissionService.createEmailSubmissionWithoutSave, @@ -5673,7 +5680,7 @@ describe('admin-form.controller', () => { expect( MockEmailSubmissionService.validateAttachments, ).toHaveBeenCalledWith(MOCK_RESPONSES) - expect(MockSubmissionService.getProcessedResponses).not.toHaveBeenCalled() + expect(MockParsedResponsesObject.parseResponses).not.toHaveBeenCalled() expect(MockAdminFormService.extractMyInfoFieldIds).not.toHaveBeenCalled() expect( MockEmailSubmissionService.createEmailSubmissionWithoutSave, @@ -5692,7 +5699,7 @@ describe('admin-form.controller', () => { }) it('should return 400 when responses cannot be processed', async () => { - MockSubmissionService.getProcessedResponses.mockReturnValueOnce( + MockParsedResponsesObject.parseResponses.mockReturnValueOnce( err(new ProcessingError()), ) const mockReq = expressHandler.mockRequest({ @@ -5726,7 +5733,7 @@ describe('admin-form.controller', () => { expect( MockEmailSubmissionService.validateAttachments, ).toHaveBeenCalledWith(MOCK_RESPONSES) - expect(MockSubmissionService.getProcessedResponses).toHaveBeenCalledWith( + expect(MockParsedResponsesObject.parseResponses).toHaveBeenCalledWith( MOCK_FORM, MOCK_RESPONSES, ) @@ -5748,7 +5755,7 @@ describe('admin-form.controller', () => { }) it('should return 409 when the submitted field IDs do not match the form field IDs', async () => { - MockSubmissionService.getProcessedResponses.mockReturnValueOnce( + MockParsedResponsesObject.parseResponses.mockReturnValueOnce( err(new ConflictError('')), ) const mockReq = expressHandler.mockRequest({ @@ -5782,7 +5789,7 @@ describe('admin-form.controller', () => { expect( MockEmailSubmissionService.validateAttachments, ).toHaveBeenCalledWith(MOCK_RESPONSES) - expect(MockSubmissionService.getProcessedResponses).toHaveBeenCalledWith( + expect(MockParsedResponsesObject.parseResponses).toHaveBeenCalledWith( MOCK_FORM, MOCK_RESPONSES, ) @@ -5804,7 +5811,7 @@ describe('admin-form.controller', () => { }) it('should return 400 when any answer is invalid', async () => { - MockSubmissionService.getProcessedResponses.mockReturnValueOnce( + MockParsedResponsesObject.parseResponses.mockReturnValueOnce( err(new ValidateFieldError()), ) const mockReq = expressHandler.mockRequest({ @@ -5838,7 +5845,7 @@ describe('admin-form.controller', () => { expect( MockEmailSubmissionService.validateAttachments, ).toHaveBeenCalledWith(MOCK_RESPONSES) - expect(MockSubmissionService.getProcessedResponses).toHaveBeenCalledWith( + expect(MockParsedResponsesObject.parseResponses).toHaveBeenCalledWith( MOCK_FORM, MOCK_RESPONSES, ) @@ -5894,7 +5901,7 @@ describe('admin-form.controller', () => { expect( MockEmailSubmissionService.validateAttachments, ).toHaveBeenCalledWith(MOCK_RESPONSES) - expect(MockSubmissionService.getProcessedResponses).toHaveBeenCalledWith( + expect(MockParsedResponsesObject.parseResponses).toHaveBeenCalledWith( MOCK_FORM, MOCK_RESPONSES, ) @@ -5959,7 +5966,7 @@ describe('admin-form.controller', () => { expect( MockEmailSubmissionService.validateAttachments, ).toHaveBeenCalledWith(MOCK_RESPONSES) - expect(MockSubmissionService.getProcessedResponses).toHaveBeenCalledWith( + expect(MockParsedResponsesObject.parseResponses).toHaveBeenCalledWith( MOCK_FORM, MOCK_RESPONSES, ) @@ -6024,7 +6031,7 @@ describe('admin-form.controller', () => { expect( MockEmailSubmissionService.validateAttachments, ).toHaveBeenCalledWith(MOCK_RESPONSES) - expect(MockSubmissionService.getProcessedResponses).toHaveBeenCalledWith( + expect(MockParsedResponsesObject.parseResponses).toHaveBeenCalledWith( MOCK_FORM, MOCK_RESPONSES, ) diff --git a/src/app/modules/form/admin-form/admin-form.controller.ts b/src/app/modules/form/admin-form/admin-form.controller.ts index 3be70728b3..311deac41a 100644 --- a/src/app/modules/form/admin-form/admin-form.controller.ts +++ b/src/app/modules/form/admin-form/admin-form.controller.ts @@ -50,10 +50,6 @@ import { } from '../../core/core.errors' import { ControllerHandler } from '../../core/core.types' import * as FeedbackService from '../../feedback/feedback.service' -import { - createCorppassParsedResponses, - createSingpassParsedResponses, -} from '../../spcp/spcp.util' import * as EmailSubmissionMiddleware from '../../submission/email-submission/email-submission.middleware' import * as EmailSubmissionService from '../../submission/email-submission/email-submission.service' import { @@ -61,6 +57,7 @@ import { mapRouteError as mapEmailSubmissionError, SubmissionEmailObj, } from '../../submission/email-submission/email-submission.util' +import ParsedResponsesObject from '../../submission/email-submission/ParsedResponsesObject.class' import * as EncryptSubmissionMiddleware from '../../submission/encrypt-submission/encrypt-submission.middleware' import * as EncryptSubmissionService from '../../submission/encrypt-submission/encrypt-submission.service' import { mapRouteError as mapEncryptSubmissionError } from '../../submission/encrypt-submission/encrypt-submission.utils' @@ -1512,7 +1509,7 @@ export const submitEmailPreview: ControllerHandler< const parsedResponsesResult = await EmailSubmissionService.validateAttachments(responses).andThen(() => - SubmissionService.getProcessedResponses(form, responses), + ParsedResponsesObject.parseResponses(form, responses), ) if (parsedResponsesResult.isErr()) { logger.error({ @@ -1529,21 +1526,22 @@ export const submitEmailPreview: ControllerHandler< const attachments = mapAttachmentsFromResponses(req.body.responses) // Handle SingPass, CorpPass and MyInfo authentication and validation - if (form.authType === AuthType.SP || form.authType === AuthType.MyInfo) { - parsedResponses.push( - ...createSingpassParsedResponses(PREVIEW_SINGPASS_UINFIN), - ) - } else if (form.authType === AuthType.CP) { - parsedResponses.push( - ...createCorppassParsedResponses( - PREVIEW_CORPPASS_UINFIN, - PREVIEW_CORPPASS_UID, - ), - ) + const { authType } = form + if (authType === AuthType.SP || authType === AuthType.MyInfo) { + parsedResponses.addNdiResponses({ + authType, + uinFin: PREVIEW_SINGPASS_UINFIN, + }) + } else if (authType === AuthType.CP) { + parsedResponses.addNdiResponses({ + authType, + uinFin: PREVIEW_CORPPASS_UINFIN, + userInfo: PREVIEW_CORPPASS_UID, + }) } const emailData = new SubmissionEmailObj( - parsedResponses, + parsedResponses.getAllResponses(), // All MyInfo fields are verified in preview new Set(AdminFormService.extractMyInfoFieldIds(form.form_fields)), form.authType, @@ -1556,7 +1554,9 @@ export const submitEmailPreview: ControllerHandler< ) const sendAdminEmailResult = await MailService.sendSubmissionToAdmin({ - replyToEmails: EmailSubmissionService.extractEmailAnswers(parsedResponses), + replyToEmails: EmailSubmissionService.extractEmailAnswers( + parsedResponses.getAllResponses(), + ), form, submission, attachments, @@ -1585,7 +1585,7 @@ export const submitEmailPreview: ControllerHandler< attachments, responsesData: emailData.autoReplyData, recipientData: extractEmailConfirmationData( - parsedResponses, + parsedResponses.getAllResponses(), form.form_fields, ), }).mapErr((error) => { diff --git a/src/app/modules/submission/__tests__/submission/submission.service.spec.ts b/src/app/modules/submission/__tests__/submission/submission.service.spec.ts index db1cf95366..93dd37a40d 100644 --- a/src/app/modules/submission/__tests__/submission/submission.service.spec.ts +++ b/src/app/modules/submission/__tests__/submission/submission.service.spec.ts @@ -9,40 +9,26 @@ import { MalformedParametersError, } from 'src/app/modules/core/core.errors' import * as SubmissionService from 'src/app/modules/submission/submission.service' -import { ProcessedFieldResponse } from 'src/app/modules/submission/submission.types' import MailService from 'src/app/services/mail/mail.service' import { createQueryWithDateParam } from 'src/app/utils/date' -import * as LogicUtil from 'src/shared/util/logic' import { AutoReplyOptions, BasicField, IAttachmentInfo, - IEmailFormSchema, IEmailSubmissionSchema, - IEncryptedFormSchema, IEncryptedSubmissionSchema, IFormSchema, - IPreventSubmitLogicSchema, ISubmissionSchema, - LogicType, - ResponseMode, SubmissionType, } from 'src/types' import { generateDefaultField, generateNewSingleAnswerResponse, - generateProcessedSingleAnswerResponse, - generateSingleAnswerResponse, } from 'tests/unit/backend/helpers/generate-form-data' import dbHandler from 'tests/unit/backend/helpers/jest-db' -import { - ConflictError, - ProcessingError, - SendEmailConfirmationError, - ValidateFieldError, -} from '../../submission.errors' +import { SendEmailConfirmationError } from '../../submission.errors' import { extractEmailConfirmationData } from '../../submission.utils' jest.mock('src/app/services/mail/mail.service') @@ -109,285 +95,6 @@ describe('submission.service', () => { afterAll(async () => await dbHandler.closeDatabase()) beforeEach(() => jest.clearAllMocks()) - describe('getProcessedResponses', () => { - it('should return list of parsed responses for encrypted form submission successfully', async () => { - // Arrange - // Only mobile and email fields are parsed, since the other fields are - // e2e encrypted from the browser. - const mobileField = generateDefaultField(BasicField.Mobile) - const emailField = generateDefaultField(BasicField.Email) - // Add answers to both mobile and email fields - const mobileResponse = generateSingleAnswerResponse( - mobileField, - '+6587654321', - ) - const emailResponse = generateSingleAnswerResponse( - emailField, - 'test@example.com', - ) - - const mobileProcessedResponse = generateProcessedSingleAnswerResponse( - mobileField, - '+6587654321', - ) - const emailProcessedResponse = generateProcessedSingleAnswerResponse( - emailField, - 'test@example.com', - ) - - // Act - const actualResult = SubmissionService.getProcessedResponses( - { - responseMode: ResponseMode.Encrypt, - form_fields: [mobileField, emailField], - } as unknown as IFormSchema, - [mobileResponse, emailResponse], - ) - - // Assert - const expectedParsed: ProcessedFieldResponse[] = [ - { ...mobileProcessedResponse, isVisible: true }, - { ...emailProcessedResponse, isVisible: true }, - ] - // Should only have email and mobile fields for encrypted forms. - expect(actualResult.isOk()).toEqual(true) - expect(actualResult._unsafeUnwrap()).toEqual(expectedParsed) - }) - - it('should return list of parsed responses for email form submission successfully', async () => { - // Arrange - // Add answer to subset of field types - const shortTextField = generateDefaultField(BasicField.ShortText) - const decimalField = generateDefaultField(BasicField.Decimal) - - // Add answers to both mobile and email fields - const shortTextResponse = generateSingleAnswerResponse( - shortTextField, - 'the quick brown fox jumps over the lazy dog', - ) - const decimalResponse = generateSingleAnswerResponse( - decimalField, - '3.142', - ) - - const shortTextProcessedResponse = generateProcessedSingleAnswerResponse( - shortTextField, - 'the quick brown fox jumps over the lazy dog', - ) - const decimalProcessedResponse = generateProcessedSingleAnswerResponse( - decimalField, - '3.142', - ) - - // Act - const actualResult = SubmissionService.getProcessedResponses( - { - responseMode: ResponseMode.Email, - form_fields: [shortTextField, decimalField], - } as unknown as IFormSchema, - [shortTextResponse, decimalResponse], - ) - - // Assert - const expectedParsed: ProcessedFieldResponse[] = [ - { ...shortTextProcessedResponse, isVisible: true }, - { ...decimalProcessedResponse, isVisible: true }, - ] - - expect(actualResult.isOk()).toEqual(true) - expect(actualResult._unsafeUnwrap()).toEqual(expectedParsed) - }) - - it('should return error when email form has more fields than responses', async () => { - // Arrange - const extraField = generateDefaultField(BasicField.Mobile) - - // Act + Assert - - const actualResult = SubmissionService.getProcessedResponses( - { - responseMode: ResponseMode.Email, - form_fields: [extraField], - } as unknown as IEmailFormSchema, - [], - ) - - expect(actualResult.isErr()).toEqual(true) - expect(actualResult._unsafeUnwrapErr()).toEqual( - new ConflictError('Some form fields are missing'), - ) - }) - - it('should return error when encrypt form has more fields than responses', async () => { - // Arrange - const extraField = generateDefaultField(BasicField.Mobile) - - // Act + Assert - - const actualResult = SubmissionService.getProcessedResponses( - { - responseMode: ResponseMode.Encrypt, - form_fields: [extraField], - } as unknown as IEncryptedFormSchema, - [], - ) - - expect(actualResult.isErr()).toEqual(true) - expect(actualResult._unsafeUnwrapErr()).toEqual( - new ConflictError('Some form fields are missing'), - ) - }) - it('should allow responses for encrypt mode hidden fields', async () => { - // Arrange - // Only check for mobile and email fields, since the other fields are - // e2e encrypted from the browser. - const mobileField = generateDefaultField(BasicField.Mobile) - const emailField = generateDefaultField(BasicField.Email) - // Add answers to both mobile and email fields - const mobileResponse = generateSingleAnswerResponse( - mobileField, - '+6587654321', - ) - - const emailResponse = generateSingleAnswerResponse( - emailField, - 'test@example.com', - ) - - const mobileProcessedResponse = generateProcessedSingleAnswerResponse( - mobileField, - '+6587654321', - ) - mobileProcessedResponse.isVisible = false - - const emailProcessedResponse = generateProcessedSingleAnswerResponse( - emailField, - 'test@example.com', - ) - emailProcessedResponse.isVisible = false - - // Act - const actualResult = SubmissionService.getProcessedResponses( - { - responseMode: ResponseMode.Encrypt, - form_fields: [mobileField, emailField], - } as unknown as IFormSchema, - [mobileResponse, emailResponse], - ) - - // Assert - const expectedParsed: ProcessedFieldResponse[] = [ - { ...mobileProcessedResponse, isVisible: true }, //getProcessedResponses sets isVisible to be true for encrypt mode - { ...emailProcessedResponse, isVisible: true }, - ] - // Should only have email and mobile fields for encrypted forms. - expect(actualResult.isOk()).toEqual(true) - expect(actualResult._unsafeUnwrap()).toEqual(expectedParsed) - }) - - it('should return error when any responses are not valid for encrypted form submission', async () => { - // Arrange - // Only mobile and email fields are parsed, since the other fields are - // e2e encrypted from the browser. - const mobileField = generateDefaultField(BasicField.Mobile) - const mobileResponse = generateSingleAnswerResponse( - mobileField, - 'invalid', - ) - - // Act + Assert - const actualResult = SubmissionService.getProcessedResponses( - { - responseMode: ResponseMode.Encrypt, - form_fields: [mobileField], - } as unknown as IEncryptedFormSchema, - [mobileResponse], - ) - - expect(actualResult.isErr()).toEqual(true) - expect(actualResult._unsafeUnwrapErr()).toEqual( - new ValidateFieldError('Invalid answer submitted'), - ) - }) - - it('should return error when any responses are not valid for email form submission', async () => { - // Arrange - // Set NRIC field in form as required. - const nricField = generateDefaultField(BasicField.Nric) - const nricResponse = generateSingleAnswerResponse(nricField, 'invalid') - - // Act + Assert - const actualResult = SubmissionService.getProcessedResponses( - { - responseMode: ResponseMode.Email, - form_fields: [nricField], - } as unknown as IEmailFormSchema, - [nricResponse], - ) - - expect(actualResult.isErr()).toEqual(true) - expect(actualResult._unsafeUnwrapErr()).toEqual( - new ValidateFieldError('Invalid answer submitted'), - ) - }) - - it('should return error when encrypted form submission is prevented by logic', async () => { - // Arrange - // Mock logic util to return non-empty to check if error is thrown - jest - .spyOn(LogicUtil, 'getLogicUnitPreventingSubmit') - .mockReturnValueOnce({ - preventSubmitMessage: 'mock prevent submit', - conditions: [], - logicType: LogicType.PreventSubmit, - _id: 'some id', - } as unknown as IPreventSubmitLogicSchema) - - // Act + Assert - const actualResult = SubmissionService.getProcessedResponses( - { - responseMode: ResponseMode.Encrypt, - form_fields: [], - } as unknown as IEncryptedFormSchema, - [], - ) - - expect(actualResult.isErr()).toEqual(true) - expect(actualResult._unsafeUnwrapErr()).toEqual( - new ProcessingError('Submission prevented by form logic'), - ) - }) - - it('should return error when email form submission is prevented by logic', async () => { - // Arrange - // Mock logic util to return non-empty to check if error is thrown. - const mockReturnLogicUnit = { - preventSubmitMessage: 'mock prevent submit', - conditions: [], - logicType: LogicType.PreventSubmit, - _id: 'some id', - } as unknown as IPreventSubmitLogicSchema - - jest - .spyOn(LogicUtil, 'getLogicUnitPreventingSubmit') - .mockReturnValueOnce(mockReturnLogicUnit) - - // Act + Assert - const actualResult = SubmissionService.getProcessedResponses( - { - responseMode: ResponseMode.Email, - form_fields: [], - } as unknown as IEmailFormSchema, - [], - ) - - expect(actualResult.isErr()).toEqual(true) - expect(actualResult._unsafeUnwrapErr()).toEqual( - new ProcessingError('Submission prevented by form logic'), - ) - }) - }) - describe('getFormSubmissionsCount', () => { const countSpy = jest.spyOn(Submission, 'countDocuments') const MOCK_FORM_ID = new ObjectId() diff --git a/src/app/modules/submission/email-submission/ParsedResponsesObject.class.ts b/src/app/modules/submission/email-submission/ParsedResponsesObject.class.ts new file mode 100644 index 0000000000..0a3d899220 --- /dev/null +++ b/src/app/modules/submission/email-submission/ParsedResponsesObject.class.ts @@ -0,0 +1,153 @@ +import { err, ok, Result } from 'neverthrow' + +import { + getLogicUnitPreventingSubmit, + getVisibleFieldIds, +} from '../../../../shared/util/logic' +import { + AuthType, + FieldResponse, + IFieldSchema, + IFormDocument, + ResponseMode, +} from '../../../../types' +import { validateField } from '../../../utils/field-validation' +import { + createCorppassParsedResponses, + createSingpassParsedResponses, +} from '../../spcp/spcp.util' +import { + ConflictError, + ProcessingError, + ValidateFieldError, +} from '../submission.errors' +import { ProcessedFieldResponse } from '../submission.types' +import { getFilteredResponses } from '../submission.utils' + +type NdiUserInfo = + | { authType: AuthType.SP | AuthType.MyInfo; uinFin: string } + | { authType: AuthType.CP; uinFin: string; userInfo: string } + +export default class ParsedResponsesObject { + public ndiResponses: ProcessedFieldResponse[] = [] + private constructor(public responses: ProcessedFieldResponse[]) {} + + addNdiResponses(info: NdiUserInfo): ParsedResponsesObject { + /** + * No typescript destructuring being done in switch statement + * because typescript isn't smart enough to do narrowing with + * destructured variable switch cases. + */ + switch (info.authType) { + case AuthType.SP: + case AuthType.MyInfo: + this.ndiResponses = createSingpassParsedResponses(info.uinFin) + break + case AuthType.CP: + this.ndiResponses = createCorppassParsedResponses( + info.uinFin, + info.userInfo, + ) + break + } + return this + } + + getAllResponses(): ProcessedFieldResponse[] { + return [...this.responses, ...this.ndiResponses] + } + + /** + * Injects response metadata such as the question, visibility state. In + * addition, validation such as input validation or signature validation on + * verified fields are also performed on the response. + * @param form The form document corresponding to the responses + * @param responses The responses to process and validate + * @returns neverthrow ok() with field responses with additional metadata injected. + * @returns neverthrow err() if response validation fails + */ + static parseResponses( + form: IFormDocument, + responses: FieldResponse[], + ): Result< + ParsedResponsesObject, + ProcessingError | ConflictError | ValidateFieldError + > { + const filteredResponsesResult = getFilteredResponses(form, responses) + if (filteredResponsesResult.isErr()) { + return err(filteredResponsesResult.error) + } + + const filteredResponses = filteredResponsesResult.value + + // Set of all visible fields + const visibleFieldIds = getVisibleFieldIds(filteredResponses, form) + + // Guard against invalid form submissions that should have been prevented by + // logic. + if ( + getLogicUnitPreventingSubmit(filteredResponses, form, visibleFieldIds) + ) { + return err(new ProcessingError('Submission prevented by form logic')) + } + + // Create a map keyed by field._id for easier access + + if (!form.form_fields) { + return err(new ProcessingError('Form fields are undefined')) + } + + const fieldMap = form.form_fields.reduce<{ + [fieldId: string]: IFieldSchema + }>((acc, field) => { + acc[field._id] = field + return acc + }, {}) + + // Validate each field in the form and inject metadata into the responses. + const processedResponses = [] + for (const response of filteredResponses) { + const responseId = response._id + const formField = fieldMap[responseId] + if (!formField) { + return err( + new ProcessingError('Response ID does not match form field IDs'), + ) + } + + const processingResponse: ProcessedFieldResponse = { + ...response, + isVisible: + // Set isVisible as true for Encrypt mode if there is a response for mobile and email field + // Because we cannot tell if the field is unhidden by logic + // This prevents downstream validateField from incorrectly preventing + // encrypt mode submissions with responses on unhidden fields + // TODO(#780): Remove this once submission service is separated into + // Email and Encrypted services + form.responseMode === ResponseMode.Encrypt + ? 'answer' in response && + typeof response.answer === 'string' && + response.answer.trim() !== '' + : visibleFieldIds.has(responseId), + question: formField.getQuestion(), + } + + if (formField.isVerifiable) { + processingResponse.isUserVerified = formField.isVerifiable + } + + // Error will be returned if the processed response is not valid. + const validateFieldResult = validateField( + form._id, + formField, + processingResponse, + ) + if (validateFieldResult.isErr()) { + return err(validateFieldResult.error) + } + processedResponses.push(processingResponse) + } + + return ok(new ParsedResponsesObject(processedResponses)) + } +} diff --git a/src/app/modules/submission/email-submission/__tests__/ParsedResponsesObject.class.spec.ts b/src/app/modules/submission/email-submission/__tests__/ParsedResponsesObject.class.spec.ts new file mode 100644 index 0000000000..b0a8bb01ae --- /dev/null +++ b/src/app/modules/submission/email-submission/__tests__/ParsedResponsesObject.class.spec.ts @@ -0,0 +1,124 @@ +import { + generateDefaultField, + generateProcessedSingleAnswerResponse, + generateSingleAnswerResponse, +} from '../../../../../../tests/unit/backend/helpers/generate-form-data' +import * as LogicUtil from '../../../../../shared/util/logic' +import { + BasicField, + IEmailFormSchema, + IFormSchema, + IPreventSubmitLogicSchema, + LogicType, + ResponseMode, +} from '../../../../../types' +import { + ConflictError, + ProcessingError, + ValidateFieldError, +} from '../../submission.errors' +import { ProcessedFieldResponse } from '../../submission.types' +import ParsedResponsesObject from '../ParsedResponsesObject.class' + +describe('ParsedResponsesObject', () => { + it('should return list of parsed responses for email form submission successfully', async () => { + // Add answer to subset of field types + const shortTextField = generateDefaultField(BasicField.ShortText) + const decimalField = generateDefaultField(BasicField.Decimal) + + // Add answers to both mobile and email fields + const shortTextResponse = generateSingleAnswerResponse( + shortTextField, + 'the quick brown fox jumps over the lazy dog', + ) + const decimalResponse = generateSingleAnswerResponse(decimalField, '3.142') + + const shortTextProcessedResponse = generateProcessedSingleAnswerResponse( + shortTextField, + 'the quick brown fox jumps over the lazy dog', + ) + const decimalProcessedResponse = generateProcessedSingleAnswerResponse( + decimalField, + '3.142', + ) + + const result = ParsedResponsesObject.parseResponses( + { + responseMode: ResponseMode.Email, + form_fields: [shortTextField, decimalField], + } as unknown as IFormSchema, + [shortTextResponse, decimalResponse], + ) + + const expectedParsed: ProcessedFieldResponse[] = [ + { ...shortTextProcessedResponse, isVisible: true }, + { ...decimalProcessedResponse, isVisible: true }, + ] + + expect(result.isOk()).toEqual(true) + expect(result._unsafeUnwrap().getAllResponses()).toEqual(expectedParsed) + }) + + it('should return error when email form has more fields than responses', async () => { + const extraField = generateDefaultField(BasicField.Mobile) + + const result = ParsedResponsesObject.parseResponses( + { + responseMode: ResponseMode.Email, + form_fields: [extraField], + } as unknown as IEmailFormSchema, + [], + ) + + expect(result.isErr()).toEqual(true) + expect(result._unsafeUnwrapErr()).toEqual( + new ConflictError('Some form fields are missing'), + ) + }) + + it('should return error when any responses are not valid for email form submission', async () => { + // Set NRIC field in form as required. + const nricField = generateDefaultField(BasicField.Nric) + const nricResponse = generateSingleAnswerResponse(nricField, 'invalid') + + const result = ParsedResponsesObject.parseResponses( + { + responseMode: ResponseMode.Email, + form_fields: [nricField], + } as unknown as IEmailFormSchema, + [nricResponse], + ) + + expect(result.isErr()).toEqual(true) + expect(result._unsafeUnwrapErr()).toEqual( + new ValidateFieldError('Invalid answer submitted'), + ) + }) + + it('should return error when email form submission is prevented by logic', async () => { + // Mock logic util to return non-empty to check if error is thrown. + const mockReturnLogicUnit = { + preventSubmitMessage: 'mock prevent submit', + conditions: [], + logicType: LogicType.PreventSubmit, + _id: 'some id', + } as unknown as IPreventSubmitLogicSchema + + jest + .spyOn(LogicUtil, 'getLogicUnitPreventingSubmit') + .mockReturnValueOnce(mockReturnLogicUnit) + + const result = ParsedResponsesObject.parseResponses( + { + responseMode: ResponseMode.Email, + form_fields: [], + } as unknown as IEmailFormSchema, + [], + ) + + expect(result.isErr()).toEqual(true) + expect(result._unsafeUnwrapErr()).toEqual( + new ProcessingError('Submission prevented by form logic'), + ) + }) +}) diff --git a/src/app/modules/submission/email-submission/email-submission.controller.ts b/src/app/modules/submission/email-submission/email-submission.controller.ts index f445414da1..4c8c215cc0 100644 --- a/src/app/modules/submission/email-submission/email-submission.controller.ts +++ b/src/app/modules/submission/email-submission/email-submission.controller.ts @@ -20,10 +20,6 @@ import { import { MyInfoFactory } from '../../myinfo/myinfo.factory' import * as MyInfoUtil from '../../myinfo/myinfo.util' import { SpcpFactory } from '../../spcp/spcp.factory' -import { - createCorppassParsedResponses, - createSingpassParsedResponses, -} from '../../spcp/spcp.util' import * as EmailSubmissionMiddleware from '../email-submission/email-submission.middleware' import * as SubmissionService from '../submission.service' import { extractEmailConfirmationData } from '../submission.utils' @@ -35,6 +31,7 @@ import { mapRouteError, SubmissionEmailObj, } from './email-submission.util' +import ParsedResponsesObject from './ParsedResponsesObject.class' const logger = createLoggerWithLabel(module) @@ -137,7 +134,7 @@ const submitEmailModeForm: ControllerHandler< // Validate responses EmailSubmissionService.validateAttachments(req.body.responses) .andThen(() => - SubmissionService.getProcessedResponses(form, req.body.responses), + ParsedResponsesObject.parseResponses(form, req.body.responses), ) .map((parsedResponses) => ({ parsedResponses, form })) .mapErr((error) => { @@ -157,10 +154,11 @@ const submitEmailModeForm: ControllerHandler< .asyncAndThen((jwt) => SpcpFactory.extractCorppassJwtPayload(jwt)) .map((jwt) => ({ form, - parsedResponses: [ - ...parsedResponses, - ...createCorppassParsedResponses(jwt.userName, jwt.userInfo), - ], + parsedResponses: parsedResponses.addNdiResponses({ + authType, + uinFin: jwt.userName, + userInfo: jwt.userInfo, + }), })) .mapErr((error) => { spcpSubmissionFailure = true @@ -176,10 +174,10 @@ const submitEmailModeForm: ControllerHandler< .asyncAndThen((jwt) => SpcpFactory.extractSingpassJwtPayload(jwt)) .map((jwt) => ({ form, - parsedResponses: [ - ...parsedResponses, - ...createSingpassParsedResponses(jwt.userName), - ], + parsedResponses: parsedResponses.addNdiResponses({ + authType, + uinFin: jwt.userName, + }), })) .mapErr((error) => { spcpSubmissionFailure = true @@ -199,16 +197,19 @@ const submitEmailModeForm: ControllerHandler< .asyncAndThen((uinFin) => MyInfoFactory.fetchMyInfoHashes(uinFin, formId) .andThen((hashes) => - MyInfoFactory.checkMyInfoHashes(parsedResponses, hashes), + MyInfoFactory.checkMyInfoHashes( + parsedResponses.responses, + hashes, + ), ) .map( (hashedFields) => ({ form, hashedFields, - parsedResponses: [ - ...parsedResponses, - ...createSingpassParsedResponses(uinFin), - ], + parsedResponses: parsedResponses.addNdiResponses({ + authType, + uinFin, + }), }), ), ) @@ -231,7 +232,7 @@ const submitEmailModeForm: ControllerHandler< .andThen(({ form, parsedResponses, hashedFields }) => { // Create data for response email as well as email confirmation const emailData = new SubmissionEmailObj( - parsedResponses, + parsedResponses.getAllResponses(), hashedFields, form.authType, ) @@ -274,8 +275,9 @@ const submitEmailModeForm: ControllerHandler< // NOTE: This should short circuit in the event of an error. // This is why sendSubmissionToAdmin is separated from sendEmailConfirmations in 2 blocks return MailService.sendSubmissionToAdmin({ - replyToEmails: - EmailSubmissionService.extractEmailAnswers(parsedResponses), + replyToEmails: EmailSubmissionService.extractEmailAnswers( + parsedResponses.getAllResponses(), + ), form, submission, attachments, @@ -313,7 +315,7 @@ const submitEmailModeForm: ControllerHandler< attachments, responsesData: emailData.autoReplyData, recipientData: extractEmailConfirmationData( - parsedResponses, + parsedResponses.getAllResponses(), form.form_fields, ), }).mapErr((error) => { diff --git a/src/app/modules/submission/email-submission/email-submission.types.ts b/src/app/modules/submission/email-submission/email-submission.types.ts index eae1936c14..d142ed7b60 100644 --- a/src/app/modules/submission/email-submission/email-submission.types.ts +++ b/src/app/modules/submission/email-submission/email-submission.types.ts @@ -4,7 +4,9 @@ import { IBaseResponse, IPopulatedEmailForm, } from '../../../../types' -import { ProcessedFieldResponse, ProcessedResponse } from '../submission.types' +import { ProcessedResponse } from '../submission.types' + +import ParsedResponsesObject from './ParsedResponsesObject.class' // When a response has been formatted for email, all answerArray // should have been converted to answer @@ -28,6 +30,6 @@ export interface SubmissionHash { export interface IPopulatedEmailFormWithResponsesAndHash { form: IPopulatedEmailForm - parsedResponses: ProcessedFieldResponse[] + parsedResponses: ParsedResponsesObject hashedFields?: Set } diff --git a/src/app/modules/submission/submission.service.ts b/src/app/modules/submission/submission.service.ts index 1dedd9b20f..281719ba1c 100644 --- a/src/app/modules/submission/submission.service.ts +++ b/src/app/modules/submission/submission.service.ts @@ -1,132 +1,24 @@ import mongoose from 'mongoose' -import { err, errAsync, ok, okAsync, Result, ResultAsync } from 'neverthrow' +import { errAsync, okAsync, ResultAsync } from 'neverthrow' -import { - getLogicUnitPreventingSubmit, - getVisibleFieldIds, -} from '../../../shared/util/logic' import { EmailRespondentConfirmationField, - FieldResponse, IAttachmentInfo, - IFieldSchema, - IFormDocument, IPopulatedForm, ISubmissionSchema, - ResponseMode, } from '../../../types' import { createLoggerWithLabel } from '../../config/logger' import getSubmissionModel from '../../models/submission.server.model' import MailService from '../../services/mail/mail.service' import { AutoReplyMailData } from '../../services/mail/mail.types' import { createQueryWithDateParam, isMalformedDate } from '../../utils/date' -import { validateField } from '../../utils/field-validation' import { DatabaseError, MalformedParametersError } from '../core/core.errors' -import { - ConflictError, - ProcessingError, - SendEmailConfirmationError, - ValidateFieldError, -} from './submission.errors' -import { ProcessedFieldResponse } from './submission.types' -import { getFilteredResponses } from './submission.utils' +import { SendEmailConfirmationError } from './submission.errors' const logger = createLoggerWithLabel(module) const SubmissionModel = getSubmissionModel(mongoose) -/** - * Injects response metadata such as the question, visibility state. In - * addition, validation such as input validation or signature validation on - * verified fields are also performed on the response. - * @param form The form document corresponding to the responses - * @param responses The responses to process and validate - * @returns neverthrow ok() with field responses with additional metadata injected. - * @returns neverthrow err() if response validation fails - */ -export const getProcessedResponses = ( - form: IFormDocument, - originalResponses: FieldResponse[], -): Result< - ProcessedFieldResponse[], - ProcessingError | ConflictError | ValidateFieldError -> => { - const filteredResponsesResult = getFilteredResponses(form, originalResponses) - if (filteredResponsesResult.isErr()) { - return err(filteredResponsesResult.error) - } - - const filteredResponses = filteredResponsesResult.value - - // Set of all visible fields - const visibleFieldIds = getVisibleFieldIds(filteredResponses, form) - - // Guard against invalid form submissions that should have been prevented by - // logic. - if (getLogicUnitPreventingSubmit(filteredResponses, form, visibleFieldIds)) { - return err(new ProcessingError('Submission prevented by form logic')) - } - - // Create a map keyed by field._id for easier access - - if (!form.form_fields) { - return err(new ProcessingError('Form fields are undefined')) - } - - const fieldMap = form.form_fields.reduce<{ - [fieldId: string]: IFieldSchema - }>((acc, field) => { - acc[field._id] = field - return acc - }, {}) - - // Validate each field in the form and inject metadata into the responses. - const processedResponses = [] - for (const response of filteredResponses) { - const responseId = response._id - const formField = fieldMap[responseId] - if (!formField) { - return err( - new ProcessingError('Response ID does not match form field IDs'), - ) - } - - const processingResponse: ProcessedFieldResponse = { - ...response, - isVisible: - // Set isVisible as true for Encrypt mode if there is a response for mobile and email field - // Because we cannot tell if the field is unhidden by logic - // This prevents downstream validateField from incorrectly preventing - // encrypt mode submissions with responses on unhidden fields - // TODO(#780): Remove this once submission service is separated into - // Email and Encrypted services - form.responseMode === ResponseMode.Encrypt - ? 'answer' in response && - typeof response.answer === 'string' && - response.answer.trim() !== '' - : visibleFieldIds.has(responseId), - question: formField.getQuestion(), - } - - if (formField.isVerifiable) { - processingResponse.isUserVerified = formField.isVerifiable - } - - // Error will be returned if the processed response is not valid. - const validateFieldResult = validateField( - form._id, - formField, - processingResponse, - ) - if (validateFieldResult.isErr()) { - return err(validateFieldResult.error) - } - processedResponses.push(processingResponse) - } - - return ok(processedResponses) -} - /** * Returns number of form submissions of given form id in the given date range. * From 4a91084d3fb304a6a0e2242c15944d3534b7f1b3 Mon Sep 17 00:00:00 2001 From: Chow Yi Yin <54089291+chowyiyin@users.noreply.github.com> Date: Tue, 22 Jun 2021 17:08:43 +0800 Subject: [PATCH 04/21] refactor: convert CsvMergedHeadersGenerator to typescript (#2080) * refactor: convert response classes to typescript and change response to abstract class * refactor: rearrange types * refactor: convert CsvMergedHeadersGenerator to typescript * refactor: edit export and import statements for CsvMergedHeadersGenerator * fix: edit import statement of generator in submissions.client.factory * refactor: edit response types * refactor: remove typing in jsdocs and edit types * refactor: export response subclasses input types * refactor: add ErrorResponse as default for response types * refactor: shift hasProps to shared * refactor: athrow error instead of returning ErrorResponse and edited typeguards * refactor: change interface to type * fix: fix incorrect import statement * test: remove single header row test * fix: fix bug caused by fieldtype guard and if else condition * refactor: remove console.log * refactor: convert extractAnswer to private function * docs: add jsdocs for response factory * docs: edit jsdocs for addRecord * refactor: extract acheck for answer array * refactor: revert extractAnswer to non-private function * refactor: remove repeated check for type Array * refactor: create SubmissionRecord type * refactor: rename ccsv generator in submissions client factory * refactor: make _data field private * test: rrestore deleted test and edit expected unprocessed record * test: remove repeated test * refactor: use private modifier in typescript classes * test: add tests for error cases * refactor: add private modifier to date comparator * refactor: remove underscore from private field * refactor: extract type in csvmhgenerator * refactor: replace strings with basic field enums * fix: fix import statement --- src/app/modules/bounce/bounce.service.ts | 2 +- src/app/modules/myinfo/myinfo.util.ts | 2 +- src/app/modules/spcp/spcp.util.ts | 2 +- ...erator.js => CsvMergedHeadersGenerator.ts} | 135 ++++++++++-------- .../ArrayAnswerResponse.class.js | 11 -- .../ArrayAnswerResponse.class.ts | 20 +++ .../csv-response-classes/Response.class.js | 21 --- .../csv-response-classes/Response.class.ts | 29 ++++ .../SingleAnswerResponse.class.js | 11 -- .../SingleAnswerResponse.class.ts | 20 +++ .../TableResponse.class.js | 15 -- .../TableResponse.class.ts | 24 ++++ .../helpers/csv-response-classes/index.js | 3 - .../helpers/csv-response-classes/index.ts | 4 + .../modules/forms/helpers/response-factory.js | 18 --- .../modules/forms/helpers/response-factory.ts | 78 ++++++++++ .../services/submissions.client.factory.js | 33 +++-- src/{app/utils => shared/util}/has-prop.ts | 0 src/types/response/index.ts | 18 +++ .../helpers/CsvMergedHeadersGenerator.test.js | 90 +++++++++++- 20 files changed, 381 insertions(+), 155 deletions(-) rename src/public/modules/forms/helpers/{CsvMergedHeadersGenerator.js => CsvMergedHeadersGenerator.ts} (63%) delete mode 100644 src/public/modules/forms/helpers/csv-response-classes/ArrayAnswerResponse.class.js create mode 100644 src/public/modules/forms/helpers/csv-response-classes/ArrayAnswerResponse.class.ts delete mode 100644 src/public/modules/forms/helpers/csv-response-classes/Response.class.js create mode 100644 src/public/modules/forms/helpers/csv-response-classes/Response.class.ts delete mode 100644 src/public/modules/forms/helpers/csv-response-classes/SingleAnswerResponse.class.js create mode 100644 src/public/modules/forms/helpers/csv-response-classes/SingleAnswerResponse.class.ts delete mode 100644 src/public/modules/forms/helpers/csv-response-classes/TableResponse.class.js create mode 100644 src/public/modules/forms/helpers/csv-response-classes/TableResponse.class.ts delete mode 100644 src/public/modules/forms/helpers/csv-response-classes/index.js create mode 100644 src/public/modules/forms/helpers/csv-response-classes/index.ts delete mode 100644 src/public/modules/forms/helpers/response-factory.js create mode 100644 src/public/modules/forms/helpers/response-factory.ts rename src/{app/utils => shared/util}/has-prop.ts (100%) diff --git a/src/app/modules/bounce/bounce.service.ts b/src/app/modules/bounce/bounce.service.ts index fa31c17aba..e78b4dbc44 100644 --- a/src/app/modules/bounce/bounce.service.ts +++ b/src/app/modules/bounce/bounce.service.ts @@ -10,6 +10,7 @@ import { ResultAsync, } from 'neverthrow' +import { hasProp } from '../../../shared/util/has-prop' import { BounceType, IBounceSchema, @@ -25,7 +26,6 @@ import { EMAIL_HEADERS, EmailType } from '../../services/mail/mail.constants' import MailService from '../../services/mail/mail.service' import { SmsFactory } from '../../services/sms/sms.factory' import { transformMongoError } from '../../utils/handle-mongo-error' -import { hasProp } from '../../utils/has-prop' import { PossibleDatabaseError } from '../core/core.errors' import { getCollabEmailsWithPermission } from '../form/form.utils' import * as UserService from '../user/user.service' diff --git a/src/app/modules/myinfo/myinfo.util.ts b/src/app/modules/myinfo/myinfo.util.ts index 6f3febbaee..5c5a05b9dd 100644 --- a/src/app/modules/myinfo/myinfo.util.ts +++ b/src/app/modules/myinfo/myinfo.util.ts @@ -6,6 +6,7 @@ import { err, ok, Result } from 'neverthrow' import { v4 as uuidv4, validate as validateUUID } from 'uuid' import { types as myInfoTypes } from '../../../shared/resources/myinfo' +import { hasProp } from '../../../shared/util/has-prop' import { AuthType, BasicField, @@ -16,7 +17,6 @@ import { MapRouteError, } from '../../../types' import { createLoggerWithLabel } from '../../config/logger' -import { hasProp } from '../../utils/has-prop' import { DatabaseError, MissingFeatureError } from '../core/core.errors' import { AuthTypeMismatchError, diff --git a/src/app/modules/spcp/spcp.util.ts b/src/app/modules/spcp/spcp.util.ts index fde8713544..3aba24bbe6 100644 --- a/src/app/modules/spcp/spcp.util.ts +++ b/src/app/modules/spcp/spcp.util.ts @@ -3,6 +3,7 @@ import crypto from 'crypto' import { StatusCodes } from 'http-status-codes' import { err, ok, Result } from 'neverthrow' +import { hasProp } from '../../../shared/util/has-prop' import { AuthType, BasicField, @@ -11,7 +12,6 @@ import { SPCPFieldTitle, } from '../../../types' import { createLoggerWithLabel } from '../../config/logger' -import { hasProp } from '../../utils/has-prop' import { MissingFeatureError } from '../core/core.errors' import { AuthTypeMismatchError, diff --git a/src/public/modules/forms/helpers/CsvMergedHeadersGenerator.js b/src/public/modules/forms/helpers/CsvMergedHeadersGenerator.ts similarity index 63% rename from src/public/modules/forms/helpers/CsvMergedHeadersGenerator.js rename to src/public/modules/forms/helpers/CsvMergedHeadersGenerator.ts index 622e95af67..9e148e92f2 100644 --- a/src/public/modules/forms/helpers/CsvMergedHeadersGenerator.js +++ b/src/public/modules/forms/helpers/CsvMergedHeadersGenerator.ts @@ -1,21 +1,40 @@ -const moment = require('moment-timezone') -const keyBy = require('lodash/keyBy') -const { CsvGenerator } = require('./CsvGenerator') -const { getResponseInstance } = require('./response-factory') - -/** - * @typedef {{ - * _id: string, - * question: string, - * answer?: string, - * answerArray?: string[], - * fieldType: string, - * isHeader?: boolean, - * }} DisplayedResponse - */ - -class CsvMergedHeadersGenerator extends CsvGenerator { - constructor(expectedNumberOfRecords, numOfMetaDataRows) { +import keyBy from 'lodash/keyBy' +import moment from 'moment-timezone' + +import { DisplayedResponseWithoutAnswer } from '../../../../types/response' + +import { Response } from './csv-response-classes' +import { CsvGenerator } from './CsvGenerator' +import { getResponseInstance } from './response-factory' + +type KeyedResponse = { [fieldId: string]: Response } + +type UnprocessedRecord = { + created: string + submissionId: string + record: KeyedResponse +} + +type SubmissionRecord = { + record: DisplayedResponseWithoutAnswer[] + created: string + submissionId: string +} + +export class CsvMergedHeadersGenerator extends CsvGenerator { + hasBeenProcessed: boolean + hasBeenSorted: boolean + fieldIdToQuestion: Map< + string, + { + created: string + question: string + } + > + fieldIdToNumCols: Record + unprocessed: UnprocessedRecord[] + + constructor(expectedNumberOfRecords: number, numOfMetaDataRows: number) { super(expectedNumberOfRecords, numOfMetaDataRows) this.hasBeenProcessed = false @@ -28,25 +47,25 @@ class CsvMergedHeadersGenerator extends CsvGenerator { /** * Returns current length of CSV file excluding header and meta-data */ - length() { + length(): number { return this.unprocessed.length } /** - * - * @param {Object} decryptedContent - * @param {DisplayedResponse[]} decryptedContent.record - * @param {string} decryptedContent.created - * @param {string} decryptedContent.submissionId + * Extracts information from input record, rearranges record and then adds an UnprocessedRecord to this.unprocessed + * @param decryptedContent + * @param decryptedContent.record + * @param decryptedContent.created + * @param decryptedContent.submissionId + * @throws Error when trying to convert record into a response instance. Should be caught in submissions client factory. */ - addRecord({ record, created, submissionId }) { + addRecord({ record, created, submissionId }: SubmissionRecord): void { // First pass, create object with { [fieldId]: question } from // decryptedContent to get all the questions. const fieldRecords = record.map((content) => { const fieldRecord = getResponseInstance(content) if (!fieldRecord.isHeader) { const currentMapping = this.fieldIdToQuestion.get(fieldRecord.id) - // Only set new mapping if it does not exist or this record is a later // submission. // Might need to differentiate the question headers if we allow @@ -77,28 +96,16 @@ class CsvMergedHeadersGenerator extends CsvGenerator { }) } - /** - * Extracts the string representation from an unprocessed record. - * @param {Object} unprocessedRecord - * @param {string} fieldId - * @returns {string} - */ - _extractAnswer(unprocessedRecord, fieldId, colIndex) { - const fieldRecord = unprocessedRecord[fieldId] - if (!fieldRecord) return '' - return fieldRecord.getAnswer(colIndex) - } - /** * Process the unprocessed records by creating the correct headers and * assigning each answer to their respective locations in each response row in * the csv data. */ - process() { + process(): void { if (this.hasBeenProcessed) return // Create a header row in CSV using the fieldIdToQuestion map. - let headers = ['Reference number', 'Timestamp'] + const headers = ['Reference number', 'Timestamp'] this.fieldIdToQuestion.forEach((value, fieldId) => { for (let i = 0; i < this.fieldIdToNumCols[fieldId]; i++) { headers.push(value.question) @@ -109,26 +116,44 @@ class CsvMergedHeadersGenerator extends CsvGenerator { // Craft a new csv row for each unprocessed record // O(qn), where q = number of unique questions, n = number of submissions. this.unprocessed.forEach((up) => { - let createdAt = moment(up.created).tz('Asia/Singapore') - createdAt = createdAt.isValid() + const createdAt = moment(up.created).tz('Asia/Singapore') + const formattedDate = createdAt.isValid() ? createdAt.format('DD MMM YYYY hh:mm:ss A') - : createdAt - let row = [up.submissionId, createdAt] - for (let [fieldId] of this.fieldIdToQuestion) { + : createdAt.toString() // just convert to string if given date is not valid + const row = [up.submissionId, formattedDate] + + this.fieldIdToQuestion.forEach((_question, fieldId) => { const numCols = this.fieldIdToNumCols[fieldId] for (let colIndex = 0; colIndex < numCols; colIndex++) { row.push(this._extractAnswer(up.record, fieldId, colIndex)) } - } + }) this.addLine(row) }) this.hasBeenProcessed = true } + /** + * Extracts the string representation from a field response + * @param unprocessedRecord + * @param fieldId + * @param colIndex + * @returns string representation of unprocessed record + */ + private _extractAnswer( + unprocessedRecord: KeyedResponse, + fieldId: string, + colIndex: number, + ): string { + const fieldRecord = unprocessedRecord[fieldId] + if (!fieldRecord) return '' + return fieldRecord.getAnswer(colIndex) + } + /** * Sorts unprocessed records from oldest to newest */ - sort() { + sort(): void { if (this.hasBeenSorted) return this.unprocessed.sort((a, b) => this._dateComparator(a.created, b.created)) this.hasBeenSorted = true @@ -138,8 +163,8 @@ class CsvMergedHeadersGenerator extends CsvGenerator { * Add meta-data as first three rows of the CSV. If there is already meta-data * added, it will be replaced by the latest counts. */ - addMetaDataFromSubmission(errorCount, unverifiedCount) { - let metaDataRows = [ + addMetaDataFromSubmission(errorCount: number, unverifiedCount: number): void { + const metaDataRows = [ ['Expected total responses', this.expectedNumberOfRecords], ['Success count', this.length()], ['Error count', errorCount], @@ -151,9 +176,9 @@ class CsvMergedHeadersGenerator extends CsvGenerator { /** * Main method to call to retrieve a downloadable csv. - * @param {string} filename + * @param filename name of csv file */ - downloadCsv(filename) { + downloadCsv(filename: string): void { this.sort() this.process() this.triggerFileDownload(filename) @@ -161,10 +186,10 @@ class CsvMergedHeadersGenerator extends CsvGenerator { /** * Comparator for dates - * @param string firstDate - * @param string secondDate + * @param firstDate + * @param secondDate */ - _dateComparator(firstDate, secondDate) { + private _dateComparator(firstDate: string, secondDate: string): number { // cast to Asia/Singapore to ensure both dates are of the same timezone const first = moment(firstDate).tz('Asia/Singapore') const second = moment(secondDate).tz('Asia/Singapore') @@ -178,5 +203,3 @@ class CsvMergedHeadersGenerator extends CsvGenerator { } } } - -module.exports = CsvMergedHeadersGenerator diff --git a/src/public/modules/forms/helpers/csv-response-classes/ArrayAnswerResponse.class.js b/src/public/modules/forms/helpers/csv-response-classes/ArrayAnswerResponse.class.js deleted file mode 100644 index 5b06db10ad..0000000000 --- a/src/public/modules/forms/helpers/csv-response-classes/ArrayAnswerResponse.class.js +++ /dev/null @@ -1,11 +0,0 @@ -const Response = require('./Response.class') - -module.exports = class ArrayAnswerResponse extends Response { - getAnswer() { - return this._data.answerArray.join(';') - } - - get numCols() { - return 1 - } -} diff --git a/src/public/modules/forms/helpers/csv-response-classes/ArrayAnswerResponse.class.ts b/src/public/modules/forms/helpers/csv-response-classes/ArrayAnswerResponse.class.ts new file mode 100644 index 0000000000..f77e486541 --- /dev/null +++ b/src/public/modules/forms/helpers/csv-response-classes/ArrayAnswerResponse.class.ts @@ -0,0 +1,20 @@ +import { ArrayResponse } from '../../../../../types/response' + +import { Response } from './Response.class' + +export class ArrayAnswerResponse extends Response { + private response: ArrayResponse + + constructor(responseData: ArrayResponse) { + super(responseData) + this.response = responseData + } + + getAnswer(): string { + return this.response.answerArray.join(';') + } + + get numCols(): number { + return 1 + } +} diff --git a/src/public/modules/forms/helpers/csv-response-classes/Response.class.js b/src/public/modules/forms/helpers/csv-response-classes/Response.class.js deleted file mode 100644 index c10cb850f4..0000000000 --- a/src/public/modules/forms/helpers/csv-response-classes/Response.class.js +++ /dev/null @@ -1,21 +0,0 @@ -module.exports = class Response { - constructor(responseData) { - this._data = responseData - } - - get id() { - return this._data._id - } - - /** - * Gets the CSV header. - * @returns {string} - */ - get question() { - return this._data.question - } - - get isHeader() { - return this._data.isHeader - } -} diff --git a/src/public/modules/forms/helpers/csv-response-classes/Response.class.ts b/src/public/modules/forms/helpers/csv-response-classes/Response.class.ts new file mode 100644 index 0000000000..9f2c1d62bc --- /dev/null +++ b/src/public/modules/forms/helpers/csv-response-classes/Response.class.ts @@ -0,0 +1,29 @@ +import { DisplayedResponseWithoutAnswer } from '../../../../../types/response' + +export abstract class Response { + private data: DisplayedResponseWithoutAnswer + + constructor(responseData: DisplayedResponseWithoutAnswer) { + this.data = responseData + } + + get id(): string { + return this.data._id + } + + /** + * Gets the CSV header. + * @returns {string} + */ + get question(): string { + return this.data.question + } + + get isHeader(): boolean { + return this.data.isHeader ?? false + } + + abstract get numCols(): number + + abstract getAnswer(colIndex?: number): string +} diff --git a/src/public/modules/forms/helpers/csv-response-classes/SingleAnswerResponse.class.js b/src/public/modules/forms/helpers/csv-response-classes/SingleAnswerResponse.class.js deleted file mode 100644 index fecb3d1d7e..0000000000 --- a/src/public/modules/forms/helpers/csv-response-classes/SingleAnswerResponse.class.js +++ /dev/null @@ -1,11 +0,0 @@ -const Response = require('./Response.class') - -module.exports = class SingleAnswerResponse extends Response { - getAnswer() { - return this._data.answer - } - - get numCols() { - return 1 - } -} diff --git a/src/public/modules/forms/helpers/csv-response-classes/SingleAnswerResponse.class.ts b/src/public/modules/forms/helpers/csv-response-classes/SingleAnswerResponse.class.ts new file mode 100644 index 0000000000..f495544e3f --- /dev/null +++ b/src/public/modules/forms/helpers/csv-response-classes/SingleAnswerResponse.class.ts @@ -0,0 +1,20 @@ +import { SingleResponse } from '../../../../../types/response' + +import { Response } from './Response.class' + +export class SingleAnswerResponse extends Response { + private response: SingleResponse + + constructor(responseData: SingleResponse) { + super(responseData) + this.response = responseData + } + + getAnswer(): string { + return this.response.answer + } + + get numCols(): number { + return 1 + } +} diff --git a/src/public/modules/forms/helpers/csv-response-classes/TableResponse.class.js b/src/public/modules/forms/helpers/csv-response-classes/TableResponse.class.js deleted file mode 100644 index 4352169a40..0000000000 --- a/src/public/modules/forms/helpers/csv-response-classes/TableResponse.class.js +++ /dev/null @@ -1,15 +0,0 @@ -const Response = require('./Response.class') - -module.exports = class TableResponse extends Response { - getAnswer(colIndex) { - // Leave cell empty if number of rows is fewer than the index - if (colIndex >= this._data.answerArray.length) { - return '' - } - return this._data.answerArray[colIndex].join(';') - } - - get numCols() { - return this._data.answerArray.length - } -} diff --git a/src/public/modules/forms/helpers/csv-response-classes/TableResponse.class.ts b/src/public/modules/forms/helpers/csv-response-classes/TableResponse.class.ts new file mode 100644 index 0000000000..cd62ec6b24 --- /dev/null +++ b/src/public/modules/forms/helpers/csv-response-classes/TableResponse.class.ts @@ -0,0 +1,24 @@ +import { NestedResponse } from '../../../../../types/response' + +import { Response } from './Response.class' + +export class TableResponse extends Response { + private response: NestedResponse + + constructor(responseData: NestedResponse) { + super(responseData) + this.response = responseData + } + + getAnswer(colIndex: number): string { + // Leave cell empty if number of rows is fewer than the index + if (colIndex >= this.response.answerArray.length) { + return '' + } + return this.response.answerArray[colIndex].join(';') + } + + get numCols(): number { + return this.response.answerArray.length + } +} diff --git a/src/public/modules/forms/helpers/csv-response-classes/index.js b/src/public/modules/forms/helpers/csv-response-classes/index.js deleted file mode 100644 index 837b2c4d38..0000000000 --- a/src/public/modules/forms/helpers/csv-response-classes/index.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports.SingleAnswerResponse = require('./SingleAnswerResponse.class') -module.exports.ArrayAnswerResponse = require('./ArrayAnswerResponse.class') -module.exports.TableResponse = require('./TableResponse.class') diff --git a/src/public/modules/forms/helpers/csv-response-classes/index.ts b/src/public/modules/forms/helpers/csv-response-classes/index.ts new file mode 100644 index 0000000000..b3ac7f159c --- /dev/null +++ b/src/public/modules/forms/helpers/csv-response-classes/index.ts @@ -0,0 +1,4 @@ +export { SingleAnswerResponse } from './SingleAnswerResponse.class' +export { ArrayAnswerResponse } from './ArrayAnswerResponse.class' +export { TableResponse } from './TableResponse.class' +export { Response } from './Response.class' diff --git a/src/public/modules/forms/helpers/response-factory.js b/src/public/modules/forms/helpers/response-factory.js deleted file mode 100644 index e78ad6e820..0000000000 --- a/src/public/modules/forms/helpers/response-factory.js +++ /dev/null @@ -1,18 +0,0 @@ -const { - SingleAnswerResponse, - ArrayAnswerResponse, - TableResponse, -} = require('./csv-response-classes') - -const getResponseInstance = (fieldRecordData) => { - switch (fieldRecordData.fieldType) { - case 'table': - return new TableResponse(fieldRecordData) - case 'checkbox': - return new ArrayAnswerResponse(fieldRecordData) - default: - return new SingleAnswerResponse(fieldRecordData) - } -} - -module.exports = { getResponseInstance } diff --git a/src/public/modules/forms/helpers/response-factory.ts b/src/public/modules/forms/helpers/response-factory.ts new file mode 100644 index 0000000000..3266101d50 --- /dev/null +++ b/src/public/modules/forms/helpers/response-factory.ts @@ -0,0 +1,78 @@ +import { hasProp } from '../../../../shared/util/has-prop' +import { BasicField } from '../../../../types/field' +import { + ArrayResponse, + DisplayedResponseWithoutAnswer, + NestedResponse, + SingleResponse, +} from '../../../../types/response' + +import { + ArrayAnswerResponse, + Response, + SingleAnswerResponse, + TableResponse, +} from './csv-response-classes' + +/** + * Converts a field record into a custom response instance + * @param fieldRecordData Field record + * @returns Response instance + * @throws Error when data does not fit any known response type + */ +export const getResponseInstance = ( + fieldRecordData: DisplayedResponseWithoutAnswer, +): Response => { + if ( + isNestedResponse(fieldRecordData) && + fieldRecordData.fieldType === BasicField.Table + ) { + return new TableResponse(fieldRecordData) + } else if ( + isArrayResponse(fieldRecordData) && + fieldRecordData.fieldType === BasicField.Checkbox + ) { + return new ArrayAnswerResponse(fieldRecordData) + } else if (isSingleResponse(fieldRecordData)) { + return new SingleAnswerResponse(fieldRecordData) + } else { + // eslint-disable-next-line typesafe/no-throw-sync-func + throw new Error('Response did not match any known type') + } +} + +const isNestedResponse = ( + response: DisplayedResponseWithoutAnswer, +): response is NestedResponse => { + return ( + hasAnswerArray(response) && + response.answerArray.every( + (arr) => + Array.isArray(arr) && + arr.every((value: unknown) => typeof value === 'string'), + ) + ) +} + +const isArrayResponse = ( + response: DisplayedResponseWithoutAnswer, +): response is ArrayResponse => { + return ( + hasAnswerArray(response) && + response.answerArray.every((value) => typeof value === 'string') + ) +} + +const isSingleResponse = ( + response: DisplayedResponseWithoutAnswer, +): response is SingleResponse => { + return hasProp(response, 'answer') && typeof response.answer === 'string' +} + +type AnswerArrayObject = { + answerArray: Array +} + +const hasAnswerArray = (response: unknown): response is AnswerArrayObject => { + return hasProp(response, 'answerArray') && Array.isArray(response.answerArray) +} diff --git a/src/public/modules/forms/services/submissions.client.factory.js b/src/public/modules/forms/services/submissions.client.factory.js index d0551a0a97..d04fae5570 100644 --- a/src/public/modules/forms/services/submissions.client.factory.js +++ b/src/public/modules/forms/services/submissions.client.factory.js @@ -1,6 +1,8 @@ 'use strict' -const CsvMHGenerator = require('../helpers/CsvMergedHeadersGenerator') +const { + CsvMergedHeadersGenerator, +} = require('../helpers/CsvMergedHeadersGenerator') const DecryptionWorker = require('../helpers/decryption.worker.js') const { fixParamsToUrl, triggerFileDownload } = require('../helpers/util') const { ndjsonStream } = require('../helpers/ndjsonStream') @@ -96,7 +98,7 @@ function SubmissionsFactory($q, $timeout, $window, GTag) { } let resUrl = generateDownloadUrl(params, downloadAttachments) - let experimentalCsvGenerator = new CsvMHGenerator( + let csvGenerator = new CsvMergedHeadersGenerator( expectedNumResponses, NUM_OF_METADATA_ROWS, ) @@ -136,8 +138,13 @@ function SubmissionsFactory($q, $timeout, $window, GTag) { } if (csvRecord.submissionData) { - // accumulate dataset if it exists, since we may have status columns available - experimentalCsvGenerator.addRecord(csvRecord.submissionData) + try { + // accumulate dataset if it exists, since we may have status columns available + csvGenerator.addRecord(csvRecord.submissionData) + } catch (error) { + errorCount++ + console.error('Error in getResponseInstance', error) + } } if (downloadAttachments && csvRecord.downloadBlob) { @@ -230,7 +237,7 @@ function SubmissionsFactory($q, $timeout, $window, GTag) { GTag.partialDecryptionFailure( params, numWorkers, - experimentalCsvGenerator.length(), + csvGenerator.length(), errorCount, attachmentErrorCount, timeDifference, @@ -240,7 +247,7 @@ function SubmissionsFactory($q, $timeout, $window, GTag) { new Error( JSON.stringify({ expectedCount: expectedNumResponses, - successCount: experimentalCsvGenerator.length(), + successCount: csvGenerator.length(), errorCount, unverifiedCount, }), @@ -248,18 +255,16 @@ function SubmissionsFactory($q, $timeout, $window, GTag) { ) } else if ( // All results have been decrypted - experimentalCsvGenerator.length() + - errorCount + - unverifiedCount >= + csvGenerator.length() + errorCount + unverifiedCount >= expectedNumResponses ) { killWorkers(workerPool) // Generate first three rows of meta-data before download - experimentalCsvGenerator.addMetaDataFromSubmission( + csvGenerator.addMetaDataFromSubmission( errorCount, unverifiedCount, ) - experimentalCsvGenerator.downloadCsv( + csvGenerator.downloadCsv( `${params.formTitle}-${params.formId}.csv`, ) @@ -271,18 +276,18 @@ function SubmissionsFactory($q, $timeout, $window, GTag) { GTag.downloadResponseSuccess( params, numWorkers, - experimentalCsvGenerator.length(), + csvGenerator.length(), timeDifference, ) resolve({ expectedCount: expectedNumResponses, - successCount: experimentalCsvGenerator.length(), + successCount: csvGenerator.length(), errorCount, unverifiedCount, }) // Kill class instance and reclaim the memory. - experimentalCsvGenerator = null + csvGenerator = null } else { $timeout(checkComplete, 100) } diff --git a/src/app/utils/has-prop.ts b/src/shared/util/has-prop.ts similarity index 100% rename from src/app/utils/has-prop.ts rename to src/shared/util/has-prop.ts diff --git a/src/types/response/index.ts b/src/types/response/index.ts index 06c9e6ceb6..a9b75836d1 100644 --- a/src/types/response/index.ts +++ b/src/types/response/index.ts @@ -56,3 +56,21 @@ export interface IClientEncryptSubmission extends IClientSubmission { encryptedContent: string version: number } + +export type DisplayedResponseWithoutAnswer = { + _id: string + question: string + fieldType: string + isHeader?: boolean +} + +export type ArrayResponse = DisplayedResponseWithoutAnswer & { + answerArray: string[] +} + +export type NestedResponse = DisplayedResponseWithoutAnswer & { + answerArray: string[][] +} +export type SingleResponse = DisplayedResponseWithoutAnswer & { + answer: string +} diff --git a/tests/unit/frontend/forms/helpers/CsvMergedHeadersGenerator.test.js b/tests/unit/frontend/forms/helpers/CsvMergedHeadersGenerator.test.js index ab72ebb532..11c0564a72 100644 --- a/tests/unit/frontend/forms/helpers/CsvMergedHeadersGenerator.test.js +++ b/tests/unit/frontend/forms/helpers/CsvMergedHeadersGenerator.test.js @@ -1,7 +1,7 @@ import { stringify } from 'csv-string' import moment from 'moment-timezone' - -import CsvMergedHeadersGenerator from '../../../../../src/public/modules/forms/helpers/CsvMergedHeadersGenerator' +import { getResponseInstance } from '../../../../../src/public/modules/forms/helpers/response-factory' +import { CsvMergedHeadersGenerator } from '../../../../../src/public/modules/forms/helpers/CsvMergedHeadersGenerator' const UTF8_BYTE_ORDER_MARK = '\uFEFF' const BOM_LENGTH = 1 @@ -33,9 +33,20 @@ const generateHeaderRow = (append) => { question: `mockQuestion${append}`, fieldType: `mockFieldType${append}`, isHeader: true, + answer: '', + } +} + +const generateEmptyRecord = (append) => { + return { + _id: `mock${append}`, + question: `mockQuestion${append}`, + fieldType: `mockFieldType${append}`, } } +const expectedErrorMessage = 'Response did not match any known type' + /** * Reshapes a mock record to match the expected shape in generator.unprocessed. * @param {Object} mockRecord @@ -46,7 +57,7 @@ const generateHeaderRow = (append) => { const generateExpectedUnprocessed = (mockRecord) => { const reshapedRecord = {} mockRecord.record.forEach((fieldRecord) => { - reshapedRecord[fieldRecord._id] = { _data: fieldRecord } + reshapedRecord[fieldRecord._id] = getResponseInstance(fieldRecord) }) return { created: mockRecord.created, @@ -238,6 +249,79 @@ describe('CsvMergedHeadersGenerator', () => { expectedQuestionHeader, ) }) + + it('should reject response without answer and answerArray', () => { + // Arrange + const mockDecryptedRecord = [generateEmptyRecord(1)] + const mockRecord = { + record: mockDecryptedRecord, + created: mockCreatedEarly, + submissionId: 'mockSubmissionId', + } + expect(generator.unprocessed.length).toEqual(0) + + // Act + const addRecord = () => generator.addRecord(mockRecord) + + // Assert + // Check error + expect(addRecord).toThrow(Error) + expect(addRecord).toThrow(expectedErrorMessage) + // Record should not be added + expect(generator.unprocessed.length).toEqual(0) + // Check headers + expect(generator.fieldIdToQuestion.size).toEqual(0) + }) + + it('should reject response with non-string answer', () => { + // Arrange + const invalidResponse = generateEmptyRecord(1) + invalidResponse.answer = 1 + const mockDecryptedRecord = [invalidResponse] + const mockRecord = { + record: mockDecryptedRecord, + created: mockCreatedEarly, + submissionId: 'mockSubmissionId', + } + expect(generator.unprocessed.length).toEqual(0) + + // Act + const addRecord = () => generator.addRecord(mockRecord) + + // Assert + // Check error + expect(addRecord).toThrow(Error) + expect(addRecord).toThrow(expectedErrorMessage) + // Record should not be added + expect(generator.unprocessed.length).toEqual(0) + // Check headers + expect(generator.fieldIdToQuestion.size).toEqual(0) + }) + + it('should reject response with non-string answerArray', () => { + // Arrange + const invalidResponse = generateEmptyRecord(1) + invalidResponse.answerArray = [1, 2, 3] + const mockDecryptedRecord = [invalidResponse] + const mockRecord = { + record: mockDecryptedRecord, + created: mockCreatedEarly, + submissionId: 'mockSubmissionId', + } + expect(generator.unprocessed.length).toEqual(0) + + // Act + const addRecord = () => generator.addRecord(mockRecord) + + // Assert + // Check error + expect(addRecord).toThrow(Error) + expect(addRecord).toThrow(expectedErrorMessage) + // Record should not be added + expect(generator.unprocessed.length).toEqual(0) + // Check headers + expect(generator.fieldIdToQuestion.size).toEqual(0) + }) }) describe('process()', () => { From 89af07f950f8fee915fd92e8a88356c01067d2f4 Mon Sep 17 00:00:00 2001 From: Antariksh Mahajan Date: Tue, 22 Jun 2021 19:11:25 +0800 Subject: [PATCH 05/21] feat(feature-manager): remove sms from feature manager (#2218) * feat(feature-manager): remove sms from feature manager * build: add mock defaults for Twilio in dev environment --- docker-compose.yml | 9 +- src/app/config/feature-manager/index.ts | 2 - src/app/config/feature-manager/sms.config.ts | 64 ++--- src/app/config/feature-manager/types.ts | 9 - .../sms/__tests__/sms.factory.spec.ts | 219 ++++++------------ src/app/services/sms/sms.factory.ts | 28 +-- 6 files changed, 117 insertions(+), 214 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index dc23191f8a..d7df8bcfd2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -55,10 +55,11 @@ services: # Keep in sync with the development key in # https://github.com/opengovsg/formsg-javascript-sdk/blob/develop/src/resource/signing-keys.ts - SIGNING_SECRET_KEY=HDBXpu+2/gu10bLHpy8HjpN89xbA6boH9GwibPGJA8BOXmB+zOUpxCP33/S5p8vBWlPokC7gLR0ca8urVwfMUQ== - - TWILIO_ACCOUNT_SID - - TWILIO_API_KEY - - TWILIO_API_SECRET - - TWILIO_MESSAGING_SERVICE_SID + # Mock Twilio credentials. SMSes do not work in dev environment. + - TWILIO_ACCOUNT_SID=ACmockTwilioAccountSid + - TWILIO_API_KEY=mockTwilioApiKey + - TWILIO_API_SECRET=mockTwilioApiSecret + - TWILIO_MESSAGING_SERVICE_SID=mockTwilioMsgSrvcSid - SES_PASS - SES_USER - OTP_LIFE_SPAN diff --git a/src/app/config/feature-manager/index.ts b/src/app/config/feature-manager/index.ts index 927a1a5dc9..c7328805b1 100644 --- a/src/app/config/feature-manager/index.ts +++ b/src/app/config/feature-manager/index.ts @@ -1,5 +1,4 @@ import FeatureManager from './util/FeatureManager.class' -import sms from './sms.config' import spcpMyInfo from './spcp-myinfo.config' export * from './types' @@ -8,6 +7,5 @@ const featureManager = new FeatureManager() // Register features and associated middleware/fallbacks featureManager.register(spcpMyInfo) -featureManager.register(sms) export default featureManager diff --git a/src/app/config/feature-manager/sms.config.ts b/src/app/config/feature-manager/sms.config.ts index e5a8546df7..10a0804b2c 100644 --- a/src/app/config/feature-manager/sms.config.ts +++ b/src/app/config/feature-manager/sms.config.ts @@ -1,33 +1,39 @@ -import { FeatureNames, RegisterableFeature } from './types' +import convict, { Schema } from 'convict' -const smsFeature: RegisterableFeature = { - name: FeatureNames.Sms, - schema: { - twilioAccountSid: { - doc: 'Twilio messaging ID', - format: String, - default: null, - env: 'TWILIO_ACCOUNT_SID', - }, - twilioApiKey: { - doc: 'Twilio standard API Key', - format: String, - default: null, - env: 'TWILIO_API_KEY', - }, - twilioApiSecret: { - doc: 'Twilio API Secret', - format: String, - default: null, - env: 'TWILIO_API_SECRET', - }, - twilioMsgSrvcSid: { - doc: 'Messaging service ID', - format: String, - default: null, - env: 'TWILIO_MESSAGING_SERVICE_SID', - }, +export interface ISms { + twilioAccountSid: string + twilioApiKey: string + twilioApiSecret: string + twilioMsgSrvcSid: string +} + +const smsSchema: Schema = { + twilioAccountSid: { + doc: 'Twilio messaging ID', + format: String, + default: null, + env: 'TWILIO_ACCOUNT_SID', + }, + twilioApiKey: { + doc: 'Twilio standard API Key', + format: String, + default: null, + env: 'TWILIO_API_KEY', + }, + twilioApiSecret: { + doc: 'Twilio API Secret', + format: String, + default: null, + env: 'TWILIO_API_SECRET', + }, + twilioMsgSrvcSid: { + doc: 'Messaging service ID', + format: String, + default: null, + env: 'TWILIO_MESSAGING_SERVICE_SID', }, } -export default smsFeature +export const smsConfig = convict(smsSchema) + .validate({ allowed: 'strict' }) + .getProperties() diff --git a/src/app/config/feature-manager/types.ts b/src/app/config/feature-manager/types.ts index 2aca8329df..b0939a04b9 100644 --- a/src/app/config/feature-manager/types.ts +++ b/src/app/config/feature-manager/types.ts @@ -2,17 +2,9 @@ import { MyInfoMode } from '@opengovsg/myinfo-gov-client' import { Schema } from 'convict' export enum FeatureNames { - Sms = 'sms', SpcpMyInfo = 'spcp-myinfo', } -export interface ISms { - twilioAccountSid: string - twilioApiKey: string - twilioApiSecret: string - twilioMsgSrvcSid: string -} - export interface ISpcpConfig { isSPMaintenance: string isCPMaintenance: string @@ -49,7 +41,6 @@ export interface IMyInfoConfig { export type ISpcpMyInfo = ISpcpConfig & IMyInfoConfig export interface IFeatureManager { - [FeatureNames.Sms]: ISms [FeatureNames.SpcpMyInfo]: ISpcpMyInfo } diff --git a/src/app/services/sms/__tests__/sms.factory.spec.ts b/src/app/services/sms/__tests__/sms.factory.spec.ts index 7927f985b2..75f5f71547 100644 --- a/src/app/services/sms/__tests__/sms.factory.spec.ts +++ b/src/app/services/sms/__tests__/sms.factory.spec.ts @@ -3,12 +3,7 @@ import { okAsync } from 'neverthrow' import { mocked } from 'ts-jest/utils' import Twilio from 'twilio' -import { - FeatureNames, - ISms, - RegisteredFeature, -} from 'src/app/config/feature-manager' -import { MissingFeatureError } from 'src/app/modules/core/core.errors' +import { ISms } from 'src/app/config/feature-manager/sms.config' import { createSmsFactory } from '../sms.factory' import * as SmsService from '../sms.service' @@ -40,147 +35,79 @@ const MOCK_BOUNCE_SMS_PARAMS: BounceNotificationSmsParams = { describe('sms.factory', () => { beforeEach(() => jest.clearAllMocks()) - describe('sms feature disabled', () => { - const MOCK_DISABLED_SMS_FEATURE: RegisteredFeature = { - isEnabled: false, - props: {} as ISms, - } - - const SmsFactory = createSmsFactory(MOCK_DISABLED_SMS_FEATURE) - - it('should return MissingFeatureError when invoking sendAdminContactOtp', async () => { - // Act - const result = await SmsFactory.sendAdminContactOtp( - 'anything', - 'anything', - 'anything', - ) - - // Assert - expect(result._unsafeUnwrapErr()).toEqual( - new MissingFeatureError(FeatureNames.Sms), - ) - }) - - it('should return MissingFeatureError when invoking sendVerificationOtp', async () => { - // Act - const result = await SmsFactory.sendVerificationOtp( - 'anything', - 'anything', - 'anything', - ) - - // Assert - expect(result._unsafeUnwrapErr()).toEqual( - new MissingFeatureError(FeatureNames.Sms), - ) - }) - - it('should return MissingFeatureError when invoking sendFormDeactivatedSms', async () => { - // Act - const result = await SmsFactory.sendFormDeactivatedSms( - MOCK_BOUNCE_SMS_PARAMS, - ) - - // Assert - expect(result._unsafeUnwrapErr()).toEqual( - new MissingFeatureError(FeatureNames.Sms), - ) - }) - - it('should return MissingFeatureError when invoking sendBouncedSubmissionSms', async () => { - // Act - const result = await SmsFactory.sendBouncedSubmissionSms( - MOCK_BOUNCE_SMS_PARAMS, - ) - - // Assert - expect(result._unsafeUnwrapErr()).toEqual( - new MissingFeatureError(FeatureNames.Sms), - ) - }) + const MOCK_SMS_FEATURE: ISms = { + twilioAccountSid: 'ACrandomTwilioSid', + twilioApiKey: 'SKrandomTwilioAPIKEY', + twilioApiSecret: 'this is a super secret', + twilioMsgSrvcSid: 'formsg-is-great-pleasehelpme', + } + const expectedTwilioConfig: TwilioConfig = { + msgSrvcSid: MOCK_SMS_FEATURE.twilioMsgSrvcSid, + client: MOCKED_TWILIO, + } + const SmsFactory = createSmsFactory(MOCK_SMS_FEATURE) + + it('should call SmsService counterpart when invoking sendAdminContactOtp', async () => { + // Arrange + MockSmsService.sendAdminContactOtp.mockReturnValueOnce(okAsync(true)) + + const mockArguments: Parameters = [ + 'mockRecipient', + 'mockOtp', + 'mockFormId', + ] + + // Act + await SmsFactory.sendAdminContactOtp(...mockArguments) + + // Assert + expect(MockSmsService.sendAdminContactOtp).toHaveBeenCalledTimes(1) + expect(MockSmsService.sendAdminContactOtp).toHaveBeenCalledWith( + ...mockArguments, + expectedTwilioConfig, + ) }) - describe('sms feature enabled', () => { - const MOCK_ENABLED_SMS_FEATURE: Required< - RegisteredFeature - > = { - isEnabled: true, - props: { - twilioAccountSid: 'ACrandomTwilioSid', - twilioApiKey: 'SKrandomTwilioAPIKEY', - twilioApiSecret: 'this is a super secret', - twilioMsgSrvcSid: 'formsg-is-great-pleasehelpme', - }, - } - const expectedTwilioConfig: TwilioConfig = { - msgSrvcSid: MOCK_ENABLED_SMS_FEATURE.props.twilioMsgSrvcSid, - client: MOCKED_TWILIO, - } - const SmsFactory = createSmsFactory(MOCK_ENABLED_SMS_FEATURE) - - it('should call SmsService counterpart when invoking sendAdminContactOtp', async () => { - // Arrange - MockSmsService.sendAdminContactOtp.mockReturnValueOnce(okAsync(true)) - - const mockArguments: Parameters = [ - 'mockRecipient', - 'mockOtp', - 'mockFormId', - ] - - // Act - await SmsFactory.sendAdminContactOtp(...mockArguments) - - // Assert - expect(MockSmsService.sendAdminContactOtp).toHaveBeenCalledTimes(1) - expect(MockSmsService.sendAdminContactOtp).toHaveBeenCalledWith( - ...mockArguments, - expectedTwilioConfig, - ) - }) - - it('should call SmsService counterpart when invoking sendVerificationOtp', async () => { - // Arrange - MockSmsService.sendVerificationOtp.mockResolvedValue(true) - - const mockArguments: Parameters = [ - 'mockRecipient', - 'mockOtp', - 'mockUserId', - ] - - // Act - await SmsFactory.sendVerificationOtp(...mockArguments) - - // Assert - expect(MockSmsService.sendVerificationOtp).toHaveBeenCalledTimes(1) - expect(MockSmsService.sendVerificationOtp).toHaveBeenCalledWith( - ...mockArguments, - expectedTwilioConfig, - ) - }) - - it('should call SmsService when sendFormDeactivatedSms is called', async () => { - MockSmsService.sendFormDeactivatedSms.mockResolvedValueOnce(true) - - await SmsFactory.sendFormDeactivatedSms(MOCK_BOUNCE_SMS_PARAMS) - - expect(MockSmsService.sendFormDeactivatedSms).toHaveBeenCalledWith( - MOCK_BOUNCE_SMS_PARAMS, - expectedTwilioConfig, - ) - }) - - it('should call SmsService when sendBouncedSubmissionSms is called', async () => { - MockSmsService.sendBouncedSubmissionSms.mockResolvedValueOnce(true) - - await SmsFactory.sendBouncedSubmissionSms(MOCK_BOUNCE_SMS_PARAMS) - - expect(MockSmsService.sendBouncedSubmissionSms).toHaveBeenCalledWith( - MOCK_BOUNCE_SMS_PARAMS, - expectedTwilioConfig, - ) - }) + it('should call SmsService counterpart when invoking sendVerificationOtp', async () => { + // Arrange + MockSmsService.sendVerificationOtp.mockReturnValue(okAsync(true)) + + const mockArguments: Parameters = [ + 'mockRecipient', + 'mockOtp', + 'mockUserId', + ] + + // Act + await SmsFactory.sendVerificationOtp(...mockArguments) + + // Assert + expect(MockSmsService.sendVerificationOtp).toHaveBeenCalledTimes(1) + expect(MockSmsService.sendVerificationOtp).toHaveBeenCalledWith( + ...mockArguments, + expectedTwilioConfig, + ) + }) + + it('should call SmsService when sendFormDeactivatedSms is called', async () => { + MockSmsService.sendFormDeactivatedSms.mockReturnValue(okAsync(true)) + + await SmsFactory.sendFormDeactivatedSms(MOCK_BOUNCE_SMS_PARAMS) + + expect(MockSmsService.sendFormDeactivatedSms).toHaveBeenCalledWith( + MOCK_BOUNCE_SMS_PARAMS, + expectedTwilioConfig, + ) + }) + + it('should call SmsService when sendBouncedSubmissionSms is called', async () => { + MockSmsService.sendBouncedSubmissionSms.mockReturnValue(okAsync(true)) + + await SmsFactory.sendBouncedSubmissionSms(MOCK_BOUNCE_SMS_PARAMS) + + expect(MockSmsService.sendBouncedSubmissionSms).toHaveBeenCalledWith( + MOCK_BOUNCE_SMS_PARAMS, + expectedTwilioConfig, + ) }) }) diff --git a/src/app/services/sms/sms.factory.ts b/src/app/services/sms/sms.factory.ts index 0af5748e39..6e95078bcc 100644 --- a/src/app/services/sms/sms.factory.ts +++ b/src/app/services/sms/sms.factory.ts @@ -1,11 +1,6 @@ -import { errAsync } from 'neverthrow' import Twilio from 'twilio' -import FeatureManager, { - FeatureNames, - RegisteredFeature, -} from '../../config/feature-manager' -import { MissingFeatureError } from '../../modules/core/core.errors' +import { ISms, smsConfig } from '../../config/feature-manager/sms.config' import { sendAdminContactOtp, @@ -56,25 +51,10 @@ interface ISmsFactory { ) => ReturnType } -const smsFeature = FeatureManager.get(FeatureNames.Sms) - // Exported for testing. -export const createSmsFactory = ( - smsFeature: RegisteredFeature, -): ISmsFactory => { - if (!smsFeature.isEnabled || !smsFeature.props) { - // Not enabled, return passthrough functions. - const error = new MissingFeatureError(FeatureNames.Sms) - return { - sendAdminContactOtp: () => errAsync(error), - sendVerificationOtp: () => errAsync(error), - sendFormDeactivatedSms: () => errAsync(error), - sendBouncedSubmissionSms: () => errAsync(error), - } - } - +export const createSmsFactory = (smsConfig: ISms): ISmsFactory => { const { twilioAccountSid, twilioApiKey, twilioApiSecret, twilioMsgSrvcSid } = - smsFeature.props + smsConfig const twilioClient = Twilio(twilioApiKey, twilioApiSecret, { accountSid: twilioAccountSid, @@ -96,4 +76,4 @@ export const createSmsFactory = ( } } -export const SmsFactory = createSmsFactory(smsFeature) +export const SmsFactory = createSmsFactory(smsConfig) From e6b163c8a938cd7e318db78e76615068ba903921 Mon Sep 17 00:00:00 2001 From: Antariksh Mahajan Date: Tue, 22 Jun 2021 19:49:43 +0800 Subject: [PATCH 06/21] feat(feature-manager): remove spcp-myinfo from feature manager (#2222) * feat(feature-manager): remove spcp-myinfo from feature manager * build: populate Mockpass defaults for dev environment * fix: fix build errors * build: empty commit to trigger build --- docker-compose.yml | 47 +-- src/app/config/feature-manager/index.ts | 4 - .../feature-manager/spcp-myinfo.config.ts | 375 ++++++++++-------- src/app/config/feature-manager/types.ts | 44 +- src/app/loaders/express/locals.ts | 21 +- .../billing/__tests__/billing.factory.spec.ts | 123 ------ .../billing/__tests__/billing.routes.spec.ts | 4 +- src/app/modules/billing/billing.controller.ts | 4 +- src/app/modules/billing/billing.factory.ts | 42 -- src/app/modules/billing/billing.service.ts | 8 - .../__tests__/public-form.controller.spec.ts | 96 ++--- .../public-form/public-form.controller.ts | 22 +- .../__tests__/myinfo.controller.spec.ts | 108 ++--- .../myinfo/__tests__/myinfo.factory.spec.ts | 142 ------- .../myinfo/__tests__/myinfo.service.spec.ts | 67 ++-- src/app/modules/myinfo/myinfo.controller.ts | 20 +- src/app/modules/myinfo/myinfo.factory.ts | 128 ------ src/app/modules/myinfo/myinfo.service.ts | 14 +- src/app/modules/myinfo/myinfo.types.ts | 2 +- .../spcp/__tests__/spcp.controller.spec.ts | 298 +++++++------- .../spcp/__tests__/spcp.factory.spec.ts | 118 ------ .../spcp/__tests__/spcp.service.spec.ts | 236 +++++------ src/app/modules/spcp/spcp.controller.ts | 24 +- src/app/modules/spcp/spcp.factory.ts | 51 --- src/app/modules/spcp/spcp.service.ts | 14 +- .../email-submission.controller.ts | 18 +- .../encrypt-submission.controller.ts | 10 +- .../__tests__/billings.routes.spec.ts | 4 +- .../public-forms.auth.routes.spec.ts | 37 +- 29 files changed, 721 insertions(+), 1360 deletions(-) delete mode 100644 src/app/modules/billing/__tests__/billing.factory.spec.ts delete mode 100644 src/app/modules/billing/billing.factory.ts delete mode 100644 src/app/modules/myinfo/__tests__/myinfo.factory.spec.ts delete mode 100644 src/app/modules/myinfo/myinfo.factory.ts delete mode 100644 src/app/modules/spcp/__tests__/spcp.factory.spec.ts delete mode 100644 src/app/modules/spcp/spcp.factory.ts diff --git a/docker-compose.yml b/docker-compose.yml index d7df8bcfd2..6a5672fcb5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -33,11 +33,6 @@ services: - SEND_AUTH_OTP_RATE_LIMIT=60 - SES_PORT=25 - SES_HOST=maildev - - MYINFO_CLIENT_CONFIG=dev - - MYINFO_FORMSG_KEY_PATH=./node_modules/@opengovsg/mockpass/static/certs/key.pem - - MYINFO_CERT_PATH=./node_modules/@opengovsg/mockpass/static/certs/spcp.crt - - MYINFO_CLIENT_ID=mockClientId - - MYINFO_CLIENT_SECRET=mockClientSecret - WEBHOOK_SQS_URL=http://localhost:4566/000000000000/local-webhooks-sqs-main - INTRANET_IP_LIST_PATH - SENTRY_CONFIG_URL=https://random@sentry.io/123456 @@ -60,28 +55,34 @@ services: - TWILIO_API_KEY=mockTwilioApiKey - TWILIO_API_SECRET=mockTwilioApiSecret - TWILIO_MESSAGING_SERVICE_SID=mockTwilioMsgSrvcSid + # Use mockpass key pairs and endpoints + - SP_FORMSG_KEY_PATH=./node_modules/@opengovsg/mockpass/static/certs/key.pem + - SP_FORMSG_CERT_PATH=./node_modules/@opengovsg/mockpass/static/certs/server.crt + - SP_IDP_CERT_PATH=./node_modules/@opengovsg/mockpass/static/certs/spcp.crt + - SINGPASS_IDP_LOGIN_URL=http://localhost:5156/singpass/logininitial + - SINGPASS_IDP_ENDPOINT=http://localhost:5156/singpass/soap + - SINGPASS_ESRVC_ID=spEsrvcId + - SINGPASS_PARTNER_ENTITY_ID=https://localhost:5000/singpass + - SINGPASS_IDP_ID=https://saml-internet.singpass.gov.sg/FIM/sps/SingpassIDPFed/saml20 + - CP_FORMSG_KEY_PATH=./node_modules/@opengovsg/mockpass/static/certs/key.pem + - CP_FORMSG_CERT_PATH=./node_modules/@opengovsg/mockpass/static/certs/server.crt + - CP_IDP_CERT_PATH=./node_modules/@opengovsg/mockpass/static/certs/spcp.crt + - CORPPASS_IDP_LOGIN_URL=http://localhost:5156/corppass/logininitial + - CORPPASS_IDP_ENDPOINT=http://localhost:5156/corppass/soap + - CORPPASS_PARTNER_ENTITY_ID=https://localhost:5000/corppass + - CORPPASS_ESRVC_ID=cpEsrvcId + - CORPPASS_IDP_ID=https://saml.corppass.gov.sg/FIM/sps/CorpIDPFed/saml20 + - IS_SP_MAINTENANCE + - IS_CP_MAINTENANCE + - MYINFO_CLIENT_CONFIG=dev + - MYINFO_FORMSG_KEY_PATH=./node_modules/@opengovsg/mockpass/static/certs/key.pem + - MYINFO_CERT_PATH=./node_modules/@opengovsg/mockpass/static/certs/spcp.crt + - MYINFO_CLIENT_ID=mockClientId + - MYINFO_CLIENT_SECRET=mockClientSecret - SES_PASS - SES_USER - OTP_LIFE_SPAN - AWS_REGION - - SP_FORMSG_KEY_PATH - - SP_FORMSG_CERT_PATH - - SP_IDP_CERT_PATH - - SINGPASS_IDP_LOGIN_URL - - SINGPASS_IDP_ENDPOINT - - SINGPASS_ESRVC_ID - - SINGPASS_PARTNER_ENTITY_ID - - SINGPASS_IDP_ID - - CP_FORMSG_KEY_PATH - - CP_FORMSG_CERT_PATH - - CP_IDP_CERT_PATH - - CORPPASS_IDP_LOGIN_URL - - CORPPASS_IDP_ENDPOINT - - CORPPASS_PARTNER_ENTITY_ID - - CORPPASS_ESRVC_ID - - CORPPASS_IDP_ID - - IS_SP_MAINTENANCE - - IS_CP_MAINTENANCE mockpass: build: https://github.com/opengovsg/mockpass.git diff --git a/src/app/config/feature-manager/index.ts b/src/app/config/feature-manager/index.ts index c7328805b1..dac9542055 100644 --- a/src/app/config/feature-manager/index.ts +++ b/src/app/config/feature-manager/index.ts @@ -1,11 +1,7 @@ import FeatureManager from './util/FeatureManager.class' -import spcpMyInfo from './spcp-myinfo.config' export * from './types' const featureManager = new FeatureManager() -// Register features and associated middleware/fallbacks -featureManager.register(spcpMyInfo) - export default featureManager diff --git a/src/app/config/feature-manager/spcp-myinfo.config.ts b/src/app/config/feature-manager/spcp-myinfo.config.ts index dcdf6157a6..9346b98779 100644 --- a/src/app/config/feature-manager/spcp-myinfo.config.ts +++ b/src/app/config/feature-manager/spcp-myinfo.config.ts @@ -1,176 +1,215 @@ import { MyInfoMode } from '@opengovsg/myinfo-gov-client' - -import { FeatureNames, RegisterableFeature } from './types' +import convict, { Schema } from 'convict' +import { url } from 'convict-format-with-validator' const HOUR_IN_MILLIS = 1000 * 60 * 60 const DAY_IN_MILLIS = 24 * HOUR_IN_MILLIS -const spcpMyInfoFeature: RegisterableFeature = { - name: FeatureNames.SpcpMyInfo, - schema: { - isSPMaintenance: { - doc: 'If set, displays a banner message on SingPass forms. Overrides IS_CP_MAINTENANCE', - format: '*', - default: null, - env: 'IS_SP_MAINTENANCE', - }, - isCPMaintenance: { - doc: 'If set, displays a banner message on CorpPass forms', - format: '*', - default: null, - env: 'IS_CP_MAINTENANCE', - }, - spCookieMaxAge: { - doc: 'Max SingPass cookie age with remember me unchecked', - format: 'int', - default: 3 * HOUR_IN_MILLIS, - env: 'SP_COOKIE_MAX_AGE', - }, - spCookieMaxAgePreserved: { - doc: 'Max SingPass cookie age with remember me checked', - format: 'int', - default: 30 * DAY_IN_MILLIS, - env: 'SPCP_COOKIE_MAX_AGE_PRESERVED', - }, - spcpCookieDomain: { - doc: 'Domain name set on cookie that holds the SPCP jwt', - format: String, - default: '', - env: 'SPCP_COOKIE_DOMAIN', - }, - cpCookieMaxAge: { - doc: 'Max CorpPass cookie age', - format: 'int', - default: 6 * HOUR_IN_MILLIS, - env: 'CP_COOKIE_MAX_AGE', - }, - spIdpId: { - doc: 'Partner ID of National Digital Identity Office for SingPass authentication', - format: 'url', - default: null, - env: 'SINGPASS_IDP_ID', - }, - cpIdpId: { - doc: 'Partner ID of National Digital Identity Office for CorpPass authentication', - format: 'url', - default: null, - env: 'CORPPASS_IDP_ID', - }, - spPartnerEntityId: { - doc: 'Partner ID registered with National Digital Identity Office for SingPass authentication', - format: 'url', - default: null, - env: 'SINGPASS_PARTNER_ENTITY_ID', - }, - cpPartnerEntityId: { - doc: 'Partner ID registered with National Digital Identity Office for CorpPass authentication', - format: 'url', - default: null, - env: 'CORPPASS_PARTNER_ENTITY_ID', - }, - spIdpLoginUrl: { - doc: 'URL of SingPass Login Page', - format: 'url', - default: null, - env: 'SINGPASS_IDP_LOGIN_URL', - }, - cpIdpLoginUrl: { - doc: 'URL of CorpPass Login Page', - format: 'url', - default: null, - env: 'CORPPASS_IDP_LOGIN_URL', - }, - spIdpEndpoint: { - doc: 'URL to retrieve NRIC of SingPass-validated user from', - format: 'url', - default: null, - env: 'SINGPASS_IDP_ENDPOINT', - }, - cpIdpEndpoint: { - doc: 'URL to retrieve UEN of CorpPass-validated user from', - format: 'url', - default: null, - env: 'CORPPASS_IDP_ENDPOINT', - }, - spEsrvcId: { - doc: 'e-service ID registered with National Digital Identity office for SingPass authentication', - format: String, - default: null, - env: 'SINGPASS_ESRVC_ID', - }, - cpEsrvcId: { - doc: 'e-service ID registered with National Digital Identity office for CorpPass authentication', - format: String, - default: null, - env: 'CORPPASS_ESRVC_ID', - }, - spFormSgKeyPath: { - doc: 'Path to X.509 key used for SingPass related communication with National Digital Identity office', - format: String, - default: null, - env: 'SP_FORMSG_KEY_PATH', - }, - cpFormSgKeyPath: { - doc: 'Path to X.509 key used for CorpPass related communication with National Digital Identity office', - format: String, - default: null, - env: 'CP_FORMSG_KEY_PATH', - }, - spFormSgCertPath: { - doc: 'Path to X.509 cert used for SingPass related communication with National Digital Identity office', - format: String, - default: null, - env: 'SP_FORMSG_CERT_PATH', - }, - cpFormSgCertPath: { - doc: 'Path to X.509 cert used for CorpPass related communication with National Digital Identity office', - format: String, - default: null, - env: 'CP_FORMSG_CERT_PATH', - }, - spIdpCertPath: { - doc: 'Path to National Digital Identity offices X.509 cert used for SingPass related communication', - format: String, - default: null, - env: 'SP_IDP_CERT_PATH', - }, - cpIdpCertPath: { - doc: 'Path to National Digital Identity offices X.509 cert used for CorpPass related communication', - format: String, - default: null, - env: 'CP_IDP_CERT_PATH', - }, - myInfoClientMode: { - doc: 'Configures MyInfoGovClient. Set this to either `stg` or `prod` to fetch MyInfo data from the corresponding endpoints.', - format: Object.values(MyInfoMode), - default: MyInfoMode.Production, - env: 'MYINFO_CLIENT_CONFIG', - }, - myInfoKeyPath: { - doc: 'Filepath to MyInfo private key, which is used to decrypt data and sign requests when communicating with MyInfo.', - format: String, - default: null, - env: 'MYINFO_FORMSG_KEY_PATH', - }, - myInfoCertPath: { - doc: "Path to MyInfo's public certificate, which is used to verify their signature.", - format: String, - default: null, - env: 'MYINFO_CERT_PATH', - }, - myInfoClientId: { - doc: 'OAuth2 client ID registered with MyInfo.', - format: String, - default: null, - env: 'MYINFO_CLIENT_ID', - }, - myInfoClientSecret: { - doc: 'OAuth2 client secret registered with MyInfo.', - format: String, - default: null, - env: 'MYINFO_CLIENT_SECRET', - }, +type ISpcpConfig = { + isSPMaintenance: string + isCPMaintenance: string + spCookieMaxAge: number + spCookieMaxAgePreserved: number + spcpCookieDomain: string + cpCookieMaxAge: number + spIdpId: string + cpIdpId: string + spPartnerEntityId: string + cpPartnerEntityId: string + spIdpLoginUrl: string + cpIdpLoginUrl: string + spIdpEndpoint: string + cpIdpEndpoint: string + spEsrvcId: string + cpEsrvcId: string + spFormSgKeyPath: string + cpFormSgKeyPath: string + spFormSgCertPath: string + cpFormSgCertPath: string + spIdpCertPath: string + cpIdpCertPath: string +} + +type IMyInfoConfig = { + myInfoClientMode: MyInfoMode + myInfoKeyPath: string + myInfoCertPath: string + myInfoClientId: string + myInfoClientSecret: string +} + +// Config of MyInfo is coupled to that of Singpass +// due to MyInfo's sharing of spCookieMaxAge. Therefore +// export both together. +export type ISpcpMyInfo = ISpcpConfig & IMyInfoConfig + +convict.addFormat(url) + +const spcpMyInfoSchema: Schema = { + isSPMaintenance: { + doc: 'If set, displays a banner message on SingPass forms. Overrides IS_CP_MAINTENANCE', + format: '*', + default: null, + env: 'IS_SP_MAINTENANCE', + }, + isCPMaintenance: { + doc: 'If set, displays a banner message on CorpPass forms', + format: '*', + default: null, + env: 'IS_CP_MAINTENANCE', + }, + spCookieMaxAge: { + doc: 'Max SingPass cookie age with remember me unchecked', + format: 'int', + default: 3 * HOUR_IN_MILLIS, + env: 'SP_COOKIE_MAX_AGE', + }, + spCookieMaxAgePreserved: { + doc: 'Max SingPass cookie age with remember me checked', + format: 'int', + default: 30 * DAY_IN_MILLIS, + env: 'SPCP_COOKIE_MAX_AGE_PRESERVED', + }, + spcpCookieDomain: { + doc: 'Domain name set on cookie that holds the SPCP jwt', + format: String, + default: '', + env: 'SPCP_COOKIE_DOMAIN', + }, + cpCookieMaxAge: { + doc: 'Max CorpPass cookie age', + format: 'int', + default: 6 * HOUR_IN_MILLIS, + env: 'CP_COOKIE_MAX_AGE', + }, + spIdpId: { + doc: 'Partner ID of National Digital Identity Office for SingPass authentication', + format: 'url', + default: null, + env: 'SINGPASS_IDP_ID', + }, + cpIdpId: { + doc: 'Partner ID of National Digital Identity Office for CorpPass authentication', + format: 'url', + default: null, + env: 'CORPPASS_IDP_ID', + }, + spPartnerEntityId: { + doc: 'Partner ID registered with National Digital Identity Office for SingPass authentication', + format: 'url', + default: null, + env: 'SINGPASS_PARTNER_ENTITY_ID', + }, + cpPartnerEntityId: { + doc: 'Partner ID registered with National Digital Identity Office for CorpPass authentication', + format: 'url', + default: null, + env: 'CORPPASS_PARTNER_ENTITY_ID', + }, + spIdpLoginUrl: { + doc: 'URL of SingPass Login Page', + format: 'url', + default: null, + env: 'SINGPASS_IDP_LOGIN_URL', + }, + cpIdpLoginUrl: { + doc: 'URL of CorpPass Login Page', + format: 'url', + default: null, + env: 'CORPPASS_IDP_LOGIN_URL', + }, + spIdpEndpoint: { + doc: 'URL to retrieve NRIC of SingPass-validated user from', + format: 'url', + default: null, + env: 'SINGPASS_IDP_ENDPOINT', + }, + cpIdpEndpoint: { + doc: 'URL to retrieve UEN of CorpPass-validated user from', + format: 'url', + default: null, + env: 'CORPPASS_IDP_ENDPOINT', + }, + spEsrvcId: { + doc: 'e-service ID registered with National Digital Identity office for SingPass authentication', + format: String, + default: null, + env: 'SINGPASS_ESRVC_ID', + }, + cpEsrvcId: { + doc: 'e-service ID registered with National Digital Identity office for CorpPass authentication', + format: String, + default: null, + env: 'CORPPASS_ESRVC_ID', + }, + spFormSgKeyPath: { + doc: 'Path to X.509 key used for SingPass related communication with National Digital Identity office', + format: String, + default: null, + env: 'SP_FORMSG_KEY_PATH', + }, + cpFormSgKeyPath: { + doc: 'Path to X.509 key used for CorpPass related communication with National Digital Identity office', + format: String, + default: null, + env: 'CP_FORMSG_KEY_PATH', + }, + spFormSgCertPath: { + doc: 'Path to X.509 cert used for SingPass related communication with National Digital Identity office', + format: String, + default: null, + env: 'SP_FORMSG_CERT_PATH', + }, + cpFormSgCertPath: { + doc: 'Path to X.509 cert used for CorpPass related communication with National Digital Identity office', + format: String, + default: null, + env: 'CP_FORMSG_CERT_PATH', + }, + spIdpCertPath: { + doc: 'Path to National Digital Identity offices X.509 cert used for SingPass related communication', + format: String, + default: null, + env: 'SP_IDP_CERT_PATH', + }, + cpIdpCertPath: { + doc: 'Path to National Digital Identity offices X.509 cert used for CorpPass related communication', + format: String, + default: null, + env: 'CP_IDP_CERT_PATH', + }, + myInfoClientMode: { + doc: 'Configures MyInfoGovClient. Set this to either `stg` or `prod` to fetch MyInfo data from the corresponding endpoints.', + format: Object.values(MyInfoMode), + default: MyInfoMode.Production, + env: 'MYINFO_CLIENT_CONFIG', + }, + myInfoKeyPath: { + doc: 'Filepath to MyInfo private key, which is used to decrypt data and sign requests when communicating with MyInfo.', + format: String, + default: null, + env: 'MYINFO_FORMSG_KEY_PATH', + }, + myInfoCertPath: { + doc: "Path to MyInfo's public certificate, which is used to verify their signature.", + format: String, + default: null, + env: 'MYINFO_CERT_PATH', + }, + myInfoClientId: { + doc: 'OAuth2 client ID registered with MyInfo.', + format: String, + default: null, + env: 'MYINFO_CLIENT_ID', + }, + myInfoClientSecret: { + doc: 'OAuth2 client secret registered with MyInfo.', + format: String, + default: null, + env: 'MYINFO_CLIENT_SECRET', }, } -export default spcpMyInfoFeature +export const spcpMyInfoConfig = convict(spcpMyInfoSchema) + .validate({ allowed: 'strict' }) + .getProperties() diff --git a/src/app/config/feature-manager/types.ts b/src/app/config/feature-manager/types.ts index b0939a04b9..8505a8d8aa 100644 --- a/src/app/config/feature-manager/types.ts +++ b/src/app/config/feature-manager/types.ts @@ -1,48 +1,8 @@ -import { MyInfoMode } from '@opengovsg/myinfo-gov-client' import { Schema } from 'convict' -export enum FeatureNames { - SpcpMyInfo = 'spcp-myinfo', -} - -export interface ISpcpConfig { - isSPMaintenance: string - isCPMaintenance: string - spCookieMaxAge: number - spCookieMaxAgePreserved: number - spcpCookieDomain: string - cpCookieMaxAge: number - spIdpId: string - cpIdpId: string - spPartnerEntityId: string - cpPartnerEntityId: string - spIdpLoginUrl: string - cpIdpLoginUrl: string - spIdpEndpoint: string - cpIdpEndpoint: string - spEsrvcId: string - cpEsrvcId: string - spFormSgKeyPath: string - cpFormSgKeyPath: string - spFormSgCertPath: string - cpFormSgCertPath: string - spIdpCertPath: string - cpIdpCertPath: string -} +export enum FeatureNames {} -export interface IMyInfoConfig { - myInfoClientMode: MyInfoMode - myInfoKeyPath: string - myInfoCertPath: string - myInfoClientId: string - myInfoClientSecret: string -} - -export type ISpcpMyInfo = ISpcpConfig & IMyInfoConfig - -export interface IFeatureManager { - [FeatureNames.SpcpMyInfo]: ISpcpMyInfo -} +export type IFeatureManager = Record export interface RegisteredFeature { isEnabled: boolean diff --git a/src/app/loaders/express/locals.ts b/src/app/loaders/express/locals.ts index aa8c735c59..5d05ea7be0 100644 --- a/src/app/loaders/express/locals.ts +++ b/src/app/loaders/express/locals.ts @@ -1,11 +1,10 @@ import ejs from 'ejs' -import { get } from 'lodash' import config from '../../config/config' -import featureManager, { FeatureNames } from '../../config/feature-manager' import { captchaConfig } from '../../config/feature-manager/captcha.config' import { googleAnalyticsConfig } from '../../config/feature-manager/google-analytics.config' import { sentryConfig } from '../../config/feature-manager/sentry.config' +import { spcpMyInfoConfig } from '../../config/feature-manager/spcp-myinfo.config' // Construct js with environment variables needed by frontend const frontendVars = { @@ -17,22 +16,10 @@ const frontendVars = { formsgSdkMode: config.formsgSdkMode, captchaPublicKey: captchaConfig.captchaPublicKey, // Recaptcha sentryConfigUrl: sentryConfig.sentryConfigUrl, // Sentry.IO - isSPMaintenance: get( - featureManager.props(FeatureNames.SpcpMyInfo), - 'isSPMaintenance', - null, - ), // Singpass maintenance message - isCPMaintenance: get( - featureManager.props(FeatureNames.SpcpMyInfo), - 'isCPMaintenance', - null, - ), // Corppass maintenance message + isSPMaintenance: spcpMyInfoConfig.isSPMaintenance, // Singpass maintenance message + isCPMaintenance: spcpMyInfoConfig.isCPMaintenance, // Corppass maintenance message GATrackingID: googleAnalyticsConfig.GATrackingID, - spcpCookieDomain: get( - featureManager.props(FeatureNames.SpcpMyInfo), - 'spcpCookieDomain', - null, - ), // Cookie domain used for removing spcp cookies + spcpCookieDomain: spcpMyInfoConfig.spcpCookieDomain, // Cookie domain used for removing spcp cookies } const environment = ejs.render( ` diff --git a/src/app/modules/billing/__tests__/billing.factory.spec.ts b/src/app/modules/billing/__tests__/billing.factory.spec.ts deleted file mode 100644 index 4657c62e57..0000000000 --- a/src/app/modules/billing/__tests__/billing.factory.spec.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { okAsync } from 'neverthrow' -import { mocked } from 'ts-jest/utils' - -import { - FeatureNames, - ISpcpMyInfo, - RegisteredFeature, -} from 'src/app/config/feature-manager' -import { - AuthType, - ILoginSchema, - IPopulatedForm, - LoginStatistic, -} from 'src/types' - -import { MissingFeatureError } from '../../core/core.errors' -import { createBillingFactory } from '../billing.factory' -import * as BillingService from '../billing.service' - -jest.mock('../billing.service') -const MockBillingService = mocked(BillingService) - -describe('billing.factory', () => { - beforeEach(() => jest.clearAllMocks()) - - describe('spcp-myinfo feature disabled', () => { - const MOCK_DISABLED_FEATURE: RegisteredFeature = { - isEnabled: false, - props: {} as ISpcpMyInfo, - } - const BillingFactory = createBillingFactory(MOCK_DISABLED_FEATURE) - - describe('getSpLoginStats', () => { - it('should return empty array passthrough when spcp-myinfo feature is disabled', async () => { - // Act - const actualResults = await BillingFactory.getSpLoginStats( - 'anything', - new Date(), - new Date(), - ) - - // Assert - expect(MockBillingService.getSpLoginStats).not.toHaveBeenCalled() - expect(actualResults.isOk()).toEqual(true) - expect(actualResults._unsafeUnwrap()).toEqual([]) - }) - }) - - describe('recordLoginByForm', () => { - it('should return MissingFeatureError when spcp-myinfo feature is disabled', async () => { - // Argument here does not matter, as the function should always return a MissingFeatureError - const result = await BillingFactory.recordLoginByForm( - {} as unknown as IPopulatedForm, - ) - expect(MockBillingService.recordLoginByForm).not.toHaveBeenCalled() - expect(result._unsafeUnwrapErr()).toEqual( - new MissingFeatureError(FeatureNames.SpcpMyInfo), - ) - }) - }) - }) - - describe('spcp-myinfo feature enabled', () => { - const MOCK_ENABLED_FEATURE: RegisteredFeature = { - isEnabled: true, - // Empty object as no relevant props needed for billing. - props: {} as ISpcpMyInfo, - } - const BillingFactory = createBillingFactory(MOCK_ENABLED_FEATURE) - - describe('getSpLoginStats', () => { - it('should invoke BillingService#getSpLoginStats', async () => { - // Arrange - const mockLoginStats: LoginStatistic[] = [ - { - adminEmail: 'mockemail@example.com', - authType: AuthType.CP, - formId: 'mock form id', - formName: 'some form name', - total: 100, - }, - ] - const serviceGetStatsSpy = - MockBillingService.getSpLoginStats.mockReturnValue( - okAsync(mockLoginStats), - ) - - // Act - const actualResults = await BillingFactory.getSpLoginStats( - 'anything', - new Date(), - new Date(), - ) - - // Assert - expect(serviceGetStatsSpy).toHaveBeenCalledTimes(1) - expect(actualResults.isOk()).toEqual(true) - expect(actualResults._unsafeUnwrap()).toEqual(mockLoginStats) - }) - }) - - describe('recordLoginByForm', () => { - it('should call BillingService.recordLoginByForm', async () => { - const mockLoginDoc = { - mockKey: 'mockValue', - } as unknown as ILoginSchema - const mockForm = { - mockFormKey: 'mockFormvalue', - } as unknown as IPopulatedForm - MockBillingService.recordLoginByForm.mockResolvedValueOnce( - okAsync(mockLoginDoc), - ) - - const result = await BillingFactory.recordLoginByForm(mockForm) - - expect(result._unsafeUnwrap()).toEqual(mockLoginDoc) - expect(MockBillingService.recordLoginByForm).toHaveBeenCalledWith( - mockForm, - ) - }) - }) - }) -}) diff --git a/src/app/modules/billing/__tests__/billing.routes.spec.ts b/src/app/modules/billing/__tests__/billing.routes.spec.ts index 0a076eea6a..9c915f480b 100644 --- a/src/app/modules/billing/__tests__/billing.routes.spec.ts +++ b/src/app/modules/billing/__tests__/billing.routes.spec.ts @@ -13,8 +13,8 @@ import { buildCelebrateError } from 'tests/unit/backend/helpers/celebrate' import dbHandler from 'tests/unit/backend/helpers/jest-db' import { DatabaseError } from '../../core/core.errors' -import { BillingFactory } from '../billing.factory' import { BillingRouter } from '../billing.routes' +import * as BillingService from '../billing.service' const app = setupApp('/billing', BillingRouter, { setupWithAuth: true, @@ -191,7 +191,7 @@ describe('billing.routes', () => { // Mock database error from service call. const retrieveStatsSpy = jest - .spyOn(BillingFactory, 'getSpLoginStats') + .spyOn(BillingService, 'getSpLoginStats') .mockReturnValueOnce(errAsync(new DatabaseError())) // Act diff --git a/src/app/modules/billing/billing.controller.ts b/src/app/modules/billing/billing.controller.ts index 959b00035a..3cef391c75 100644 --- a/src/app/modules/billing/billing.controller.ts +++ b/src/app/modules/billing/billing.controller.ts @@ -6,7 +6,7 @@ import { createLoggerWithLabel } from '../../config/logger' import { createReqMeta } from '../../utils/request' import { ControllerHandler } from '../core/core.types' -import { BillingFactory } from './billing.factory' +import * as BillingService from './billing.service' const logger = createLoggerWithLabel(module) @@ -32,7 +32,7 @@ export const handleGetBillInfo: ControllerHandler< .startOf('month') const endOfMonth = moment(startOfMonth).endOf('month') - const loginStatsResult = await BillingFactory.getSpLoginStats( + const loginStatsResult = await BillingService.getSpLoginStats( esrvcId, startOfMonth.toDate(), endOfMonth.toDate(), diff --git a/src/app/modules/billing/billing.factory.ts b/src/app/modules/billing/billing.factory.ts deleted file mode 100644 index 9414cf225e..0000000000 --- a/src/app/modules/billing/billing.factory.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { errAsync, okAsync, ResultAsync } from 'neverthrow' - -import { ILoginSchema, IPopulatedForm } from '../../../types' -import FeatureManager, { - FeatureNames, - RegisteredFeature, -} from '../../config/feature-manager' -import { DatabaseError, MissingFeatureError } from '../core/core.errors' - -import { FormHasNoAuthError } from './billing.errors' -import * as BillingService from './billing.service' - -interface IBillingFactory { - getSpLoginStats: typeof BillingService.getSpLoginStats - recordLoginByForm: ( - form: IPopulatedForm, - ) => ResultAsync< - ILoginSchema, - FormHasNoAuthError | DatabaseError | MissingFeatureError - > -} - -const spcpFeature = FeatureManager.get(FeatureNames.SpcpMyInfo) - -// Exported for testing. -export const createBillingFactory = ({ - isEnabled, - props, -}: RegisteredFeature): IBillingFactory => { - if (isEnabled && props) { - return BillingService - } - - // Not enabled, return passthrough functions. - const error = new MissingFeatureError(FeatureNames.SpcpMyInfo) - return { - getSpLoginStats: () => okAsync([]), - recordLoginByForm: () => errAsync(error), - } -} - -export const BillingFactory = createBillingFactory(spcpFeature) diff --git a/src/app/modules/billing/billing.service.ts b/src/app/modules/billing/billing.service.ts index 6045e9b3c6..b706324411 100644 --- a/src/app/modules/billing/billing.service.ts +++ b/src/app/modules/billing/billing.service.ts @@ -1,9 +1,3 @@ -/** - * ! Important: Note that this service class should only be called by - * ! BillingFactory in billing.factory as access to the functions in this class - * ! should be determined by whether the `spcp-myinfo` feature is enabled. - */ - import mongoose from 'mongoose' import { errAsync, ResultAsync } from 'neverthrow' @@ -24,8 +18,6 @@ const logger = createLoggerWithLabel(module) const LoginModel = getLoginModel(mongoose) /** - * !!! This function should only be called by {@link BillingFactory}. - * * Retrieves SingPass login statistics including total logins for each of the * forms with given esrvcId in the given date range. * @param esrvcId the esrvcId to filter retrieved login statistics diff --git a/src/app/modules/form/public-form/__tests__/public-form.controller.spec.ts b/src/app/modules/form/public-form/__tests__/public-form.controller.spec.ts index 8f5eab18ed..6a5e3718eb 100644 --- a/src/app/modules/form/public-form/__tests__/public-form.controller.spec.ts +++ b/src/app/modules/form/public-form/__tests__/public-form.controller.spec.ts @@ -35,14 +35,14 @@ import expressHandler from 'tests/unit/backend/helpers/jest-express' import * as AuthService from '../../../auth/auth.service' import { MyInfoCookieStateError } from '../../../myinfo/myinfo.errors' -import { MyInfoFactory } from '../../../myinfo/myinfo.factory' +import { MyInfoService } from '../../../myinfo/myinfo.service' import { CreateRedirectUrlError, FetchLoginPageError, LoginPageValidationError, MissingJwtError, } from '../../../spcp/spcp.errors' -import { SpcpFactory } from '../../../spcp/spcp.factory' +import { SpcpService } from '../../../spcp/spcp.service' import { AuthTypeMismatchError, FormAuthNoEsrvcIdError, @@ -58,14 +58,14 @@ import { Metatags } from '../public-form.types' jest.mock('../public-form.service') jest.mock('../../form.service') jest.mock('../../../auth/auth.service') -jest.mock('../../../spcp/spcp.factory') -jest.mock('../../../myinfo/myinfo.factory') +jest.mock('../../../spcp/spcp.service') +jest.mock('../../../myinfo/myinfo.service') const MockFormService = mocked(FormService) const MockPublicFormService = mocked(PublicFormService) const MockAuthService = mocked(AuthService) -const MockSpcpFactory = mocked(SpcpFactory, true) -const MockMyInfoFactory = mocked(MyInfoFactory, true) +const MockSpcpService = mocked(SpcpService, true) +const MockMyInfoService = mocked(MyInfoService, true) const FormFeedbackModel = getFormFeedbackModel(mongoose) @@ -531,7 +531,7 @@ describe('public-form.controller', () => { MockFormService.checkFormSubmissionLimitAndDeactivateForm.mockReturnValueOnce( okAsync(MOCK_SP_AUTH_FORM), ) - MockSpcpFactory.extractJwtPayloadFromRequest.mockReturnValueOnce( + MockSpcpService.extractJwtPayloadFromRequest.mockReturnValueOnce( okAsync(MOCK_SPCP_SESSION), ) @@ -565,7 +565,7 @@ describe('public-form.controller', () => { MockFormService.checkFormSubmissionLimitAndDeactivateForm.mockReturnValueOnce( okAsync(MOCK_CP_AUTH_FORM), ) - MockSpcpFactory.extractJwtPayloadFromRequest.mockReturnValueOnce( + MockSpcpService.extractJwtPayloadFromRequest.mockReturnValueOnce( okAsync(MOCK_SPCP_SESSION), ) // Act @@ -605,10 +605,10 @@ describe('public-form.controller', () => { MockFormService.checkFormSubmissionLimitAndDeactivateForm.mockReturnValueOnce( okAsync(MOCK_MYINFO_AUTH_FORM), ) - MockMyInfoFactory.getMyInfoDataForForm.mockReturnValueOnce( + MockMyInfoService.getMyInfoDataForForm.mockReturnValueOnce( okAsync(MOCK_MYINFO_DATA), ) - MockMyInfoFactory.prefillAndSaveMyInfoFields.mockReturnValueOnce( + MockMyInfoService.prefillAndSaveMyInfoFields.mockReturnValueOnce( okAsync([]), ) @@ -658,7 +658,7 @@ describe('public-form.controller', () => { clearCookie: jest.fn().mockReturnThis(), }) - MockMyInfoFactory.getMyInfoDataForForm.mockReturnValueOnce( + MockMyInfoService.getMyInfoDataForForm.mockReturnValueOnce( errAsync(new MyInfoMissingAccessTokenError()), ) @@ -685,7 +685,7 @@ describe('public-form.controller', () => { clearCookie: jest.fn().mockReturnThis(), }) - MockMyInfoFactory.getMyInfoDataForForm.mockReturnValueOnce( + MockMyInfoService.getMyInfoDataForForm.mockReturnValueOnce( errAsync(new MyInfoCookieAccessError()), ) @@ -712,7 +712,7 @@ describe('public-form.controller', () => { clearCookie: jest.fn().mockReturnThis(), }) - MockMyInfoFactory.getMyInfoDataForForm.mockReturnValueOnce( + MockMyInfoService.getMyInfoDataForForm.mockReturnValueOnce( errAsync(new MyInfoCookieStateError()), ) @@ -739,7 +739,7 @@ describe('public-form.controller', () => { clearCookie: jest.fn().mockReturnThis(), }) - MockMyInfoFactory.getMyInfoDataForForm.mockReturnValueOnce( + MockMyInfoService.getMyInfoDataForForm.mockReturnValueOnce( errAsync(new AuthTypeMismatchError()), ) @@ -766,7 +766,7 @@ describe('public-form.controller', () => { clearCookie: jest.fn().mockReturnThis(), }) - MockMyInfoFactory.getMyInfoDataForForm.mockReturnValueOnce( + MockMyInfoService.getMyInfoDataForForm.mockReturnValueOnce( errAsync(new FormAuthNoEsrvcIdError()), ) @@ -793,7 +793,7 @@ describe('public-form.controller', () => { clearCookie: jest.fn().mockReturnThis(), }) - MockMyInfoFactory.getMyInfoDataForForm.mockReturnValueOnce( + MockMyInfoService.getMyInfoDataForForm.mockReturnValueOnce( errAsync( new MissingFeatureError( 'testing is the missing feature' as FeatureNames, @@ -827,10 +827,10 @@ describe('public-form.controller', () => { const mockRes = expressHandler.mockResponse({ clearCookie: jest.fn().mockReturnThis(), }) - MockMyInfoFactory.getMyInfoDataForForm.mockReturnValueOnce( + MockMyInfoService.getMyInfoDataForForm.mockReturnValueOnce( okAsync(MOCK_MYINFO_DATA), ) - MockMyInfoFactory.prefillAndSaveMyInfoFields.mockReturnValueOnce( + MockMyInfoService.prefillAndSaveMyInfoFields.mockReturnValueOnce( errAsync(expected), ) @@ -867,7 +867,7 @@ describe('public-form.controller', () => { MockFormService.checkFormSubmissionLimitAndDeactivateForm.mockReturnValueOnce( okAsync(MOCK_SPCP_FORM), ) - MockSpcpFactory.extractJwtPayloadFromRequest.mockReturnValueOnce( + MockSpcpService.extractJwtPayloadFromRequest.mockReturnValueOnce( errAsync(new MissingJwtError()), ) @@ -1037,7 +1037,7 @@ describe('public-form.controller', () => { const mockRes = expressHandler.mockResponse() - MockSpcpFactory.extractJwtPayloadFromRequest.mockReturnValueOnce( + MockSpcpService.extractJwtPayloadFromRequest.mockReturnValueOnce( okAsync(MOCK_SPCP_SESSION), ) MockFormService.checkIsIntranetFormAccess.mockReturnValueOnce(true) @@ -1073,7 +1073,7 @@ describe('public-form.controller', () => { const mockRes = expressHandler.mockResponse() MockFormService.checkIsIntranetFormAccess.mockReturnValueOnce(true) - MockSpcpFactory.extractJwtPayloadFromRequest.mockReturnValueOnce( + MockSpcpService.extractJwtPayloadFromRequest.mockReturnValueOnce( okAsync(MOCK_SPCP_SESSION), ) MockAuthService.getFormIfPublic.mockReturnValueOnce( @@ -1122,10 +1122,10 @@ describe('public-form.controller', () => { okAsync(MOCK_MYINFO_AUTH_FORM), ) MockFormService.checkIsIntranetFormAccess.mockReturnValueOnce(true) - MockMyInfoFactory.getMyInfoDataForForm.mockReturnValueOnce( + MockMyInfoService.getMyInfoDataForForm.mockReturnValueOnce( okAsync(MOCK_MYINFO_DATA), ) - MockMyInfoFactory.prefillAndSaveMyInfoFields.mockReturnValueOnce( + MockMyInfoService.prefillAndSaveMyInfoFields.mockReturnValueOnce( okAsync([]), ) @@ -1169,7 +1169,7 @@ describe('public-form.controller', () => { MockFormService.retrieveFullFormById.mockReturnValueOnce( okAsync(MOCK_FORM), ) - MockSpcpFactory.createRedirectUrl.mockReturnValueOnce( + MockSpcpService.createRedirectUrl.mockReturnValueOnce( ok(MOCK_REDIRECT_URL), ) @@ -1200,7 +1200,7 @@ describe('public-form.controller', () => { MockFormService.retrieveFullFormById.mockReturnValueOnce( okAsync(MOCK_FORM), ) - MockSpcpFactory.createRedirectUrl.mockReturnValueOnce( + MockSpcpService.createRedirectUrl.mockReturnValueOnce( ok(MOCK_REDIRECT_URL), ) @@ -1234,7 +1234,7 @@ describe('public-form.controller', () => { MockFormService.retrieveFullFormById.mockReturnValueOnce( okAsync(MOCK_FORM), ) - MockSpcpFactory.createRedirectUrl.mockReturnValueOnce( + MockSpcpService.createRedirectUrl.mockReturnValueOnce( ok(MOCK_REDIRECT_URL), ) @@ -1261,7 +1261,7 @@ describe('public-form.controller', () => { MockFormService.retrieveFullFormById.mockReturnValueOnce( okAsync(MOCK_FORM), ) - MockSpcpFactory.createRedirectUrl.mockReturnValueOnce( + MockSpcpService.createRedirectUrl.mockReturnValueOnce( ok(MOCK_REDIRECT_URL), ) @@ -1289,7 +1289,7 @@ describe('public-form.controller', () => { MockFormService.retrieveFullFormById.mockReturnValueOnce( okAsync(MOCK_FORM), ) - MockMyInfoFactory.createRedirectURL.mockReturnValueOnce( + MockMyInfoService.createRedirectURL.mockReturnValueOnce( ok(MOCK_REDIRECT_URL), ) @@ -1443,7 +1443,7 @@ describe('public-form.controller', () => { MockFormService.retrieveFullFormById.mockReturnValueOnce( okAsync(MOCK_FORM), ) - MockSpcpFactory.createRedirectUrl.mockReturnValueOnce( + MockSpcpService.createRedirectUrl.mockReturnValueOnce( err(new CreateRedirectUrlError()), ) @@ -1472,7 +1472,7 @@ describe('public-form.controller', () => { MockFormService.retrieveFullFormById.mockReturnValueOnce( okAsync(MOCK_FORM), ) - MockMyInfoFactory.createRedirectURL.mockReturnValueOnce( + MockMyInfoService.createRedirectURL.mockReturnValueOnce( err(new MissingFeatureError('Redirect url')), ) @@ -1508,13 +1508,13 @@ describe('public-form.controller', () => { const mockRes = expressHandler.mockResponse() const expectedResBody = { isValid: true } MockFormService.retrieveFormById.mockReturnValueOnce(okAsync(MOCK_FORM)) - MockSpcpFactory.createRedirectUrl.mockReturnValueOnce( + MockSpcpService.createRedirectUrl.mockReturnValueOnce( ok(MOCK_REDIRECT_URL), ) - MockSpcpFactory.fetchLoginPage.mockReturnValueOnce( + MockSpcpService.fetchLoginPage.mockReturnValueOnce( okAsync('this is raw html'), ) - MockSpcpFactory.validateLoginPage.mockReturnValueOnce(ok(expectedResBody)) + MockSpcpService.validateLoginPage.mockReturnValueOnce(ok(expectedResBody)) // Act await PublicFormController.handleValidateFormEsrvcId( @@ -1537,13 +1537,13 @@ describe('public-form.controller', () => { const mockRes = expressHandler.mockResponse() const expectedResBody = { isValid: true } MockFormService.retrieveFormById.mockReturnValueOnce(okAsync(MOCK_FORM)) - MockSpcpFactory.createRedirectUrl.mockReturnValueOnce( + MockSpcpService.createRedirectUrl.mockReturnValueOnce( ok(MOCK_REDIRECT_URL), ) - MockSpcpFactory.fetchLoginPage.mockReturnValueOnce( + MockSpcpService.fetchLoginPage.mockReturnValueOnce( okAsync('this is raw html'), ) - MockSpcpFactory.validateLoginPage.mockReturnValueOnce(ok(expectedResBody)) + MockSpcpService.validateLoginPage.mockReturnValueOnce(ok(expectedResBody)) // Act await PublicFormController.handleValidateFormEsrvcId( @@ -1565,13 +1565,13 @@ describe('public-form.controller', () => { const mockRes = expressHandler.mockResponse() const expectedResBody = { isValid: false, errorCode: '138' } MockFormService.retrieveFormById.mockReturnValueOnce(okAsync(MOCK_FORM)) - MockSpcpFactory.createRedirectUrl.mockReturnValueOnce( + MockSpcpService.createRedirectUrl.mockReturnValueOnce( ok(MOCK_REDIRECT_URL), ) - MockSpcpFactory.fetchLoginPage.mockReturnValueOnce( + MockSpcpService.fetchLoginPage.mockReturnValueOnce( okAsync('this is raw html'), ) - MockSpcpFactory.validateLoginPage.mockReturnValueOnce(ok(expectedResBody)) + MockSpcpService.validateLoginPage.mockReturnValueOnce(ok(expectedResBody)) // Act await PublicFormController.handleValidateFormEsrvcId( @@ -1594,13 +1594,13 @@ describe('public-form.controller', () => { const mockRes = expressHandler.mockResponse() const expectedResBody = { isValid: false, errorCode: '138' } MockFormService.retrieveFormById.mockReturnValueOnce(okAsync(MOCK_FORM)) - MockSpcpFactory.createRedirectUrl.mockReturnValueOnce( + MockSpcpService.createRedirectUrl.mockReturnValueOnce( ok(MOCK_REDIRECT_URL), ) - MockSpcpFactory.fetchLoginPage.mockReturnValueOnce( + MockSpcpService.fetchLoginPage.mockReturnValueOnce( okAsync('this is raw html'), ) - MockSpcpFactory.validateLoginPage.mockReturnValueOnce(ok(expectedResBody)) + MockSpcpService.validateLoginPage.mockReturnValueOnce(ok(expectedResBody)) // Act await PublicFormController.handleValidateFormEsrvcId( @@ -1722,11 +1722,11 @@ describe('public-form.controller', () => { message: 'Error while contacting SingPass. Please try again.', } MockFormService.retrieveFormById.mockReturnValueOnce(okAsync(MOCK_FORM)) - MockSpcpFactory.createRedirectUrl.mockReturnValueOnce( + MockSpcpService.createRedirectUrl.mockReturnValueOnce( ok(MOCK_REDIRECT_URL), ) - MockSpcpFactory.fetchLoginPage.mockReturnValue(okAsync('')) - MockSpcpFactory.validateLoginPage.mockReturnValueOnce( + MockSpcpService.fetchLoginPage.mockReturnValue(okAsync('')) + MockSpcpService.validateLoginPage.mockReturnValueOnce( err(new LoginPageValidationError()), ) @@ -1775,7 +1775,7 @@ describe('public-form.controller', () => { message: 'Sorry, something went wrong. Please try again.', } MockFormService.retrieveFormById.mockReturnValueOnce(okAsync(MOCK_FORM)) - MockSpcpFactory.createRedirectUrl.mockReturnValueOnce( + MockSpcpService.createRedirectUrl.mockReturnValueOnce( err(new CreateRedirectUrlError()), ) @@ -1802,10 +1802,10 @@ describe('public-form.controller', () => { message: 'Failed to contact SingPass. Please try again.', } MockFormService.retrieveFormById.mockReturnValueOnce(okAsync(MOCK_FORM)) - MockSpcpFactory.createRedirectUrl.mockReturnValueOnce( + MockSpcpService.createRedirectUrl.mockReturnValueOnce( ok(MOCK_REDIRECT_URL), ) - MockSpcpFactory.fetchLoginPage.mockReturnValue( + MockSpcpService.fetchLoginPage.mockReturnValue( errAsync(new FetchLoginPageError()), ) diff --git a/src/app/modules/form/public-form/public-form.controller.ts b/src/app/modules/form/public-form/public-form.controller.ts index 722d1b06f5..4a062ccaa3 100644 --- a/src/app/modules/form/public-form/public-form.controller.ts +++ b/src/app/modules/form/public-form/public-form.controller.ts @@ -25,13 +25,13 @@ import { MyInfoCookieAccessError, MyInfoMissingAccessTokenError, } from '../../myinfo/myinfo.errors' -import { MyInfoFactory } from '../../myinfo/myinfo.factory' +import { MyInfoService } from '../../myinfo/myinfo.service' import { extractAndAssertMyInfoCookieValidity, validateMyInfoForm, } from '../../myinfo/myinfo.util' import { InvalidJwtError, VerifyJwtError } from '../../spcp/spcp.errors' -import { SpcpFactory } from '../../spcp/spcp.factory' +import { SpcpService } from '../../spcp/spcp.service' import { getRedirectTarget, validateSpcpForm } from '../../spcp/spcp.util' import { AuthTypeMismatchError, PrivateFormError } from '../form.errors' import * as FormService from '../form.service' @@ -270,7 +270,7 @@ export const handleGetPublicForm: ControllerHandler< return res.json({ form: publicForm, isIntranetUser }) case AuthType.SP: case AuthType.CP: - return SpcpFactory.extractJwtPayloadFromRequest(authType, req.cookies) + return SpcpService.extractJwtPayloadFromRequest(authType, req.cookies) .map(({ userName }) => res.json({ form: publicForm, @@ -295,9 +295,9 @@ export const handleGetPublicForm: ControllerHandler< case AuthType.MyInfo: { // Step 1. Fetch required data and fill the form based off data retrieved return ( - MyInfoFactory.getMyInfoDataForForm(form, req.cookies) + MyInfoService.getMyInfoDataForForm(form, req.cookies) .andThen((myInfoData) => { - return MyInfoFactory.prefillAndSaveMyInfoFields( + return MyInfoService.prefillAndSaveMyInfoFields( form._id, myInfoData, form.toJSON().form_fields, @@ -391,7 +391,7 @@ export const _handleFormAuthRedirect: ControllerHandler< switch (form.authType) { case AuthType.MyInfo: return validateMyInfoForm(form).andThen((form) => - MyInfoFactory.createRedirectURL({ + MyInfoService.createRedirectURL({ formEsrvcId: form.esrvcId, formId, requestedAttributes: form.getUniqueMyInfoAttrs(), @@ -407,7 +407,7 @@ export const _handleFormAuthRedirect: ControllerHandler< form.authType, isPersistentLogin, ) - return SpcpFactory.createRedirectUrl( + return SpcpService.createRedirectUrl( form.authType, target, form.esrvcId, @@ -476,11 +476,11 @@ export const handleValidateFormEsrvcId: ControllerHandler< switch (form.authType) { case AuthType.MyInfo: return validateMyInfoForm(form).andThen((form) => - SpcpFactory.createRedirectUrl(AuthType.SP, formId, form.esrvcId), + SpcpService.createRedirectUrl(AuthType.SP, formId, form.esrvcId), ) case AuthType.SP: return validateSpcpForm(form).andThen((form) => - SpcpFactory.createRedirectUrl(form.authType, formId, form.esrvcId), + SpcpService.createRedirectUrl(form.authType, formId, form.esrvcId), ) default: return err( @@ -488,8 +488,8 @@ export const handleValidateFormEsrvcId: ControllerHandler< ) } }) - .andThen(SpcpFactory.fetchLoginPage) - .andThen(SpcpFactory.validateLoginPage) + .andThen(SpcpService.fetchLoginPage) + .andThen(SpcpService.validateLoginPage) .map((result) => res.status(StatusCodes.OK).json(result)) .mapErr((error) => { logger.error({ diff --git a/src/app/modules/myinfo/__tests__/myinfo.controller.spec.ts b/src/app/modules/myinfo/__tests__/myinfo.controller.spec.ts index 231c643d3c..ccf596e93e 100644 --- a/src/app/modules/myinfo/__tests__/myinfo.controller.spec.ts +++ b/src/app/modules/myinfo/__tests__/myinfo.controller.spec.ts @@ -6,7 +6,7 @@ import { AuthType, IFormSchema, ILoginSchema, IPopulatedForm } from 'src/types' import expressHandler from 'tests/unit/backend/helpers/jest-express' -import { BillingFactory } from '../../billing/billing.factory' +import * as BillingService from '../../billing/billing.service' import { DatabaseError } from '../../core/core.errors' import { FormNotFoundError } from '../../form/form.errors' import * as FormService from '../../form/form.service' @@ -18,11 +18,11 @@ import { FetchLoginPageError, LoginPageValidationError, } from '../../spcp/spcp.errors' -import { SpcpFactory } from '../../spcp/spcp.factory' +import { SpcpService } from '../../spcp/spcp.service' import { MYINFO_COOKIE_NAME, MYINFO_COOKIE_OPTIONS } from '../myinfo.constants' import * as MyInfoController from '../myinfo.controller' import { MyInfoFetchError } from '../myinfo.errors' -import { MyInfoFactory } from '../myinfo.factory' +import { MyInfoService } from '../myinfo.service' import { MyInfoCookieState } from '../myinfo.types' import { @@ -34,17 +34,17 @@ import { MOCK_REDIRECT_URL, } from './myinfo.test.constants' -jest.mock('../myinfo.factory') -const MockMyInfoFactory = mocked(MyInfoFactory, true) +jest.mock('../myinfo.service') +const MockMyInfoService = mocked(MyInfoService, true) jest.mock('../../form/form.service') const MockFormService = mocked(FormService, true) -jest.mock('../../spcp/spcp.factory') -const MockSpcpFactory = mocked(SpcpFactory, true) +jest.mock('../../spcp/spcp.service') +const MockSpcpService = mocked(SpcpService, true) -jest.mock('../../billing/billing.factory') -const MockBillingFactory = mocked(BillingFactory, true) +jest.mock('../../billing/billing.service') +const MockBillingService = mocked(BillingService, true) describe('MyInfoController', () => { afterEach(() => jest.clearAllMocks()) @@ -61,7 +61,7 @@ describe('MyInfoController', () => { MockFormService.retrieveFormById.mockReturnValueOnce( okAsync(MOCK_MYINFO_FORM), ) - MockMyInfoFactory.createRedirectURL.mockReturnValueOnce( + MockMyInfoService.createRedirectURL.mockReturnValueOnce( ok(MOCK_REDIRECT_URL), ) @@ -70,7 +70,7 @@ describe('MyInfoController', () => { expect(MockFormService.retrieveFormById).toHaveBeenCalledWith( MOCK_MYINFO_FORM._id, ) - expect(MockMyInfoFactory.createRedirectURL).toHaveBeenCalledWith({ + expect(MockMyInfoService.createRedirectURL).toHaveBeenCalledWith({ formEsrvcId: MOCK_MYINFO_FORM.esrvcId, formId: MOCK_MYINFO_FORM._id, requestedAttributes: MOCK_MYINFO_FORM.getUniqueMyInfoAttrs(), @@ -90,7 +90,7 @@ describe('MyInfoController', () => { expect(MockFormService.retrieveFormById).toHaveBeenCalledWith( MOCK_MYINFO_FORM._id, ) - expect(MockMyInfoFactory.createRedirectURL).not.toHaveBeenCalled() + expect(MockMyInfoService.createRedirectURL).not.toHaveBeenCalled() expect(mockRes.json).toHaveBeenCalledWith({ message: expect.any(String), }) @@ -107,7 +107,7 @@ describe('MyInfoController', () => { expect(MockFormService.retrieveFormById).toHaveBeenCalledWith( MOCK_MYINFO_FORM._id, ) - expect(MockMyInfoFactory.createRedirectURL).not.toHaveBeenCalled() + expect(MockMyInfoService.createRedirectURL).not.toHaveBeenCalled() expect(mockRes.json).toHaveBeenCalledWith({ message: expect.any(String), }) @@ -124,7 +124,7 @@ describe('MyInfoController', () => { expect(MockFormService.retrieveFormById).toHaveBeenCalledWith( MOCK_MYINFO_FORM._id, ) - expect(MockMyInfoFactory.createRedirectURL).not.toHaveBeenCalled() + expect(MockMyInfoService.createRedirectURL).not.toHaveBeenCalled() expect(mockRes.json).toHaveBeenCalledWith({ message: expect.any(String), }) @@ -149,27 +149,27 @@ describe('MyInfoController', () => { }) it('should return 200 with isValid true if validation passes', async () => { - MockSpcpFactory.createRedirectUrl.mockReturnValueOnce( + MockSpcpService.createRedirectUrl.mockReturnValueOnce( ok(MOCK_REDIRECT_URL), ) - MockSpcpFactory.fetchLoginPage.mockReturnValueOnce( + MockSpcpService.fetchLoginPage.mockReturnValueOnce( okAsync(MOCK_LOGIN_HTML), ) - MockSpcpFactory.validateLoginPage.mockReturnValueOnce( + MockSpcpService.validateLoginPage.mockReturnValueOnce( ok({ isValid: true }), ) await MyInfoController.checkMyInfoEServiceId(mockReq, mockRes, jest.fn()) - expect(MockSpcpFactory.createRedirectUrl).toHaveBeenCalledWith( + expect(MockSpcpService.createRedirectUrl).toHaveBeenCalledWith( AuthType.SP, MOCK_MYINFO_FORM._id, MOCK_MYINFO_FORM.esrvcId, ) - expect(MockSpcpFactory.fetchLoginPage).toHaveBeenCalledWith( + expect(MockSpcpService.fetchLoginPage).toHaveBeenCalledWith( MOCK_REDIRECT_URL, ) - expect(MockSpcpFactory.validateLoginPage).toHaveBeenCalledWith( + expect(MockSpcpService.validateLoginPage).toHaveBeenCalledWith( MOCK_LOGIN_HTML, ) expect(mockRes.status).toHaveBeenCalledWith(StatusCodes.OK) @@ -179,27 +179,27 @@ describe('MyInfoController', () => { }) it('should return 200 with isValid false if validation fails', async () => { - MockSpcpFactory.createRedirectUrl.mockReturnValueOnce( + MockSpcpService.createRedirectUrl.mockReturnValueOnce( ok(MOCK_REDIRECT_URL), ) - MockSpcpFactory.fetchLoginPage.mockReturnValueOnce( + MockSpcpService.fetchLoginPage.mockReturnValueOnce( okAsync(MOCK_LOGIN_HTML), ) - MockSpcpFactory.validateLoginPage.mockReturnValueOnce( + MockSpcpService.validateLoginPage.mockReturnValueOnce( ok({ isValid: false, errorCode: MOCK_ERROR_CODE }), ) await MyInfoController.checkMyInfoEServiceId(mockReq, mockRes, jest.fn()) - expect(MockSpcpFactory.createRedirectUrl).toHaveBeenCalledWith( + expect(MockSpcpService.createRedirectUrl).toHaveBeenCalledWith( AuthType.SP, MOCK_MYINFO_FORM._id, MOCK_MYINFO_FORM.esrvcId, ) - expect(MockSpcpFactory.fetchLoginPage).toHaveBeenCalledWith( + expect(MockSpcpService.fetchLoginPage).toHaveBeenCalledWith( MOCK_REDIRECT_URL, ) - expect(MockSpcpFactory.validateLoginPage).toHaveBeenCalledWith( + expect(MockSpcpService.validateLoginPage).toHaveBeenCalledWith( MOCK_LOGIN_HTML, ) expect(mockRes.status).toHaveBeenCalledWith(StatusCodes.OK) @@ -210,24 +210,24 @@ describe('MyInfoController', () => { }) it('should return 503 when FetchLoginPageError occurs', async () => { - MockSpcpFactory.createRedirectUrl.mockReturnValueOnce( + MockSpcpService.createRedirectUrl.mockReturnValueOnce( ok(MOCK_REDIRECT_URL), ) - MockSpcpFactory.fetchLoginPage.mockReturnValueOnce( + MockSpcpService.fetchLoginPage.mockReturnValueOnce( errAsync(new FetchLoginPageError()), ) await MyInfoController.checkMyInfoEServiceId(mockReq, mockRes, jest.fn()) - expect(MockSpcpFactory.createRedirectUrl).toHaveBeenCalledWith( + expect(MockSpcpService.createRedirectUrl).toHaveBeenCalledWith( AuthType.SP, MOCK_MYINFO_FORM._id, MOCK_MYINFO_FORM.esrvcId, ) - expect(MockSpcpFactory.fetchLoginPage).toHaveBeenCalledWith( + expect(MockSpcpService.fetchLoginPage).toHaveBeenCalledWith( MOCK_REDIRECT_URL, ) - expect(MockSpcpFactory.validateLoginPage).not.toHaveBeenCalled() + expect(MockSpcpService.validateLoginPage).not.toHaveBeenCalled() expect(mockRes.status).toHaveBeenCalledWith( StatusCodes.SERVICE_UNAVAILABLE, ) @@ -237,27 +237,27 @@ describe('MyInfoController', () => { }) it('should return 503 when LoginPageValidationError occurs', async () => { - MockSpcpFactory.createRedirectUrl.mockReturnValueOnce( + MockSpcpService.createRedirectUrl.mockReturnValueOnce( ok(MOCK_REDIRECT_URL), ) - MockSpcpFactory.fetchLoginPage.mockReturnValueOnce( + MockSpcpService.fetchLoginPage.mockReturnValueOnce( okAsync(MOCK_LOGIN_HTML), ) - MockSpcpFactory.validateLoginPage.mockReturnValueOnce( + MockSpcpService.validateLoginPage.mockReturnValueOnce( err(new LoginPageValidationError()), ) await MyInfoController.checkMyInfoEServiceId(mockReq, mockRes, jest.fn()) - expect(MockSpcpFactory.createRedirectUrl).toHaveBeenCalledWith( + expect(MockSpcpService.createRedirectUrl).toHaveBeenCalledWith( AuthType.SP, MOCK_MYINFO_FORM._id, MOCK_MYINFO_FORM.esrvcId, ) - expect(MockSpcpFactory.fetchLoginPage).toHaveBeenCalledWith( + expect(MockSpcpService.fetchLoginPage).toHaveBeenCalledWith( MOCK_REDIRECT_URL, ) - expect(MockSpcpFactory.validateLoginPage).toHaveBeenCalledWith( + expect(MockSpcpService.validateLoginPage).toHaveBeenCalledWith( MOCK_LOGIN_HTML, ) expect(mockRes.status).toHaveBeenCalledWith( @@ -288,12 +288,12 @@ describe('MyInfoController', () => { MockFormService.retrieveFullFormById.mockReturnValue( okAsync(MOCK_MYINFO_FORM as IPopulatedForm), ) - MockMyInfoFactory.parseMyInfoRelayState.mockReturnValue(ok(mockState)) - MockMyInfoFactory.retrieveAccessToken.mockReturnValue( + MockMyInfoService.parseMyInfoRelayState.mockReturnValue(ok(mockState)) + MockMyInfoService.retrieveAccessToken.mockReturnValue( okAsync(MOCK_ACCESS_TOKEN), ) // Return value is ignored - MockBillingFactory.recordLoginByForm.mockReturnValue( + MockBillingService.recordLoginByForm.mockReturnValue( okAsync({} as unknown as ILoginSchema), ) }) @@ -304,10 +304,10 @@ describe('MyInfoController', () => { expect(MockFormService.retrieveFullFormById).toHaveBeenCalledWith( MOCK_MYINFO_FORM._id, ) - expect(MockMyInfoFactory.retrieveAccessToken).toHaveBeenCalledWith( + expect(MockMyInfoService.retrieveAccessToken).toHaveBeenCalledWith( MOCK_AUTH_CODE, ) - expect(MockBillingFactory.recordLoginByForm).toHaveBeenCalledWith( + expect(MockBillingService.recordLoginByForm).toHaveBeenCalledWith( MOCK_MYINFO_FORM, ) expect(mockRes.cookie).toHaveBeenCalledWith( @@ -332,8 +332,8 @@ describe('MyInfoController', () => { expect(MockFormService.retrieveFullFormById).toHaveBeenCalledWith( MOCK_MYINFO_FORM._id, ) - expect(MockMyInfoFactory.retrieveAccessToken).not.toHaveBeenCalled() - expect(MockBillingFactory.recordLoginByForm).not.toHaveBeenCalled() + expect(MockMyInfoService.retrieveAccessToken).not.toHaveBeenCalled() + expect(MockBillingService.recordLoginByForm).not.toHaveBeenCalled() expect(mockRes.cookie).not.toHaveBeenCalled() expect(mockRes.redirect).toHaveBeenCalledWith(`/`) }) @@ -352,8 +352,8 @@ describe('MyInfoController', () => { expect(MockFormService.retrieveFullFormById).toHaveBeenCalledWith( MOCK_MYINFO_FORM._id, ) - expect(MockMyInfoFactory.retrieveAccessToken).not.toHaveBeenCalled() - expect(MockBillingFactory.recordLoginByForm).not.toHaveBeenCalled() + expect(MockMyInfoService.retrieveAccessToken).not.toHaveBeenCalled() + expect(MockBillingService.recordLoginByForm).not.toHaveBeenCalled() expect(mockRes.cookie).toHaveBeenCalledWith( MYINFO_COOKIE_NAME, { @@ -378,8 +378,8 @@ describe('MyInfoController', () => { expect(MockFormService.retrieveFullFormById).toHaveBeenCalledWith( MOCK_MYINFO_FORM._id, ) - expect(MockMyInfoFactory.retrieveAccessToken).not.toHaveBeenCalled() - expect(MockBillingFactory.recordLoginByForm).not.toHaveBeenCalled() + expect(MockMyInfoService.retrieveAccessToken).not.toHaveBeenCalled() + expect(MockBillingService.recordLoginByForm).not.toHaveBeenCalled() expect(mockRes.cookie).toHaveBeenCalledWith( MYINFO_COOKIE_NAME, { @@ -391,7 +391,7 @@ describe('MyInfoController', () => { }) it('should set error cookie and redirect to form when access token is invalid', async () => { - MockMyInfoFactory.retrieveAccessToken.mockReturnValue( + MockMyInfoService.retrieveAccessToken.mockReturnValue( errAsync(new MyInfoFetchError()), ) @@ -400,10 +400,10 @@ describe('MyInfoController', () => { expect(MockFormService.retrieveFullFormById).toHaveBeenCalledWith( MOCK_MYINFO_FORM._id, ) - expect(MockMyInfoFactory.retrieveAccessToken).toHaveBeenCalledWith( + expect(MockMyInfoService.retrieveAccessToken).toHaveBeenCalledWith( MOCK_AUTH_CODE, ) - expect(MockBillingFactory.recordLoginByForm).not.toHaveBeenCalled() + expect(MockBillingService.recordLoginByForm).not.toHaveBeenCalled() expect(mockRes.cookie).toHaveBeenCalledWith( MYINFO_COOKIE_NAME, { @@ -415,7 +415,7 @@ describe('MyInfoController', () => { }) it('should set error cookie and redirect to form when recording login is unsuccessful', async () => { - MockBillingFactory.recordLoginByForm.mockReturnValue( + MockBillingService.recordLoginByForm.mockReturnValue( errAsync(new DatabaseError()), ) @@ -424,10 +424,10 @@ describe('MyInfoController', () => { expect(MockFormService.retrieveFullFormById).toHaveBeenCalledWith( MOCK_MYINFO_FORM._id, ) - expect(MockMyInfoFactory.retrieveAccessToken).toHaveBeenCalledWith( + expect(MockMyInfoService.retrieveAccessToken).toHaveBeenCalledWith( MOCK_AUTH_CODE, ) - expect(MockBillingFactory.recordLoginByForm).toHaveBeenCalledWith( + expect(MockBillingService.recordLoginByForm).toHaveBeenCalledWith( MOCK_MYINFO_FORM, ) expect(mockRes.cookie).toHaveBeenCalledWith( diff --git a/src/app/modules/myinfo/__tests__/myinfo.factory.spec.ts b/src/app/modules/myinfo/__tests__/myinfo.factory.spec.ts deleted file mode 100644 index 7f63539a37..0000000000 --- a/src/app/modules/myinfo/__tests__/myinfo.factory.spec.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { mocked } from 'ts-jest/utils' - -import config from 'src/app/config/config' -import { ISpcpMyInfo } from 'src/app/config/feature-manager' -import { Environment, IPopulatedForm } from 'src/types' - -import { MyInfoData } from '../myinfo.adapter' -import { createMyInfoFactory } from '../myinfo.factory' -import * as MyInfoServiceModule from '../myinfo.service' -import { IMyInfoRedirectURLArgs } from '../myinfo.types' - -import { MOCK_APP_URL, MOCK_NODE_ENV } from './myinfo.test.constants' - -jest.mock('../myinfo.service', () => ({ - MyInfoService: jest.fn(), -})) -const MockMyInfoService = mocked(MyInfoServiceModule, true) -jest.mock('src/app/config/config') -const MockConfig = mocked(config, true) -MockConfig.nodeEnv = MOCK_NODE_ENV as Environment -MockConfig.app = { - title: '', - description: '', - appUrl: MOCK_APP_URL, - keywords: '', - images: [''], - twitterImage: '', -} - -describe('myinfo.factory', () => { - afterEach(() => jest.clearAllMocks()) - it('should return error functions when isEnabled is false', async () => { - const MyInfoFactory = createMyInfoFactory({ - isEnabled: false, - props: {} as ISpcpMyInfo, - }) - const error = new Error( - 'spcp-myinfo is not activated, but a feature-specific function was called.', - ) - const retrieveAccessTokenResult = await MyInfoFactory.retrieveAccessToken( - '', - ) - const createRedirectURLResult = MyInfoFactory.createRedirectURL( - {} as IMyInfoRedirectURLArgs, - ) - const parseMyInfoRelayStateResult = MyInfoFactory.parseMyInfoRelayState('') - const extractUinFinResult = MyInfoFactory.extractUinFin('') - const getMyInfoDataForFormResult = await MyInfoFactory.getMyInfoDataForForm( - {} as unknown as IPopulatedForm, - {}, - ) - const prefillAndSaveMyInfoFieldsResult = - await MyInfoFactory.prefillAndSaveMyInfoFields('', {} as MyInfoData, []) - const saveMyInfoHashesResult = await MyInfoFactory.saveMyInfoHashes( - '', - '', - [], - ) - const fetchMyInfoHashesResult = await MyInfoFactory.fetchMyInfoHashes( - '', - '', - ) - const checkMyInfoHashesResult = await MyInfoFactory.checkMyInfoHashes( - [], - {}, - ) - expect(retrieveAccessTokenResult._unsafeUnwrapErr()).toEqual(error) - expect(parseMyInfoRelayStateResult._unsafeUnwrapErr()).toEqual(error) - expect(createRedirectURLResult._unsafeUnwrapErr()).toEqual(error) - expect(extractUinFinResult._unsafeUnwrapErr()).toEqual(error) - expect(getMyInfoDataForFormResult._unsafeUnwrapErr()).toEqual(error) - expect(prefillAndSaveMyInfoFieldsResult._unsafeUnwrapErr()).toEqual(error) - expect(saveMyInfoHashesResult._unsafeUnwrapErr()).toEqual(error) - expect(fetchMyInfoHashesResult._unsafeUnwrapErr()).toEqual(error) - expect(checkMyInfoHashesResult._unsafeUnwrapErr()).toEqual(error) - }) - - it('should return error functions when props is falsey', async () => { - const MyInfoFactory = createMyInfoFactory({ - isEnabled: true, - props: undefined, - }) - const error = new Error( - 'spcp-myinfo is not activated, but a feature-specific function was called.', - ) - const retrieveAccessTokenResult = await MyInfoFactory.retrieveAccessToken( - '', - ) - const createRedirectURLResult = MyInfoFactory.createRedirectURL( - {} as IMyInfoRedirectURLArgs, - ) - const parseMyInfoRelayStateResult = MyInfoFactory.parseMyInfoRelayState('') - const extractUinFinResult = MyInfoFactory.extractUinFin('') - const getMyInfoDataForFormResult = await MyInfoFactory.getMyInfoDataForForm( - {} as unknown as IPopulatedForm, - {}, - ) - const prefillAndSaveMyInfoFieldsResult = - await MyInfoFactory.prefillAndSaveMyInfoFields('', {} as MyInfoData, []) - const saveMyInfoHashesResult = await MyInfoFactory.saveMyInfoHashes( - '', - '', - [], - ) - const fetchMyInfoHashesResult = await MyInfoFactory.fetchMyInfoHashes( - '', - '', - ) - const checkMyInfoHashesResult = await MyInfoFactory.checkMyInfoHashes( - [], - {}, - ) - expect(retrieveAccessTokenResult._unsafeUnwrapErr()).toEqual(error) - expect(parseMyInfoRelayStateResult._unsafeUnwrapErr()).toEqual(error) - expect(createRedirectURLResult._unsafeUnwrapErr()).toEqual(error) - expect(extractUinFinResult._unsafeUnwrapErr()).toEqual(error) - expect(getMyInfoDataForFormResult._unsafeUnwrapErr()).toEqual(error) - expect(prefillAndSaveMyInfoFieldsResult._unsafeUnwrapErr()).toEqual(error) - expect(saveMyInfoHashesResult._unsafeUnwrapErr()).toEqual(error) - expect(fetchMyInfoHashesResult._unsafeUnwrapErr()).toEqual(error) - expect(checkMyInfoHashesResult._unsafeUnwrapErr()).toEqual(error) - }) - - it('should call the MyInfoService constructor when isEnabled is true and props is truthy', () => { - const mockProps = { - myInfoClientMode: 'mock1', - myInfoKeyPath: 'mock2', - spCookieMaxAge: 200, - spEsrvcId: 'mock3', - } as unknown as ISpcpMyInfo - createMyInfoFactory({ - isEnabled: true, - props: mockProps, - }) - - expect(MockMyInfoService.MyInfoService).toHaveBeenCalledWith({ - spcpMyInfoConfig: mockProps, - nodeEnv: MOCK_NODE_ENV, - appUrl: MOCK_APP_URL, - }) - }) -}) diff --git a/src/app/modules/myinfo/__tests__/myinfo.service.spec.ts b/src/app/modules/myinfo/__tests__/myinfo.service.spec.ts index 59d08aab77..ec94358ce6 100644 --- a/src/app/modules/myinfo/__tests__/myinfo.service.spec.ts +++ b/src/app/modules/myinfo/__tests__/myinfo.service.spec.ts @@ -1,10 +1,11 @@ +import { MyInfoGovClient } from '@opengovsg/myinfo-gov-client' import bcrypt from 'bcrypt' import { ObjectId } from 'bson-ext' import mongoose from 'mongoose' import { mocked } from 'ts-jest/utils' import { v4 as uuidv4 } from 'uuid' -import { MyInfoService } from 'src/app/modules/myinfo/myinfo.service' +import { MyInfoServiceClass } from 'src/app/modules/myinfo/myinfo.service' import getMyInfoHashModel from 'src/app/modules/myinfo/myinfo_hash.model' import { ProcessedFieldResponse } from 'src/app/modules/submission/submission.types' import { @@ -52,34 +53,35 @@ import { const MyInfoHash = getMyInfoHashModel(mongoose) -const mockGetPerson = jest.fn() -const mockCreateRedirectURL = jest.fn() -const mockGetAccessToken = jest.fn() -const mockExtractUinFin = jest.fn() - -jest.mock('@opengovsg/myinfo-gov-client', () => ({ - MyInfoGovClient: jest.fn().mockImplementation(() => ({ - getPerson: mockGetPerson, - createRedirectURL: mockCreateRedirectURL, - getAccessToken: mockGetAccessToken, - extractUinFin: mockExtractUinFin, - })), - MyInfoMode: jest.requireActual('@opengovsg/myinfo-gov-client').MyInfoMode, - MyInfoSource: jest.requireActual('@opengovsg/myinfo-gov-client').MyInfoSource, - MyInfoAddressType: jest.requireActual('@opengovsg/myinfo-gov-client') - .MyInfoAddressType, - MyInfoAttribute: jest.requireActual('@opengovsg/myinfo-gov-client') - .MyInfoAttribute, -})) +jest.mock('@opengovsg/myinfo-gov-client') +const MockMyInfoGovClient = mocked(MyInfoGovClient, true) jest.mock('bcrypt') const MockBcrypt = mocked(bcrypt, true) -describe('MyInfoService', () => { - let myInfoService = new MyInfoService(MOCK_SERVICE_PARAMS) +describe('MyInfoServiceClass', () => { + let myInfoService: MyInfoServiceClass = new MyInfoServiceClass( + MOCK_SERVICE_PARAMS, + ) + const mockGetPerson = jest.fn() + const mockCreateRedirectURL = jest.fn() + const mockGetAccessToken = jest.fn() + const mockExtractUinFin = jest.fn() beforeAll(async () => await dbHandler.connect()) - beforeEach(() => jest.clearAllMocks()) + beforeEach(() => { + jest.clearAllMocks() + MockMyInfoGovClient.mockImplementation( + () => + ({ + getPerson: mockGetPerson, + createRedirectURL: mockCreateRedirectURL, + getAccessToken: mockGetAccessToken, + extractUinFin: mockExtractUinFin, + } as unknown as MyInfoGovClient), + ) + myInfoService = new MyInfoServiceClass(MOCK_SERVICE_PARAMS) + }) afterEach(async () => await dbHandler.clearDatabase()) afterAll(async () => await dbHandler.closeDatabase()) @@ -99,15 +101,12 @@ describe('MyInfoService', () => { requestedAttributes: MOCK_REQUESTED_ATTRS, }) - const actualArgs = mockCreateRedirectURL.mock.calls[0][0] - const actualParsedRelayState = JSON.parse(actualArgs.relayState) - - expect(actualArgs.purpose).toBe(MYINFO_CONSENT_PAGE_PURPOSE) - expect(actualParsedRelayState.formId).toBe(MOCK_FORM_ID) - expect(actualArgs.requestedAttributes).toEqual( - expect.arrayContaining(MOCK_REQUESTED_ATTRS), - ) - expect(actualArgs.singpassEserviceId).toEqual(MOCK_ESRVC_ID) + expect(mockCreateRedirectURL).toHaveBeenCalledWith({ + purpose: MYINFO_CONSENT_PAGE_PURPOSE, + relayState: expect.stringContaining(MOCK_FORM_ID), + requestedAttributes: expect.arrayContaining(MOCK_REQUESTED_ATTRS), + singpassEserviceId: MOCK_ESRVC_ID, + }) expect(result._unsafeUnwrap()).toBe(MOCK_REDIRECT_URL) }) }) @@ -157,7 +156,7 @@ describe('MyInfoService', () => { describe('retrieveAccessToken', () => { beforeEach(() => { - myInfoService = new MyInfoService(MOCK_SERVICE_PARAMS) + myInfoService = new MyInfoServiceClass(MOCK_SERVICE_PARAMS) }) it('should call MyInfoGovClient.getAccessToken with the correct parameters', async () => { @@ -383,7 +382,7 @@ describe('MyInfoService', () => { describe('getMyInfoDataForForm', () => { // NOTE: Mocks the underlying circuit breaker implementation to avoid network calls beforeEach(() => { - myInfoService = new MyInfoService(MOCK_SERVICE_PARAMS) + myInfoService = new MyInfoServiceClass(MOCK_SERVICE_PARAMS) }) it('should return myInfo data when the provided form and cookie is valid', async () => { diff --git a/src/app/modules/myinfo/myinfo.controller.ts b/src/app/modules/myinfo/myinfo.controller.ts index e048aab386..d009a33bf3 100644 --- a/src/app/modules/myinfo/myinfo.controller.ts +++ b/src/app/modules/myinfo/myinfo.controller.ts @@ -5,13 +5,13 @@ import { AuthType } from '../../../types' import { PublicFormAuthValidateEsrvcIdDto } from '../../../types/api' import { createLoggerWithLabel } from '../../config/logger' import { createReqMeta } from '../../utils/request' -import { BillingFactory } from '../billing/billing.factory' +import * as BillingService from '../billing/billing.service' import { ControllerHandler } from '../core/core.types' import * as FormService from '../form/form.service' -import { SpcpFactory } from '../spcp/spcp.factory' +import { SpcpService } from '../spcp/spcp.service' import { MYINFO_COOKIE_NAME, MYINFO_COOKIE_OPTIONS } from './myinfo.constants' -import { MyInfoFactory } from './myinfo.factory' +import { MyInfoService } from './myinfo.service' import { MyInfoCookiePayload, MyInfoCookieState } from './myinfo.types' import { mapEServiceIdCheckError, @@ -49,7 +49,7 @@ export const respondWithRedirectURL: ControllerHandler< return FormService.retrieveFormById(formId) .andThen((form) => validateMyInfoForm(form)) .andThen((form) => - MyInfoFactory.createRedirectURL({ + MyInfoService.createRedirectURL({ formEsrvcId: form.esrvcId, formId, requestedAttributes: form.getUniqueMyInfoAttrs(), @@ -106,10 +106,10 @@ export const checkMyInfoEServiceId: ControllerHandler< return FormService.retrieveFormById(formId) .andThen((form) => validateMyInfoForm(form)) .andThen((form) => - SpcpFactory.createRedirectUrl(AuthType.SP, formId, form.esrvcId), + SpcpService.createRedirectUrl(AuthType.SP, formId, form.esrvcId), ) - .andThen(SpcpFactory.fetchLoginPage) - .andThen(SpcpFactory.validateLoginPage) + .andThen(SpcpService.fetchLoginPage) + .andThen(SpcpService.validateLoginPage) .map((result) => res.status(StatusCodes.OK).json(result)) .mapErr((error) => { logger.error({ @@ -184,7 +184,7 @@ export const loginToMyInfo: ControllerHandler< action: 'loginToMyInfo', state, } - const parseStateResult = MyInfoFactory.parseMyInfoRelayState(state) + const parseStateResult = MyInfoService.parseMyInfoRelayState(state) if (parseStateResult.isErr()) { logger.error({ message: 'Invalid MyInfo login query parameters', @@ -239,7 +239,7 @@ export const loginToMyInfo: ControllerHandler< } // Consent flow successful, hence code is present - const accessTokenResult = await MyInfoFactory.retrieveAccessToken( + const accessTokenResult = await MyInfoService.retrieveAccessToken( req.query.code, ) if (accessTokenResult.isErr()) { @@ -255,7 +255,7 @@ export const loginToMyInfo: ControllerHandler< // Once access token is retrieved, the request is assured to be legitimate, // so add the login - return BillingFactory.recordLoginByForm(form) + return BillingService.recordLoginByForm(form) .map(() => { const cookiePayload: MyInfoCookiePayload = { accessToken, diff --git a/src/app/modules/myinfo/myinfo.factory.ts b/src/app/modules/myinfo/myinfo.factory.ts deleted file mode 100644 index a62fe99902..0000000000 --- a/src/app/modules/myinfo/myinfo.factory.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { LeanDocument } from 'mongoose' -import { err, errAsync, Result, ResultAsync } from 'neverthrow' - -import { - IFieldSchema, - IHashes, - IMyInfoHashSchema, - IPopulatedForm, - IPossiblyPrefilledField, -} from '../../../types' -import config from '../../config/config' -import FeatureManager, { - FeatureNames, - RegisteredFeature, -} from '../../config/feature-manager' -import { DatabaseError, MissingFeatureError } from '../core/core.errors' -import { - AuthTypeMismatchError, - FormAuthNoEsrvcIdError, -} from '../form/form.errors' -import { ProcessedFieldResponse } from '../submission/submission.types' - -import { MyInfoData } from './myinfo.adapter' -import { - MyInfoCircuitBreakerError, - MyInfoCookieAccessError, - MyInfoCookieStateError, - MyInfoFetchError, - MyInfoHashDidNotMatchError, - MyInfoHashingError, - MyInfoInvalidAccessTokenError, - MyInfoMissingAccessTokenError, - MyInfoMissingHashError, - MyInfoParseRelayStateError, -} from './myinfo.errors' -import { MyInfoService } from './myinfo.service' -import { IMyInfoRedirectURLArgs, MyInfoParsedRelayState } from './myinfo.types' - -interface IMyInfoFactory { - createRedirectURL: ( - params: IMyInfoRedirectURLArgs, - ) => Result - retrieveAccessToken: ( - authCode: string, - ) => ResultAsync - parseMyInfoRelayState: ( - relayState: string, - ) => Result< - MyInfoParsedRelayState, - MyInfoParseRelayStateError | MissingFeatureError - > - prefillAndSaveMyInfoFields: ( - formId: string, - myInfoData: MyInfoData, - currFormFields: LeanDocument, - ) => ResultAsync< - IPossiblyPrefilledField[], - MyInfoHashingError | DatabaseError - > - saveMyInfoHashes: ( - uinFin: string, - formId: string, - prefilledFormFields: IPossiblyPrefilledField[], - ) => ResultAsync< - IMyInfoHashSchema | null, - MyInfoHashingError | DatabaseError | MissingFeatureError - > - fetchMyInfoHashes: ( - uinFin: string, - formId: string, - ) => ResultAsync< - IHashes, - DatabaseError | MyInfoMissingHashError | MissingFeatureError - > - checkMyInfoHashes: ( - responses: ProcessedFieldResponse[], - hashes: IHashes, - ) => ResultAsync< - Set, - MyInfoHashingError | MyInfoHashDidNotMatchError | MissingFeatureError - > - extractUinFin: ( - accessToken: string, - ) => Result - - getMyInfoDataForForm: ( - form: IPopulatedForm, - cookies: Record, - ) => ResultAsync< - MyInfoData, - | MyInfoMissingAccessTokenError - | MyInfoCookieStateError - | FormAuthNoEsrvcIdError - | AuthTypeMismatchError - | MyInfoCircuitBreakerError - | MyInfoFetchError - | MissingFeatureError - | MyInfoCookieAccessError - > -} - -export const createMyInfoFactory = ({ - isEnabled, - props, -}: RegisteredFeature): IMyInfoFactory => { - if (!isEnabled || !props) { - const error = new MissingFeatureError(FeatureNames.SpcpMyInfo) - return { - retrieveAccessToken: () => errAsync(error), - prefillAndSaveMyInfoFields: () => errAsync(error), - saveMyInfoHashes: () => errAsync(error), - fetchMyInfoHashes: () => errAsync(error), - checkMyInfoHashes: () => errAsync(error), - createRedirectURL: () => err(error), - parseMyInfoRelayState: () => err(error), - extractUinFin: () => err(error), - getMyInfoDataForForm: () => errAsync(error), - } - } - return new MyInfoService({ - spcpMyInfoConfig: props, - nodeEnv: config.nodeEnv, - appUrl: config.app.appUrl, - }) -} - -const myInfoFeature = FeatureManager.get(FeatureNames.SpcpMyInfo) -export const MyInfoFactory = createMyInfoFactory(myInfoFeature) diff --git a/src/app/modules/myinfo/myinfo.service.ts b/src/app/modules/myinfo/myinfo.service.ts index 06d753aa95..a5a8eba05e 100644 --- a/src/app/modules/myinfo/myinfo.service.ts +++ b/src/app/modules/myinfo/myinfo.service.ts @@ -19,6 +19,8 @@ import { IPossiblyPrefilledField, MyInfoAttribute, } from '../../../types' +import config from '../../config/config' +import { spcpMyInfoConfig } from '../../config/feature-manager/spcp-myinfo.config' import { createLoggerWithLabel } from '../../config/logger' import { DatabaseError, MissingFeatureError } from '../core/core.errors' import { @@ -72,7 +74,11 @@ const BREAKER_PARAMS = { volumeThreshold: 5, // min number of requests within statistical window before breaker trips } -export class MyInfoService { +/** + * Class for managing MyInfo-related functionality. + * Exported for testing. + */ +export class MyInfoServiceClass { /** * Instance of MyInfoGovClient configured with Form credentials. */ @@ -519,3 +525,9 @@ export class MyInfoService { ) } } + +export const MyInfoService = new MyInfoServiceClass({ + spcpMyInfoConfig, + appUrl: config.app.appUrl, + nodeEnv: config.nodeEnv, +}) diff --git a/src/app/modules/myinfo/myinfo.types.ts b/src/app/modules/myinfo/myinfo.types.ts index bea4ab40db..dbc6c4180e 100644 --- a/src/app/modules/myinfo/myinfo.types.ts +++ b/src/app/modules/myinfo/myinfo.types.ts @@ -5,7 +5,7 @@ import { IMyInfo, MyInfoAttribute, } from '../../../types' -import { ISpcpMyInfo } from '../../config/feature-manager' +import { ISpcpMyInfo } from '../../config/feature-manager/spcp-myinfo.config' import { ProcessedFieldResponse } from '../submission/submission.types' export interface IMyInfoServiceConfig { diff --git a/src/app/modules/spcp/__tests__/spcp.controller.spec.ts b/src/app/modules/spcp/__tests__/spcp.controller.spec.ts index 31c07bdc37..c43e20e1dd 100644 --- a/src/app/modules/spcp/__tests__/spcp.controller.spec.ts +++ b/src/app/modules/spcp/__tests__/spcp.controller.spec.ts @@ -8,7 +8,7 @@ import { AuthType } from 'src/types' import expressHandler from 'tests/unit/backend/helpers/jest-express' -import { BillingFactory } from '../../billing/billing.factory' +import * as BillingService from '../../billing/billing.service' import { ApplicationError, DatabaseError } from '../../core/core.errors' import { FormNotFoundError } from '../../form/form.errors' import * as SpcpController from '../spcp.controller' @@ -20,7 +20,7 @@ import { MissingAttributesError, RetrieveAttributesError, } from '../spcp.errors' -import { SpcpFactory } from '../spcp.factory' +import { SpcpService } from '../spcp.service' import { MOCK_ATTRIBUTES, @@ -42,10 +42,10 @@ import { MOCK_TARGET, } from './spcp.test.constants' -jest.mock('../spcp.factory') -const MockSpcpFactory = mocked(SpcpFactory, true) -jest.mock('../../billing/billing.factory') -const MockBillingFactory = mocked(BillingFactory, true) +jest.mock('../spcp.service') +const MockSpcpService = mocked(SpcpService, true) +jest.mock('../../billing/billing.service') +const MockBillingService = mocked(BillingService, true) jest.mock('src/app/modules/form/form.service') const MockFormService = mocked(FormService, true) jest.mock('src/app/config/config') @@ -79,13 +79,13 @@ describe('spcp.controller', () => { describe('handleRedirect', () => { it('should return the redirect URL correctly', () => { - MockSpcpFactory.createRedirectUrl.mockReturnValueOnce( + MockSpcpService.createRedirectUrl.mockReturnValueOnce( ok(MOCK_REDIRECT_URL), ) SpcpController.handleRedirect(MOCK_REDIRECT_REQ, MOCK_RESPONSE, jest.fn()) - expect(MockSpcpFactory.createRedirectUrl).toHaveBeenCalledWith( + expect(MockSpcpService.createRedirectUrl).toHaveBeenCalledWith( AuthType.SP, MOCK_RELAY_STATE, MOCK_ESRVCID, @@ -97,13 +97,13 @@ describe('spcp.controller', () => { }) it('should return 500 if auth client throws an error', () => { - MockSpcpFactory.createRedirectUrl.mockReturnValueOnce( + MockSpcpService.createRedirectUrl.mockReturnValueOnce( err(new CreateRedirectUrlError()), ) SpcpController.handleRedirect(MOCK_REDIRECT_REQ, MOCK_RESPONSE, jest.fn()) - expect(MockSpcpFactory.createRedirectUrl).toHaveBeenCalledWith( + expect(MockSpcpService.createRedirectUrl).toHaveBeenCalledWith( AuthType.SP, MOCK_RELAY_STATE, MOCK_ESRVCID, @@ -117,13 +117,13 @@ describe('spcp.controller', () => { describe('handleValidate', () => { it('should return 200 with isValid true if validation passes', async () => { - MockSpcpFactory.createRedirectUrl.mockReturnValueOnce( + MockSpcpService.createRedirectUrl.mockReturnValueOnce( ok(MOCK_REDIRECT_URL), ) - MockSpcpFactory.fetchLoginPage.mockReturnValueOnce( + MockSpcpService.fetchLoginPage.mockReturnValueOnce( okAsync(MOCK_LOGIN_HTML), ) - MockSpcpFactory.validateLoginPage.mockReturnValueOnce( + MockSpcpService.validateLoginPage.mockReturnValueOnce( ok({ isValid: true }), ) @@ -133,15 +133,15 @@ describe('spcp.controller', () => { jest.fn(), ) - expect(MockSpcpFactory.createRedirectUrl).toHaveBeenCalledWith( + expect(MockSpcpService.createRedirectUrl).toHaveBeenCalledWith( AuthType.SP, MOCK_TARGET, MOCK_ESRVCID, ) - expect(MockSpcpFactory.fetchLoginPage).toHaveBeenCalledWith( + expect(MockSpcpService.fetchLoginPage).toHaveBeenCalledWith( MOCK_REDIRECT_URL, ) - expect(MockSpcpFactory.validateLoginPage).toHaveBeenCalledWith( + expect(MockSpcpService.validateLoginPage).toHaveBeenCalledWith( MOCK_LOGIN_HTML, ) expect(MOCK_RESPONSE.status).toHaveBeenCalledWith(200) @@ -151,13 +151,13 @@ describe('spcp.controller', () => { }) it('should return 200 with isValid false if validation fails', async () => { - MockSpcpFactory.createRedirectUrl.mockReturnValueOnce( + MockSpcpService.createRedirectUrl.mockReturnValueOnce( ok(MOCK_REDIRECT_URL), ) - MockSpcpFactory.fetchLoginPage.mockReturnValueOnce( + MockSpcpService.fetchLoginPage.mockReturnValueOnce( okAsync(MOCK_LOGIN_HTML), ) - MockSpcpFactory.validateLoginPage.mockReturnValueOnce( + MockSpcpService.validateLoginPage.mockReturnValueOnce( ok({ isValid: false, errorCode: MOCK_ERROR_CODE }), ) @@ -167,15 +167,15 @@ describe('spcp.controller', () => { jest.fn(), ) - expect(MockSpcpFactory.createRedirectUrl).toHaveBeenCalledWith( + expect(MockSpcpService.createRedirectUrl).toHaveBeenCalledWith( AuthType.SP, MOCK_TARGET, MOCK_ESRVCID, ) - expect(MockSpcpFactory.fetchLoginPage).toHaveBeenCalledWith( + expect(MockSpcpService.fetchLoginPage).toHaveBeenCalledWith( MOCK_REDIRECT_URL, ) - expect(MockSpcpFactory.validateLoginPage).toHaveBeenCalledWith( + expect(MockSpcpService.validateLoginPage).toHaveBeenCalledWith( MOCK_LOGIN_HTML, ) expect(MOCK_RESPONSE.status).toHaveBeenCalledWith(200) @@ -186,10 +186,10 @@ describe('spcp.controller', () => { }) it('should return 503 when FetchLoginPageError occurs', async () => { - MockSpcpFactory.createRedirectUrl.mockReturnValueOnce( + MockSpcpService.createRedirectUrl.mockReturnValueOnce( ok(MOCK_REDIRECT_URL), ) - MockSpcpFactory.fetchLoginPage.mockReturnValueOnce( + MockSpcpService.fetchLoginPage.mockReturnValueOnce( errAsync(new FetchLoginPageError()), ) @@ -199,15 +199,15 @@ describe('spcp.controller', () => { jest.fn(), ) - expect(MockSpcpFactory.createRedirectUrl).toHaveBeenCalledWith( + expect(MockSpcpService.createRedirectUrl).toHaveBeenCalledWith( AuthType.SP, MOCK_TARGET, MOCK_ESRVCID, ) - expect(MockSpcpFactory.fetchLoginPage).toHaveBeenCalledWith( + expect(MockSpcpService.fetchLoginPage).toHaveBeenCalledWith( MOCK_REDIRECT_URL, ) - expect(MockSpcpFactory.validateLoginPage).not.toHaveBeenCalled() + expect(MockSpcpService.validateLoginPage).not.toHaveBeenCalled() expect(MOCK_RESPONSE.status).toHaveBeenCalledWith(503) expect(MOCK_RESPONSE.json).toHaveBeenCalledWith({ message: 'Failed to contact SingPass. Please try again.', @@ -215,13 +215,13 @@ describe('spcp.controller', () => { }) it('should return 502 when LoginPageValidationError occurs', async () => { - MockSpcpFactory.createRedirectUrl.mockReturnValueOnce( + MockSpcpService.createRedirectUrl.mockReturnValueOnce( ok(MOCK_REDIRECT_URL), ) - MockSpcpFactory.fetchLoginPage.mockReturnValueOnce( + MockSpcpService.fetchLoginPage.mockReturnValueOnce( okAsync(MOCK_LOGIN_HTML), ) - MockSpcpFactory.validateLoginPage.mockReturnValueOnce( + MockSpcpService.validateLoginPage.mockReturnValueOnce( err(new LoginPageValidationError()), ) @@ -231,15 +231,15 @@ describe('spcp.controller', () => { jest.fn(), ) - expect(MockSpcpFactory.createRedirectUrl).toHaveBeenCalledWith( + expect(MockSpcpService.createRedirectUrl).toHaveBeenCalledWith( AuthType.SP, MOCK_TARGET, MOCK_ESRVCID, ) - expect(MockSpcpFactory.fetchLoginPage).toHaveBeenCalledWith( + expect(MockSpcpService.fetchLoginPage).toHaveBeenCalledWith( MOCK_REDIRECT_URL, ) - expect(MockSpcpFactory.validateLoginPage).toHaveBeenCalledWith( + expect(MockSpcpService.validateLoginPage).toHaveBeenCalledWith( MOCK_LOGIN_HTML, ) expect(MOCK_RESPONSE.status).toHaveBeenCalledWith(502) @@ -254,7 +254,7 @@ describe('spcp.controller', () => { const loginHandler = SpcpController.handleLogin(AuthType.SP) beforeEach(() => { - MockSpcpFactory.parseOOBParams.mockReturnValue( + MockSpcpService.parseOOBParams.mockReturnValue( ok({ formId: MOCK_TARGET, destination: MOCK_DESTINATION, @@ -266,20 +266,20 @@ describe('spcp.controller', () => { MockFormService.retrieveFullFormById.mockReturnValue( okAsync(MOCK_SP_FORM), ) - MockSpcpFactory.getSpcpAttributes.mockReturnValue( + MockSpcpService.getSpcpAttributes.mockReturnValue( okAsync(MOCK_ATTRIBUTES), ) - MockSpcpFactory.createJWTPayload.mockReturnValue(ok(MOCK_JWT_PAYLOAD)) - MockSpcpFactory.createJWT.mockReturnValue(ok(MOCK_JWT)) - MockBillingFactory.recordLoginByForm.mockReturnValue( + MockSpcpService.createJWTPayload.mockReturnValue(ok(MOCK_JWT_PAYLOAD)) + MockSpcpService.createJWT.mockReturnValue(ok(MOCK_JWT)) + MockBillingService.recordLoginByForm.mockReturnValue( okAsync(MOCK_LOGIN_DOC), ) - MockSpcpFactory.getCookieSettings.mockReturnValue(MOCK_COOKIE_SETTINGS) + MockSpcpService.getCookieSettings.mockReturnValue(MOCK_COOKIE_SETTINGS) }) it('should set the cookie with the correct params and redirect to the destination', async () => { await loginHandler(MOCK_SP_LOGIN_REQ, MOCK_RESPONSE, jest.fn()) - expect(MockSpcpFactory.parseOOBParams).toHaveBeenCalledWith( + expect(MockSpcpService.parseOOBParams).toHaveBeenCalledWith( MOCK_SP_SAML, MOCK_RELAY_STATE, AuthType.SP, @@ -287,22 +287,22 @@ describe('spcp.controller', () => { expect(MockFormService.retrieveFullFormById).toHaveBeenCalledWith( MOCK_TARGET, ) - expect(MockSpcpFactory.getSpcpAttributes).toHaveBeenCalledWith( + expect(MockSpcpService.getSpcpAttributes).toHaveBeenCalledWith( MOCK_SP_SAML, MOCK_DESTINATION, AuthType.SP, ) - expect(MockSpcpFactory.createJWTPayload).toHaveBeenCalledWith( + expect(MockSpcpService.createJWTPayload).toHaveBeenCalledWith( MOCK_ATTRIBUTES, MOCK_REMEMBER_ME, AuthType.SP, ) - expect(MockSpcpFactory.createJWT).toHaveBeenCalledWith( + expect(MockSpcpService.createJWT).toHaveBeenCalledWith( MOCK_JWT_PAYLOAD, MOCK_COOKIE_AGE, AuthType.SP, ) - expect(MockBillingFactory.recordLoginByForm).toHaveBeenCalledWith( + expect(MockBillingService.recordLoginByForm).toHaveBeenCalledWith( MOCK_SP_FORM, ) expect(MOCK_RESPONSE.cookie).toHaveBeenCalledWith('jwtSp', MOCK_JWT, { @@ -316,11 +316,11 @@ describe('spcp.controller', () => { }) it('should return 400 when params cannot be parsed', async () => { - MockSpcpFactory.parseOOBParams.mockReturnValue( + MockSpcpService.parseOOBParams.mockReturnValue( err(new InvalidOOBParamsError()), ) await loginHandler(MOCK_SP_LOGIN_REQ, MOCK_RESPONSE, jest.fn()) - expect(MockSpcpFactory.parseOOBParams).toHaveBeenCalledWith( + expect(MockSpcpService.parseOOBParams).toHaveBeenCalledWith( MOCK_SP_SAML, MOCK_RELAY_STATE, AuthType.SP, @@ -329,11 +329,11 @@ describe('spcp.controller', () => { expect(MOCK_RESPONSE.cookie).not.toHaveBeenCalled() expect(MOCK_RESPONSE.redirect).not.toHaveBeenCalled() expect(MockFormService.retrieveFullFormById).not.toHaveBeenCalled() - expect(MockSpcpFactory.getSpcpAttributes).not.toHaveBeenCalled() - expect(MockSpcpFactory.createJWTPayload).not.toHaveBeenCalled() - expect(MockSpcpFactory.createJWT).not.toHaveBeenCalled() - expect(MockBillingFactory.recordLoginByForm).not.toHaveBeenCalled() - expect(MockSpcpFactory.getCookieSettings).not.toHaveBeenCalled() + expect(MockSpcpService.getSpcpAttributes).not.toHaveBeenCalled() + expect(MockSpcpService.createJWTPayload).not.toHaveBeenCalled() + expect(MockSpcpService.createJWT).not.toHaveBeenCalled() + expect(MockBillingService.recordLoginByForm).not.toHaveBeenCalled() + expect(MockSpcpService.getCookieSettings).not.toHaveBeenCalled() expect(MOCK_RESPONSE.cookie).not.toHaveBeenCalled() }) @@ -342,7 +342,7 @@ describe('spcp.controller', () => { errAsync(new FormNotFoundError()), ) await loginHandler(MOCK_SP_LOGIN_REQ, MOCK_RESPONSE, jest.fn()) - expect(MockSpcpFactory.parseOOBParams).toHaveBeenCalledWith( + expect(MockSpcpService.parseOOBParams).toHaveBeenCalledWith( MOCK_SP_SAML, MOCK_RELAY_STATE, AuthType.SP, @@ -353,11 +353,11 @@ describe('spcp.controller', () => { expect(MOCK_RESPONSE.sendStatus).toHaveBeenCalledWith(404) expect(MOCK_RESPONSE.cookie).not.toHaveBeenCalled() expect(MOCK_RESPONSE.redirect).not.toHaveBeenCalled() - expect(MockSpcpFactory.getSpcpAttributes).not.toHaveBeenCalled() - expect(MockSpcpFactory.createJWTPayload).not.toHaveBeenCalled() - expect(MockSpcpFactory.createJWT).not.toHaveBeenCalled() - expect(MockBillingFactory.recordLoginByForm).not.toHaveBeenCalled() - expect(MockSpcpFactory.getCookieSettings).not.toHaveBeenCalled() + expect(MockSpcpService.getSpcpAttributes).not.toHaveBeenCalled() + expect(MockSpcpService.createJWTPayload).not.toHaveBeenCalled() + expect(MockSpcpService.createJWT).not.toHaveBeenCalled() + expect(MockBillingService.recordLoginByForm).not.toHaveBeenCalled() + expect(MockSpcpService.getCookieSettings).not.toHaveBeenCalled() expect(MOCK_RESPONSE.cookie).not.toHaveBeenCalled() }) @@ -367,7 +367,7 @@ describe('spcp.controller', () => { okAsync(MOCK_CP_FORM), ) await loginHandler(MOCK_SP_LOGIN_REQ, MOCK_RESPONSE, jest.fn()) - expect(MockSpcpFactory.parseOOBParams).toHaveBeenCalledWith( + expect(MockSpcpService.parseOOBParams).toHaveBeenCalledWith( MOCK_SP_SAML, MOCK_RELAY_STATE, AuthType.SP, @@ -377,19 +377,19 @@ describe('spcp.controller', () => { ) expect(MOCK_RESPONSE.cookie).toHaveBeenCalledWith('isLoginError', true) expect(MOCK_RESPONSE.redirect).toHaveBeenCalledWith(MOCK_DESTINATION) - expect(MockSpcpFactory.getSpcpAttributes).not.toHaveBeenCalled() - expect(MockSpcpFactory.createJWTPayload).not.toHaveBeenCalled() - expect(MockSpcpFactory.createJWT).not.toHaveBeenCalled() - expect(MockBillingFactory.recordLoginByForm).not.toHaveBeenCalled() - expect(MockSpcpFactory.getCookieSettings).not.toHaveBeenCalled() + expect(MockSpcpService.getSpcpAttributes).not.toHaveBeenCalled() + expect(MockSpcpService.createJWTPayload).not.toHaveBeenCalled() + expect(MockSpcpService.createJWT).not.toHaveBeenCalled() + expect(MockBillingService.recordLoginByForm).not.toHaveBeenCalled() + expect(MockSpcpService.getCookieSettings).not.toHaveBeenCalled() }) it('should set isLoginError cookie and redirect when getSpcpAttributes errors', async () => { - MockSpcpFactory.getSpcpAttributes.mockReturnValue( + MockSpcpService.getSpcpAttributes.mockReturnValue( errAsync(new RetrieveAttributesError()), ) await loginHandler(MOCK_SP_LOGIN_REQ, MOCK_RESPONSE, jest.fn()) - expect(MockSpcpFactory.parseOOBParams).toHaveBeenCalledWith( + expect(MockSpcpService.parseOOBParams).toHaveBeenCalledWith( MOCK_SP_SAML, MOCK_RELAY_STATE, AuthType.SP, @@ -397,25 +397,25 @@ describe('spcp.controller', () => { expect(MockFormService.retrieveFullFormById).toHaveBeenCalledWith( MOCK_TARGET, ) - expect(MockSpcpFactory.getSpcpAttributes).toHaveBeenCalledWith( + expect(MockSpcpService.getSpcpAttributes).toHaveBeenCalledWith( MOCK_SP_SAML, MOCK_DESTINATION, AuthType.SP, ) expect(MOCK_RESPONSE.cookie).toHaveBeenCalledWith('isLoginError', true) expect(MOCK_RESPONSE.redirect).toHaveBeenCalledWith(MOCK_DESTINATION) - expect(MockSpcpFactory.createJWTPayload).not.toHaveBeenCalled() - expect(MockSpcpFactory.createJWT).not.toHaveBeenCalled() - expect(MockBillingFactory.recordLoginByForm).not.toHaveBeenCalled() - expect(MockSpcpFactory.getCookieSettings).not.toHaveBeenCalled() + expect(MockSpcpService.createJWTPayload).not.toHaveBeenCalled() + expect(MockSpcpService.createJWT).not.toHaveBeenCalled() + expect(MockBillingService.recordLoginByForm).not.toHaveBeenCalled() + expect(MockSpcpService.getCookieSettings).not.toHaveBeenCalled() }) it('should set isLoginError cookie and redirect when createJWTPayload errors', async () => { - MockSpcpFactory.createJWTPayload.mockReturnValue( + MockSpcpService.createJWTPayload.mockReturnValue( err(new MissingAttributesError()), ) await loginHandler(MOCK_SP_LOGIN_REQ, MOCK_RESPONSE, jest.fn()) - expect(MockSpcpFactory.parseOOBParams).toHaveBeenCalledWith( + expect(MockSpcpService.parseOOBParams).toHaveBeenCalledWith( MOCK_SP_SAML, MOCK_RELAY_STATE, AuthType.SP, @@ -423,27 +423,27 @@ describe('spcp.controller', () => { expect(MockFormService.retrieveFullFormById).toHaveBeenCalledWith( MOCK_TARGET, ) - expect(MockSpcpFactory.getSpcpAttributes).toHaveBeenCalledWith( + expect(MockSpcpService.getSpcpAttributes).toHaveBeenCalledWith( MOCK_SP_SAML, MOCK_DESTINATION, AuthType.SP, ) - expect(MockSpcpFactory.createJWTPayload).toHaveBeenCalledWith( + expect(MockSpcpService.createJWTPayload).toHaveBeenCalledWith( MOCK_ATTRIBUTES, MOCK_REMEMBER_ME, AuthType.SP, ) expect(MOCK_RESPONSE.cookie).toHaveBeenCalledWith('isLoginError', true) expect(MOCK_RESPONSE.redirect).toHaveBeenCalledWith(MOCK_DESTINATION) - expect(MockSpcpFactory.createJWT).not.toHaveBeenCalled() - expect(MockBillingFactory.recordLoginByForm).not.toHaveBeenCalled() - expect(MockSpcpFactory.getCookieSettings).not.toHaveBeenCalled() + expect(MockSpcpService.createJWT).not.toHaveBeenCalled() + expect(MockBillingService.recordLoginByForm).not.toHaveBeenCalled() + expect(MockSpcpService.getCookieSettings).not.toHaveBeenCalled() }) it('should set isLoginError cookie and redirect when createJWT errors', async () => { - MockSpcpFactory.createJWT.mockReturnValue(err(new ApplicationError())) + MockSpcpService.createJWT.mockReturnValue(err(new ApplicationError())) await loginHandler(MOCK_SP_LOGIN_REQ, MOCK_RESPONSE, jest.fn()) - expect(MockSpcpFactory.parseOOBParams).toHaveBeenCalledWith( + expect(MockSpcpService.parseOOBParams).toHaveBeenCalledWith( MOCK_SP_SAML, MOCK_RELAY_STATE, AuthType.SP, @@ -451,33 +451,33 @@ describe('spcp.controller', () => { expect(MockFormService.retrieveFullFormById).toHaveBeenCalledWith( MOCK_TARGET, ) - expect(MockSpcpFactory.getSpcpAttributes).toHaveBeenCalledWith( + expect(MockSpcpService.getSpcpAttributes).toHaveBeenCalledWith( MOCK_SP_SAML, MOCK_DESTINATION, AuthType.SP, ) - expect(MockSpcpFactory.createJWTPayload).toHaveBeenCalledWith( + expect(MockSpcpService.createJWTPayload).toHaveBeenCalledWith( MOCK_ATTRIBUTES, MOCK_REMEMBER_ME, AuthType.SP, ) - expect(MockSpcpFactory.createJWT).toHaveBeenCalledWith( + expect(MockSpcpService.createJWT).toHaveBeenCalledWith( MOCK_JWT_PAYLOAD, MOCK_COOKIE_AGE, AuthType.SP, ) expect(MOCK_RESPONSE.cookie).toHaveBeenCalledWith('isLoginError', true) expect(MOCK_RESPONSE.redirect).toHaveBeenCalledWith(MOCK_DESTINATION) - expect(MockBillingFactory.recordLoginByForm).not.toHaveBeenCalled() - expect(MockSpcpFactory.getCookieSettings).not.toHaveBeenCalled() + expect(MockBillingService.recordLoginByForm).not.toHaveBeenCalled() + expect(MockSpcpService.getCookieSettings).not.toHaveBeenCalled() }) it('should set isLoginError cookie and redirect when recordLoginByForm errors', async () => { - MockBillingFactory.recordLoginByForm.mockReturnValue( + MockBillingService.recordLoginByForm.mockReturnValue( errAsync(new DatabaseError()), ) await loginHandler(MOCK_SP_LOGIN_REQ, MOCK_RESPONSE, jest.fn()) - expect(MockSpcpFactory.parseOOBParams).toHaveBeenCalledWith( + expect(MockSpcpService.parseOOBParams).toHaveBeenCalledWith( MOCK_SP_SAML, MOCK_RELAY_STATE, AuthType.SP, @@ -485,27 +485,27 @@ describe('spcp.controller', () => { expect(MockFormService.retrieveFullFormById).toHaveBeenCalledWith( MOCK_TARGET, ) - expect(MockSpcpFactory.getSpcpAttributes).toHaveBeenCalledWith( + expect(MockSpcpService.getSpcpAttributes).toHaveBeenCalledWith( MOCK_SP_SAML, MOCK_DESTINATION, AuthType.SP, ) - expect(MockSpcpFactory.createJWTPayload).toHaveBeenCalledWith( + expect(MockSpcpService.createJWTPayload).toHaveBeenCalledWith( MOCK_ATTRIBUTES, MOCK_REMEMBER_ME, AuthType.SP, ) - expect(MockSpcpFactory.createJWT).toHaveBeenCalledWith( + expect(MockSpcpService.createJWT).toHaveBeenCalledWith( MOCK_JWT_PAYLOAD, MOCK_COOKIE_AGE, AuthType.SP, ) - expect(MockBillingFactory.recordLoginByForm).toHaveBeenCalledWith( + expect(MockBillingService.recordLoginByForm).toHaveBeenCalledWith( MOCK_SP_FORM, ) expect(MOCK_RESPONSE.cookie).toHaveBeenCalledWith('isLoginError', true) expect(MOCK_RESPONSE.redirect).toHaveBeenCalledWith(MOCK_DESTINATION) - expect(MockSpcpFactory.getCookieSettings).not.toHaveBeenCalled() + expect(MockSpcpService.getCookieSettings).not.toHaveBeenCalled() }) }) @@ -513,7 +513,7 @@ describe('spcp.controller', () => { const loginHandler = SpcpController.handleLogin(AuthType.CP) beforeEach(() => { - MockSpcpFactory.parseOOBParams.mockReturnValue( + MockSpcpService.parseOOBParams.mockReturnValue( ok({ formId: MOCK_TARGET, destination: MOCK_DESTINATION, @@ -525,20 +525,20 @@ describe('spcp.controller', () => { MockFormService.retrieveFullFormById.mockReturnValue( okAsync(MOCK_CP_FORM), ) - MockSpcpFactory.getSpcpAttributes.mockReturnValue( + MockSpcpService.getSpcpAttributes.mockReturnValue( okAsync(MOCK_ATTRIBUTES), ) - MockSpcpFactory.createJWTPayload.mockReturnValue(ok(MOCK_JWT_PAYLOAD)) - MockSpcpFactory.createJWT.mockReturnValue(ok(MOCK_JWT)) - MockBillingFactory.recordLoginByForm.mockReturnValue( + MockSpcpService.createJWTPayload.mockReturnValue(ok(MOCK_JWT_PAYLOAD)) + MockSpcpService.createJWT.mockReturnValue(ok(MOCK_JWT)) + MockBillingService.recordLoginByForm.mockReturnValue( okAsync(MOCK_LOGIN_DOC), ) - MockSpcpFactory.getCookieSettings.mockReturnValue(MOCK_COOKIE_SETTINGS) + MockSpcpService.getCookieSettings.mockReturnValue(MOCK_COOKIE_SETTINGS) }) it('should set the cookie with the correct params and redirect to the destination', async () => { await loginHandler(MOCK_CP_LOGIN_REQ, MOCK_RESPONSE, jest.fn()) - expect(MockSpcpFactory.parseOOBParams).toHaveBeenCalledWith( + expect(MockSpcpService.parseOOBParams).toHaveBeenCalledWith( MOCK_CP_SAML, MOCK_RELAY_STATE, AuthType.CP, @@ -546,22 +546,22 @@ describe('spcp.controller', () => { expect(MockFormService.retrieveFullFormById).toHaveBeenCalledWith( MOCK_TARGET, ) - expect(MockSpcpFactory.getSpcpAttributes).toHaveBeenCalledWith( + expect(MockSpcpService.getSpcpAttributes).toHaveBeenCalledWith( MOCK_CP_SAML, MOCK_DESTINATION, AuthType.CP, ) - expect(MockSpcpFactory.createJWTPayload).toHaveBeenCalledWith( + expect(MockSpcpService.createJWTPayload).toHaveBeenCalledWith( MOCK_ATTRIBUTES, MOCK_REMEMBER_ME, AuthType.CP, ) - expect(MockSpcpFactory.createJWT).toHaveBeenCalledWith( + expect(MockSpcpService.createJWT).toHaveBeenCalledWith( MOCK_JWT_PAYLOAD, MOCK_COOKIE_AGE, AuthType.CP, ) - expect(MockBillingFactory.recordLoginByForm).toHaveBeenCalledWith( + expect(MockBillingService.recordLoginByForm).toHaveBeenCalledWith( MOCK_CP_FORM, ) expect(MOCK_RESPONSE.cookie).toHaveBeenCalledWith('jwtCp', MOCK_JWT, { @@ -575,11 +575,11 @@ describe('spcp.controller', () => { }) it('should return 400 when params cannot be parsed', async () => { - MockSpcpFactory.parseOOBParams.mockReturnValue( + MockSpcpService.parseOOBParams.mockReturnValue( err(new InvalidOOBParamsError()), ) await loginHandler(MOCK_CP_LOGIN_REQ, MOCK_RESPONSE, jest.fn()) - expect(MockSpcpFactory.parseOOBParams).toHaveBeenCalledWith( + expect(MockSpcpService.parseOOBParams).toHaveBeenCalledWith( MOCK_CP_SAML, MOCK_RELAY_STATE, AuthType.CP, @@ -588,11 +588,11 @@ describe('spcp.controller', () => { expect(MOCK_RESPONSE.cookie).not.toHaveBeenCalled() expect(MOCK_RESPONSE.redirect).not.toHaveBeenCalled() expect(MockFormService.retrieveFullFormById).not.toHaveBeenCalled() - expect(MockSpcpFactory.getSpcpAttributes).not.toHaveBeenCalled() - expect(MockSpcpFactory.createJWTPayload).not.toHaveBeenCalled() - expect(MockSpcpFactory.createJWT).not.toHaveBeenCalled() - expect(MockBillingFactory.recordLoginByForm).not.toHaveBeenCalled() - expect(MockSpcpFactory.getCookieSettings).not.toHaveBeenCalled() + expect(MockSpcpService.getSpcpAttributes).not.toHaveBeenCalled() + expect(MockSpcpService.createJWTPayload).not.toHaveBeenCalled() + expect(MockSpcpService.createJWT).not.toHaveBeenCalled() + expect(MockBillingService.recordLoginByForm).not.toHaveBeenCalled() + expect(MockSpcpService.getCookieSettings).not.toHaveBeenCalled() expect(MOCK_RESPONSE.cookie).not.toHaveBeenCalled() }) @@ -602,7 +602,7 @@ describe('spcp.controller', () => { okAsync(MOCK_SP_FORM), ) await loginHandler(MOCK_CP_LOGIN_REQ, MOCK_RESPONSE, jest.fn()) - expect(MockSpcpFactory.parseOOBParams).toHaveBeenCalledWith( + expect(MockSpcpService.parseOOBParams).toHaveBeenCalledWith( MOCK_CP_SAML, MOCK_RELAY_STATE, AuthType.CP, @@ -612,11 +612,11 @@ describe('spcp.controller', () => { ) expect(MOCK_RESPONSE.cookie).toHaveBeenCalledWith('isLoginError', true) expect(MOCK_RESPONSE.redirect).toHaveBeenCalledWith(MOCK_DESTINATION) - expect(MockSpcpFactory.getSpcpAttributes).not.toHaveBeenCalled() - expect(MockSpcpFactory.createJWTPayload).not.toHaveBeenCalled() - expect(MockSpcpFactory.createJWT).not.toHaveBeenCalled() - expect(MockBillingFactory.recordLoginByForm).not.toHaveBeenCalled() - expect(MockSpcpFactory.getCookieSettings).not.toHaveBeenCalled() + expect(MockSpcpService.getSpcpAttributes).not.toHaveBeenCalled() + expect(MockSpcpService.createJWTPayload).not.toHaveBeenCalled() + expect(MockSpcpService.createJWT).not.toHaveBeenCalled() + expect(MockBillingService.recordLoginByForm).not.toHaveBeenCalled() + expect(MockSpcpService.getCookieSettings).not.toHaveBeenCalled() }) it('should return 404 when form cannot be found', async () => { @@ -624,7 +624,7 @@ describe('spcp.controller', () => { errAsync(new FormNotFoundError()), ) await loginHandler(MOCK_CP_LOGIN_REQ, MOCK_RESPONSE, jest.fn()) - expect(MockSpcpFactory.parseOOBParams).toHaveBeenCalledWith( + expect(MockSpcpService.parseOOBParams).toHaveBeenCalledWith( MOCK_CP_SAML, MOCK_RELAY_STATE, AuthType.CP, @@ -635,20 +635,20 @@ describe('spcp.controller', () => { expect(MOCK_RESPONSE.sendStatus).toHaveBeenCalledWith(404) expect(MOCK_RESPONSE.cookie).not.toHaveBeenCalled() expect(MOCK_RESPONSE.redirect).not.toHaveBeenCalled() - expect(MockSpcpFactory.getSpcpAttributes).not.toHaveBeenCalled() - expect(MockSpcpFactory.createJWTPayload).not.toHaveBeenCalled() - expect(MockSpcpFactory.createJWT).not.toHaveBeenCalled() - expect(MockBillingFactory.recordLoginByForm).not.toHaveBeenCalled() - expect(MockSpcpFactory.getCookieSettings).not.toHaveBeenCalled() + expect(MockSpcpService.getSpcpAttributes).not.toHaveBeenCalled() + expect(MockSpcpService.createJWTPayload).not.toHaveBeenCalled() + expect(MockSpcpService.createJWT).not.toHaveBeenCalled() + expect(MockBillingService.recordLoginByForm).not.toHaveBeenCalled() + expect(MockSpcpService.getCookieSettings).not.toHaveBeenCalled() expect(MOCK_RESPONSE.cookie).not.toHaveBeenCalled() }) it('should set isLoginError cookie and redirect when getSpcpAttributes errors', async () => { - MockSpcpFactory.getSpcpAttributes.mockReturnValue( + MockSpcpService.getSpcpAttributes.mockReturnValue( errAsync(new RetrieveAttributesError()), ) await loginHandler(MOCK_CP_LOGIN_REQ, MOCK_RESPONSE, jest.fn()) - expect(MockSpcpFactory.parseOOBParams).toHaveBeenCalledWith( + expect(MockSpcpService.parseOOBParams).toHaveBeenCalledWith( MOCK_CP_SAML, MOCK_RELAY_STATE, AuthType.CP, @@ -656,25 +656,25 @@ describe('spcp.controller', () => { expect(MockFormService.retrieveFullFormById).toHaveBeenCalledWith( MOCK_TARGET, ) - expect(MockSpcpFactory.getSpcpAttributes).toHaveBeenCalledWith( + expect(MockSpcpService.getSpcpAttributes).toHaveBeenCalledWith( MOCK_CP_SAML, MOCK_DESTINATION, AuthType.CP, ) expect(MOCK_RESPONSE.cookie).toHaveBeenCalledWith('isLoginError', true) expect(MOCK_RESPONSE.redirect).toHaveBeenCalledWith(MOCK_DESTINATION) - expect(MockSpcpFactory.createJWTPayload).not.toHaveBeenCalled() - expect(MockSpcpFactory.createJWT).not.toHaveBeenCalled() - expect(MockBillingFactory.recordLoginByForm).not.toHaveBeenCalled() - expect(MockSpcpFactory.getCookieSettings).not.toHaveBeenCalled() + expect(MockSpcpService.createJWTPayload).not.toHaveBeenCalled() + expect(MockSpcpService.createJWT).not.toHaveBeenCalled() + expect(MockBillingService.recordLoginByForm).not.toHaveBeenCalled() + expect(MockSpcpService.getCookieSettings).not.toHaveBeenCalled() }) it('should set isLoginError cookie and redirect when createJWTPayload errors', async () => { - MockSpcpFactory.createJWTPayload.mockReturnValue( + MockSpcpService.createJWTPayload.mockReturnValue( err(new MissingAttributesError()), ) await loginHandler(MOCK_CP_LOGIN_REQ, MOCK_RESPONSE, jest.fn()) - expect(MockSpcpFactory.parseOOBParams).toHaveBeenCalledWith( + expect(MockSpcpService.parseOOBParams).toHaveBeenCalledWith( MOCK_CP_SAML, MOCK_RELAY_STATE, AuthType.CP, @@ -682,27 +682,27 @@ describe('spcp.controller', () => { expect(MockFormService.retrieveFullFormById).toHaveBeenCalledWith( MOCK_TARGET, ) - expect(MockSpcpFactory.getSpcpAttributes).toHaveBeenCalledWith( + expect(MockSpcpService.getSpcpAttributes).toHaveBeenCalledWith( MOCK_CP_SAML, MOCK_DESTINATION, AuthType.CP, ) - expect(MockSpcpFactory.createJWTPayload).toHaveBeenCalledWith( + expect(MockSpcpService.createJWTPayload).toHaveBeenCalledWith( MOCK_ATTRIBUTES, MOCK_REMEMBER_ME, AuthType.CP, ) expect(MOCK_RESPONSE.cookie).toHaveBeenCalledWith('isLoginError', true) expect(MOCK_RESPONSE.redirect).toHaveBeenCalledWith(MOCK_DESTINATION) - expect(MockSpcpFactory.createJWT).not.toHaveBeenCalled() - expect(MockBillingFactory.recordLoginByForm).not.toHaveBeenCalled() - expect(MockSpcpFactory.getCookieSettings).not.toHaveBeenCalled() + expect(MockSpcpService.createJWT).not.toHaveBeenCalled() + expect(MockBillingService.recordLoginByForm).not.toHaveBeenCalled() + expect(MockSpcpService.getCookieSettings).not.toHaveBeenCalled() }) it('should set isLoginError cookie and redirect when createJWT errors', async () => { - MockSpcpFactory.createJWT.mockReturnValue(err(new ApplicationError())) + MockSpcpService.createJWT.mockReturnValue(err(new ApplicationError())) await loginHandler(MOCK_CP_LOGIN_REQ, MOCK_RESPONSE, jest.fn()) - expect(MockSpcpFactory.parseOOBParams).toHaveBeenCalledWith( + expect(MockSpcpService.parseOOBParams).toHaveBeenCalledWith( MOCK_CP_SAML, MOCK_RELAY_STATE, AuthType.CP, @@ -710,33 +710,33 @@ describe('spcp.controller', () => { expect(MockFormService.retrieveFullFormById).toHaveBeenCalledWith( MOCK_TARGET, ) - expect(MockSpcpFactory.getSpcpAttributes).toHaveBeenCalledWith( + expect(MockSpcpService.getSpcpAttributes).toHaveBeenCalledWith( MOCK_CP_SAML, MOCK_DESTINATION, AuthType.CP, ) - expect(MockSpcpFactory.createJWTPayload).toHaveBeenCalledWith( + expect(MockSpcpService.createJWTPayload).toHaveBeenCalledWith( MOCK_ATTRIBUTES, MOCK_REMEMBER_ME, AuthType.CP, ) - expect(MockSpcpFactory.createJWT).toHaveBeenCalledWith( + expect(MockSpcpService.createJWT).toHaveBeenCalledWith( MOCK_JWT_PAYLOAD, MOCK_COOKIE_AGE, AuthType.CP, ) expect(MOCK_RESPONSE.cookie).toHaveBeenCalledWith('isLoginError', true) expect(MOCK_RESPONSE.redirect).toHaveBeenCalledWith(MOCK_DESTINATION) - expect(MockBillingFactory.recordLoginByForm).not.toHaveBeenCalled() - expect(MockSpcpFactory.getCookieSettings).not.toHaveBeenCalled() + expect(MockBillingService.recordLoginByForm).not.toHaveBeenCalled() + expect(MockSpcpService.getCookieSettings).not.toHaveBeenCalled() }) it('should set isLoginError cookie and redirect when recordLoginByForm errors', async () => { - MockBillingFactory.recordLoginByForm.mockReturnValue( + MockBillingService.recordLoginByForm.mockReturnValue( errAsync(new DatabaseError()), ) await loginHandler(MOCK_CP_LOGIN_REQ, MOCK_RESPONSE, jest.fn()) - expect(MockSpcpFactory.parseOOBParams).toHaveBeenCalledWith( + expect(MockSpcpService.parseOOBParams).toHaveBeenCalledWith( MOCK_CP_SAML, MOCK_RELAY_STATE, AuthType.CP, @@ -744,27 +744,27 @@ describe('spcp.controller', () => { expect(MockFormService.retrieveFullFormById).toHaveBeenCalledWith( MOCK_TARGET, ) - expect(MockSpcpFactory.getSpcpAttributes).toHaveBeenCalledWith( + expect(MockSpcpService.getSpcpAttributes).toHaveBeenCalledWith( MOCK_CP_SAML, MOCK_DESTINATION, AuthType.CP, ) - expect(MockSpcpFactory.createJWTPayload).toHaveBeenCalledWith( + expect(MockSpcpService.createJWTPayload).toHaveBeenCalledWith( MOCK_ATTRIBUTES, MOCK_REMEMBER_ME, AuthType.CP, ) - expect(MockSpcpFactory.createJWT).toHaveBeenCalledWith( + expect(MockSpcpService.createJWT).toHaveBeenCalledWith( MOCK_JWT_PAYLOAD, MOCK_COOKIE_AGE, AuthType.CP, ) - expect(MockBillingFactory.recordLoginByForm).toHaveBeenCalledWith( + expect(MockBillingService.recordLoginByForm).toHaveBeenCalledWith( MOCK_CP_FORM, ) expect(MOCK_RESPONSE.cookie).toHaveBeenCalledWith('isLoginError', true) expect(MOCK_RESPONSE.redirect).toHaveBeenCalledWith(MOCK_DESTINATION) - expect(MockSpcpFactory.getCookieSettings).not.toHaveBeenCalled() + expect(MockSpcpService.getCookieSettings).not.toHaveBeenCalled() }) }) }) diff --git a/src/app/modules/spcp/__tests__/spcp.factory.spec.ts b/src/app/modules/spcp/__tests__/spcp.factory.spec.ts deleted file mode 100644 index 0999570c5e..0000000000 --- a/src/app/modules/spcp/__tests__/spcp.factory.spec.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { mocked } from 'ts-jest/utils' - -import { FeatureNames, ISpcpMyInfo } from 'src/app/config/feature-manager' -import { AuthType } from 'src/types' - -import { MissingFeatureError } from '../../core/core.errors' -import { createSpcpFactory } from '../spcp.factory' -import { SpcpService } from '../spcp.service' -import { JwtPayload } from '../spcp.types' - -import { MOCK_SERVICE_PARAMS } from './spcp.test.constants' - -jest.mock('../spcp.service') -const MockSpcpService = mocked(SpcpService, true) - -describe('spcp.factory', () => { - it('should return error functions when isEnabled is false', async () => { - const SpcpFactory = createSpcpFactory({ - isEnabled: false, - props: {} as ISpcpMyInfo, - }) - const error = new MissingFeatureError(FeatureNames.SpcpMyInfo) - const createRedirectUrlResult = SpcpFactory.createRedirectUrl( - AuthType.SP, - '', - '', - ) - const fetchLoginPageResult = await SpcpFactory.fetchLoginPage('') - const validateLoginPageResult = SpcpFactory.validateLoginPage('') - const extractSingpassJwtPayloadResult = - await SpcpFactory.extractSingpassJwtPayload('') - const extractCorppassJwtPayloadResult = - await SpcpFactory.extractCorppassJwtPayload('') - const parseOOBParamsResult = SpcpFactory.parseOOBParams('', '', AuthType.SP) - const getSpcpAttributesResult = await SpcpFactory.getSpcpAttributes( - '', - '', - AuthType.SP, - ) - const createJWTResult = SpcpFactory.createJWT( - {} as unknown as JwtPayload, - 0, - AuthType.SP, - ) - const createJWTPayloadResult = SpcpFactory.createJWTPayload( - {}, - true, - AuthType.SP, - ) - const cookieSettings = await SpcpFactory.getCookieSettings() - expect(createRedirectUrlResult._unsafeUnwrapErr()).toEqual(error) - expect(fetchLoginPageResult._unsafeUnwrapErr()).toEqual(error) - expect(validateLoginPageResult._unsafeUnwrapErr()).toEqual(error) - expect(extractSingpassJwtPayloadResult._unsafeUnwrapErr()).toEqual(error) - expect(extractCorppassJwtPayloadResult._unsafeUnwrapErr()).toEqual(error) - expect(parseOOBParamsResult._unsafeUnwrapErr()).toEqual(error) - expect(getSpcpAttributesResult._unsafeUnwrapErr()).toEqual(error) - expect(createJWTResult._unsafeUnwrapErr()).toEqual(error) - expect(createJWTPayloadResult._unsafeUnwrapErr()).toEqual(error) - expect(cookieSettings).toEqual({}) - }) - - it('should return error functions when props is undefined', async () => { - const SpcpFactory = createSpcpFactory({ - isEnabled: true, - props: undefined, - }) - const error = new Error( - 'spcp-myinfo is not activated, but a feature-specific function was called.', - ) - const createRedirectUrlResult = SpcpFactory.createRedirectUrl( - AuthType.SP, - '', - '', - ) - const fetchLoginPageResult = await SpcpFactory.fetchLoginPage('') - const validateLoginPageResult = SpcpFactory.validateLoginPage('') - const extractSingpassJwtPayloadResult = - await SpcpFactory.extractSingpassJwtPayload('') - const extractCorppassJwtPayloadResult = - await SpcpFactory.extractCorppassJwtPayload('') - const parseOOBParamsResult = SpcpFactory.parseOOBParams('', '', AuthType.SP) - const getSpcpAttributesResult = await SpcpFactory.getSpcpAttributes( - '', - '', - AuthType.SP, - ) - const createJWTResult = SpcpFactory.createJWT( - {} as unknown as JwtPayload, - 0, - AuthType.SP, - ) - const createJWTPayloadResult = SpcpFactory.createJWTPayload( - {}, - true, - AuthType.SP, - ) - const cookieSettings = await SpcpFactory.getCookieSettings() - expect(createRedirectUrlResult._unsafeUnwrapErr()).toEqual(error) - expect(fetchLoginPageResult._unsafeUnwrapErr()).toEqual(error) - expect(validateLoginPageResult._unsafeUnwrapErr()).toEqual(error) - expect(extractSingpassJwtPayloadResult._unsafeUnwrapErr()).toEqual(error) - expect(extractCorppassJwtPayloadResult._unsafeUnwrapErr()).toEqual(error) - expect(parseOOBParamsResult._unsafeUnwrapErr()).toEqual(error) - expect(getSpcpAttributesResult._unsafeUnwrapErr()).toEqual(error) - expect(createJWTResult._unsafeUnwrapErr()).toEqual(error) - expect(createJWTPayloadResult._unsafeUnwrapErr()).toEqual(error) - expect(cookieSettings).toEqual({}) - }) - - it('should call the SpcpService constructor when isEnabled is true and props is truthy', () => { - createSpcpFactory({ - isEnabled: true, - props: MOCK_SERVICE_PARAMS, - }) - expect(MockSpcpService).toHaveBeenCalledWith(MOCK_SERVICE_PARAMS) - }) -}) diff --git a/src/app/modules/spcp/__tests__/spcp.service.spec.ts b/src/app/modules/spcp/__tests__/spcp.service.spec.ts index dde6630c65..70def2d43e 100644 --- a/src/app/modules/spcp/__tests__/spcp.service.spec.ts +++ b/src/app/modules/spcp/__tests__/spcp.service.spec.ts @@ -4,7 +4,7 @@ import fs from 'fs' import { omit } from 'lodash' import { mocked } from 'ts-jest/utils' -import { ISpcpMyInfo } from 'src/app/config/feature-manager' +import { ISpcpMyInfo } from 'src/app/config/feature-manager/spcp-myinfo.config' import { MOCK_COOKIE_AGE } from 'src/app/modules/myinfo/__tests__/myinfo.test.constants' import { AuthType } from 'src/types' @@ -21,7 +21,7 @@ import { RetrieveAttributesError, VerifyJwtError, } from '../spcp.errors' -import { SpcpService } from '../spcp.service' +import { SpcpServiceClass } from '../spcp.service' import { JwtName } from '../spcp.types' import { @@ -63,8 +63,8 @@ describe('spcp.service', () => { afterAll(async () => await dbHandler.closeDatabase()) describe('class constructor', () => { it('should instantiate auth clients with the correct params', () => { - const spcpService = new SpcpService(MOCK_PARAMS) - expect(spcpService).toBeTruthy() + const spcpServiceClass = new SpcpServiceClass(MOCK_PARAMS) + expect(spcpServiceClass).toBeTruthy() expect(MockAuthClient).toHaveBeenCalledTimes(2) expect(MockAuthClient).toHaveBeenCalledWith({ partnerEntityId: MOCK_PARAMS.spPartnerEntityId, @@ -91,11 +91,11 @@ describe('spcp.service', () => { describe('createRedirectUrl', () => { it('should call SP auth client createRedirectUrl with the correct params', () => { - const spcpService = new SpcpService(MOCK_PARAMS) + const spcpServiceClass = new SpcpServiceClass(MOCK_PARAMS) // Assumes that SP auth client was instantiated first const mockClient = mocked(MockAuthClient.mock.instances[0], true) mockClient.createRedirectURL.mockReturnValueOnce(MOCK_REDIRECT_URL) - const redirectUrl = spcpService.createRedirectUrl( + const redirectUrl = spcpServiceClass.createRedirectUrl( AuthType.SP, MOCK_TARGET, MOCK_ESRVCID, @@ -109,11 +109,11 @@ describe('spcp.service', () => { }) it('should call CP auth client createRedirectUrl with the correct params', () => { - const spcpService = new SpcpService(MOCK_PARAMS) + const spcpServiceClass = new SpcpServiceClass(MOCK_PARAMS) // Assumes that SP auth client was instantiated first const mockClient = mocked(MockAuthClient.mock.instances[1], true) mockClient.createRedirectURL.mockReturnValueOnce(MOCK_REDIRECT_URL) - const redirectUrl = spcpService.createRedirectUrl( + const redirectUrl = spcpServiceClass.createRedirectUrl( AuthType.CP, MOCK_TARGET, MOCK_ESRVCID, @@ -127,11 +127,11 @@ describe('spcp.service', () => { }) it('should return CreateRedirectUrlError if auth client returns error', () => { - const spcpService = new SpcpService(MOCK_PARAMS) + const spcpServiceClass = new SpcpServiceClass(MOCK_PARAMS) // Assumes that SP auth client was instantiated first const mockClient = mocked(MockAuthClient.mock.instances[1], true) mockClient.createRedirectURL.mockReturnValueOnce(new Error()) - const redirectUrl = spcpService.createRedirectUrl( + const redirectUrl = spcpServiceClass.createRedirectUrl( AuthType.CP, MOCK_TARGET, MOCK_ESRVCID, @@ -149,12 +149,12 @@ describe('spcp.service', () => { describe('fetchLoginPage', () => { it('should GET the correct URL and return the response when request succeeds', async () => { - const spcpService = new SpcpService(MOCK_PARAMS) + const spcpServiceClass = new SpcpServiceClass(MOCK_PARAMS) MockAxios.get.mockResolvedValueOnce({ data: MOCK_LOGIN_HTML, }) - const result = await spcpService.fetchLoginPage(MOCK_REDIRECT_URL) + const result = await spcpServiceClass.fetchLoginPage(MOCK_REDIRECT_URL) expect(MockAxios.get).toHaveBeenCalledWith( MOCK_REDIRECT_URL, @@ -170,10 +170,10 @@ describe('spcp.service', () => { }) it('should return FetchLoginPageError when request fails', async () => { - const spcpService = new SpcpService(MOCK_PARAMS) + const spcpServiceClass = new SpcpServiceClass(MOCK_PARAMS) MockAxios.get.mockRejectedValueOnce('') - const result = await spcpService.fetchLoginPage(MOCK_REDIRECT_URL) + const result = await spcpServiceClass.fetchLoginPage(MOCK_REDIRECT_URL) expect(MockAxios.get).toHaveBeenCalledWith( MOCK_REDIRECT_URL, @@ -191,16 +191,16 @@ describe('spcp.service', () => { describe('validateLoginPage', () => { it('should return null when there is a title and no error', () => { - const spcpService = new SpcpService(MOCK_PARAMS) + const spcpServiceClass = new SpcpServiceClass(MOCK_PARAMS) const mockHtml = `${MOCK_TITLE}` - const result = spcpService.validateLoginPage(mockHtml) + const result = spcpServiceClass.validateLoginPage(mockHtml) expect(result._unsafeUnwrap()).toEqual({ isValid: true }) }) it('should return error code when there is error in title', () => { - const spcpService = new SpcpService(MOCK_PARAMS) + const spcpServiceClass = new SpcpServiceClass(MOCK_PARAMS) const mockHtml = `ErrorSystem Code: ${MOCK_ERROR_CODE}` - const result = spcpService.validateLoginPage(mockHtml) + const result = spcpServiceClass.validateLoginPage(mockHtml) expect(result._unsafeUnwrap()).toEqual({ isValid: false, errorCode: MOCK_ERROR_CODE, @@ -208,46 +208,46 @@ describe('spcp.service', () => { }) it('should return LoginPageValidationError when there is no title', () => { - const spcpService = new SpcpService(MOCK_PARAMS) + const spcpServiceClass = new SpcpServiceClass(MOCK_PARAMS) const mockHtml = 'mock' - const result = spcpService.validateLoginPage(mockHtml) + const result = spcpServiceClass.validateLoginPage(mockHtml) expect(result._unsafeUnwrapErr()).toEqual(new LoginPageValidationError()) }) }) describe('extractSingpassJwtPayload', () => { it('should return the correct payload for Singpass when JWT is valid', async () => { - const spcpService = new SpcpService(MOCK_PARAMS) + const spcpServiceClass = new SpcpServiceClass(MOCK_PARAMS) // Assumes that SP auth client was instantiated first const mockClient = mocked(MockAuthClient.mock.instances[0], true) mockClient.verifyJWT.mockImplementationOnce((jwt, cb) => cb(null, MOCK_SP_JWT_PAYLOAD), ) - const result = await spcpService.extractSingpassJwtPayload(MOCK_JWT) + const result = await spcpServiceClass.extractSingpassJwtPayload(MOCK_JWT) expect(result._unsafeUnwrap()).toEqual(MOCK_SP_JWT_PAYLOAD) }) it('should return VerifyJwtError when SingPass JWT could not be verified', async () => { - const spcpService = new SpcpService(MOCK_PARAMS) + const spcpServiceClass = new SpcpServiceClass(MOCK_PARAMS) // Assumes that SP auth client was instantiated first const mockClient = mocked(MockAuthClient.mock.instances[0], true) mockClient.verifyJWT.mockImplementationOnce((_jwt, cb) => cb(new Error(), null), ) - const result = await spcpService.extractSingpassJwtPayload(MOCK_JWT) + const result = await spcpServiceClass.extractSingpassJwtPayload(MOCK_JWT) expect(result._unsafeUnwrapErr()).toEqual(new VerifyJwtError()) }) it('should return InvalidJwtError when SP JWT has invalid shape', async () => { // Arrange - const spcpService = new SpcpService(MOCK_PARAMS) + const spcpServiceClass = new SpcpServiceClass(MOCK_PARAMS) // Assumes that SP auth client was instantiated first const mockClient = mocked(MockAuthClient.mock.instances[0], true) mockClient.verifyJWT.mockImplementationOnce((jwt, cb) => cb(null, {})) const expected = new InvalidJwtError() // Act - const result = await spcpService.extractSingpassJwtPayload(MOCK_JWT) + const result = await spcpServiceClass.extractSingpassJwtPayload(MOCK_JWT) // Assert expect(result._unsafeUnwrapErr()).toEqual(expected) @@ -256,37 +256,37 @@ describe('spcp.service', () => { describe('extractCorppassJwtPayload', () => { it('should return the correct payload for Corppass when JWT is valid', async () => { - const spcpService = new SpcpService(MOCK_PARAMS) + const spcpServiceClass = new SpcpServiceClass(MOCK_PARAMS) // Assumes that SP auth client was instantiated first const mockClient = mocked(MockAuthClient.mock.instances[1], true) mockClient.verifyJWT.mockImplementationOnce((jwt, cb) => cb(null, MOCK_CP_JWT_PAYLOAD), ) - const result = await spcpService.extractCorppassJwtPayload(MOCK_JWT) + const result = await spcpServiceClass.extractCorppassJwtPayload(MOCK_JWT) expect(result._unsafeUnwrap()).toEqual(MOCK_CP_JWT_PAYLOAD) }) it('should return VerifyJwtError when CorpPass JWT could not be verified', async () => { - const spcpService = new SpcpService(MOCK_PARAMS) + const spcpServiceClass = new SpcpServiceClass(MOCK_PARAMS) // Assumes that SP auth client was instantiated first const mockClient = mocked(MockAuthClient.mock.instances[1], true) mockClient.verifyJWT.mockImplementationOnce((_jwt, cb) => cb(new Error(), null), ) - const result = await spcpService.extractCorppassJwtPayload(MOCK_JWT) + const result = await spcpServiceClass.extractCorppassJwtPayload(MOCK_JWT) expect(result._unsafeUnwrapErr()).toEqual(new VerifyJwtError()) }) it('should return InvalidJwtError when CorpPass JWT has invalid shape', async () => { // Arrange - const spcpService = new SpcpService(MOCK_PARAMS) + const spcpServiceClass = new SpcpServiceClass(MOCK_PARAMS) // Assumes that SP auth client was instantiated first const mockClient = mocked(MockAuthClient.mock.instances[1], true) mockClient.verifyJWT.mockImplementationOnce((jwt, cb) => cb(null, {})) const expected = new InvalidJwtError() // Act - const result = await spcpService.extractCorppassJwtPayload(MOCK_JWT) + const result = await spcpServiceClass.extractCorppassJwtPayload(MOCK_JWT) // Assert expect(result._unsafeUnwrapErr()).toEqual(expected) @@ -295,10 +295,10 @@ describe('spcp.service', () => { describe('parseOOBParams', () => { it('should parse SP params correctly when rememberMe is true', () => { - const spcpService = new SpcpService(MOCK_PARAMS) + const spcpServiceClass = new SpcpServiceClass(MOCK_PARAMS) const mockRelayState = `/${MOCK_TARGET},true` - const parsedResult = spcpService.parseOOBParams( + const parsedResult = spcpServiceClass.parseOOBParams( MOCK_SP_SAML, mockRelayState, AuthType.SP, @@ -312,10 +312,10 @@ describe('spcp.service', () => { }) it('should parse CP params correctly when rememberMe is true', () => { - const spcpService = new SpcpService(MOCK_PARAMS) + const spcpServiceClass = new SpcpServiceClass(MOCK_PARAMS) const mockRelayState = `/${MOCK_TARGET},true` - const parsedResult = spcpService.parseOOBParams( + const parsedResult = spcpServiceClass.parseOOBParams( MOCK_CP_SAML, mockRelayState, AuthType.CP, @@ -329,10 +329,10 @@ describe('spcp.service', () => { }) it('should parse SP params correctly when rememberMe is false', () => { - const spcpService = new SpcpService(MOCK_PARAMS) + const spcpServiceClass = new SpcpServiceClass(MOCK_PARAMS) const mockRelayState = `/${MOCK_TARGET},false` - const parsedResult = spcpService.parseOOBParams( + const parsedResult = spcpServiceClass.parseOOBParams( MOCK_SP_SAML, mockRelayState, AuthType.SP, @@ -346,10 +346,10 @@ describe('spcp.service', () => { }) it('should parse CP params correctly when rememberMe is false', () => { - const spcpService = new SpcpService(MOCK_PARAMS) + const spcpServiceClass = new SpcpServiceClass(MOCK_PARAMS) const mockRelayState = `/${MOCK_TARGET},false` - const parsedResult = spcpService.parseOOBParams( + const parsedResult = spcpServiceClass.parseOOBParams( MOCK_CP_SAML, mockRelayState, AuthType.CP, @@ -363,10 +363,10 @@ describe('spcp.service', () => { }) it('should parse SP params correctly when rememberMe is malformed', () => { - const spcpService = new SpcpService(MOCK_PARAMS) + const spcpServiceClass = new SpcpServiceClass(MOCK_PARAMS) const mockRelayState = `/${MOCK_TARGET},asdf` - const parsedResult = spcpService.parseOOBParams( + const parsedResult = spcpServiceClass.parseOOBParams( MOCK_SP_SAML, mockRelayState, AuthType.SP, @@ -380,10 +380,10 @@ describe('spcp.service', () => { }) it('should parse CP params correctly when rememberMe is malformed', () => { - const spcpService = new SpcpService(MOCK_PARAMS) + const spcpServiceClass = new SpcpServiceClass(MOCK_PARAMS) const mockRelayState = `/${MOCK_TARGET},asdf` - const parsedResult = spcpService.parseOOBParams( + const parsedResult = spcpServiceClass.parseOOBParams( MOCK_CP_SAML, mockRelayState, AuthType.CP, @@ -397,10 +397,10 @@ describe('spcp.service', () => { }) it('should parse SP params correctly when target has trailing slash', () => { - const spcpService = new SpcpService(MOCK_PARAMS) + const spcpServiceClass = new SpcpServiceClass(MOCK_PARAMS) const mockRelayState = `/${MOCK_TARGET}/,true` - const parsedResult = spcpService.parseOOBParams( + const parsedResult = spcpServiceClass.parseOOBParams( MOCK_SP_SAML, mockRelayState, AuthType.SP, @@ -414,10 +414,10 @@ describe('spcp.service', () => { }) it('should parse CP params correctly when target has trailing slash', () => { - const spcpService = new SpcpService(MOCK_PARAMS) + const spcpServiceClass = new SpcpServiceClass(MOCK_PARAMS) const mockRelayState = `/${MOCK_TARGET}/,true` - const parsedResult = spcpService.parseOOBParams( + const parsedResult = spcpServiceClass.parseOOBParams( MOCK_CP_SAML, mockRelayState, AuthType.CP, @@ -431,9 +431,9 @@ describe('spcp.service', () => { }) it('should return InvalidOOBParamsError when SP relay state has 0 commas', () => { - const spcpService = new SpcpService(MOCK_PARAMS) + const spcpServiceClass = new SpcpServiceClass(MOCK_PARAMS) const mockRelayState = `/${MOCK_TARGET}true` - const parsedResult = spcpService.parseOOBParams( + const parsedResult = spcpServiceClass.parseOOBParams( MOCK_SP_SAML, mockRelayState, AuthType.SP, @@ -444,9 +444,9 @@ describe('spcp.service', () => { }) it('should return InvalidOOBParamsError when CP relay state has 0 commas', () => { - const spcpService = new SpcpService(MOCK_PARAMS) + const spcpServiceClass = new SpcpServiceClass(MOCK_PARAMS) const mockRelayState = `/${MOCK_TARGET}true` - const parsedResult = spcpService.parseOOBParams( + const parsedResult = spcpServiceClass.parseOOBParams( MOCK_CP_SAML, mockRelayState, AuthType.CP, @@ -457,9 +457,9 @@ describe('spcp.service', () => { }) it('should return InvalidOOBParamsError when SP relay state has >1 commas', () => { - const spcpService = new SpcpService(MOCK_PARAMS) + const spcpServiceClass = new SpcpServiceClass(MOCK_PARAMS) const mockRelayState = `/${MOCK_TARGET},t,rue` - const parsedResult = spcpService.parseOOBParams( + const parsedResult = spcpServiceClass.parseOOBParams( MOCK_SP_SAML, mockRelayState, AuthType.SP, @@ -470,9 +470,9 @@ describe('spcp.service', () => { }) it('should return InvalidOOBParamsError when CP relay state has >1 commas', () => { - const spcpService = new SpcpService(MOCK_PARAMS) + const spcpServiceClass = new SpcpServiceClass(MOCK_PARAMS) const mockRelayState = `/${MOCK_TARGET},t,rue` - const parsedResult = spcpService.parseOOBParams( + const parsedResult = spcpServiceClass.parseOOBParams( MOCK_CP_SAML, mockRelayState, AuthType.CP, @@ -483,9 +483,9 @@ describe('spcp.service', () => { }) it('should return InvalidOOBParamsError when SP formId is malformed', () => { - const spcpService = new SpcpService(MOCK_PARAMS) + const spcpServiceClass = new SpcpServiceClass(MOCK_PARAMS) const mockRelayState = `/-,true` - const parsedResult = spcpService.parseOOBParams( + const parsedResult = spcpServiceClass.parseOOBParams( MOCK_SP_SAML, mockRelayState, AuthType.SP, @@ -496,9 +496,9 @@ describe('spcp.service', () => { }) it('should return InvalidOOBParamsError when CP formId is malformed', () => { - const spcpService = new SpcpService(MOCK_PARAMS) + const spcpServiceClass = new SpcpServiceClass(MOCK_PARAMS) const mockRelayState = `/-,true` - const parsedResult = spcpService.parseOOBParams( + const parsedResult = spcpServiceClass.parseOOBParams( MOCK_CP_SAML, mockRelayState, AuthType.CP, @@ -509,9 +509,9 @@ describe('spcp.service', () => { }) it('should return InvalidOOBParamsError when SP typecode does not match', () => { - const spcpService = new SpcpService(MOCK_PARAMS) + const spcpServiceClass = new SpcpServiceClass(MOCK_PARAMS) const mockRelayState = `/${MOCK_TARGET},true` - const parsedResult = spcpService.parseOOBParams( + const parsedResult = spcpServiceClass.parseOOBParams( MOCK_SP_SAML_WRONG_TYPECODE, mockRelayState, AuthType.SP, @@ -522,9 +522,9 @@ describe('spcp.service', () => { }) it('should return InvalidOOBParamsError when CP typecode does not match', () => { - const spcpService = new SpcpService(MOCK_PARAMS) + const spcpServiceClass = new SpcpServiceClass(MOCK_PARAMS) const mockRelayState = `/${MOCK_TARGET},true` - const parsedResult = spcpService.parseOOBParams( + const parsedResult = spcpServiceClass.parseOOBParams( MOCK_SP_SAML_WRONG_TYPECODE, mockRelayState, AuthType.CP, @@ -535,9 +535,9 @@ describe('spcp.service', () => { }) it('should return InvalidOOBParamsError when SP hash does not match', () => { - const spcpService = new SpcpService(MOCK_PARAMS) + const spcpServiceClass = new SpcpServiceClass(MOCK_PARAMS) const mockRelayState = `/${MOCK_TARGET},true` - const parsedResult = spcpService.parseOOBParams( + const parsedResult = spcpServiceClass.parseOOBParams( MOCK_SP_SAML_WRONG_HASH, mockRelayState, AuthType.SP, @@ -548,9 +548,9 @@ describe('spcp.service', () => { }) it('should return InvalidOOBParamsError when CP hash does not match', () => { - const spcpService = new SpcpService(MOCK_PARAMS) + const spcpServiceClass = new SpcpServiceClass(MOCK_PARAMS) const mockRelayState = `/${MOCK_TARGET},true` - const parsedResult = spcpService.parseOOBParams( + const parsedResult = spcpServiceClass.parseOOBParams( MOCK_SP_SAML_WRONG_HASH, mockRelayState, AuthType.CP, @@ -563,14 +563,14 @@ describe('spcp.service', () => { describe('getSpcpAttributes', () => { it('should call SingPass auth client correctly and return the result', async () => { - const spcpService = new SpcpService(MOCK_PARAMS) + const spcpServiceClass = new SpcpServiceClass(MOCK_PARAMS) // Assumes that SP auth client was instantiated first const mockClient = mocked(MockAuthClient.mock.instances[0], true) mockClient.getAttributes.mockImplementationOnce((_samlArt, _dest, cb) => cb(null, MOCK_GET_ATTRIBUTES_RETURN_VALUE), ) - const result = await spcpService.getSpcpAttributes( + const result = await spcpServiceClass.getSpcpAttributes( MOCK_SP_SAML, MOCK_DESTINATION, AuthType.SP, @@ -587,14 +587,14 @@ describe('spcp.service', () => { }) it('should call CorpPass auth client correctly and return the result', async () => { - const spcpService = new SpcpService(MOCK_PARAMS) + const spcpServiceClass = new SpcpServiceClass(MOCK_PARAMS) // Assumes that SP auth client was instantiated first const mockClient = mocked(MockAuthClient.mock.instances[1], true) mockClient.getAttributes.mockImplementationOnce((_samlArt, _dest, cb) => cb(null, MOCK_GET_ATTRIBUTES_RETURN_VALUE), ) - const result = await spcpService.getSpcpAttributes( + const result = await spcpServiceClass.getSpcpAttributes( MOCK_CP_SAML, MOCK_DESTINATION, AuthType.CP, @@ -611,14 +611,14 @@ describe('spcp.service', () => { }) it('should return RetrieveAttributesError if SingPass client errors', async () => { - const spcpService = new SpcpService(MOCK_PARAMS) + const spcpServiceClass = new SpcpServiceClass(MOCK_PARAMS) // Assumes that SP auth client was instantiated first const mockClient = mocked(MockAuthClient.mock.instances[0], true) mockClient.getAttributes.mockImplementationOnce(() => { throw new Error() }) - const result = await spcpService.getSpcpAttributes( + const result = await spcpServiceClass.getSpcpAttributes( MOCK_SP_SAML, MOCK_DESTINATION, AuthType.SP, @@ -633,14 +633,14 @@ describe('spcp.service', () => { }) it('should return RetrieveAttributesError if CorpPass client errors', async () => { - const spcpService = new SpcpService(MOCK_PARAMS) + const spcpServiceClass = new SpcpServiceClass(MOCK_PARAMS) // Assumes that SP auth client was instantiated first const mockClient = mocked(MockAuthClient.mock.instances[1], true) mockClient.getAttributes.mockImplementationOnce(() => { throw new Error() }) - const result = await spcpService.getSpcpAttributes( + const result = await spcpServiceClass.getSpcpAttributes( MOCK_CP_SAML, MOCK_DESTINATION, AuthType.CP, @@ -657,11 +657,11 @@ describe('spcp.service', () => { describe('createJWT', () => { it('should call SingPass auth client with the correct params', () => { - const spcpService = new SpcpService(MOCK_PARAMS) + const spcpServiceClass = new SpcpServiceClass(MOCK_PARAMS) // Assumes that SP auth client was instantiated first const mockClient = mocked(MockAuthClient.mock.instances[0], true) mockClient.createJWT.mockReturnValueOnce(MOCK_JWT) - const jwtResult = spcpService.createJWT( + const jwtResult = spcpServiceClass.createJWT( MOCK_JWT_PAYLOAD, MOCK_COOKIE_AGE, AuthType.SP, @@ -674,11 +674,11 @@ describe('spcp.service', () => { }) it('should call CorpPass auth client with the correct params', () => { - const spcpService = new SpcpService(MOCK_PARAMS) + const spcpServiceClass = new SpcpServiceClass(MOCK_PARAMS) // Assumes that SP auth client was instantiated first const mockClient = mocked(MockAuthClient.mock.instances[1], true) mockClient.createJWT.mockReturnValueOnce(MOCK_JWT) - const jwtResult = spcpService.createJWT( + const jwtResult = spcpServiceClass.createJWT( MOCK_JWT_PAYLOAD, MOCK_COOKIE_AGE, AuthType.CP, @@ -693,8 +693,8 @@ describe('spcp.service', () => { describe('createJWTPayload', () => { it('should return the correct SingPass attributes', () => { - const spcpService = new SpcpService(MOCK_PARAMS) - const result = spcpService.createJWTPayload( + const spcpServiceClass = new SpcpServiceClass(MOCK_PARAMS) + const result = spcpServiceClass.createJWTPayload( { UserName: MOCK_JWT_PAYLOAD.userName }, true, AuthType.SP, @@ -706,14 +706,14 @@ describe('spcp.service', () => { }) it('should return MissingAttributesError if SP UserName does not exist', () => { - const spcpService = new SpcpService(MOCK_PARAMS) - const result = spcpService.createJWTPayload({}, true, AuthType.SP) + const spcpServiceClass = new SpcpServiceClass(MOCK_PARAMS) + const result = spcpServiceClass.createJWTPayload({}, true, AuthType.SP) expect(result._unsafeUnwrapErr()).toEqual(new MissingAttributesError()) }) it('should return the correct CorpPass attributes', () => { - const spcpService = new SpcpService(MOCK_PARAMS) - const result = spcpService.createJWTPayload( + const spcpServiceClass = new SpcpServiceClass(MOCK_PARAMS) + const result = spcpServiceClass.createJWTPayload( { UserInfo: { CPEntID: MOCK_JWT_PAYLOAD.userName, @@ -731,14 +731,14 @@ describe('spcp.service', () => { }) it('should return MissingAttributesError if CorpPass UserInfo is missing', () => { - const spcpService = new SpcpService(MOCK_PARAMS) - const result = spcpService.createJWTPayload({}, true, AuthType.SP) + const spcpServiceClass = new SpcpServiceClass(MOCK_PARAMS) + const result = spcpServiceClass.createJWTPayload({}, true, AuthType.SP) expect(result._unsafeUnwrapErr()).toEqual(new MissingAttributesError()) }) it('should return MissingAttributesError if CorpPass CPEntID is missing', () => { - const spcpService = new SpcpService(MOCK_PARAMS) - const result = spcpService.createJWTPayload( + const spcpServiceClass = new SpcpServiceClass(MOCK_PARAMS) + const result = spcpServiceClass.createJWTPayload( { UserInfo: { CPUID: MOCK_JWT_PAYLOAD.userInfo, @@ -751,8 +751,8 @@ describe('spcp.service', () => { }) it('should return MissingAttributesError if CorpPass CPUID is missing', () => { - const spcpService = new SpcpService(MOCK_PARAMS) - const result = spcpService.createJWTPayload( + const spcpServiceClass = new SpcpServiceClass(MOCK_PARAMS) + const result = spcpServiceClass.createJWTPayload( { UserInfo: { CPEntID: MOCK_JWT_PAYLOAD.userName, @@ -767,37 +767,37 @@ describe('spcp.service', () => { describe('getCookieSettings', () => { it('should return the correct cookie settings if spcpCookieDomain is truthy', () => { - const spcpService = new SpcpService(MOCK_PARAMS) - expect(spcpService.getCookieSettings()).toEqual({ + const spcpServiceClass = new SpcpServiceClass(MOCK_PARAMS) + expect(spcpServiceClass.getCookieSettings()).toEqual({ domain: MOCK_PARAMS.spcpCookieDomain, path: '/', }) }) it('should return empty object if spcpCookieDomain is falsy', () => { - const spcpService = new SpcpService( + const spcpServiceClass = new SpcpServiceClass( omit(MOCK_PARAMS, 'spcpCookieDomain') as ISpcpMyInfo, ) - expect(spcpService.getCookieSettings()).toEqual({}) + expect(spcpServiceClass.getCookieSettings()).toEqual({}) }) }) describe('extractJwt', () => { it('should return SingPass JWT correctly', () => { - const spcpService = new SpcpService(MOCK_PARAMS) - const result = spcpService.extractJwt(MOCK_COOKIES, AuthType.SP) + const spcpServiceClass = new SpcpServiceClass(MOCK_PARAMS) + const result = spcpServiceClass.extractJwt(MOCK_COOKIES, AuthType.SP) expect(result._unsafeUnwrap()).toEqual(MOCK_COOKIES[JwtName.SP]) }) it('should return CorpPass JWT correctly', () => { - const spcpService = new SpcpService(MOCK_PARAMS) - const result = spcpService.extractJwt(MOCK_COOKIES, AuthType.CP) + const spcpServiceClass = new SpcpServiceClass(MOCK_PARAMS) + const result = spcpServiceClass.extractJwt(MOCK_COOKIES, AuthType.CP) expect(result._unsafeUnwrap()).toEqual(MOCK_COOKIES[JwtName.CP]) }) it('should return MissingJwtError if there is no JWT', () => { - const spcpService = new SpcpService(MOCK_PARAMS) - const result = spcpService.extractJwt({}, AuthType.CP) + const spcpServiceClass = new SpcpServiceClass(MOCK_PARAMS) + const result = spcpServiceClass.extractJwt({}, AuthType.CP) expect(result._unsafeUnwrapErr()).toEqual(new MissingJwtError()) }) }) @@ -805,7 +805,7 @@ describe('spcp.service', () => { describe('extractJwtPayloadFromRequest', () => { it('should return a SP JWT payload when there is a valid JWT in the request', async () => { // Arrange - const spcpService = new SpcpService(MOCK_PARAMS) + const spcpServiceClass = new SpcpServiceClass(MOCK_PARAMS) // Assumes that SP auth client was instantiated first const mockClient = mocked(MockAuthClient.mock.instances[0], true) mockClient.verifyJWT.mockImplementationOnce((jwt, cb) => @@ -813,7 +813,7 @@ describe('spcp.service', () => { ) // Act - const result = await spcpService.extractJwtPayloadFromRequest( + const result = await spcpServiceClass.extractJwtPayloadFromRequest( AuthType.SP, MOCK_COOKIES, ) @@ -824,7 +824,7 @@ describe('spcp.service', () => { it('should return a CP JWT payload when there is a valid JWT in the request', async () => { // Arrange - const spcpService = new SpcpService(MOCK_PARAMS) + const spcpServiceClass = new SpcpServiceClass(MOCK_PARAMS) // Assumes that CP auth client was instantiated first const mockClient = mocked(MockAuthClient.mock.instances[1], true) mockClient.verifyJWT.mockImplementationOnce((jwt, cb) => @@ -832,7 +832,7 @@ describe('spcp.service', () => { ) // Act - const result = await spcpService.extractJwtPayloadFromRequest( + const result = await spcpServiceClass.extractJwtPayloadFromRequest( AuthType.CP, MOCK_COOKIES, ) @@ -843,11 +843,11 @@ describe('spcp.service', () => { it('should return MissingJwtError if there is no JWT when client authenticates using SP', async () => { // Arrange - const spcpService = new SpcpService(MOCK_PARAMS) + const spcpServiceClass = new SpcpServiceClass(MOCK_PARAMS) const expected = new MissingJwtError() // Act - const result = await spcpService.extractJwtPayloadFromRequest( + const result = await spcpServiceClass.extractJwtPayloadFromRequest( AuthType.SP, {}, ) @@ -858,11 +858,11 @@ describe('spcp.service', () => { it('should return MissingJwtError when client authenticates using CP and there is no JWT', async () => { // Arrange - const spcpService = new SpcpService(MOCK_PARAMS) + const spcpServiceClass = new SpcpServiceClass(MOCK_PARAMS) const expected = new MissingJwtError() // Act - const result = await spcpService.extractJwtPayloadFromRequest( + const result = await spcpServiceClass.extractJwtPayloadFromRequest( AuthType.CP, {}, ) @@ -873,7 +873,7 @@ describe('spcp.service', () => { it('should return InvalidJwtError when the client authenticates using SP and the JWT has wrong shape', async () => { // Arrange - const spcpService = new SpcpService(MOCK_PARAMS) + const spcpServiceClass = new SpcpServiceClass(MOCK_PARAMS) // Assumes that SP auth client was instantiated first const mockClient = mocked(MockAuthClient.mock.instances[0], true) mockClient.verifyJWT.mockImplementationOnce((jwt, cb) => @@ -882,7 +882,7 @@ describe('spcp.service', () => { const expected = new VerifyJwtError() // Act - const result = await spcpService.extractJwtPayloadFromRequest( + const result = await spcpServiceClass.extractJwtPayloadFromRequest( AuthType.SP, MOCK_COOKIES, ) @@ -893,7 +893,7 @@ describe('spcp.service', () => { it('should return VerifyJwtError when the client authenticates using CP and the JWT has wrong shape', async () => { // Arrange - const spcpService = new SpcpService(MOCK_PARAMS) + const spcpServiceClass = new SpcpServiceClass(MOCK_PARAMS) // Assumes that SP auth client was instantiated first const mockClient = mocked(MockAuthClient.mock.instances[1], true) mockClient.verifyJWT.mockImplementationOnce((jwt, cb) => @@ -902,7 +902,7 @@ describe('spcp.service', () => { const expected = new VerifyJwtError() // Act - const result = await spcpService.extractJwtPayloadFromRequest( + const result = await spcpServiceClass.extractJwtPayloadFromRequest( AuthType.CP, MOCK_COOKIES, ) @@ -912,14 +912,14 @@ describe('spcp.service', () => { }) it('should return InvalidJwtError when the client authenticates using SP and the JWT has invalid shape', async () => { // Arrange - const spcpService = new SpcpService(MOCK_PARAMS) + const spcpServiceClass = new SpcpServiceClass(MOCK_PARAMS) // Assumes that SP auth client was instantiated first const mockClient = mocked(MockAuthClient.mock.instances[0], true) mockClient.verifyJWT.mockImplementationOnce((jwt, cb) => cb(null, {})) const expected = new InvalidJwtError() // Act - const result = await spcpService.extractJwtPayloadFromRequest( + const result = await spcpServiceClass.extractJwtPayloadFromRequest( AuthType.SP, MOCK_COOKIES, ) @@ -930,14 +930,14 @@ describe('spcp.service', () => { it('should return InvalidJwtError when the client authenticates using CP and the JWT has invalid shape', async () => { // Arrange - const spcpService = new SpcpService(MOCK_PARAMS) + const spcpServiceClass = new SpcpServiceClass(MOCK_PARAMS) // Assumes that SP auth client was instantiated first const mockClient = mocked(MockAuthClient.mock.instances[1], true) mockClient.verifyJWT.mockImplementationOnce((jwt, cb) => cb(null, {})) const expected = new InvalidJwtError() // Act - const result = await spcpService.extractJwtPayloadFromRequest( + const result = await spcpServiceClass.extractJwtPayloadFromRequest( AuthType.CP, MOCK_COOKIES, ) diff --git a/src/app/modules/spcp/spcp.controller.ts b/src/app/modules/spcp/spcp.controller.ts index 8fe4dc8cbd..56e4668872 100644 --- a/src/app/modules/spcp/spcp.controller.ts +++ b/src/app/modules/spcp/spcp.controller.ts @@ -5,11 +5,11 @@ import { PublicFormAuthValidateEsrvcIdDto } from '../../../types/api' import config from '../../config/config' import { createLoggerWithLabel } from '../../config/logger' import { createReqMeta } from '../../utils/request' -import { BillingFactory } from '../billing/billing.factory' +import * as BillingService from '../billing/billing.service' import { ControllerHandler } from '../core/core.types' import * as FormService from '../form/form.service' -import { SpcpFactory } from './spcp.factory' +import { SpcpService } from './spcp.service' import { JwtName } from './spcp.types' import { mapRouteError } from './spcp.util' @@ -38,7 +38,7 @@ export const handleRedirect: ControllerHandler< target, esrvcId, } - return SpcpFactory.createRedirectUrl(authType, target, esrvcId) + return SpcpService.createRedirectUrl(authType, target, esrvcId) .map((redirectURL) => { return res.status(StatusCodes.OK).json({ redirectURL }) }) @@ -69,9 +69,9 @@ export const handleValidate: ControllerHandler< } > = (req, res) => { const { target, authType, esrvcId } = req.query - return SpcpFactory.createRedirectUrl(authType, target, esrvcId) - .asyncAndThen(SpcpFactory.fetchLoginPage) - .andThen(SpcpFactory.validateLoginPage) + return SpcpService.createRedirectUrl(authType, target, esrvcId) + .asyncAndThen(SpcpService.fetchLoginPage) + .andThen(SpcpService.validateLoginPage) .map((result) => res.status(StatusCodes.OK).json(result)) .mapErr((error) => { logger.error({ @@ -109,7 +109,7 @@ export const handleLogin: ( samlArt: SAMLart, relayState: RelayState, } - const parseResult = SpcpFactory.parseOOBParams(SAMLart, RelayState, authType) + const parseResult = SpcpService.parseOOBParams(SAMLart, RelayState, authType) if (parseResult.isErr()) { logger.error({ message: 'Invalid SPCP login parameters', @@ -141,16 +141,16 @@ export const handleLogin: ( res.cookie('isLoginError', true) return res.redirect(destination) } - const jwtResult = await SpcpFactory.getSpcpAttributes( + const jwtResult = await SpcpService.getSpcpAttributes( SAMLart, destination, authType, ) .andThen((attributes) => - SpcpFactory.createJWTPayload(attributes, rememberMe, authType), + SpcpService.createJWTPayload(attributes, rememberMe, authType), ) .andThen((jwtPayload) => - SpcpFactory.createJWT(jwtPayload, cookieDuration, authType), + SpcpService.createJWT(jwtPayload, cookieDuration, authType), ) if (jwtResult.isErr()) { logger.error({ @@ -161,14 +161,14 @@ export const handleLogin: ( res.cookie('isLoginError', true) return res.redirect(destination) } - return BillingFactory.recordLoginByForm(form) + return BillingService.recordLoginByForm(form) .map(() => { res.cookie(JwtName[authType], jwtResult.value, { maxAge: cookieDuration, httpOnly: false, // the JWT needs to be read by client-side JS sameSite: 'lax', // Setting to 'strict' prevents Singpass login on Safari, Firefox secure: !config.isDev, - ...SpcpFactory.getCookieSettings(), + ...SpcpService.getCookieSettings(), }) return res.redirect(destination) }) diff --git a/src/app/modules/spcp/spcp.factory.ts b/src/app/modules/spcp/spcp.factory.ts deleted file mode 100644 index 0b5efb3437..0000000000 --- a/src/app/modules/spcp/spcp.factory.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { err, errAsync } from 'neverthrow' - -import FeatureManager, { - FeatureNames, - RegisteredFeature, -} from '../../config/feature-manager' -import { MissingFeatureError } from '../core/core.errors' - -import { SpcpService } from './spcp.service' - -interface ISpcpFactory { - createRedirectUrl: SpcpService['createRedirectUrl'] - fetchLoginPage: SpcpService['fetchLoginPage'] - validateLoginPage: SpcpService['validateLoginPage'] - extractJwt: SpcpService['extractJwt'] - extractSingpassJwtPayload: SpcpService['extractSingpassJwtPayload'] - extractCorppassJwtPayload: SpcpService['extractCorppassJwtPayload'] - parseOOBParams: SpcpService['parseOOBParams'] - getSpcpAttributes: SpcpService['getSpcpAttributes'] - createJWT: SpcpService['createJWT'] - createJWTPayload: SpcpService['createJWTPayload'] - getCookieSettings: SpcpService['getCookieSettings'] - extractJwtPayloadFromRequest: SpcpService['extractJwtPayloadFromRequest'] -} - -export const createSpcpFactory = ({ - isEnabled, - props, -}: RegisteredFeature): ISpcpFactory => { - if (!isEnabled || !props) { - const error = new MissingFeatureError(FeatureNames.SpcpMyInfo) - return { - createRedirectUrl: () => err(error), - fetchLoginPage: () => errAsync(error), - validateLoginPage: () => err(error), - extractSingpassJwtPayload: () => errAsync(error), - extractCorppassJwtPayload: () => errAsync(error), - extractJwt: () => err(error), - parseOOBParams: () => err(error), - getSpcpAttributes: () => errAsync(error), - createJWT: () => err(error), - createJWTPayload: () => err(error), - getCookieSettings: () => ({}), - extractJwtPayloadFromRequest: () => errAsync(error), - } - } - return new SpcpService(props) -} - -const spcpFeature = FeatureManager.get(FeatureNames.SpcpMyInfo) -export const SpcpFactory = createSpcpFactory(spcpFeature) diff --git a/src/app/modules/spcp/spcp.service.ts b/src/app/modules/spcp/spcp.service.ts index d0014d0ea5..7a6c3d361e 100644 --- a/src/app/modules/spcp/spcp.service.ts +++ b/src/app/modules/spcp/spcp.service.ts @@ -6,7 +6,10 @@ import { err, errAsync, ok, okAsync, Result, ResultAsync } from 'neverthrow' import { AuthType } from '../../../types' import { PublicFormAuthValidateEsrvcIdDto } from '../../../types/api' -import { ISpcpMyInfo } from '../../config/feature-manager' +import { + ISpcpMyInfo, + spcpMyInfoConfig, +} from '../../config/feature-manager/spcp-myinfo.config' import { createLoggerWithLabel } from '../../config/logger' import { ApplicationError } from '../core/core.errors' @@ -47,7 +50,12 @@ const logger = createLoggerWithLabel(module) const LOGIN_PAGE_HEADERS = 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3' const LOGIN_PAGE_TIMEOUT = 10000 // 10 seconds -export class SpcpService { + +/** + * Class for executing Singpass/Corppass-related services. + * Exported for testing. + */ +export class SpcpServiceClass { #singpassAuthClient: SPCPAuthClient #corppassAuthClient: SPCPAuthClient #spcpProps: ISpcpMyInfo @@ -458,3 +466,5 @@ export class SpcpService { }) } } + +export const SpcpService = new SpcpServiceClass(spcpMyInfoConfig) diff --git a/src/app/modules/submission/email-submission/email-submission.controller.ts b/src/app/modules/submission/email-submission/email-submission.controller.ts index 4c8c215cc0..2e94c14898 100644 --- a/src/app/modules/submission/email-submission/email-submission.controller.ts +++ b/src/app/modules/submission/email-submission/email-submission.controller.ts @@ -17,9 +17,9 @@ import { MYINFO_COOKIE_NAME, MYINFO_COOKIE_OPTIONS, } from '../../myinfo/myinfo.constants' -import { MyInfoFactory } from '../../myinfo/myinfo.factory' +import { MyInfoService } from '../../myinfo/myinfo.service' import * as MyInfoUtil from '../../myinfo/myinfo.util' -import { SpcpFactory } from '../../spcp/spcp.factory' +import { SpcpService } from '../../spcp/spcp.service' import * as EmailSubmissionMiddleware from '../email-submission/email-submission.middleware' import * as SubmissionService from '../submission.service' import { extractEmailConfirmationData } from '../submission.utils' @@ -150,8 +150,8 @@ const submitEmailModeForm: ControllerHandler< const { authType } = form switch (authType) { case AuthType.CP: - return SpcpFactory.extractJwt(req.cookies, authType) - .asyncAndThen((jwt) => SpcpFactory.extractCorppassJwtPayload(jwt)) + return SpcpService.extractJwt(req.cookies, authType) + .asyncAndThen((jwt) => SpcpService.extractCorppassJwtPayload(jwt)) .map((jwt) => ({ form, parsedResponses: parsedResponses.addNdiResponses({ @@ -170,8 +170,8 @@ const submitEmailModeForm: ControllerHandler< return error }) case AuthType.SP: - return SpcpFactory.extractJwt(req.cookies, authType) - .asyncAndThen((jwt) => SpcpFactory.extractSingpassJwtPayload(jwt)) + return SpcpService.extractJwt(req.cookies, authType) + .asyncAndThen((jwt) => SpcpService.extractSingpassJwtPayload(jwt)) .map((jwt) => ({ form, parsedResponses: parsedResponses.addNdiResponses({ @@ -192,12 +192,12 @@ const submitEmailModeForm: ControllerHandler< return MyInfoUtil.extractMyInfoCookie(req.cookies) .andThen(MyInfoUtil.extractAccessTokenFromCookie) .andThen((accessToken) => - MyInfoFactory.extractUinFin(accessToken), + MyInfoService.extractUinFin(accessToken), ) .asyncAndThen((uinFin) => - MyInfoFactory.fetchMyInfoHashes(uinFin, formId) + MyInfoService.fetchMyInfoHashes(uinFin, formId) .andThen((hashes) => - MyInfoFactory.checkMyInfoHashes( + MyInfoService.checkMyInfoHashes( parsedResponses.responses, hashes, ), diff --git a/src/app/modules/submission/encrypt-submission/encrypt-submission.controller.ts b/src/app/modules/submission/encrypt-submission/encrypt-submission.controller.ts index 5ea91acacd..adc5aad469 100644 --- a/src/app/modules/submission/encrypt-submission/encrypt-submission.controller.ts +++ b/src/app/modules/submission/encrypt-submission/encrypt-submission.controller.ts @@ -29,7 +29,7 @@ import { import { ControllerHandler } from '../../core/core.types' import { PermissionLevel } from '../../form/admin-form/admin-form.types' import * as FormService from '../../form/form.service' -import { SpcpFactory } from '../../spcp/spcp.factory' +import { SpcpService } from '../../spcp/spcp.service' import { getPopulatedUserById } from '../../user/user.service' import * as VerifiedContentService from '../../verified-content/verified-content.service' import { WebhookFactory } from '../../webhook/webhook.factory' @@ -201,10 +201,10 @@ const submitEncryptModeForm: ControllerHandler< return res.status(statusCode).json({ message: errorMessage }) } case AuthType.SP: { - const jwtPayloadResult = await SpcpFactory.extractJwt( + const jwtPayloadResult = await SpcpService.extractJwt( req.cookies, authType, - ).asyncAndThen((jwt) => SpcpFactory.extractSingpassJwtPayload(jwt)) + ).asyncAndThen((jwt) => SpcpService.extractSingpassJwtPayload(jwt)) if (jwtPayloadResult.isErr()) { const { statusCode, errorMessage } = mapRouteError( jwtPayloadResult.error, @@ -223,10 +223,10 @@ const submitEncryptModeForm: ControllerHandler< break } case AuthType.CP: { - const jwtPayloadResult = await SpcpFactory.extractJwt( + const jwtPayloadResult = await SpcpService.extractJwt( req.cookies, authType, - ).asyncAndThen((jwt) => SpcpFactory.extractCorppassJwtPayload(jwt)) + ).asyncAndThen((jwt) => SpcpService.extractCorppassJwtPayload(jwt)) if (jwtPayloadResult.isErr()) { const { statusCode, errorMessage } = mapRouteError( jwtPayloadResult.error, diff --git a/src/app/routes/api/v3/billings/__tests__/billings.routes.spec.ts b/src/app/routes/api/v3/billings/__tests__/billings.routes.spec.ts index 7628bc38a4..836af8e5f4 100644 --- a/src/app/routes/api/v3/billings/__tests__/billings.routes.spec.ts +++ b/src/app/routes/api/v3/billings/__tests__/billings.routes.spec.ts @@ -12,7 +12,7 @@ import { setupApp } from 'tests/integration/helpers/express-setup' import { buildCelebrateError } from 'tests/unit/backend/helpers/celebrate' import dbHandler from 'tests/unit/backend/helpers/jest-db' -import { BillingFactory } from '../../../../../modules/billing/billing.factory' +import * as BillingService from '../../../../../modules/billing/billing.service' import { DatabaseError } from '../../../../../modules/core/core.errors' import { BillingsRouter } from '../billings.routes' @@ -191,7 +191,7 @@ describe('billings.routes', () => { // Mock database error from service call. const retrieveStatsSpy = jest - .spyOn(BillingFactory, 'getSpLoginStats') + .spyOn(BillingService, 'getSpLoginStats') .mockReturnValueOnce(errAsync(new DatabaseError())) // Act diff --git a/src/app/routes/api/v3/forms/__tests__/public-forms.auth.routes.spec.ts b/src/app/routes/api/v3/forms/__tests__/public-forms.auth.routes.spec.ts index e1e139e021..a12886f01e 100644 --- a/src/app/routes/api/v3/forms/__tests__/public-forms.auth.routes.spec.ts +++ b/src/app/routes/api/v3/forms/__tests__/public-forms.auth.routes.spec.ts @@ -5,10 +5,7 @@ import { err, errAsync } from 'neverthrow' import supertest, { Session } from 'supertest-session' import { mocked } from 'ts-jest/utils' -import { - DatabaseError, - MissingFeatureError, -} from 'src/app/modules/core/core.errors' +import { DatabaseError } from 'src/app/modules/core/core.errors' import { getRedirectTarget } from 'src/app/modules/spcp/spcp.util' import { AuthType, Status } from 'src/types' @@ -18,12 +15,11 @@ import dbHandler from 'tests/unit/backend/helpers/jest-db' import { jsonParseStringify } from 'tests/unit/backend/helpers/serialize-data' import * as FormService from '../../../../../modules/form/form.service' -import { MyInfoFactory } from '../../../../../modules/myinfo/myinfo.factory' import { CreateRedirectUrlError, FetchLoginPageError, } from '../../../../../modules/spcp/spcp.errors' -import { SpcpFactory } from '../../../../../modules/spcp/spcp.factory' +import { SpcpService } from '../../../../../modules/spcp/spcp.service' import { PublicFormsRouter } from '../public-forms.routes' // NOTE: Mocking axios here because there is a network call to an external service @@ -235,33 +231,6 @@ describe('public-form.auth.routes', () => { expect(response.body).toEqual(expectedResponse) }) - it('should return 500 when the redirect url feature is not enabled', async () => { - // Arrange - const MOCK_FEATURE_NAME = 'no direct only direct' - const { form } = await dbHandler.insertEmailForm({ - formOptions: { - authType: AuthType.MyInfo, - status: Status.Public, - esrvcId: new ObjectId().toHexString(), - }, - }) - const expectedResponse = jsonParseStringify({ - message: `Sorry, something went wrong. Please try again.`, - }) - jest - .spyOn(MyInfoFactory, 'createRedirectURL') - .mockReturnValueOnce(err(new MissingFeatureError(MOCK_FEATURE_NAME))) - - // Act - const response = await request - .get(`/forms/${form._id}/auth/redirect`) - .query({ isPersistentLogin: false }) - - // Assert - expect(response.status).toEqual(StatusCodes.INTERNAL_SERVER_ERROR) - expect(response.body).toEqual(expectedResponse) - }) - it('should return 500 when the redirect url could not be created', async () => { // Arrange const { form } = await dbHandler.insertEmailForm({ @@ -275,7 +244,7 @@ describe('public-form.auth.routes', () => { message: 'Sorry, something went wrong. Please try again.', }) jest - .spyOn(SpcpFactory, 'createRedirectUrl') + .spyOn(SpcpService, 'createRedirectUrl') .mockReturnValueOnce(err(new CreateRedirectUrlError())) // Act From aee42eeec682d826c6779f40d2b850794db361d2 Mon Sep 17 00:00:00 2001 From: Antariksh Mahajan Date: Tue, 22 Jun 2021 20:08:55 +0800 Subject: [PATCH 07/21] refactor(feature-manager): delete remaining unused code (#2223) * refactor(feature-manager): delete FeatureManager class * refactor(feature-manager): delete MissingFeatureError * refactor(feature-manager): delete FeatureManager types * refactor(feature-manager): rename config dir to features * test: fix remaining test --- src/app/config/feature-manager/index.ts | 7 - src/app/config/feature-manager/types.ts | 15 --- .../util/FeatureManager.class.ts | 123 ------------------ .../captcha.config.ts | 0 .../google-analytics.config.ts | 0 .../intranet.config.ts | 0 .../sentry.config.ts | 0 .../sms.config.ts | 0 .../spcp-myinfo.config.ts | 0 .../verified-fields.config.ts | 0 .../webhook-verified-content.config.ts | 0 src/app/config/formsg-sdk.ts | 4 +- .../loaders/express/__tests__/helmet.spec.ts | 4 +- src/app/loaders/express/helmet.ts | 2 +- src/app/loaders/express/locals.ts | 8 +- src/app/modules/core/core.errors.ts | 14 -- .../__tests__/public-form.controller.spec.ts | 42 +----- .../form/public-form/public-form.utils.ts | 7 +- src/app/modules/myinfo/myinfo.service.ts | 6 +- src/app/modules/myinfo/myinfo.types.ts | 2 +- src/app/modules/myinfo/myinfo.util.ts | 5 +- .../spcp/__tests__/spcp.service.spec.ts | 2 +- src/app/modules/spcp/spcp.service.ts | 2 +- src/app/modules/spcp/spcp.util.ts | 2 - .../email-submission/email-submission.util.ts | 2 - .../encrypt-submission.controller.ts | 14 +- .../encrypt-submission.utils.ts | 2 - src/app/modules/user/user.utils.ts | 5 - .../modules/verification/verification.util.ts | 2 - .../verified-content.service.ts | 2 +- .../verified-content.types.ts | 5 - src/app/modules/webhook/webhook.factory.ts | 2 +- src/app/services/captcha/captcha.service.ts | 2 +- src/app/services/captcha/captcha.util.ts | 7 - src/app/services/intranet/intranet.service.ts | 2 +- .../sms/__tests__/sms.factory.spec.ts | 2 +- src/app/services/sms/sms.factory.ts | 2 +- 37 files changed, 29 insertions(+), 265 deletions(-) delete mode 100644 src/app/config/feature-manager/index.ts delete mode 100644 src/app/config/feature-manager/types.ts delete mode 100644 src/app/config/feature-manager/util/FeatureManager.class.ts rename src/app/config/{feature-manager => features}/captcha.config.ts (100%) rename src/app/config/{feature-manager => features}/google-analytics.config.ts (100%) rename src/app/config/{feature-manager => features}/intranet.config.ts (100%) rename src/app/config/{feature-manager => features}/sentry.config.ts (100%) rename src/app/config/{feature-manager => features}/sms.config.ts (100%) rename src/app/config/{feature-manager => features}/spcp-myinfo.config.ts (100%) rename src/app/config/{feature-manager => features}/verified-fields.config.ts (100%) rename src/app/config/{feature-manager => features}/webhook-verified-content.config.ts (100%) diff --git a/src/app/config/feature-manager/index.ts b/src/app/config/feature-manager/index.ts deleted file mode 100644 index dac9542055..0000000000 --- a/src/app/config/feature-manager/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import FeatureManager from './util/FeatureManager.class' - -export * from './types' - -const featureManager = new FeatureManager() - -export default featureManager diff --git a/src/app/config/feature-manager/types.ts b/src/app/config/feature-manager/types.ts deleted file mode 100644 index 8505a8d8aa..0000000000 --- a/src/app/config/feature-manager/types.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Schema } from 'convict' - -export enum FeatureNames {} - -export type IFeatureManager = Record - -export interface RegisteredFeature { - isEnabled: boolean - props?: IFeatureManager[T] -} - -export interface RegisterableFeature { - name: K - schema: Schema -} diff --git a/src/app/config/feature-manager/util/FeatureManager.class.ts b/src/app/config/feature-manager/util/FeatureManager.class.ts deleted file mode 100644 index 0e4ebaee36..0000000000 --- a/src/app/config/feature-manager/util/FeatureManager.class.ts +++ /dev/null @@ -1,123 +0,0 @@ -import convict from 'convict' -import validator from 'convict-format-with-validator' -import _ from 'lodash' - -import { createLoggerWithLabel } from '../../logger' -import { - FeatureNames, - IFeatureManager, - RegisterableFeature, - RegisteredFeature, -} from '../types' - -const logger = createLoggerWithLabel(module) -convict.addFormat(validator.url) - -export default class FeatureManager { - /* eslint-disable typesafe/no-throw-sync-func - -------- - This class bootstraps the application and should - cause a crash in case of errors. */ - public states: Partial> - // Map some feature names to some env vars - private properties: Partial - constructor() { - this.states = {} - this.properties = {} - } - - /** - * Register values/fallbacks associated with feature if namespace is available - * @param params - * @param params.name Name of feature - * @param params.schema Convict schema - */ - register({ - name, - schema, - }: RegisterableFeature): void { - // Check that namespace is available - if (this.properties[name] || this.states[name]) { - throw new Error( - `A feature called ${name} already exists. Please choose another name!`, - ) - } - - // Feature is only enabled if all required environment variables are present and defined - const config = convict(schema) - const properties = config.getProperties() - const isEnabled = Object.keys(schema).every((variable) => { - const val = _.get(properties, variable, null) - // empty strings (i.e. '') are considered defined - const isDefined = !_.isNil(val) - return isDefined - }) - - // Update isEnabled state in instance property - this.states[name] = isEnabled - - // Update properties in instance property if feature enabled - // Validate configuration if feature enabled - Error will be thrown if validation fails - if (isEnabled) { - config.validate({ allowed: 'strict' }) - this.properties[name] = properties - } - - // Inform whether feature is enabled or not - if (isEnabled) { - logger.info({ - message: `${name} feature will be enabled on app`, - meta: { - action: 'register', - }, - }) - } else { - logger.warn({ - message: `\n!!! WARNING !!! \nEnv vars for ${name} are not detected. \n${name} feature will be disabled on app`, - meta: { - action: 'register', - }, - }) - } - } - - /** - * Return true if requested feature is enabled. Else, throw error. - * @param name the feature name to check enabledness - * @returns true if the feature is enabled, false if disabled - * @throws {Error} if name given is not a registered feature - */ - isEnabled(name: FeatureNames): boolean { - if (this.states[name] !== undefined) { - return !!this.states[name] - } - - // Only throw error if state is undefined. - throw new Error(`A feature called ${name} does not exist`) - } - - /** - * Return props registered for requested feature - * @param name the feature to return properties for - */ - props(name: K): IFeatureManager[K] { - if (this.states[name] !== undefined) { - return this.properties[name] as IFeatureManager[K] - } - // Not enabled or not in state. - throw new Error(`A feature called ${name} does not exist`) - } - - /** - * Return properties registered for requested feature - * and whether requested feature is enabled - * @param name the name of the feature to return - */ - get(name: K): RegisteredFeature { - return { - isEnabled: this.isEnabled(name), - props: this.props(name), - } - } - /* eslint-enable typesafe/no-throw-sync-func */ -} diff --git a/src/app/config/feature-manager/captcha.config.ts b/src/app/config/features/captcha.config.ts similarity index 100% rename from src/app/config/feature-manager/captcha.config.ts rename to src/app/config/features/captcha.config.ts diff --git a/src/app/config/feature-manager/google-analytics.config.ts b/src/app/config/features/google-analytics.config.ts similarity index 100% rename from src/app/config/feature-manager/google-analytics.config.ts rename to src/app/config/features/google-analytics.config.ts diff --git a/src/app/config/feature-manager/intranet.config.ts b/src/app/config/features/intranet.config.ts similarity index 100% rename from src/app/config/feature-manager/intranet.config.ts rename to src/app/config/features/intranet.config.ts diff --git a/src/app/config/feature-manager/sentry.config.ts b/src/app/config/features/sentry.config.ts similarity index 100% rename from src/app/config/feature-manager/sentry.config.ts rename to src/app/config/features/sentry.config.ts diff --git a/src/app/config/feature-manager/sms.config.ts b/src/app/config/features/sms.config.ts similarity index 100% rename from src/app/config/feature-manager/sms.config.ts rename to src/app/config/features/sms.config.ts diff --git a/src/app/config/feature-manager/spcp-myinfo.config.ts b/src/app/config/features/spcp-myinfo.config.ts similarity index 100% rename from src/app/config/feature-manager/spcp-myinfo.config.ts rename to src/app/config/features/spcp-myinfo.config.ts diff --git a/src/app/config/feature-manager/verified-fields.config.ts b/src/app/config/features/verified-fields.config.ts similarity index 100% rename from src/app/config/feature-manager/verified-fields.config.ts rename to src/app/config/features/verified-fields.config.ts diff --git a/src/app/config/feature-manager/webhook-verified-content.config.ts b/src/app/config/features/webhook-verified-content.config.ts similarity index 100% rename from src/app/config/feature-manager/webhook-verified-content.config.ts rename to src/app/config/features/webhook-verified-content.config.ts diff --git a/src/app/config/formsg-sdk.ts b/src/app/config/formsg-sdk.ts index 1c41d41981..c4916ddb50 100644 --- a/src/app/config/formsg-sdk.ts +++ b/src/app/config/formsg-sdk.ts @@ -2,8 +2,8 @@ import formsgSdkPackage from '@opengovsg/formsg-sdk' import * as vfnConstants from '../../shared/util/verification' -import { verifiedFieldsConfig } from './feature-manager/verified-fields.config' -import { webhooksAndVerifiedContentConfig } from './feature-manager/webhook-verified-content.config' +import { verifiedFieldsConfig } from './features/verified-fields.config' +import { webhooksAndVerifiedContentConfig } from './features/webhook-verified-content.config' import { formsgSdkMode } from './config' const formsgSdk = formsgSdkPackage({ diff --git a/src/app/loaders/express/__tests__/helmet.spec.ts b/src/app/loaders/express/__tests__/helmet.spec.ts index c3b91117a9..12f4210fba 100644 --- a/src/app/loaders/express/__tests__/helmet.spec.ts +++ b/src/app/loaders/express/__tests__/helmet.spec.ts @@ -2,7 +2,7 @@ import helmet from 'helmet' import { mocked } from 'ts-jest/utils' import config from 'src/app/config/config' -import { sentryConfig } from 'src/app/config/feature-manager/sentry.config' +import { sentryConfig } from 'src/app/config/features/sentry.config' import expressHandler from 'tests/unit/backend/helpers/jest-express' @@ -13,7 +13,7 @@ describe('helmetMiddlewares', () => { const mockHelmet = mocked(helmet, true) jest.mock('src/app/config/config') const mockConfig = mocked(config, true) - jest.mock('src/app/config/feature-manager/sentry.config') + jest.mock('src/app/config/features/sentry.config') const mockSentryConfig = mocked(sentryConfig, true) const cspCoreDirectives = { diff --git a/src/app/loaders/express/helmet.ts b/src/app/loaders/express/helmet.ts index 7baef9a361..88b0522197 100644 --- a/src/app/loaders/express/helmet.ts +++ b/src/app/loaders/express/helmet.ts @@ -3,7 +3,7 @@ import helmet from 'helmet' import { ContentSecurityPolicyOptions } from 'helmet/dist/middlewares/content-security-policy' import config from '../../config/config' -import { sentryConfig } from '../../config/feature-manager/sentry.config' +import { sentryConfig } from '../../config/features/sentry.config' const helmetMiddlewares = () => { // Only add the "Strict-Transport-Security" header if request is https. diff --git a/src/app/loaders/express/locals.ts b/src/app/loaders/express/locals.ts index 5d05ea7be0..cf01c5ddd3 100644 --- a/src/app/loaders/express/locals.ts +++ b/src/app/loaders/express/locals.ts @@ -1,10 +1,10 @@ import ejs from 'ejs' import config from '../../config/config' -import { captchaConfig } from '../../config/feature-manager/captcha.config' -import { googleAnalyticsConfig } from '../../config/feature-manager/google-analytics.config' -import { sentryConfig } from '../../config/feature-manager/sentry.config' -import { spcpMyInfoConfig } from '../../config/feature-manager/spcp-myinfo.config' +import { captchaConfig } from '../../config/features/captcha.config' +import { googleAnalyticsConfig } from '../../config/features/google-analytics.config' +import { sentryConfig } from '../../config/features/sentry.config' +import { spcpMyInfoConfig } from '../../config/features/spcp-myinfo.config' // Construct js with environment variables needed by frontend const frontendVars = { diff --git a/src/app/modules/core/core.errors.ts b/src/app/modules/core/core.errors.ts index 51090f54b1..9529470c90 100644 --- a/src/app/modules/core/core.errors.ts +++ b/src/app/modules/core/core.errors.ts @@ -1,5 +1,3 @@ -import { FeatureNames } from '../../config/feature-manager' - /** * A custom base error class that encapsulates the name, message, status code, * and logging meta string (if any) for the error. @@ -62,18 +60,6 @@ export class MalformedParametersError extends ApplicationError { } } -/** - * Error thrown when feature-specific functions are called - * despite those features not being activated. - */ -export class MissingFeatureError extends ApplicationError { - constructor(missingFeature: FeatureNames) { - super( - `${missingFeature} is not activated, but a feature-specific function was called.`, - ) - } -} - /** * Error thrown when attachment upload fails */ diff --git a/src/app/modules/form/public-form/__tests__/public-form.controller.spec.ts b/src/app/modules/form/public-form/__tests__/public-form.controller.spec.ts index 6a5e3718eb..c695f23f24 100644 --- a/src/app/modules/form/public-form/__tests__/public-form.controller.spec.ts +++ b/src/app/modules/form/public-form/__tests__/public-form.controller.spec.ts @@ -6,15 +6,12 @@ import { err, errAsync, ok, okAsync } from 'neverthrow' import querystring from 'querystring' import { mocked } from 'ts-jest/utils' -import { FeatureNames } from 'src/app/config/feature-manager/types' import getFormFeedbackModel from 'src/app/models/form_feedback.server.model' -import { - DatabaseError, - MissingFeatureError, -} from 'src/app/modules/core/core.errors' +import { DatabaseError } from 'src/app/modules/core/core.errors' import { MyInfoData } from 'src/app/modules/myinfo/myinfo.adapter' import { MyInfoCookieAccessError, + MyInfoFetchError, MyInfoMissingAccessTokenError, } from 'src/app/modules/myinfo/myinfo.errors' import { @@ -794,11 +791,7 @@ describe('public-form.controller', () => { }) MockMyInfoService.getMyInfoDataForForm.mockReturnValueOnce( - errAsync( - new MissingFeatureError( - 'testing is the missing feature' as FeatureNames, - ), - ), + errAsync(new MyInfoFetchError()), ) // Act @@ -1460,35 +1453,6 @@ describe('public-form.controller', () => { message: 'Sorry, something went wrong. Please try again.', }) }) - - it('should return 500 when the redirectURL feature is not implemented', async () => { - // Arrange - const MOCK_FORM = { - esrvcId: '234', - authType: AuthType.MyInfo, - getUniqueMyInfoAttrs: jest.fn().mockReturnValue([]), - } as unknown as SpcpForm - const mockRes = expressHandler.mockResponse() - MockFormService.retrieveFullFormById.mockReturnValueOnce( - okAsync(MOCK_FORM), - ) - MockMyInfoService.createRedirectURL.mockReturnValueOnce( - err(new MissingFeatureError('Redirect url')), - ) - - // Act - await PublicFormController._handleFormAuthRedirect( - MOCK_REQ, - mockRes, - jest.fn(), - ) - - // Assert - expect(mockRes.status).toBeCalledWith(500) - expect(mockRes.json).toBeCalledWith({ - message: 'Sorry, something went wrong. Please try again.', - }) - }) }) describe('handleValidateFormEsrvcId', () => { diff --git a/src/app/modules/form/public-form/public-form.utils.ts b/src/app/modules/form/public-form/public-form.utils.ts index 1a443d8c30..db5cd0a86e 100644 --- a/src/app/modules/form/public-form/public-form.utils.ts +++ b/src/app/modules/form/public-form/public-form.utils.ts @@ -3,11 +3,7 @@ import { getReasonPhrase, StatusCodes } from 'http-status-codes' import { MapRouteError } from 'src/types' import { createLoggerWithLabel } from '../../../config/logger' -import { - ApplicationError, - DatabaseError, - MissingFeatureError, -} from '../../core/core.errors' +import { ApplicationError, DatabaseError } from '../../core/core.errors' import { ErrorResponseData } from '../../core/core.types' import { CreateRedirectUrlError, @@ -105,7 +101,6 @@ export const mapFormAuthError: MapRouteError = ( } case DatabaseError: case CreateRedirectUrlError: - case MissingFeatureError: return { statusCode: StatusCodes.INTERNAL_SERVER_ERROR, errorMessage: coreErrorMessage, diff --git a/src/app/modules/myinfo/myinfo.service.ts b/src/app/modules/myinfo/myinfo.service.ts index a5a8eba05e..bfbb3324af 100644 --- a/src/app/modules/myinfo/myinfo.service.ts +++ b/src/app/modules/myinfo/myinfo.service.ts @@ -20,9 +20,9 @@ import { MyInfoAttribute, } from '../../../types' import config from '../../config/config' -import { spcpMyInfoConfig } from '../../config/feature-manager/spcp-myinfo.config' +import { spcpMyInfoConfig } from '../../config/features/spcp-myinfo.config' import { createLoggerWithLabel } from '../../config/logger' -import { DatabaseError, MissingFeatureError } from '../core/core.errors' +import { DatabaseError } from '../core/core.errors' import { AuthTypeMismatchError, FormAuthNoEsrvcIdError, @@ -495,7 +495,6 @@ export class MyInfoServiceClass { * @returns err(AuthTypeMismatchError) if the client was not authenticated using MyInfo * @returns err(MyInfoCircuitBreakerError) if circuit breaker was active * @returns err(MyInfoFetchError) if validated but the data could not be retrieved - * @returns err(MissingFeatureError) if using an outdated version that does not support myInfo */ getMyInfoDataForForm( form: IPopulatedForm, @@ -508,7 +507,6 @@ export class MyInfoServiceClass { | AuthTypeMismatchError | MyInfoCircuitBreakerError | MyInfoFetchError - | MissingFeatureError | MyInfoCookieAccessError > { const requestedAttributes = form.getUniqueMyInfoAttrs() diff --git a/src/app/modules/myinfo/myinfo.types.ts b/src/app/modules/myinfo/myinfo.types.ts index dbc6c4180e..a33ad0551d 100644 --- a/src/app/modules/myinfo/myinfo.types.ts +++ b/src/app/modules/myinfo/myinfo.types.ts @@ -5,7 +5,7 @@ import { IMyInfo, MyInfoAttribute, } from '../../../types' -import { ISpcpMyInfo } from '../../config/feature-manager/spcp-myinfo.config' +import { ISpcpMyInfo } from '../../config/features/spcp-myinfo.config' import { ProcessedFieldResponse } from '../submission/submission.types' export interface IMyInfoServiceConfig { diff --git a/src/app/modules/myinfo/myinfo.util.ts b/src/app/modules/myinfo/myinfo.util.ts index 5c5a05b9dd..e7715b6341 100644 --- a/src/app/modules/myinfo/myinfo.util.ts +++ b/src/app/modules/myinfo/myinfo.util.ts @@ -17,7 +17,7 @@ import { MapRouteError, } from '../../../types' import { createLoggerWithLabel } from '../../config/logger' -import { DatabaseError, MissingFeatureError } from '../core/core.errors' +import { DatabaseError } from '../core/core.errors' import { AuthTypeMismatchError, FormAuthNoEsrvcIdError, @@ -127,7 +127,6 @@ export const compareHashedValues = ( */ export const mapVerifyMyInfoError: MapRouteError = (error) => { switch (error.constructor) { - case MissingFeatureError: case MyInfoHashingError: case DatabaseError: return { @@ -184,7 +183,6 @@ export const mapRedirectURLError: MapRouteError = ( 'This form does not have MyInfo enabled. Please refresh and try again.', } case DatabaseError: - case MissingFeatureError: return { statusCode: StatusCodes.INTERNAL_SERVER_ERROR, errorMessage: coreErrorMessage, @@ -239,7 +237,6 @@ export const mapEServiceIdCheckError: MapRouteError = ( errorMessage: 'Failed to contact SingPass. Please try again.', } case DatabaseError: - case MissingFeatureError: case CreateRedirectUrlError: return { statusCode: StatusCodes.INTERNAL_SERVER_ERROR, diff --git a/src/app/modules/spcp/__tests__/spcp.service.spec.ts b/src/app/modules/spcp/__tests__/spcp.service.spec.ts index 70def2d43e..b27719c289 100644 --- a/src/app/modules/spcp/__tests__/spcp.service.spec.ts +++ b/src/app/modules/spcp/__tests__/spcp.service.spec.ts @@ -4,7 +4,7 @@ import fs from 'fs' import { omit } from 'lodash' import { mocked } from 'ts-jest/utils' -import { ISpcpMyInfo } from 'src/app/config/feature-manager/spcp-myinfo.config' +import { ISpcpMyInfo } from 'src/app/config/features/spcp-myinfo.config' import { MOCK_COOKIE_AGE } from 'src/app/modules/myinfo/__tests__/myinfo.test.constants' import { AuthType } from 'src/types' diff --git a/src/app/modules/spcp/spcp.service.ts b/src/app/modules/spcp/spcp.service.ts index 7a6c3d361e..497474e6ee 100644 --- a/src/app/modules/spcp/spcp.service.ts +++ b/src/app/modules/spcp/spcp.service.ts @@ -9,7 +9,7 @@ import { PublicFormAuthValidateEsrvcIdDto } from '../../../types/api' import { ISpcpMyInfo, spcpMyInfoConfig, -} from '../../config/feature-manager/spcp-myinfo.config' +} from '../../config/features/spcp-myinfo.config' import { createLoggerWithLabel } from '../../config/logger' import { ApplicationError } from '../core/core.errors' diff --git a/src/app/modules/spcp/spcp.util.ts b/src/app/modules/spcp/spcp.util.ts index 3aba24bbe6..60195fd7e4 100644 --- a/src/app/modules/spcp/spcp.util.ts +++ b/src/app/modules/spcp/spcp.util.ts @@ -12,7 +12,6 @@ import { SPCPFieldTitle, } from '../../../types' import { createLoggerWithLabel } from '../../config/logger' -import { MissingFeatureError } from '../core/core.errors' import { AuthTypeMismatchError, FormAuthNoEsrvcIdError, @@ -256,7 +255,6 @@ export const mapRouteError: MapRouteError = ( coreErrorMessage = 'Sorry, something went wrong. Please try again.', ) => { switch (error.constructor) { - case MissingFeatureError: case CreateRedirectUrlError: return { statusCode: StatusCodes.INTERNAL_SERVER_ERROR, diff --git a/src/app/modules/submission/email-submission/email-submission.util.ts b/src/app/modules/submission/email-submission/email-submission.util.ts index 93fc5f8fc5..0a143f3fc1 100644 --- a/src/app/modules/submission/email-submission/email-submission.util.ts +++ b/src/app/modules/submission/email-submission/email-submission.util.ts @@ -36,7 +36,6 @@ import { DatabaseError, DatabasePayloadSizeError, DatabaseValidationError, - MissingFeatureError, } from '../../core/core.errors' import { ForbiddenFormError, @@ -365,7 +364,6 @@ export const mapRouteError: MapRouteError = (error) => { } case DatabaseError: case SubmissionHashError: - case MissingFeatureError: return { statusCode: StatusCodes.INTERNAL_SERVER_ERROR, errorMessage: diff --git a/src/app/modules/submission/encrypt-submission/encrypt-submission.controller.ts b/src/app/modules/submission/encrypt-submission/encrypt-submission.controller.ts index adc5aad469..1f4cee219c 100644 --- a/src/app/modules/submission/encrypt-submission/encrypt-submission.controller.ts +++ b/src/app/modules/submission/encrypt-submission/encrypt-submission.controller.ts @@ -22,10 +22,7 @@ import * as CaptchaMiddleware from '../../../services/captcha/captcha.middleware import * as CaptchaService from '../../../services/captcha/captcha.service' import { createReqMeta, getRequestIp } from '../../../utils/request' import { getFormAfterPermissionChecks } from '../../auth/auth.service' -import { - MalformedParametersError, - MissingFeatureError, -} from '../../core/core.errors' +import { MalformedParametersError } from '../../core/core.errors' import { ControllerHandler } from '../../core/core.types' import { PermissionLevel } from '../../form/admin-form/admin-form.types' import * as FormService from '../../form/form.service' @@ -269,12 +266,9 @@ const submitEncryptModeForm: ControllerHandler< error, }) - // Passthrough if feature is not activated. - if (!(error instanceof MissingFeatureError)) { - return res - .status(StatusCodes.BAD_REQUEST) - .json({ message: 'Invalid data was found. Please submit again.' }) - } + return res + .status(StatusCodes.BAD_REQUEST) + .json({ message: 'Invalid data was found. Please submit again.' }) } else { // No errors, set local variable to the encrypted string. verified = encryptVerifiedContentResult.value diff --git a/src/app/modules/submission/encrypt-submission/encrypt-submission.utils.ts b/src/app/modules/submission/encrypt-submission/encrypt-submission.utils.ts index 21ff1a1e17..503b6e0088 100644 --- a/src/app/modules/submission/encrypt-submission/encrypt-submission.utils.ts +++ b/src/app/modules/submission/encrypt-submission/encrypt-submission.utils.ts @@ -23,7 +23,6 @@ import { DatabaseValidationError, EmptyErrorFieldError, MalformedParametersError, - MissingFeatureError, } from '../../core/core.errors' import { CreatePresignedUrlError } from '../../form/admin-form/admin-form.errors' import { @@ -68,7 +67,6 @@ const errorMapper: MapRouteError = ( errorMessage: 'Could not upload attachments for submission. For assistance, please contact the person who asked you to fill in this form.', } - case MissingFeatureError: case CreateRedirectUrlError: return { statusCode: StatusCodes.INTERNAL_SERVER_ERROR, diff --git a/src/app/modules/user/user.utils.ts b/src/app/modules/user/user.utils.ts index 75f7d92c28..bf30278bc9 100644 --- a/src/app/modules/user/user.utils.ts +++ b/src/app/modules/user/user.utils.ts @@ -22,11 +22,6 @@ export const mapRouteError = ( coreErrorMessage?: string, ): ErrorResponseData => { switch (error.constructor) { - case CoreErrors.MissingFeatureError: - return { - statusCode: StatusCodes.INTERNAL_SERVER_ERROR, - errorMessage: 'Sms feature unavailable', - } case UserErrors.InvalidOtpError: return { statusCode: StatusCodes.UNAUTHORIZED, diff --git a/src/app/modules/verification/verification.util.ts b/src/app/modules/verification/verification.util.ts index 3d590c5e76..da915b3afb 100644 --- a/src/app/modules/verification/verification.util.ts +++ b/src/app/modules/verification/verification.util.ts @@ -21,7 +21,6 @@ import { DatabasePayloadSizeError, DatabaseValidationError, MalformedParametersError, - MissingFeatureError, } from '../core/core.errors' import { FormNotFoundError } from '../form/form.errors' @@ -193,7 +192,6 @@ export const mapRouteError: MapRouteError = ( } case HashingError: case DatabaseError: - case MissingFeatureError: return { errorMessage: coreErrorMsg, statusCode: StatusCodes.INTERNAL_SERVER_ERROR, diff --git a/src/app/modules/verified-content/verified-content.service.ts b/src/app/modules/verified-content/verified-content.service.ts index f936cf8304..0d69530695 100644 --- a/src/app/modules/verified-content/verified-content.service.ts +++ b/src/app/modules/verified-content/verified-content.service.ts @@ -1,7 +1,7 @@ import { err, ok, Result } from 'neverthrow' import { AuthType } from '../../../types' -import { webhooksAndVerifiedContentConfig } from '../../config/feature-manager/webhook-verified-content.config' +import { webhooksAndVerifiedContentConfig } from '../../config/features/webhook-verified-content.config' import formsgSdk from '../../config/formsg-sdk' import { createLoggerWithLabel } from '../../config/logger' diff --git a/src/app/modules/verified-content/verified-content.types.ts b/src/app/modules/verified-content/verified-content.types.ts index fd3cbfb81f..82559da122 100644 --- a/src/app/modules/verified-content/verified-content.types.ts +++ b/src/app/modules/verified-content/verified-content.types.ts @@ -2,7 +2,6 @@ import { Result } from 'neverthrow' import { VerifiedKeys } from '../../../shared/util/verified-content' import { AuthType } from '../../../types' -import { MissingFeatureError } from '../core/core.errors' import { MalformedVerifiedContentError } from './verified-content.errors' @@ -27,10 +26,6 @@ export type SpVerifiedContent = { } export type VerifiedContentResult = Result -export type FactoryVerifiedContentResult = Result< - T, - E | MissingFeatureError -> export type EncryptVerificationContentParams = { verifiedContent: CpVerifiedContent | SpVerifiedContent diff --git a/src/app/modules/webhook/webhook.factory.ts b/src/app/modules/webhook/webhook.factory.ts index da76b450df..7ef54419c8 100644 --- a/src/app/modules/webhook/webhook.factory.ts +++ b/src/app/modules/webhook/webhook.factory.ts @@ -1,4 +1,4 @@ -import { webhooksAndVerifiedContentConfig } from '../../config/feature-manager/webhook-verified-content.config' +import { webhooksAndVerifiedContentConfig } from '../../config/features/webhook-verified-content.config' import { startWebhookConsumer } from './webhook.consumer' import { WebhookProducer } from './webhook.producer' diff --git a/src/app/services/captcha/captcha.service.ts b/src/app/services/captcha/captcha.service.ts index 61af6e2a55..6c508e1bf4 100644 --- a/src/app/services/captcha/captcha.service.ts +++ b/src/app/services/captcha/captcha.service.ts @@ -1,7 +1,7 @@ import axios from 'axios' import { errAsync, okAsync, ResultAsync } from 'neverthrow' -import { captchaConfig } from '../../config/feature-manager/captcha.config' +import { captchaConfig } from '../../config/features/captcha.config' import { createLoggerWithLabel } from '../../config/logger' import { GOOGLE_RECAPTCHA_URL } from './captcha.constants' diff --git a/src/app/services/captcha/captcha.util.ts b/src/app/services/captcha/captcha.util.ts index 9edb7f0add..8cc0a2c08c 100644 --- a/src/app/services/captcha/captcha.util.ts +++ b/src/app/services/captcha/captcha.util.ts @@ -2,7 +2,6 @@ import { StatusCodes } from 'http-status-codes' import { MapRouteError } from '../../../types' import { createLoggerWithLabel } from '../../config/logger' -import { MissingFeatureError } from '../../modules/core/core.errors' import { CaptchaConnectionError, @@ -30,12 +29,6 @@ export const mapRouteError: MapRouteError = (error) => { statusCode: StatusCodes.BAD_REQUEST, errorMessage: 'Captcha was missing. Please refresh and submit again.', } - case MissingFeatureError: - return { - statusCode: StatusCodes.INTERNAL_SERVER_ERROR, - errorMessage: - 'Captcha verification unavailable. Please try again later.', - } default: logger.error({ message: 'mapRouteError called with unknown error type', diff --git a/src/app/services/intranet/intranet.service.ts b/src/app/services/intranet/intranet.service.ts index 158b01932d..08bdef057a 100644 --- a/src/app/services/intranet/intranet.service.ts +++ b/src/app/services/intranet/intranet.service.ts @@ -3,7 +3,7 @@ import fs from 'fs' import { IIntranet, intranetConfig, -} from '../../config/feature-manager/intranet.config' +} from '../../config/features/intranet.config' import { createLoggerWithLabel } from '../../config/logger' const logger = createLoggerWithLabel(module) diff --git a/src/app/services/sms/__tests__/sms.factory.spec.ts b/src/app/services/sms/__tests__/sms.factory.spec.ts index 75f5f71547..2741ff0b6b 100644 --- a/src/app/services/sms/__tests__/sms.factory.spec.ts +++ b/src/app/services/sms/__tests__/sms.factory.spec.ts @@ -3,7 +3,7 @@ import { okAsync } from 'neverthrow' import { mocked } from 'ts-jest/utils' import Twilio from 'twilio' -import { ISms } from 'src/app/config/feature-manager/sms.config' +import { ISms } from 'src/app/config/features/sms.config' import { createSmsFactory } from '../sms.factory' import * as SmsService from '../sms.service' diff --git a/src/app/services/sms/sms.factory.ts b/src/app/services/sms/sms.factory.ts index 6e95078bcc..3d76c73937 100644 --- a/src/app/services/sms/sms.factory.ts +++ b/src/app/services/sms/sms.factory.ts @@ -1,6 +1,6 @@ import Twilio from 'twilio' -import { ISms, smsConfig } from '../../config/feature-manager/sms.config' +import { ISms, smsConfig } from '../../config/features/sms.config' import { sendAdminContactOtp, From 8d545d6c914a5d3ff7212ea1a379a19e8fb4504b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Jun 2021 17:26:10 +0000 Subject: [PATCH 08/21] fix(deps): bump aws-sdk from 2.931.0 to 2.932.0 (#2237) Bumps [aws-sdk](https://github.com/aws/aws-sdk-js) from 2.931.0 to 2.932.0. - [Release notes](https://github.com/aws/aws-sdk-js/releases) - [Changelog](https://github.com/aws/aws-sdk-js/blob/master/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-js/compare/v2.931.0...v2.932.0) --- updated-dependencies: - dependency-name: aws-sdk dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 292dec807c..7c2d44a2e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8021,9 +8021,9 @@ "integrity": "sha512-24q5Rh3bno7ldoyCq99d6hpnLI+PAMocdeVaaGt/5BTQMprvDwQToHfNnruqN11odCHZZIQbRBw+nZo1lTCH9g==" }, "aws-sdk": { - "version": "2.931.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.931.0.tgz", - "integrity": "sha512-Db97/aJq8zYl8mHzY6dNO6m9S89TqN4HEUUc2aCYQCTyMb/eNrjf+uZTnutnQbJkClqHzxFcWc3aqe5VlTac/A==", + "version": "2.932.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.932.0.tgz", + "integrity": "sha512-U6MWUtFD0npWa+ReVEgm0fCIM0fMOYahFp14GLv8fC+BWOTvh5Iwt/gF8NrLomx42bBjA1Abaw6yhmiaSJDQHQ==", "requires": { "buffer": "4.9.2", "events": "1.1.1", diff --git a/package.json b/package.json index cab15e5612..33a271307c 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "angular-ui-bootstrap": "~2.5.6", "angular-ui-router": "~1.0.29", "aws-info": "^1.2.0", - "aws-sdk": "^2.931.0", + "aws-sdk": "^2.932.0", "axios": "^0.21.1", "bcrypt": "^5.0.1", "bluebird": "^3.5.2", From 3228708a0722247afb0affee9e8f9496d166cdf1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Jun 2021 17:38:08 +0000 Subject: [PATCH 09/21] chore(deps-dev): bump @babel/preset-env from 7.14.5 to 7.14.7 (#2238) Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.14.5 to 7.14.7. - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel/commits/v7.14.7/packages/babel-preset-env) --- updated-dependencies: - dependency-name: "@babel/preset-env" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 1314 ++++++++++----------------------------------- package.json | 2 +- 2 files changed, 272 insertions(+), 1044 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7c2d44a2e2..17eefa602d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1504,9 +1504,9 @@ } }, "@babel/helper-create-class-features-plugin": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.14.5.tgz", - "integrity": "sha512-Uq9z2e7ZtcnDMirRqAGLRaLwJn+Lrh388v5ETrR3pALJnElVh2zqQmdbz4W2RUJYohAPh2mtyPUgyMHMzXMncQ==", + "version": "7.14.6", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.14.6.tgz", + "integrity": "sha512-Z6gsfGofTxH/+LQXqYEK45kxmcensbzmk/oi8DmaQytlQCgqNZt9XQF8iqlI/SeXWVjaMNxvYvzaYw+kh42mDg==", "dev": true, "requires": { "@babel/helper-annotate-as-pure": "^7.14.5", @@ -1538,9 +1538,9 @@ } }, "@babel/helper-member-expression-to-functions": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.14.5.tgz", - "integrity": "sha512-UxUeEYPrqH1Q/k0yRku1JE7dyfyehNwT6SVkMHvYvPDv4+uu627VXBckVj891BO8ruKBkiDoGnZf4qPDD8abDQ==", + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.14.7.tgz", + "integrity": "sha512-TMUt4xKxJn6ccjcOW7c4hlwyJArizskAhoSTOCkA0uZ+KghIaci0Qg9R043kUMWI9mtQfgny+NQ5QATnZ+paaA==", "dev": true, "requires": { "@babel/types": "^7.14.5" @@ -1600,9 +1600,9 @@ } }, "@babel/parser": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.5.tgz", - "integrity": "sha512-TM8C+xtH/9n1qzX+JNHi7AN2zHMTiPUtspO0ZdHflW8KaskkALhMmuMHb4bCmNdv9VAPzJX3/bXqkVLnAvsPfg==", + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.7.tgz", + "integrity": "sha512-X67Z5y+VBJuHB/RjwECp8kSl5uYi0BvRbNeWqkaJCVh+LiTPl19WBUfG627psSgp9rSf6ojuXghQM3ha6qHHdA==", "dev": true }, "@babel/template": { @@ -1617,9 +1617,9 @@ } }, "@babel/traverse": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.5.tgz", - "integrity": "sha512-G3BiS15vevepdmFqmUc9X+64y0viZYygubAMO8SvBmKARuF6CPSZtH4Ng9vi/lrWlZFGe3FWdXNy835akH8Glg==", + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.7.tgz", + "integrity": "sha512-9vDr5NzHu27wgwejuKL7kIOm4bwEtaPQ4Z6cpCmjSuaRqpH/7xc4qcGEscwMqlkwgcXl6MvqoAjZkQ24uSdIZQ==", "dev": true, "requires": { "@babel/code-frame": "^7.14.5", @@ -1627,7 +1627,7 @@ "@babel/helper-function-name": "^7.14.5", "@babel/helper-hoist-variables": "^7.14.5", "@babel/helper-split-export-declaration": "^7.14.5", - "@babel/parser": "^7.14.5", + "@babel/parser": "^7.14.7", "@babel/types": "^7.14.5", "debug": "^4.1.0", "globals": "^11.1.0" @@ -2061,9 +2061,9 @@ } }, "@babel/helper-create-class-features-plugin": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.14.5.tgz", - "integrity": "sha512-Uq9z2e7ZtcnDMirRqAGLRaLwJn+Lrh388v5ETrR3pALJnElVh2zqQmdbz4W2RUJYohAPh2mtyPUgyMHMzXMncQ==", + "version": "7.14.6", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.14.6.tgz", + "integrity": "sha512-Z6gsfGofTxH/+LQXqYEK45kxmcensbzmk/oi8DmaQytlQCgqNZt9XQF8iqlI/SeXWVjaMNxvYvzaYw+kh42mDg==", "dev": true, "requires": { "@babel/helper-annotate-as-pure": "^7.14.5", @@ -2095,9 +2095,9 @@ } }, "@babel/helper-member-expression-to-functions": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.14.5.tgz", - "integrity": "sha512-UxUeEYPrqH1Q/k0yRku1JE7dyfyehNwT6SVkMHvYvPDv4+uu627VXBckVj891BO8ruKBkiDoGnZf4qPDD8abDQ==", + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.14.7.tgz", + "integrity": "sha512-TMUt4xKxJn6ccjcOW7c4hlwyJArizskAhoSTOCkA0uZ+KghIaci0Qg9R043kUMWI9mtQfgny+NQ5QATnZ+paaA==", "dev": true, "requires": { "@babel/types": "^7.14.5" @@ -2157,9 +2157,9 @@ } }, "@babel/parser": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.5.tgz", - "integrity": "sha512-TM8C+xtH/9n1qzX+JNHi7AN2zHMTiPUtspO0ZdHflW8KaskkALhMmuMHb4bCmNdv9VAPzJX3/bXqkVLnAvsPfg==", + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.7.tgz", + "integrity": "sha512-X67Z5y+VBJuHB/RjwECp8kSl5uYi0BvRbNeWqkaJCVh+LiTPl19WBUfG627psSgp9rSf6ojuXghQM3ha6qHHdA==", "dev": true }, "@babel/template": { @@ -2174,9 +2174,9 @@ } }, "@babel/traverse": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.5.tgz", - "integrity": "sha512-G3BiS15vevepdmFqmUc9X+64y0viZYygubAMO8SvBmKARuF6CPSZtH4Ng9vi/lrWlZFGe3FWdXNy835akH8Glg==", + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.7.tgz", + "integrity": "sha512-9vDr5NzHu27wgwejuKL7kIOm4bwEtaPQ4Z6cpCmjSuaRqpH/7xc4qcGEscwMqlkwgcXl6MvqoAjZkQ24uSdIZQ==", "dev": true, "requires": { "@babel/code-frame": "^7.14.5", @@ -2184,7 +2184,7 @@ "@babel/helper-function-name": "^7.14.5", "@babel/helper-hoist-variables": "^7.14.5", "@babel/helper-split-export-declaration": "^7.14.5", - "@babel/parser": "^7.14.5", + "@babel/parser": "^7.14.7", "@babel/types": "^7.14.5", "debug": "^4.1.0", "globals": "^11.1.0" @@ -2253,9 +2253,9 @@ } }, "@babel/helper-create-class-features-plugin": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.14.5.tgz", - "integrity": "sha512-Uq9z2e7ZtcnDMirRqAGLRaLwJn+Lrh388v5ETrR3pALJnElVh2zqQmdbz4W2RUJYohAPh2mtyPUgyMHMzXMncQ==", + "version": "7.14.6", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.14.6.tgz", + "integrity": "sha512-Z6gsfGofTxH/+LQXqYEK45kxmcensbzmk/oi8DmaQytlQCgqNZt9XQF8iqlI/SeXWVjaMNxvYvzaYw+kh42mDg==", "dev": true, "requires": { "@babel/helper-annotate-as-pure": "^7.14.5", @@ -2287,9 +2287,9 @@ } }, "@babel/helper-member-expression-to-functions": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.14.5.tgz", - "integrity": "sha512-UxUeEYPrqH1Q/k0yRku1JE7dyfyehNwT6SVkMHvYvPDv4+uu627VXBckVj891BO8ruKBkiDoGnZf4qPDD8abDQ==", + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.14.7.tgz", + "integrity": "sha512-TMUt4xKxJn6ccjcOW7c4hlwyJArizskAhoSTOCkA0uZ+KghIaci0Qg9R043kUMWI9mtQfgny+NQ5QATnZ+paaA==", "dev": true, "requires": { "@babel/types": "^7.14.5" @@ -2349,9 +2349,9 @@ } }, "@babel/parser": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.5.tgz", - "integrity": "sha512-TM8C+xtH/9n1qzX+JNHi7AN2zHMTiPUtspO0ZdHflW8KaskkALhMmuMHb4bCmNdv9VAPzJX3/bXqkVLnAvsPfg==", + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.7.tgz", + "integrity": "sha512-X67Z5y+VBJuHB/RjwECp8kSl5uYi0BvRbNeWqkaJCVh+LiTPl19WBUfG627psSgp9rSf6ojuXghQM3ha6qHHdA==", "dev": true }, "@babel/template": { @@ -2366,9 +2366,9 @@ } }, "@babel/traverse": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.5.tgz", - "integrity": "sha512-G3BiS15vevepdmFqmUc9X+64y0viZYygubAMO8SvBmKARuF6CPSZtH4Ng9vi/lrWlZFGe3FWdXNy835akH8Glg==", + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.7.tgz", + "integrity": "sha512-9vDr5NzHu27wgwejuKL7kIOm4bwEtaPQ4Z6cpCmjSuaRqpH/7xc4qcGEscwMqlkwgcXl6MvqoAjZkQ24uSdIZQ==", "dev": true, "requires": { "@babel/code-frame": "^7.14.5", @@ -2376,7 +2376,7 @@ "@babel/helper-function-name": "^7.14.5", "@babel/helper-hoist-variables": "^7.14.5", "@babel/helper-split-export-declaration": "^7.14.5", - "@babel/parser": "^7.14.5", + "@babel/parser": "^7.14.7", "@babel/types": "^7.14.5", "debug": "^4.1.0", "globals": "^11.1.0" @@ -2770,9 +2770,9 @@ } }, "@babel/plugin-transform-destructuring": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.14.5.tgz", - "integrity": "sha512-wU9tYisEbRMxqDezKUqC9GleLycCRoUsai9ddlsq54r8QRLaeEhc+d+9DqCG+kV9W2GgQjTZESPTpn5bAFMDww==", + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.14.7.tgz", + "integrity": "sha512-0mDE99nK+kVh3xlc5vKwB6wnP9ecuSj+zQCa/n0voENtP/zymdT4HH6QEb65wjjcbqr1Jb/7z9Qp7TF5FtwYGw==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.14.5" @@ -2929,9 +2929,9 @@ } }, "@babel/parser": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.5.tgz", - "integrity": "sha512-TM8C+xtH/9n1qzX+JNHi7AN2zHMTiPUtspO0ZdHflW8KaskkALhMmuMHb4bCmNdv9VAPzJX3/bXqkVLnAvsPfg==", + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.7.tgz", + "integrity": "sha512-X67Z5y+VBJuHB/RjwECp8kSl5uYi0BvRbNeWqkaJCVh+LiTPl19WBUfG627psSgp9rSf6ojuXghQM3ha6qHHdA==", "dev": true }, "@babel/template": { @@ -3001,6 +3001,115 @@ "@babel/helper-plugin-utils": "^7.14.5", "babel-plugin-dynamic-import-node": "^2.3.3" }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz", + "integrity": "sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ==", + "dev": true + } + } + }, + "@babel/plugin-transform-modules-commonjs": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.14.5.tgz", + "integrity": "sha512-en8GfBtgnydoao2PS+87mKyw62k02k7kJ9ltbKe0fXTHrQmG6QZZflYuGI1VVG7sVpx4E1n7KBpNlPb8m78J+A==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-simple-access": "^7.14.5", + "babel-plugin-dynamic-import-node": "^2.3.3" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz", + "integrity": "sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ==", + "dev": true + } + } + }, + "@babel/plugin-transform-modules-systemjs": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.14.5.tgz", + "integrity": "sha512-mNMQdvBEE5DcMQaL5LbzXFMANrQjd2W7FPzg34Y4yEz7dBgdaC+9B84dSO+/1Wba98zoDbInctCDo4JGxz1VYA==", + "dev": true, + "requires": { + "@babel/helper-hoist-variables": "^7.14.5", + "@babel/helper-module-transforms": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-validator-identifier": "^7.14.5", + "babel-plugin-dynamic-import-node": "^2.3.3" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz", + "integrity": "sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ==", + "dev": true + }, + "@babel/helper-validator-identifier": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.5.tgz", + "integrity": "sha512-5lsetuxCLilmVGyiLEfoHBRX8UCFD+1m2x3Rj97WrW3V7H3u4RWRXA4evMjImCsin2J2YT0QaVDGf+z8ondbAg==", + "dev": true + } + } + }, + "@babel/plugin-transform-modules-umd": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.14.5.tgz", + "integrity": "sha512-RfPGoagSngC06LsGUYyM9QWSXZ8MysEjDJTAea1lqRjNECE3y0qIJF/qbvJxc4oA4s99HumIMdXOrd+TdKaAAA==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.14.5", + "@babel/helper-plugin-utils": "^7.14.5" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz", + "integrity": "sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ==", + "dev": true + } + } + }, + "@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.14.7.tgz", + "integrity": "sha512-DTNOTaS7TkW97xsDMrp7nycUVh6sn/eq22VaxWfEdzuEbRsiaOU0pqU7DlyUGHVsbQbSghvjKRpEl+nUCKGQSg==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.14.5" + } + }, + "@babel/plugin-transform-new-target": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.14.5.tgz", + "integrity": "sha512-Nx054zovz6IIRWEB49RDRuXGI4Gy0GMgqG0cII9L3MxqgXz/+rgII+RU58qpo4g7tNEx1jG7rRVH4ihZoP4esQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz", + "integrity": "sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ==", + "dev": true + } + } + }, + "@babel/plugin-transform-object-super": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.14.5.tgz", + "integrity": "sha512-MKfOBWzK0pZIrav9z/hkRqIk/2bTv9qvxHzPQc12RcVkMOzpIKnFCNYJip00ssKWYkd8Sf5g0Wr7pqJ+cmtuFg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5", + "@babel/helper-replace-supers": "^7.14.5" + }, "dependencies": { "@babel/code-frame": { "version": "7.14.5", @@ -3043,36 +3152,11 @@ } }, "@babel/helper-member-expression-to-functions": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.14.5.tgz", - "integrity": "sha512-UxUeEYPrqH1Q/k0yRku1JE7dyfyehNwT6SVkMHvYvPDv4+uu627VXBckVj891BO8ruKBkiDoGnZf4qPDD8abDQ==", - "dev": true, - "requires": { - "@babel/types": "^7.14.5" - } - }, - "@babel/helper-module-imports": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.14.5.tgz", - "integrity": "sha512-SwrNHu5QWS84XlHwGYPDtCxcA0hrSlL2yhWYLgeOc0w7ccOl2qv4s/nARI0aYZW+bSwAL5CukeXA47B/1NKcnQ==", - "dev": true, - "requires": { - "@babel/types": "^7.14.5" - } - }, - "@babel/helper-module-transforms": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.14.5.tgz", - "integrity": "sha512-iXpX4KW8LVODuAieD7MzhNjmM6dzYY5tfRqT+R9HDXWl0jPn/djKmA+G9s/2C2T9zggw5tK1QNqZ70USfedOwA==", + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.14.7.tgz", + "integrity": "sha512-TMUt4xKxJn6ccjcOW7c4hlwyJArizskAhoSTOCkA0uZ+KghIaci0Qg9R043kUMWI9mtQfgny+NQ5QATnZ+paaA==", "dev": true, "requires": { - "@babel/helper-module-imports": "^7.14.5", - "@babel/helper-replace-supers": "^7.14.5", - "@babel/helper-simple-access": "^7.14.5", - "@babel/helper-split-export-declaration": "^7.14.5", - "@babel/helper-validator-identifier": "^7.14.5", - "@babel/template": "^7.14.5", - "@babel/traverse": "^7.14.5", "@babel/types": "^7.14.5" } }, @@ -3103,15 +3187,6 @@ "@babel/types": "^7.14.5" } }, - "@babel/helper-simple-access": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.14.5.tgz", - "integrity": "sha512-nfBN9xvmCt6nrMZjfhkl7i0oTV3yxR4/FztsbOASyTvVcoYd0TRHh7eMLdlEcCqobydC0LAF3LtC92Iwxo0wyw==", - "dev": true, - "requires": { - "@babel/types": "^7.14.5" - } - }, "@babel/helper-split-export-declaration": { "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.14.5.tgz", @@ -3139,9 +3214,9 @@ } }, "@babel/parser": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.5.tgz", - "integrity": "sha512-TM8C+xtH/9n1qzX+JNHi7AN2zHMTiPUtspO0ZdHflW8KaskkALhMmuMHb4bCmNdv9VAPzJX3/bXqkVLnAvsPfg==", + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.7.tgz", + "integrity": "sha512-X67Z5y+VBJuHB/RjwECp8kSl5uYi0BvRbNeWqkaJCVh+LiTPl19WBUfG627psSgp9rSf6ojuXghQM3ha6qHHdA==", "dev": true }, "@babel/template": { @@ -3156,9 +3231,9 @@ } }, "@babel/traverse": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.5.tgz", - "integrity": "sha512-G3BiS15vevepdmFqmUc9X+64y0viZYygubAMO8SvBmKARuF6CPSZtH4Ng9vi/lrWlZFGe3FWdXNy835akH8Glg==", + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.7.tgz", + "integrity": "sha512-9vDr5NzHu27wgwejuKL7kIOm4bwEtaPQ4Z6cpCmjSuaRqpH/7xc4qcGEscwMqlkwgcXl6MvqoAjZkQ24uSdIZQ==", "dev": true, "requires": { "@babel/code-frame": "^7.14.5", @@ -3166,7 +3241,7 @@ "@babel/helper-function-name": "^7.14.5", "@babel/helper-hoist-variables": "^7.14.5", "@babel/helper-split-export-declaration": "^7.14.5", - "@babel/parser": "^7.14.5", + "@babel/parser": "^7.14.7", "@babel/types": "^7.14.5", "debug": "^4.1.0", "globals": "^11.1.0" @@ -3193,875 +3268,74 @@ } } }, - "@babel/plugin-transform-modules-commonjs": { + "@babel/plugin-transform-parameters": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.13.0.tgz", + "integrity": "sha512-Jt8k/h/mIwE2JFEOb3lURoY5C85ETcYPnbuAJ96zRBzh1XHtQZfs62ChZ6EP22QlC8c7Xqr9q+e1SU5qttwwjw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.13.0" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.13.0.tgz", + "integrity": "sha512-ZPafIPSwzUlAoWT8DKs1W2VyF2gOWthGd5NGFMsBcMMol+ZhK+EQY/e6V96poa6PA/Bh+C9plWN0hXO1uB8AfQ==", + "dev": true + } + } + }, + "@babel/plugin-transform-property-literals": { "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.14.5.tgz", - "integrity": "sha512-en8GfBtgnydoao2PS+87mKyw62k02k7kJ9ltbKe0fXTHrQmG6QZZflYuGI1VVG7sVpx4E1n7KBpNlPb8m78J+A==", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.14.5.tgz", + "integrity": "sha512-r1uilDthkgXW8Z1vJz2dKYLV1tuw2xsbrp3MrZmD99Wh9vsfKoob+JTgri5VUb/JqyKRXotlOtwgu4stIYCmnw==", "dev": true, "requires": { - "@babel/helper-module-transforms": "^7.14.5", - "@babel/helper-plugin-utils": "^7.14.5", - "@babel/helper-simple-access": "^7.14.5", - "babel-plugin-dynamic-import-node": "^2.3.3" + "@babel/helper-plugin-utils": "^7.14.5" }, "dependencies": { - "@babel/code-frame": { + "@babel/helper-plugin-utils": { "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz", - "integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==", - "dev": true, - "requires": { - "@babel/highlight": "^7.14.5" - } - }, - "@babel/generator": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.5.tgz", - "integrity": "sha512-y3rlP+/G25OIX3mYKKIOlQRcqj7YgrvHxOLbVmyLJ9bPmi5ttvUmpydVjcFjZphOktWuA7ovbx91ECloWTfjIA==", - "dev": true, - "requires": { - "@babel/types": "^7.14.5", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" - } - }, - "@babel/helper-function-name": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.14.5.tgz", - "integrity": "sha512-Gjna0AsXWfFvrAuX+VKcN/aNNWonizBj39yGwUzVDVTlMYJMK2Wp6xdpy72mfArFq5uK+NOuexfzZlzI1z9+AQ==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.14.5", - "@babel/template": "^7.14.5", - "@babel/types": "^7.14.5" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.14.5.tgz", - "integrity": "sha512-I1Db4Shst5lewOM4V+ZKJzQ0JGGaZ6VY1jYvMghRjqs6DWgxLCIyFt30GlnKkfUeFLpJt2vzbMVEXVSXlIFYUg==", - "dev": true, - "requires": { - "@babel/types": "^7.14.5" - } - }, - "@babel/helper-member-expression-to-functions": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.14.5.tgz", - "integrity": "sha512-UxUeEYPrqH1Q/k0yRku1JE7dyfyehNwT6SVkMHvYvPDv4+uu627VXBckVj891BO8ruKBkiDoGnZf4qPDD8abDQ==", - "dev": true, - "requires": { - "@babel/types": "^7.14.5" - } - }, - "@babel/helper-module-imports": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.14.5.tgz", - "integrity": "sha512-SwrNHu5QWS84XlHwGYPDtCxcA0hrSlL2yhWYLgeOc0w7ccOl2qv4s/nARI0aYZW+bSwAL5CukeXA47B/1NKcnQ==", - "dev": true, - "requires": { - "@babel/types": "^7.14.5" - } - }, - "@babel/helper-module-transforms": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.14.5.tgz", - "integrity": "sha512-iXpX4KW8LVODuAieD7MzhNjmM6dzYY5tfRqT+R9HDXWl0jPn/djKmA+G9s/2C2T9zggw5tK1QNqZ70USfedOwA==", - "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.14.5", - "@babel/helper-replace-supers": "^7.14.5", - "@babel/helper-simple-access": "^7.14.5", - "@babel/helper-split-export-declaration": "^7.14.5", - "@babel/helper-validator-identifier": "^7.14.5", - "@babel/template": "^7.14.5", - "@babel/traverse": "^7.14.5", - "@babel/types": "^7.14.5" - } - }, - "@babel/helper-optimise-call-expression": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.14.5.tgz", - "integrity": "sha512-IqiLIrODUOdnPU9/F8ib1Fx2ohlgDhxnIDU7OEVi+kAbEZcyiF7BLU8W6PfvPi9LzztjS7kcbzbmL7oG8kD6VA==", - "dev": true, - "requires": { - "@babel/types": "^7.14.5" - } - }, - "@babel/helper-plugin-utils": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz", - "integrity": "sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ==", - "dev": true - }, - "@babel/helper-replace-supers": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.14.5.tgz", - "integrity": "sha512-3i1Qe9/8x/hCHINujn+iuHy+mMRLoc77b2nI9TB0zjH1hvn9qGlXjWlggdwUcju36PkPCy/lpM7LLUdcTyH4Ow==", - "dev": true, - "requires": { - "@babel/helper-member-expression-to-functions": "^7.14.5", - "@babel/helper-optimise-call-expression": "^7.14.5", - "@babel/traverse": "^7.14.5", - "@babel/types": "^7.14.5" - } - }, - "@babel/helper-simple-access": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.14.5.tgz", - "integrity": "sha512-nfBN9xvmCt6nrMZjfhkl7i0oTV3yxR4/FztsbOASyTvVcoYd0TRHh7eMLdlEcCqobydC0LAF3LtC92Iwxo0wyw==", - "dev": true, - "requires": { - "@babel/types": "^7.14.5" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.14.5.tgz", - "integrity": "sha512-hprxVPu6e5Kdp2puZUmvOGjaLv9TCe58E/Fl6hRq4YiVQxIcNvuq6uTM2r1mT/oPskuS9CgR+I94sqAYv0NGKA==", - "dev": true, - "requires": { - "@babel/types": "^7.14.5" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.5.tgz", - "integrity": "sha512-5lsetuxCLilmVGyiLEfoHBRX8UCFD+1m2x3Rj97WrW3V7H3u4RWRXA4evMjImCsin2J2YT0QaVDGf+z8ondbAg==", - "dev": true - }, - "@babel/highlight": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", - "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.14.5", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.5.tgz", - "integrity": "sha512-TM8C+xtH/9n1qzX+JNHi7AN2zHMTiPUtspO0ZdHflW8KaskkALhMmuMHb4bCmNdv9VAPzJX3/bXqkVLnAvsPfg==", - "dev": true - }, - "@babel/template": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.14.5.tgz", - "integrity": "sha512-6Z3Po85sfxRGachLULUhOmvAaOo7xCvqGQtxINai2mEGPFm6pQ4z5QInFnUrRpfoSV60BnjyF5F3c+15fxFV1g==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.14.5", - "@babel/parser": "^7.14.5", - "@babel/types": "^7.14.5" - } - }, - "@babel/traverse": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.5.tgz", - "integrity": "sha512-G3BiS15vevepdmFqmUc9X+64y0viZYygubAMO8SvBmKARuF6CPSZtH4Ng9vi/lrWlZFGe3FWdXNy835akH8Glg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.14.5", - "@babel/generator": "^7.14.5", - "@babel/helper-function-name": "^7.14.5", - "@babel/helper-hoist-variables": "^7.14.5", - "@babel/helper-split-export-declaration": "^7.14.5", - "@babel/parser": "^7.14.5", - "@babel/types": "^7.14.5", - "debug": "^4.1.0", - "globals": "^11.1.0" - } - }, - "@babel/types": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.5.tgz", - "integrity": "sha512-M/NzBpEL95I5Hh4dwhin5JlE7EzO5PHMAuzjxss3tiOBD46KfQvVedN/3jEPZvdRvtsK2222XfdHogNIttFgcg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.14.5", - "to-fast-properties": "^2.0.0" - } - }, - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - } - } - }, - "@babel/plugin-transform-modules-systemjs": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.14.5.tgz", - "integrity": "sha512-mNMQdvBEE5DcMQaL5LbzXFMANrQjd2W7FPzg34Y4yEz7dBgdaC+9B84dSO+/1Wba98zoDbInctCDo4JGxz1VYA==", - "dev": true, - "requires": { - "@babel/helper-hoist-variables": "^7.14.5", - "@babel/helper-module-transforms": "^7.14.5", - "@babel/helper-plugin-utils": "^7.14.5", - "@babel/helper-validator-identifier": "^7.14.5", - "babel-plugin-dynamic-import-node": "^2.3.3" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz", - "integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==", - "dev": true, - "requires": { - "@babel/highlight": "^7.14.5" - } - }, - "@babel/generator": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.5.tgz", - "integrity": "sha512-y3rlP+/G25OIX3mYKKIOlQRcqj7YgrvHxOLbVmyLJ9bPmi5ttvUmpydVjcFjZphOktWuA7ovbx91ECloWTfjIA==", - "dev": true, - "requires": { - "@babel/types": "^7.14.5", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" - } - }, - "@babel/helper-function-name": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.14.5.tgz", - "integrity": "sha512-Gjna0AsXWfFvrAuX+VKcN/aNNWonizBj39yGwUzVDVTlMYJMK2Wp6xdpy72mfArFq5uK+NOuexfzZlzI1z9+AQ==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.14.5", - "@babel/template": "^7.14.5", - "@babel/types": "^7.14.5" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.14.5.tgz", - "integrity": "sha512-I1Db4Shst5lewOM4V+ZKJzQ0JGGaZ6VY1jYvMghRjqs6DWgxLCIyFt30GlnKkfUeFLpJt2vzbMVEXVSXlIFYUg==", - "dev": true, - "requires": { - "@babel/types": "^7.14.5" - } - }, - "@babel/helper-member-expression-to-functions": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.14.5.tgz", - "integrity": "sha512-UxUeEYPrqH1Q/k0yRku1JE7dyfyehNwT6SVkMHvYvPDv4+uu627VXBckVj891BO8ruKBkiDoGnZf4qPDD8abDQ==", - "dev": true, - "requires": { - "@babel/types": "^7.14.5" - } - }, - "@babel/helper-module-imports": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.14.5.tgz", - "integrity": "sha512-SwrNHu5QWS84XlHwGYPDtCxcA0hrSlL2yhWYLgeOc0w7ccOl2qv4s/nARI0aYZW+bSwAL5CukeXA47B/1NKcnQ==", - "dev": true, - "requires": { - "@babel/types": "^7.14.5" - } - }, - "@babel/helper-module-transforms": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.14.5.tgz", - "integrity": "sha512-iXpX4KW8LVODuAieD7MzhNjmM6dzYY5tfRqT+R9HDXWl0jPn/djKmA+G9s/2C2T9zggw5tK1QNqZ70USfedOwA==", - "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.14.5", - "@babel/helper-replace-supers": "^7.14.5", - "@babel/helper-simple-access": "^7.14.5", - "@babel/helper-split-export-declaration": "^7.14.5", - "@babel/helper-validator-identifier": "^7.14.5", - "@babel/template": "^7.14.5", - "@babel/traverse": "^7.14.5", - "@babel/types": "^7.14.5" - } - }, - "@babel/helper-optimise-call-expression": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.14.5.tgz", - "integrity": "sha512-IqiLIrODUOdnPU9/F8ib1Fx2ohlgDhxnIDU7OEVi+kAbEZcyiF7BLU8W6PfvPi9LzztjS7kcbzbmL7oG8kD6VA==", - "dev": true, - "requires": { - "@babel/types": "^7.14.5" - } - }, - "@babel/helper-plugin-utils": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz", - "integrity": "sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ==", - "dev": true - }, - "@babel/helper-replace-supers": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.14.5.tgz", - "integrity": "sha512-3i1Qe9/8x/hCHINujn+iuHy+mMRLoc77b2nI9TB0zjH1hvn9qGlXjWlggdwUcju36PkPCy/lpM7LLUdcTyH4Ow==", - "dev": true, - "requires": { - "@babel/helper-member-expression-to-functions": "^7.14.5", - "@babel/helper-optimise-call-expression": "^7.14.5", - "@babel/traverse": "^7.14.5", - "@babel/types": "^7.14.5" - } - }, - "@babel/helper-simple-access": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.14.5.tgz", - "integrity": "sha512-nfBN9xvmCt6nrMZjfhkl7i0oTV3yxR4/FztsbOASyTvVcoYd0TRHh7eMLdlEcCqobydC0LAF3LtC92Iwxo0wyw==", - "dev": true, - "requires": { - "@babel/types": "^7.14.5" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.14.5.tgz", - "integrity": "sha512-hprxVPu6e5Kdp2puZUmvOGjaLv9TCe58E/Fl6hRq4YiVQxIcNvuq6uTM2r1mT/oPskuS9CgR+I94sqAYv0NGKA==", - "dev": true, - "requires": { - "@babel/types": "^7.14.5" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.5.tgz", - "integrity": "sha512-5lsetuxCLilmVGyiLEfoHBRX8UCFD+1m2x3Rj97WrW3V7H3u4RWRXA4evMjImCsin2J2YT0QaVDGf+z8ondbAg==", - "dev": true - }, - "@babel/highlight": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", - "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.14.5", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.5.tgz", - "integrity": "sha512-TM8C+xtH/9n1qzX+JNHi7AN2zHMTiPUtspO0ZdHflW8KaskkALhMmuMHb4bCmNdv9VAPzJX3/bXqkVLnAvsPfg==", - "dev": true - }, - "@babel/template": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.14.5.tgz", - "integrity": "sha512-6Z3Po85sfxRGachLULUhOmvAaOo7xCvqGQtxINai2mEGPFm6pQ4z5QInFnUrRpfoSV60BnjyF5F3c+15fxFV1g==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.14.5", - "@babel/parser": "^7.14.5", - "@babel/types": "^7.14.5" - } - }, - "@babel/traverse": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.5.tgz", - "integrity": "sha512-G3BiS15vevepdmFqmUc9X+64y0viZYygubAMO8SvBmKARuF6CPSZtH4Ng9vi/lrWlZFGe3FWdXNy835akH8Glg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.14.5", - "@babel/generator": "^7.14.5", - "@babel/helper-function-name": "^7.14.5", - "@babel/helper-hoist-variables": "^7.14.5", - "@babel/helper-split-export-declaration": "^7.14.5", - "@babel/parser": "^7.14.5", - "@babel/types": "^7.14.5", - "debug": "^4.1.0", - "globals": "^11.1.0" - } - }, - "@babel/types": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.5.tgz", - "integrity": "sha512-M/NzBpEL95I5Hh4dwhin5JlE7EzO5PHMAuzjxss3tiOBD46KfQvVedN/3jEPZvdRvtsK2222XfdHogNIttFgcg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.14.5", - "to-fast-properties": "^2.0.0" - } - }, - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - } - } - }, - "@babel/plugin-transform-modules-umd": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.14.5.tgz", - "integrity": "sha512-RfPGoagSngC06LsGUYyM9QWSXZ8MysEjDJTAea1lqRjNECE3y0qIJF/qbvJxc4oA4s99HumIMdXOrd+TdKaAAA==", - "dev": true, - "requires": { - "@babel/helper-module-transforms": "^7.14.5", - "@babel/helper-plugin-utils": "^7.14.5" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz", - "integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==", - "dev": true, - "requires": { - "@babel/highlight": "^7.14.5" - } - }, - "@babel/generator": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.5.tgz", - "integrity": "sha512-y3rlP+/G25OIX3mYKKIOlQRcqj7YgrvHxOLbVmyLJ9bPmi5ttvUmpydVjcFjZphOktWuA7ovbx91ECloWTfjIA==", - "dev": true, - "requires": { - "@babel/types": "^7.14.5", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" - } - }, - "@babel/helper-function-name": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.14.5.tgz", - "integrity": "sha512-Gjna0AsXWfFvrAuX+VKcN/aNNWonizBj39yGwUzVDVTlMYJMK2Wp6xdpy72mfArFq5uK+NOuexfzZlzI1z9+AQ==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.14.5", - "@babel/template": "^7.14.5", - "@babel/types": "^7.14.5" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.14.5.tgz", - "integrity": "sha512-I1Db4Shst5lewOM4V+ZKJzQ0JGGaZ6VY1jYvMghRjqs6DWgxLCIyFt30GlnKkfUeFLpJt2vzbMVEXVSXlIFYUg==", - "dev": true, - "requires": { - "@babel/types": "^7.14.5" - } - }, - "@babel/helper-member-expression-to-functions": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.14.5.tgz", - "integrity": "sha512-UxUeEYPrqH1Q/k0yRku1JE7dyfyehNwT6SVkMHvYvPDv4+uu627VXBckVj891BO8ruKBkiDoGnZf4qPDD8abDQ==", - "dev": true, - "requires": { - "@babel/types": "^7.14.5" - } - }, - "@babel/helper-module-imports": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.14.5.tgz", - "integrity": "sha512-SwrNHu5QWS84XlHwGYPDtCxcA0hrSlL2yhWYLgeOc0w7ccOl2qv4s/nARI0aYZW+bSwAL5CukeXA47B/1NKcnQ==", - "dev": true, - "requires": { - "@babel/types": "^7.14.5" - } - }, - "@babel/helper-module-transforms": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.14.5.tgz", - "integrity": "sha512-iXpX4KW8LVODuAieD7MzhNjmM6dzYY5tfRqT+R9HDXWl0jPn/djKmA+G9s/2C2T9zggw5tK1QNqZ70USfedOwA==", - "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.14.5", - "@babel/helper-replace-supers": "^7.14.5", - "@babel/helper-simple-access": "^7.14.5", - "@babel/helper-split-export-declaration": "^7.14.5", - "@babel/helper-validator-identifier": "^7.14.5", - "@babel/template": "^7.14.5", - "@babel/traverse": "^7.14.5", - "@babel/types": "^7.14.5" - } - }, - "@babel/helper-optimise-call-expression": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.14.5.tgz", - "integrity": "sha512-IqiLIrODUOdnPU9/F8ib1Fx2ohlgDhxnIDU7OEVi+kAbEZcyiF7BLU8W6PfvPi9LzztjS7kcbzbmL7oG8kD6VA==", - "dev": true, - "requires": { - "@babel/types": "^7.14.5" - } - }, - "@babel/helper-plugin-utils": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz", - "integrity": "sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ==", - "dev": true - }, - "@babel/helper-replace-supers": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.14.5.tgz", - "integrity": "sha512-3i1Qe9/8x/hCHINujn+iuHy+mMRLoc77b2nI9TB0zjH1hvn9qGlXjWlggdwUcju36PkPCy/lpM7LLUdcTyH4Ow==", - "dev": true, - "requires": { - "@babel/helper-member-expression-to-functions": "^7.14.5", - "@babel/helper-optimise-call-expression": "^7.14.5", - "@babel/traverse": "^7.14.5", - "@babel/types": "^7.14.5" - } - }, - "@babel/helper-simple-access": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.14.5.tgz", - "integrity": "sha512-nfBN9xvmCt6nrMZjfhkl7i0oTV3yxR4/FztsbOASyTvVcoYd0TRHh7eMLdlEcCqobydC0LAF3LtC92Iwxo0wyw==", - "dev": true, - "requires": { - "@babel/types": "^7.14.5" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.14.5.tgz", - "integrity": "sha512-hprxVPu6e5Kdp2puZUmvOGjaLv9TCe58E/Fl6hRq4YiVQxIcNvuq6uTM2r1mT/oPskuS9CgR+I94sqAYv0NGKA==", - "dev": true, - "requires": { - "@babel/types": "^7.14.5" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.5.tgz", - "integrity": "sha512-5lsetuxCLilmVGyiLEfoHBRX8UCFD+1m2x3Rj97WrW3V7H3u4RWRXA4evMjImCsin2J2YT0QaVDGf+z8ondbAg==", - "dev": true - }, - "@babel/highlight": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", - "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.14.5", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.5.tgz", - "integrity": "sha512-TM8C+xtH/9n1qzX+JNHi7AN2zHMTiPUtspO0ZdHflW8KaskkALhMmuMHb4bCmNdv9VAPzJX3/bXqkVLnAvsPfg==", - "dev": true - }, - "@babel/template": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.14.5.tgz", - "integrity": "sha512-6Z3Po85sfxRGachLULUhOmvAaOo7xCvqGQtxINai2mEGPFm6pQ4z5QInFnUrRpfoSV60BnjyF5F3c+15fxFV1g==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.14.5", - "@babel/parser": "^7.14.5", - "@babel/types": "^7.14.5" - } - }, - "@babel/traverse": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.5.tgz", - "integrity": "sha512-G3BiS15vevepdmFqmUc9X+64y0viZYygubAMO8SvBmKARuF6CPSZtH4Ng9vi/lrWlZFGe3FWdXNy835akH8Glg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.14.5", - "@babel/generator": "^7.14.5", - "@babel/helper-function-name": "^7.14.5", - "@babel/helper-hoist-variables": "^7.14.5", - "@babel/helper-split-export-declaration": "^7.14.5", - "@babel/parser": "^7.14.5", - "@babel/types": "^7.14.5", - "debug": "^4.1.0", - "globals": "^11.1.0" - } - }, - "@babel/types": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.5.tgz", - "integrity": "sha512-M/NzBpEL95I5Hh4dwhin5JlE7EzO5PHMAuzjxss3tiOBD46KfQvVedN/3jEPZvdRvtsK2222XfdHogNIttFgcg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.14.5", - "to-fast-properties": "^2.0.0" - } - }, - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - } - } - }, - "@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.14.5.tgz", - "integrity": "sha512-+Xe5+6MWFo311U8SchgeX5c1+lJM+eZDBZgD+tvXu9VVQPXwwVzeManMMjYX6xw2HczngfOSZjoFYKwdeB/Jvw==", - "dev": true, - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.14.5" - } - }, - "@babel/plugin-transform-new-target": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.14.5.tgz", - "integrity": "sha512-Nx054zovz6IIRWEB49RDRuXGI4Gy0GMgqG0cII9L3MxqgXz/+rgII+RU58qpo4g7tNEx1jG7rRVH4ihZoP4esQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz", - "integrity": "sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ==", - "dev": true - } - } - }, - "@babel/plugin-transform-object-super": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.14.5.tgz", - "integrity": "sha512-MKfOBWzK0pZIrav9z/hkRqIk/2bTv9qvxHzPQc12RcVkMOzpIKnFCNYJip00ssKWYkd8Sf5g0Wr7pqJ+cmtuFg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5", - "@babel/helper-replace-supers": "^7.14.5" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz", - "integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==", - "dev": true, - "requires": { - "@babel/highlight": "^7.14.5" - } - }, - "@babel/generator": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.5.tgz", - "integrity": "sha512-y3rlP+/G25OIX3mYKKIOlQRcqj7YgrvHxOLbVmyLJ9bPmi5ttvUmpydVjcFjZphOktWuA7ovbx91ECloWTfjIA==", - "dev": true, - "requires": { - "@babel/types": "^7.14.5", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" - } - }, - "@babel/helper-function-name": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.14.5.tgz", - "integrity": "sha512-Gjna0AsXWfFvrAuX+VKcN/aNNWonizBj39yGwUzVDVTlMYJMK2Wp6xdpy72mfArFq5uK+NOuexfzZlzI1z9+AQ==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.14.5", - "@babel/template": "^7.14.5", - "@babel/types": "^7.14.5" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.14.5.tgz", - "integrity": "sha512-I1Db4Shst5lewOM4V+ZKJzQ0JGGaZ6VY1jYvMghRjqs6DWgxLCIyFt30GlnKkfUeFLpJt2vzbMVEXVSXlIFYUg==", - "dev": true, - "requires": { - "@babel/types": "^7.14.5" - } - }, - "@babel/helper-member-expression-to-functions": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.14.5.tgz", - "integrity": "sha512-UxUeEYPrqH1Q/k0yRku1JE7dyfyehNwT6SVkMHvYvPDv4+uu627VXBckVj891BO8ruKBkiDoGnZf4qPDD8abDQ==", - "dev": true, - "requires": { - "@babel/types": "^7.14.5" - } - }, - "@babel/helper-optimise-call-expression": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.14.5.tgz", - "integrity": "sha512-IqiLIrODUOdnPU9/F8ib1Fx2ohlgDhxnIDU7OEVi+kAbEZcyiF7BLU8W6PfvPi9LzztjS7kcbzbmL7oG8kD6VA==", - "dev": true, - "requires": { - "@babel/types": "^7.14.5" - } - }, - "@babel/helper-plugin-utils": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz", - "integrity": "sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ==", - "dev": true - }, - "@babel/helper-replace-supers": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.14.5.tgz", - "integrity": "sha512-3i1Qe9/8x/hCHINujn+iuHy+mMRLoc77b2nI9TB0zjH1hvn9qGlXjWlggdwUcju36PkPCy/lpM7LLUdcTyH4Ow==", - "dev": true, - "requires": { - "@babel/helper-member-expression-to-functions": "^7.14.5", - "@babel/helper-optimise-call-expression": "^7.14.5", - "@babel/traverse": "^7.14.5", - "@babel/types": "^7.14.5" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.14.5.tgz", - "integrity": "sha512-hprxVPu6e5Kdp2puZUmvOGjaLv9TCe58E/Fl6hRq4YiVQxIcNvuq6uTM2r1mT/oPskuS9CgR+I94sqAYv0NGKA==", - "dev": true, - "requires": { - "@babel/types": "^7.14.5" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.5.tgz", - "integrity": "sha512-5lsetuxCLilmVGyiLEfoHBRX8UCFD+1m2x3Rj97WrW3V7H3u4RWRXA4evMjImCsin2J2YT0QaVDGf+z8ondbAg==", - "dev": true - }, - "@babel/highlight": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", - "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.14.5", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.5.tgz", - "integrity": "sha512-TM8C+xtH/9n1qzX+JNHi7AN2zHMTiPUtspO0ZdHflW8KaskkALhMmuMHb4bCmNdv9VAPzJX3/bXqkVLnAvsPfg==", - "dev": true - }, - "@babel/template": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.14.5.tgz", - "integrity": "sha512-6Z3Po85sfxRGachLULUhOmvAaOo7xCvqGQtxINai2mEGPFm6pQ4z5QInFnUrRpfoSV60BnjyF5F3c+15fxFV1g==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.14.5", - "@babel/parser": "^7.14.5", - "@babel/types": "^7.14.5" - } - }, - "@babel/traverse": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.5.tgz", - "integrity": "sha512-G3BiS15vevepdmFqmUc9X+64y0viZYygubAMO8SvBmKARuF6CPSZtH4Ng9vi/lrWlZFGe3FWdXNy835akH8Glg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.14.5", - "@babel/generator": "^7.14.5", - "@babel/helper-function-name": "^7.14.5", - "@babel/helper-hoist-variables": "^7.14.5", - "@babel/helper-split-export-declaration": "^7.14.5", - "@babel/parser": "^7.14.5", - "@babel/types": "^7.14.5", - "debug": "^4.1.0", - "globals": "^11.1.0" - } - }, - "@babel/types": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.5.tgz", - "integrity": "sha512-M/NzBpEL95I5Hh4dwhin5JlE7EzO5PHMAuzjxss3tiOBD46KfQvVedN/3jEPZvdRvtsK2222XfdHogNIttFgcg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.14.5", - "to-fast-properties": "^2.0.0" - } - }, - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - } - } - }, - "@babel/plugin-transform-parameters": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.13.0.tgz", - "integrity": "sha512-Jt8k/h/mIwE2JFEOb3lURoY5C85ETcYPnbuAJ96zRBzh1XHtQZfs62ChZ6EP22QlC8c7Xqr9q+e1SU5qttwwjw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.13.0" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.13.0.tgz", - "integrity": "sha512-ZPafIPSwzUlAoWT8DKs1W2VyF2gOWthGd5NGFMsBcMMol+ZhK+EQY/e6V96poa6PA/Bh+C9plWN0hXO1uB8AfQ==", - "dev": true - } - } - }, - "@babel/plugin-transform-property-literals": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.14.5.tgz", - "integrity": "sha512-r1uilDthkgXW8Z1vJz2dKYLV1tuw2xsbrp3MrZmD99Wh9vsfKoob+JTgri5VUb/JqyKRXotlOtwgu4stIYCmnw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz", - "integrity": "sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ==", - "dev": true - } - } - }, - "@babel/plugin-transform-react-display-name": { - "version": "7.14.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.14.2.tgz", - "integrity": "sha512-zCubvP+jjahpnFJvPaHPiGVfuVUjXHhFvJKQdNnsmSsiU9kR/rCZ41jHc++tERD2zV+p7Hr6is+t5b6iWTCqSw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.13.0" - }, - "dependencies": { - "@babel/helper-plugin-utils": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.13.0.tgz", - "integrity": "sha512-ZPafIPSwzUlAoWT8DKs1W2VyF2gOWthGd5NGFMsBcMMol+ZhK+EQY/e6V96poa6PA/Bh+C9plWN0hXO1uB8AfQ==", - "dev": true - } - } - }, - "@babel/plugin-transform-react-jsx": { - "version": "7.13.12", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.13.12.tgz", - "integrity": "sha512-jcEI2UqIcpCqB5U5DRxIl0tQEProI2gcu+g8VTIqxLO5Iidojb4d77q+fwGseCvd8af/lJ9masp4QWzBXFE2xA==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.12.13", - "@babel/helper-module-imports": "^7.13.12", - "@babel/helper-plugin-utils": "^7.13.0", - "@babel/plugin-syntax-jsx": "^7.12.13", - "@babel/types": "^7.13.12" - }, - "dependencies": { - "@babel/helper-annotate-as-pure": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.12.13.tgz", - "integrity": "sha512-7YXfX5wQ5aYM/BOlbSccHDbuXXFPxeoUmfWtz8le2yTkTZc+BxsiEnENFoi2SlmA8ewDkG2LgIMIVzzn2h8kfw==", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz", + "integrity": "sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ==", + "dev": true + } + } + }, + "@babel/plugin-transform-react-display-name": { + "version": "7.14.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.14.2.tgz", + "integrity": "sha512-zCubvP+jjahpnFJvPaHPiGVfuVUjXHhFvJKQdNnsmSsiU9kR/rCZ41jHc++tERD2zV+p7Hr6is+t5b6iWTCqSw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.13.0" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.13.0.tgz", + "integrity": "sha512-ZPafIPSwzUlAoWT8DKs1W2VyF2gOWthGd5NGFMsBcMMol+ZhK+EQY/e6V96poa6PA/Bh+C9plWN0hXO1uB8AfQ==", + "dev": true + } + } + }, + "@babel/plugin-transform-react-jsx": { + "version": "7.13.12", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.13.12.tgz", + "integrity": "sha512-jcEI2UqIcpCqB5U5DRxIl0tQEProI2gcu+g8VTIqxLO5Iidojb4d77q+fwGseCvd8af/lJ9masp4QWzBXFE2xA==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.12.13", + "@babel/helper-module-imports": "^7.13.12", + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/plugin-syntax-jsx": "^7.12.13", + "@babel/types": "^7.13.12" + }, + "dependencies": { + "@babel/helper-annotate-as-pure": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.12.13.tgz", + "integrity": "sha512-7YXfX5wQ5aYM/BOlbSccHDbuXXFPxeoUmfWtz8le2yTkTZc+BxsiEnENFoi2SlmA8ewDkG2LgIMIVzzn2h8kfw==", "dev": true, "requires": { "@babel/types": "^7.12.13" @@ -4249,9 +3523,9 @@ } }, "@babel/plugin-transform-spread": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.14.5.tgz", - "integrity": "sha512-/3iqoQdiWergnShZYl0xACb4ADeYCJ7X/RgmwtXshn6cIvautRPAFzhd58frQlokLO6Jb4/3JXvmm6WNTPtiTw==", + "version": "7.14.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.14.6.tgz", + "integrity": "sha512-Zr0x0YroFJku7n7+/HH3A2eIrGMjbmAIbJSVv0IZ+t3U2WUQUA64S/oeied2e+MaGSjmt4alzBCsK9E8gh+fag==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.14.5", @@ -4353,17 +3627,17 @@ } }, "@babel/preset-env": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.14.5.tgz", - "integrity": "sha512-ci6TsS0bjrdPpWGnQ+m4f+JSSzDKlckqKIJJt9UZ/+g7Zz9k0N8lYU8IeLg/01o2h8LyNZDMLGgRLDTxpudLsA==", + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.14.7.tgz", + "integrity": "sha512-itOGqCKLsSUl0Y+1nSfhbuuOlTs0MJk2Iv7iSH+XT/mR8U1zRLO7NjWlYXB47yhK4J/7j+HYty/EhFZDYKa/VA==", "dev": true, "requires": { - "@babel/compat-data": "^7.14.5", + "@babel/compat-data": "^7.14.7", "@babel/helper-compilation-targets": "^7.14.5", "@babel/helper-plugin-utils": "^7.14.5", "@babel/helper-validator-option": "^7.14.5", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.14.5", - "@babel/plugin-proposal-async-generator-functions": "^7.14.5", + "@babel/plugin-proposal-async-generator-functions": "^7.14.7", "@babel/plugin-proposal-class-properties": "^7.14.5", "@babel/plugin-proposal-class-static-block": "^7.14.5", "@babel/plugin-proposal-dynamic-import": "^7.14.5", @@ -4372,7 +3646,7 @@ "@babel/plugin-proposal-logical-assignment-operators": "^7.14.5", "@babel/plugin-proposal-nullish-coalescing-operator": "^7.14.5", "@babel/plugin-proposal-numeric-separator": "^7.14.5", - "@babel/plugin-proposal-object-rest-spread": "^7.14.5", + "@babel/plugin-proposal-object-rest-spread": "^7.14.7", "@babel/plugin-proposal-optional-catch-binding": "^7.14.5", "@babel/plugin-proposal-optional-chaining": "^7.14.5", "@babel/plugin-proposal-private-methods": "^7.14.5", @@ -4398,7 +3672,7 @@ "@babel/plugin-transform-block-scoping": "^7.14.5", "@babel/plugin-transform-classes": "^7.14.5", "@babel/plugin-transform-computed-properties": "^7.14.5", - "@babel/plugin-transform-destructuring": "^7.14.5", + "@babel/plugin-transform-destructuring": "^7.14.7", "@babel/plugin-transform-dotall-regex": "^7.14.5", "@babel/plugin-transform-duplicate-keys": "^7.14.5", "@babel/plugin-transform-exponentiation-operator": "^7.14.5", @@ -4410,7 +3684,7 @@ "@babel/plugin-transform-modules-commonjs": "^7.14.5", "@babel/plugin-transform-modules-systemjs": "^7.14.5", "@babel/plugin-transform-modules-umd": "^7.14.5", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.14.5", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.14.7", "@babel/plugin-transform-new-target": "^7.14.5", "@babel/plugin-transform-object-super": "^7.14.5", "@babel/plugin-transform-parameters": "^7.14.5", @@ -4418,7 +3692,7 @@ "@babel/plugin-transform-regenerator": "^7.14.5", "@babel/plugin-transform-reserved-words": "^7.14.5", "@babel/plugin-transform-shorthand-properties": "^7.14.5", - "@babel/plugin-transform-spread": "^7.14.5", + "@babel/plugin-transform-spread": "^7.14.6", "@babel/plugin-transform-sticky-regex": "^7.14.5", "@babel/plugin-transform-template-literals": "^7.14.5", "@babel/plugin-transform-typeof-symbol": "^7.14.5", @@ -4429,7 +3703,7 @@ "babel-plugin-polyfill-corejs2": "^0.2.2", "babel-plugin-polyfill-corejs3": "^0.2.2", "babel-plugin-polyfill-regenerator": "^0.2.2", - "core-js-compat": "^3.14.0", + "core-js-compat": "^3.15.0", "semver": "^6.3.0" }, "dependencies": { @@ -4443,9 +3717,9 @@ } }, "@babel/compat-data": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.14.5.tgz", - "integrity": "sha512-kixrYn4JwfAVPa0f2yfzc2AWti6WRRyO3XjWW5PJAvtE11qhSayrrcrEnee05KAtNaPC+EwehE8Qt1UedEVB8w==", + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.14.7.tgz", + "integrity": "sha512-nS6dZaISCXJ3+518CWiBfEr//gHyMO02uDxBkXTKZDN5POruCnOZ1N4YBRZDCabwF8nZMWBpRxIicmXtBs+fvw==", "dev": true }, "@babel/generator": { @@ -4491,9 +3765,9 @@ } }, "@babel/helper-create-class-features-plugin": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.14.5.tgz", - "integrity": "sha512-Uq9z2e7ZtcnDMirRqAGLRaLwJn+Lrh388v5ETrR3pALJnElVh2zqQmdbz4W2RUJYohAPh2mtyPUgyMHMzXMncQ==", + "version": "7.14.6", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.14.6.tgz", + "integrity": "sha512-Z6gsfGofTxH/+LQXqYEK45kxmcensbzmk/oi8DmaQytlQCgqNZt9XQF8iqlI/SeXWVjaMNxvYvzaYw+kh42mDg==", "dev": true, "requires": { "@babel/helper-annotate-as-pure": "^7.14.5", @@ -4504,22 +3778,6 @@ "@babel/helper-split-export-declaration": "^7.14.5" } }, - "@babel/helper-define-polyfill-provider": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.2.3.tgz", - "integrity": "sha512-RH3QDAfRMzj7+0Nqu5oqgO5q9mFtQEVvCRsi8qCEfzLR9p2BHfn5FzhSB2oj1fF7I2+DcTORkYaQ6aTR9Cofew==", - "dev": true, - "requires": { - "@babel/helper-compilation-targets": "^7.13.0", - "@babel/helper-module-imports": "^7.12.13", - "@babel/helper-plugin-utils": "^7.13.0", - "@babel/traverse": "^7.13.0", - "debug": "^4.1.1", - "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2", - "semver": "^6.1.2" - } - }, "@babel/helper-explode-assignable-expression": { "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.14.5.tgz", @@ -4550,9 +3808,9 @@ } }, "@babel/helper-member-expression-to-functions": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.14.5.tgz", - "integrity": "sha512-UxUeEYPrqH1Q/k0yRku1JE7dyfyehNwT6SVkMHvYvPDv4+uu627VXBckVj891BO8ruKBkiDoGnZf4qPDD8abDQ==", + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.14.7.tgz", + "integrity": "sha512-TMUt4xKxJn6ccjcOW7c4hlwyJArizskAhoSTOCkA0uZ+KghIaci0Qg9R043kUMWI9mtQfgny+NQ5QATnZ+paaA==", "dev": true, "requires": { "@babel/types": "^7.14.5" @@ -4650,15 +3908,15 @@ } }, "@babel/parser": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.5.tgz", - "integrity": "sha512-TM8C+xtH/9n1qzX+JNHi7AN2zHMTiPUtspO0ZdHflW8KaskkALhMmuMHb4bCmNdv9VAPzJX3/bXqkVLnAvsPfg==", + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.7.tgz", + "integrity": "sha512-X67Z5y+VBJuHB/RjwECp8kSl5uYi0BvRbNeWqkaJCVh+LiTPl19WBUfG627psSgp9rSf6ojuXghQM3ha6qHHdA==", "dev": true }, "@babel/plugin-proposal-async-generator-functions": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.14.5.tgz", - "integrity": "sha512-tbD/CG3l43FIXxmu4a7RBe4zH7MLJ+S/lFowPFO7HetS2hyOZ/0nnnznegDuzFzfkyQYTxqdTH/hKmuBngaDAA==", + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.14.7.tgz", + "integrity": "sha512-RK8Wj7lXLY3bqei69/cc25gwS5puEc3dknoFPFbqfy3XxYQBQFvu4ioWpafMBAB+L9NyptQK4nMOa5Xz16og8Q==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.14.5", @@ -4677,12 +3935,12 @@ } }, "@babel/plugin-proposal-object-rest-spread": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.14.5.tgz", - "integrity": "sha512-VzMyY6PWNPPT3pxc5hi9LloKNr4SSrVCg7Yr6aZpW4Ym07r7KqSU/QXYwjXLVxqwSv0t/XSXkFoKBPUkZ8vb2A==", + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.14.7.tgz", + "integrity": "sha512-082hsZz+sVabfmDWo1Oct1u1AgbKbUAyVgmX4otIc7bdsRgHBXwTwb3DpDmD4Eyyx6DNiuz5UAATT655k+kL5g==", "dev": true, "requires": { - "@babel/compat-data": "^7.14.5", + "@babel/compat-data": "^7.14.7", "@babel/helper-compilation-targets": "^7.14.5", "@babel/helper-plugin-utils": "^7.14.5", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", @@ -4791,9 +4049,9 @@ } }, "@babel/traverse": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.5.tgz", - "integrity": "sha512-G3BiS15vevepdmFqmUc9X+64y0viZYygubAMO8SvBmKARuF6CPSZtH4Ng9vi/lrWlZFGe3FWdXNy835akH8Glg==", + "version": "7.14.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.7.tgz", + "integrity": "sha512-9vDr5NzHu27wgwejuKL7kIOm4bwEtaPQ4Z6cpCmjSuaRqpH/7xc4qcGEscwMqlkwgcXl6MvqoAjZkQ24uSdIZQ==", "dev": true, "requires": { "@babel/code-frame": "^7.14.5", @@ -4801,7 +4059,7 @@ "@babel/helper-function-name": "^7.14.5", "@babel/helper-hoist-variables": "^7.14.5", "@babel/helper-split-export-declaration": "^7.14.5", - "@babel/parser": "^7.14.5", + "@babel/parser": "^7.14.7", "@babel/types": "^7.14.5", "debug": "^4.1.0", "globals": "^11.1.0" @@ -4817,36 +4075,6 @@ "to-fast-properties": "^2.0.0" } }, - "babel-plugin-polyfill-corejs2": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.2.2.tgz", - "integrity": "sha512-kISrENsJ0z5dNPq5eRvcctITNHYXWOA4DUZRFYCz3jYCcvTb/A546LIddmoGNMVYg2U38OyFeNosQwI9ENTqIQ==", - "dev": true, - "requires": { - "@babel/compat-data": "^7.13.11", - "@babel/helper-define-polyfill-provider": "^0.2.2", - "semver": "^6.1.1" - } - }, - "babel-plugin-polyfill-corejs3": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.2.2.tgz", - "integrity": "sha512-l1Cf8PKk12eEk5QP/NQ6TH8A1pee6wWDJ96WjxrMXFLHLOBFzYM4moG80HFgduVhTqAFez4alnZKEhP/bYHg0A==", - "dev": true, - "requires": { - "@babel/helper-define-polyfill-provider": "^0.2.2", - "core-js-compat": "^3.9.1" - } - }, - "babel-plugin-polyfill-regenerator": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.2.2.tgz", - "integrity": "sha512-Goy5ghsc21HgPDFtzRkSirpZVW35meGoTmTOb2bxqdl60ghub4xOidgNTHaZfQ2FaxQsKmwvXtOAkcIS4SMBWg==", - "dev": true, - "requires": { - "@babel/helper-define-polyfill-provider": "^0.2.2" - } - }, "browserslist": { "version": "4.16.6", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz", @@ -4861,9 +4089,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001236", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001236.tgz", - "integrity": "sha512-o0PRQSrSCGJKCPZcgMzl5fUaj5xHe8qA2m4QRvnyY4e1lITqoNkr7q/Oh1NcpGSy0Th97UZ35yoKcINPoq7YOQ==", + "version": "1.0.30001239", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001239.tgz", + "integrity": "sha512-cyBkXJDMeI4wthy8xJ2FvDU6+0dtcZSJW3voUF8+e9f1bBeuvyZfc3PNbkOETyhbR+dGCPzn9E7MA3iwzusOhQ==", "dev": true }, "colorette": { @@ -4873,9 +4101,9 @@ "dev": true }, "core-js-compat": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.14.0.tgz", - "integrity": "sha512-R4NS2eupxtiJU+VwgkF9WTpnSfZW4pogwKHd8bclWU2sp93Pr5S1uYJI84cMOubJRou7bcfL0vmwtLslWN5p3A==", + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.15.0.tgz", + "integrity": "sha512-8X6lWsG+s7IfOKzV93a7fRYfWRZobOfjw5V5rrq43Vh/W+V6qYxl7Akalsvgab4PFT/4L/pjQbdBUEM36NXKrw==", "dev": true, "requires": { "browserslist": "^4.16.6", @@ -4900,9 +4128,9 @@ } }, "electron-to-chromium": { - "version": "1.3.752", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.752.tgz", - "integrity": "sha512-2Tg+7jSl3oPxgsBsWKh5H83QazTkmWG/cnNwJplmyZc7KcN61+I10oUgaXSVk/NwfvN3BdkKDR4FYuRBQQ2v0A==", + "version": "1.3.754", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.754.tgz", + "integrity": "sha512-Q50dJbfYYRtwK3G9mFP/EsJVzlgcYwKxFjbXmvVa1lDAbdviPcT9QOpFoufDApub4j0hBfDRL6v3lWNLEdEDXQ==", "dev": true }, "node-releases": { diff --git a/package.json b/package.json index 33a271307c..f5994929a4 100644 --- a/package.json +++ b/package.json @@ -159,7 +159,7 @@ "devDependencies": { "@babel/core": "^7.14.6", "@babel/plugin-transform-runtime": "^7.14.5", - "@babel/preset-env": "^7.14.5", + "@babel/preset-env": "^7.14.7", "@opengovsg/mockpass": "^2.7.4", "@types/bcrypt": "^5.0.0", "@types/bluebird": "^3.5.35", From 6f63ca55da70149b95fb57835057d6ea8b79696c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Jun 2021 02:42:13 +0000 Subject: [PATCH 10/21] fix(deps): bump nocache from 3.0.0 to 3.0.1 (#2236) Bumps [nocache](https://github.com/helmetjs/nocache) from 3.0.0 to 3.0.1. - [Release notes](https://github.com/helmetjs/nocache/releases) - [Changelog](https://github.com/helmetjs/nocache/blob/main/CHANGELOG.md) - [Commits](https://github.com/helmetjs/nocache/compare/v3.0.0...v3.0.1) --- updated-dependencies: - dependency-name: nocache dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 17eefa602d..f4b1b08d9b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18927,9 +18927,9 @@ } }, "nocache": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/nocache/-/nocache-3.0.0.tgz", - "integrity": "sha512-fd31hZ7uOvBrRSvzTsX51A8nW1SLKfcK7dESRDkEyltsGmpYfeINND8HoFGPEGkQZuhCsB5csAzcrLPXTtcRKg==" + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/nocache/-/nocache-3.0.1.tgz", + "integrity": "sha512-Gh39xwJwBKy0OvFmWfBs/vDO4Nl7JhnJtkqNP76OUinQz7BiMoszHYrIDHHAaqVl/QKVxCEy4ZxC/XZninu7nQ==" }, "node-abi": { "version": "2.19.1", diff --git a/package.json b/package.json index f5994929a4..794acc1a11 100644 --- a/package.json +++ b/package.json @@ -128,7 +128,7 @@ "ng-infinite-scroll": "^1.3.0", "ng-table": "^3.0.1", "ngclipboard": "^2.0.0", - "nocache": "^3.0.0", + "nocache": "^3.0.1", "node-cache": "^5.1.2", "nodemailer": "^6.6.2", "opossum": "^6.1.0", From 9fc8c9e6562fe55e7e13c593c2bb1a4c96cbfe96 Mon Sep 17 00:00:00 2001 From: LoneRifle Date: Tue, 15 Jun 2021 13:20:04 +0800 Subject: [PATCH 11/21] feat(config): support env var config via dotenv Allow Form to be configured using a .env file, read by dotenv and populated into `process.env`. - `npm install dotenv` - pick up the `.env` config using dotenv just before we invoke the application - remove `.env` entry from `.dockerignore` --- .dockerignore | 1 - README.md | 3 --- package-lock.json | 3 +-- package.json | 7 ++++--- 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/.dockerignore b/.dockerignore index fd52bb262d..9733fcdbf0 100644 --- a/.dockerignore +++ b/.dockerignore @@ -5,7 +5,6 @@ **/.elasticbeanstalk **/node_modules **/public/lib -**/.env .eslintrc.json .prettierrc.json .travis.yml diff --git a/README.md b/README.md index 244d460204..c5839949d0 100755 --- a/README.md +++ b/README.md @@ -91,9 +91,6 @@ FormSG requires some environment variables in order to function. More information about the required environment variables can be seen in [DEPLOYMENT_SETUP.md](/docs/DEPLOYMENT_SETUP.md). -The docker-compose file declares some blank environment variables that are secret and cannot be committed into -the repository. See below instructions to get them injected into the container. - We provide a [`.template-env`](./.template-env) file with the secrets blanked out. You can copy and paste the variables described into a self-created `.env` file, replacing the required values with your own. diff --git a/package-lock.json b/package-lock.json index f4b1b08d9b..3e59fd19d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10929,8 +10929,7 @@ "dotenv": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", - "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", - "dev": true + "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==" }, "duplexify": { "version": "3.7.1", diff --git a/package.json b/package.json index 794acc1a11..55ded4a923 100644 --- a/package.json +++ b/package.json @@ -24,9 +24,9 @@ "build-frontend": "webpack --config webpack.prod.js", "build-frontend-dev": "webpack --config webpack.dev.js", "build-frontend-dev:watch": "webpack --config webpack.dev.js --watch", - "start": "node dist/backend/app/server.js", + "start": "node -r dotenv/config dist/backend/app/server.js", "dev": "docker-compose up --build", - "docker-dev": "npm run build-frontend-dev:watch & ts-node-dev --respawn --transpile-only --inspect=0.0.0.0 --exit-child -- src/app/server.ts", + "docker-dev": "npm run build-frontend-dev:watch & ts-node-dev --respawn --transpile-only --inspect=0.0.0.0 --exit-child -r dotenv/config -- src/app/server.ts", "test": "npm run test-backend && npm run test-frontend", "download-binary": "node tests/end-to-end/helpers/get-mongo-binary.js", "test-e2e": "npm run test-e2e-build && npm run test-e2e-ci", @@ -99,6 +99,7 @@ "csv-string": "^4.0.1", "date-fns": "^2.22.1", "dedent-js": "~1.0.1", + "dotenv": "^10.0.0", "ejs": "^3.1.6", "express": "^4.16.4", "express-device": "~0.4.2", @@ -253,4 +254,4 @@ "webpack-merge": "^4.1.3", "worker-loader": "^2.0.0" } -} +} \ No newline at end of file From ac2724276bef8e4bd47211efa32b24a5a8cb1738 Mon Sep 17 00:00:00 2001 From: LoneRifle Date: Wed, 23 Jun 2021 09:46:45 +0800 Subject: [PATCH 12/21] build(ci): create .env files in EB with Param Store - use an .ebextensions config to create an .env from Parameter Store - determine the environment name by looking at container config files directly [1] - mount the .env into /opt/formsg/.env References: [1]: https://aws.amazon.com/premiumsupport/knowledge-center/view-environment-properties-elastic-beanstalk/ --- .ebextensions/env-file-creation.config | 41 ++++++++++++++++++++++++++ Dockerrun.aws.json | 6 +++- 2 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 .ebextensions/env-file-creation.config diff --git a/.ebextensions/env-file-creation.config b/.ebextensions/env-file-creation.config new file mode 100644 index 0000000000..3c9a9437fc --- /dev/null +++ b/.ebextensions/env-file-creation.config @@ -0,0 +1,41 @@ +# Creates an .env file from AWS SSM Parameter Store + +commands: + 01-create-env: + command: "/tmp/create-env.sh" + +files: + "/tmp/create-env.sh": + mode: "000755" + content : | + #!/bin/bash + # Reach into the undocumented container config + AWS_REGION='`{"Ref": "AWS::Region"}`' + ENV_NAME=$(jq .system.environment_name /opt/elasticbeanstalk/deploy/configuration/containerconfiguration | sed 's/"//g') + TARGET_DIR=/etc/formsg + + echo "Checking if ${TARGET_DIR} exists..." + if [ ! -d ${TARGET_DIR} ]; then + echo "Creating directory ${TARGET_DIR} ..." + mkdir -p ${TARGET_DIR} + if [ $? -ne 0 ]; then + echo 'ERROR: Directory creation failed!' + exit 1 + fi + else + echo "Directory ${TARGET_DIR} already exists!" + fi + echo "Creating config for ${ENV_NAME} in ${AWS_REGION}" + aws ssm get-parameter --name "${ENV_NAME}-general" --with-decryption --region $AWS_REGION | jq -r '.Parameter.Value' > $TARGET_DIR/.env + aws ssm get-parameter --name "${ENV_NAME}-captcha" --with-decryption --region $AWS_REGION | jq -r '.Parameter.Value' >> $TARGET_DIR/.env + aws ssm get-parameter --name "${ENV_NAME}-ga" --with-decryption --region $AWS_REGION | jq -r '.Parameter.Value' >> $TARGET_DIR/.env + aws ssm get-parameter --name "${ENV_NAME}-intranet" --with-decryption --region $AWS_REGION | jq -r '.Parameter.Value' >> $TARGET_DIR/.env + aws ssm get-parameter --name "${ENV_NAME}-sentry" --with-decryption --region $AWS_REGION | jq -r '.Parameter.Value' >> $TARGET_DIR/.env + aws ssm get-parameter --name "${ENV_NAME}-sms" --with-decryption --region $AWS_REGION | jq -r '.Parameter.Value' >> $TARGET_DIR/.env + aws ssm get-parameter --name "${ENV_NAME}-ndi" --with-decryption --region $AWS_REGION | jq -r '.Parameter.Value' >> $TARGET_DIR/.env + aws ssm get-parameter --name "${ENV_NAME}-verified-fields" --with-decryption --region $AWS_REGION | jq -r '.Parameter.Value' >> $TARGET_DIR/.env + aws ssm get-parameter --name "${ENV_NAME}-webhook-verified-content" --with-decryption --region $AWS_REGION | jq -r '.Parameter.Value' >> $TARGET_DIR/.env + +packages: + yum: + jq: [] \ No newline at end of file diff --git a/Dockerrun.aws.json b/Dockerrun.aws.json index b981ab4556..d47772957f 100644 --- a/Dockerrun.aws.json +++ b/Dockerrun.aws.json @@ -13,6 +13,10 @@ { "HostDirectory": "/certs", "ContainerDirectory": "/certs" + }, + { + "HostDirectory": "/etc/formsg/.env", + "ContainerDirectory": "/opt/formsg/.env" } ] -} +} \ No newline at end of file From b32a3fbce923e3a9bec1178db1688eae2d23844a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Jun 2021 03:44:35 +0000 Subject: [PATCH 13/21] chore(deps-dev): bump @typescript-eslint/eslint-plugin (#2239) Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 4.27.0 to 4.28.0. - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v4.28.0/packages/eslint-plugin) --- updated-dependencies: - dependency-name: "@typescript-eslint/eslint-plugin" dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 63 +++++++++++++++++++++++------------------------ package.json | 2 +- 2 files changed, 32 insertions(+), 33 deletions(-) diff --git a/package-lock.json b/package-lock.json index f4b1b08d9b..06a52ecf3e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5907,16 +5907,15 @@ } }, "@typescript-eslint/eslint-plugin": { - "version": "4.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.27.0.tgz", - "integrity": "sha512-DsLqxeUfLVNp3AO7PC3JyaddmEHTtI9qTSAs+RB6ja27QvIM0TA8Cizn1qcS6vOu+WDLFJzkwkgweiyFhssDdQ==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.28.0.tgz", + "integrity": "sha512-KcF6p3zWhf1f8xO84tuBailV5cN92vhS+VT7UJsPzGBm9VnQqfI9AsiMUFUCYHTYPg1uCCo+HyiDnpDuvkAMfQ==", "dev": true, "requires": { - "@typescript-eslint/experimental-utils": "4.27.0", - "@typescript-eslint/scope-manager": "4.27.0", + "@typescript-eslint/experimental-utils": "4.28.0", + "@typescript-eslint/scope-manager": "4.28.0", "debug": "^4.3.1", "functional-red-black-tree": "^1.0.1", - "lodash": "^4.17.21", "regexpp": "^3.1.0", "semver": "^7.3.5", "tsutils": "^3.21.0" @@ -5929,43 +5928,43 @@ "dev": true }, "@typescript-eslint/experimental-utils": { - "version": "4.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.27.0.tgz", - "integrity": "sha512-n5NlbnmzT2MXlyT+Y0Jf0gsmAQzCnQSWXKy4RGSXVStjDvS5we9IWbh7qRVKdGcxT0WYlgcCYUK/HRg7xFhvjQ==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.28.0.tgz", + "integrity": "sha512-9XD9s7mt3QWMk82GoyUpc/Ji03vz4T5AYlHF9DcoFNfJ/y3UAclRsfGiE2gLfXtyC+JRA3trR7cR296TEb1oiQ==", "dev": true, "requires": { "@types/json-schema": "^7.0.7", - "@typescript-eslint/scope-manager": "4.27.0", - "@typescript-eslint/types": "4.27.0", - "@typescript-eslint/typescript-estree": "4.27.0", + "@typescript-eslint/scope-manager": "4.28.0", + "@typescript-eslint/types": "4.28.0", + "@typescript-eslint/typescript-estree": "4.28.0", "eslint-scope": "^5.1.1", "eslint-utils": "^3.0.0" } }, "@typescript-eslint/scope-manager": { - "version": "4.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.27.0.tgz", - "integrity": "sha512-DY73jK6SEH6UDdzc6maF19AHQJBFVRf6fgAXHPXCGEmpqD4vYgPEzqpFz1lf/daSbOcMpPPj9tyXXDPW2XReAw==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.28.0.tgz", + "integrity": "sha512-eCALCeScs5P/EYjwo6se9bdjtrh8ByWjtHzOkC4Tia6QQWtQr3PHovxh3TdYTuFcurkYI4rmFsRFpucADIkseg==", "dev": true, "requires": { - "@typescript-eslint/types": "4.27.0", - "@typescript-eslint/visitor-keys": "4.27.0" + "@typescript-eslint/types": "4.28.0", + "@typescript-eslint/visitor-keys": "4.28.0" } }, "@typescript-eslint/types": { - "version": "4.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.27.0.tgz", - "integrity": "sha512-I4ps3SCPFCKclRcvnsVA/7sWzh7naaM/b4pBO2hVxnM3wrU51Lveybdw5WoIktU/V4KfXrTt94V9b065b/0+wA==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.28.0.tgz", + "integrity": "sha512-p16xMNKKoiJCVZY5PW/AfILw2xe1LfruTcfAKBj3a+wgNYP5I9ZEKNDOItoRt53p4EiPV6iRSICy8EPanG9ZVA==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "4.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.27.0.tgz", - "integrity": "sha512-KH03GUsUj41sRLLEy2JHstnezgpS5VNhrJouRdmh6yNdQ+yl8w5LrSwBkExM+jWwCJa7Ct2c8yl8NdtNRyQO6g==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.0.tgz", + "integrity": "sha512-m19UQTRtxMzKAm8QxfKpvh6OwQSXaW1CdZPoCaQuLwAq7VZMNuhJmZR4g5281s2ECt658sldnJfdpSZZaxUGMQ==", "dev": true, "requires": { - "@typescript-eslint/types": "4.27.0", - "@typescript-eslint/visitor-keys": "4.27.0", + "@typescript-eslint/types": "4.28.0", + "@typescript-eslint/visitor-keys": "4.28.0", "debug": "^4.3.1", "globby": "^11.0.3", "is-glob": "^4.0.1", @@ -5974,12 +5973,12 @@ } }, "@typescript-eslint/visitor-keys": { - "version": "4.27.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.27.0.tgz", - "integrity": "sha512-es0GRYNZp0ieckZ938cEANfEhsfHrzuLrePukLKtY3/KPXcq1Xd555Mno9/GOgXhKzn0QfkDLVgqWO3dGY80bg==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.0.tgz", + "integrity": "sha512-PjJyTWwrlrvM5jazxYF5ZPs/nl0kHDZMVbuIcbpawVXaDPelp3+S9zpOz5RmVUfS/fD5l5+ZXNKnWhNYjPzCvw==", "dev": true, "requires": { - "@typescript-eslint/types": "4.27.0", + "@typescript-eslint/types": "4.28.0", "eslint-visitor-keys": "^2.0.0" } }, @@ -6035,9 +6034,9 @@ } }, "globby": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.3.tgz", - "integrity": "sha512-ffdmosjA807y7+lA1NM0jELARVmYul/715xiILEjo3hBLPTcirgQNnXECn5g3mtR8TOLCVbkfua1Hpen25/Xcg==", + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.4.tgz", + "integrity": "sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==", "dev": true, "requires": { "array-union": "^2.1.0", diff --git a/package.json b/package.json index 794acc1a11..761c64956d 100644 --- a/package.json +++ b/package.json @@ -191,7 +191,7 @@ "@types/uid-generator": "^2.0.2", "@types/uuid": "^8.3.0", "@types/validator": "^13.1.4", - "@typescript-eslint/eslint-plugin": "^4.27.0", + "@typescript-eslint/eslint-plugin": "^4.28.0", "@typescript-eslint/parser": "^4.28.0", "auto-changelog": "^2.3.0", "axios-mock-adapter": "^1.19.0", From d147ecccf781bb4990e1338effa78cebf4bf63b2 Mon Sep 17 00:00:00 2001 From: Antariksh Mahajan Date: Wed, 23 Jun 2021 12:16:50 +0800 Subject: [PATCH 14/21] test: fix flaky form feedback test (#2241) --- .../__tests__/feedback.service.spec.ts | 40 ++++++++++++------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/src/app/modules/feedback/__tests__/feedback.service.spec.ts b/src/app/modules/feedback/__tests__/feedback.service.spec.ts index e49be28534..ef0aa91fa7 100644 --- a/src/app/modules/feedback/__tests__/feedback.service.spec.ts +++ b/src/app/modules/feedback/__tests__/feedback.service.spec.ts @@ -1,6 +1,6 @@ import { ObjectId } from 'bson-ext' import { compareAsc } from 'date-fns' -import { times } from 'lodash' +import { omit, times } from 'lodash' import moment from 'moment-timezone' import mongoose from 'mongoose' @@ -126,11 +126,14 @@ describe('feedback.service', () => { rating: 1, }) const expectedCreatedFbs = await Promise.all(expectedFbPromises) - const expectedFeedbackList = expectedCreatedFbs + // The returned feedback also has an `index` key. However, its value is + // nondeterministic as feedback with identical timestamps can be returned + // in any order. Hence omit the `index` key when checking for the expected + // feedback. + const expectedFeedbackListWithoutIndex = expectedCreatedFbs // Feedback is returned in date order .sort((a, b) => compareAsc(a.created!, b.created!)) - .map((fb, idx) => ({ - index: idx + 1, + .map((fb) => ({ timestamp: moment(fb.created).valueOf(), rating: fb.rating, comment: fb.comment, @@ -140,6 +143,9 @@ describe('feedback.service', () => { // Act const actualResult = await FeedbackService.getFormFeedbacks(mockFormId) const actual = actualResult._unsafeUnwrap() + const actualFeedbackWithoutIndex = actual.feedback.map((f) => + omit(f, 'index'), + ) // Assert // Should only average from the feedbacks for given formId. @@ -147,18 +153,22 @@ describe('feedback.service', () => { expectedCreatedFbs.reduce((acc, curr) => acc + curr.rating, 0) / expectedCount ).toFixed(2) - expect(actual).toEqual({ - average: expectedAverage, - count: expectedCount, - // Feedback may not be returned in same order, so perform unordered check. - // We cannot simply sort the arrays and expect them to be equal, as the order - // is non-deterministic if the timestamps are identical. - feedback: expect.arrayContaining(expectedFeedbackList), - }) + expect(actual.average).toBe(expectedAverage) + expect(actual.count).toBe(expectedCount) + // Feedback may not be returned in same order, so perform unordered check. + // We cannot simply sort the arrays and expect them to be equal, as the order + // is non-deterministic if the timestamps are identical. + expect(actualFeedbackWithoutIndex).toEqual( + expect.arrayContaining(expectedFeedbackListWithoutIndex), + ) // Check that there are no extra elements - expect(actual.feedback.length).toBe(expectedFeedbackList.length) - // Check that feedback is returned in date order. - expect(expectedFeedbackList.map((f) => f.timestamp)).toEqual( + expect(actualFeedbackWithoutIndex.length).toBe( + expectedFeedbackListWithoutIndex.length, + ) + // Check that feedback is returned in date order. This works even if there are + // elements with identical timestamps, as we are purely checking for the timestamp order, + // without checking any other keys. + expect(expectedFeedbackListWithoutIndex.map((f) => f.timestamp)).toEqual( actual.feedback.map((f) => f.timestamp), ) }) From f22e59c8330fbee5e2d66b28acbac1812fd54647 Mon Sep 17 00:00:00 2001 From: Kar Rui Lau Date: Wed, 23 Jun 2021 13:35:36 +0800 Subject: [PATCH 15/21] docs(script): add script to sync (has)AllowedEmailDomains (#2234) --- .../script.js | 169 ++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 scripts/20210622_sync-allowedEmailDomains-with-hasAllowedEmailDomains/script.js diff --git a/scripts/20210622_sync-allowedEmailDomains-with-hasAllowedEmailDomains/script.js b/scripts/20210622_sync-allowedEmailDomains-with-hasAllowedEmailDomains/script.js new file mode 100644 index 0000000000..92afd53a84 --- /dev/null +++ b/scripts/20210622_sync-allowedEmailDomains-with-hasAllowedEmailDomains/script.js @@ -0,0 +1,169 @@ +/* eslint-disable */ + +// Optional: Retrieve forms with fields that has a populated allowedEmailDomains but has not allowed email domains. +// For sampling to check if form has been fixed properly after migration. +db.getCollection('forms').aggregate([ + { $unwind: '$form_fields' }, + { + $match: { + 'form_fields.fieldType': 'email', + 'form_fields.hasAllowedEmailDomains': false, + 'form_fields.allowedEmailDomains': { $ne: [] }, + }, + }, + { + $group: { + _id: '$_id', + form_fields: { $addToSet: '$form_fields' }, + }, + } +]) + +// Retrieve counts of various form states +// Count forms with email fields isVerifiable=true, hasAllowedEmailDomains=true, non-empty allowedEmailDomains +// Basically forms with restricted domain email fields + +// COUNT_A = + +db.getCollection('forms').count({ + 'form_fields': { + $elemMatch: { + fieldType: 'email', + isVerifiable: true, + hasAllowedEmailDomains: true, + allowedEmailDomains: { $ne: [] } + } + } +}) + +// Count forms with isVerifiable=true, but hasAllowedEmailDomains=false +// Basically forms with verification but not restrictions + +// COUNT_B = + +db.getCollection('forms').count({ + 'form_fields': { + $elemMatch: { + fieldType: 'email', + isVerifiable: true, + hasAllowedEmailDomains: false, + } + } +}) + +// Count forms with non-empty allowed email domains even if hasAllowedEmailDomains is false, +// so we can reset array before removing hasAllowedEmailDomains key. + +// COUNT_C = + +db.getCollection('forms').count({ + 'form_fields': { + $elemMatch: { + fieldType: 'email', + hasAllowedEmailDomains: false, + allowedEmailDomains: { $ne: [] } + } + } +}) + +// Count forms with inconsistent email states +// hasAllowedEmailDomains = true but allowedEmailDomains is empty +// Should not matter since allowedEmailDomains is the source of truth + +// COUNT_D = + +db.getCollection('forms').count({ + 'form_fields': { + $elemMatch: { + fieldType: 'email', + hasAllowedEmailDomains: true, + allowedEmailDomains: { $eq: [] } + } + } +}) + + +// !!! Update !!! +// Set all current fields with hasAllowedEmailDomains = false to empty allowedEmailDomains array []. +// updateCount should be === COUNT_C +// updateCount = + +db.getCollection('forms').updateMany( + { + 'form_fields': { + $elemMatch: { + fieldType: 'email', + hasAllowedEmailDomains: false, + allowedEmailDomains: { $ne: [] } + } + } + }, + { + $set: { + 'form_fields.$[elem].allowedEmailDomains': [], + }, + }, + { + arrayFilters: [ + { + 'elem.fieldType': 'email', + 'elem.hasAllowedEmailDomains': false, + 'elem.allowedEmailDomains': { $ne: [] }, + }, + ], + multi: true, + } +) + +// Should now have 0 non-empty domains if hasAllowedEmailDomains is false +db.getCollection('forms').count({ + 'form_fields': { + $elemMatch: { + fieldType: 'email', + hasAllowedEmailDomains: false, + allowedEmailDomains: { $ne: [] } + } + } +}) + +// !!! Second update +// Set all current fields with hasAllowedEmailDomains = true WITH empty allowedEmailDomains array [] to hasAllowedEmailDomains = false. +// updateCount should be === COUNT_D +// updateCount = +db.getCollection('forms').updateMany( + { + 'form_fields': { + $elemMatch: { + fieldType: 'email', + hasAllowedEmailDomains: true, + allowedEmailDomains: { $eq: [] } + } + } + }, + { + $set: { + 'form_fields.$[elem].hasAllowedEmailDomains': false, + }, + }, + { + arrayFilters: [ + { + 'elem.fieldType': 'email', + 'elem.hasAllowedEmailDomains': true, + 'elem.allowedEmailDomains': { $eq: [] }, + }, + ], + multi: true, + } +) + +// Should now have 0 empty domains if hasAllowedEmailDomains is true +db.getCollection('forms').count({ + 'form_fields': { + $elemMatch: { + fieldType: 'email', + hasAllowedEmailDomains: true, + allowedEmailDomains: { $eq: [] } + } + } +}) \ No newline at end of file From f8e8125b25f1e518679a36cfcb3d51b82ae0e0ad Mon Sep 17 00:00:00 2001 From: LoneRifle Date: Wed, 23 Jun 2021 12:55:35 +0800 Subject: [PATCH 16/21] refactor(env): drive SSM name prefix by env var --- .ebextensions/env-file-creation.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ebextensions/env-file-creation.config b/.ebextensions/env-file-creation.config index 3c9a9437fc..0d2fd1a6c6 100644 --- a/.ebextensions/env-file-creation.config +++ b/.ebextensions/env-file-creation.config @@ -11,7 +11,7 @@ files: #!/bin/bash # Reach into the undocumented container config AWS_REGION='`{"Ref": "AWS::Region"}`' - ENV_NAME=$(jq .system.environment_name /opt/elasticbeanstalk/deploy/configuration/containerconfiguration | sed 's/"//g') + ENV_NAME=$(/opt/elasticbeanstalk/bin/get-config environment -k SSM_PREFIX) TARGET_DIR=/etc/formsg echo "Checking if ${TARGET_DIR} exists..." From b744da5eddcaa5736fd241bb5639f23dbf73a48f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Jun 2021 17:27:58 +0000 Subject: [PATCH 17/21] chore(deps-dev): bump core-js from 3.15.0 to 3.15.1 (#2243) Bumps [core-js](https://github.com/zloirock/core-js/tree/HEAD/packages/core-js) from 3.15.0 to 3.15.1. - [Release notes](https://github.com/zloirock/core-js/releases) - [Changelog](https://github.com/zloirock/core-js/blob/master/CHANGELOG.md) - [Commits](https://github.com/zloirock/core-js/commits/v3.15.1/packages/core-js) --- updated-dependencies: - dependency-name: core-js dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 06a52ecf3e..a0fa065059 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9600,9 +9600,9 @@ } }, "core-js": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.15.0.tgz", - "integrity": "sha512-GUbtPllXMYRzIgHNZ4dTYTcUemls2cni83Q4Q/TrFONHfhcg9oEGOtaGHfb0cpzec60P96UKPvMkjX1jET8rUw==", + "version": "3.15.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.15.1.tgz", + "integrity": "sha512-h8VbZYnc9pDzueiS2610IULDkpFFPunHwIpl8yRwFahAEEdSpHlTy3h3z3rKq5h11CaUdBEeRViu9AYvbxiMeg==", "dev": true }, "core-js-compat": { diff --git a/package.json b/package.json index 761c64956d..00b8ae6466 100644 --- a/package.json +++ b/package.json @@ -198,7 +198,7 @@ "babel-loader": "^8.2.2", "concurrently": "^6.2.0", "copy-webpack-plugin": "^6.0.2", - "core-js": "^3.15.0", + "core-js": "^3.15.1", "coveralls": "^3.1.0", "css-loader": "^2.1.1", "csv-parse": "^4.16.0", From e41b690d3c38aa9e0ed4081771ab90c98f80087a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Jun 2021 23:18:26 +0000 Subject: [PATCH 18/21] chore(deps-dev): bump @types/node from 14.17.3 to 14.17.4 (#2245) Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 14.17.3 to 14.17.4. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) --- updated-dependencies: - dependency-name: "@types/node" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index a0fa065059..0b5626768d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5693,9 +5693,9 @@ "dev": true }, "@types/node": { - "version": "14.17.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.3.tgz", - "integrity": "sha512-e6ZowgGJmTuXa3GyaPbTGxX17tnThl2aSSizrFthQ7m9uLGZBXiGhgE55cjRZTF5kjZvYn9EOPOMljdjwbflxw==" + "version": "14.17.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.4.tgz", + "integrity": "sha512-8kQ3+wKGRNN0ghtEn7EGps/B8CzuBz1nXZEIGGLP2GnwbqYn4dbTs7k+VKLTq1HvZLRCIDtN3Snx1Ege8B7L5A==" }, "@types/nodemailer": { "version": "6.4.2", diff --git a/package.json b/package.json index 00b8ae6466..86276b20b0 100644 --- a/package.json +++ b/package.json @@ -180,7 +180,7 @@ "@types/json-stringify-safe": "^5.0.0", "@types/mongodb": "^3.6.18", "@types/mongodb-uri": "^0.9.0", - "@types/node": "^14.17.3", + "@types/node": "^14.17.4", "@types/nodemailer": "^6.4.2", "@types/opossum": "^4.1.1", "@types/promise-retry": "^1.1.3", From ef7d79fdfac7c482504aa2e2621b4b190479f070 Mon Sep 17 00:00:00 2001 From: LoneRifle Date: Thu, 24 Jun 2021 11:31:14 +0800 Subject: [PATCH 19/21] docs(deploy): add information concerning SSM params --- docs/DEPLOYMENT_SETUP.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/DEPLOYMENT_SETUP.md b/docs/DEPLOYMENT_SETUP.md index 1d73dead40..066553d5f6 100644 --- a/docs/DEPLOYMENT_SETUP.md +++ b/docs/DEPLOYMENT_SETUP.md @@ -19,6 +19,7 @@ Infrastructure - AWS Elastic Beanstalk / EC2 for hosting and deployment - AWS Elastic File System for mounting files (i.e. SingPass/MyInfo private keys into the `/certs` directory) - AWS S3 for image and logo hosting, attachments for Storage Mode forms +- AWS Service Manager - Parameter Store, for holding environment variable configuration DevOps @@ -104,8 +105,22 @@ The following env variables are set in Travis: ## Environment Variables +These are configured by creating groups of environment variables formatted like `.env` files in the Parameter +Store of AWS Service Manager. These groups have names formatted as `-`. + +The environment for each group is user-defined, and should be specified in the Elastic Beanstalk configuration +as the environment variable `SSM_PREFIX`. + +The list of categories can be inferred by looking at the file `.ebextensions/env-file-creation.config`. + ### Core Features +#### AWS Service Manager + +| Variable | Description | +| :------------------ | ---------------------------------------------------------------------------------------------------------------- | +| `SSM_PREFIX` | String prefix (typically the environment name) for AWS SSM parameter names to create a .env file for FormSG. | + #### App Config | Variable | Description | From 5237a0b007635e495ab7eef29dfd1747dd22b41a Mon Sep 17 00:00:00 2001 From: Antariksh Mahajan Date: Thu, 24 Jun 2021 15:28:22 +0800 Subject: [PATCH 20/21] refactor: revert "refactor(email-submission): encapsulate parsedResponses (#2206)" (#2248) This reverts commit 6512a84c6456665e6f12a8fad5a7e8ccd9860d28. --- .../__tests__/admin-form.controller.spec.ts | 49 ++- .../form/admin-form/admin-form.controller.ts | 38 +-- .../submission/submission.service.spec.ts | 295 +++++++++++++++++- .../ParsedResponsesObject.class.ts | 153 --------- .../ParsedResponsesObject.class.spec.ts | 124 -------- .../email-submission.controller.ts | 46 ++- .../email-submission.types.ts | 6 +- .../modules/submission/submission.service.ts | 112 ++++++- 8 files changed, 468 insertions(+), 355 deletions(-) delete mode 100644 src/app/modules/submission/email-submission/ParsedResponsesObject.class.ts delete mode 100644 src/app/modules/submission/email-submission/__tests__/ParsedResponsesObject.class.spec.ts diff --git a/src/app/modules/form/admin-form/__tests__/admin-form.controller.spec.ts b/src/app/modules/form/admin-form/__tests__/admin-form.controller.spec.ts index 3fefae6160..05a20e680d 100644 --- a/src/app/modules/form/admin-form/__tests__/admin-form.controller.spec.ts +++ b/src/app/modules/form/admin-form/__tests__/admin-form.controller.spec.ts @@ -77,7 +77,6 @@ import { } from 'tests/unit/backend/helpers/generate-form-data' import expressHandler from 'tests/unit/backend/helpers/jest-express' -import ParsedResponsesObject from '../../../submission/email-submission/ParsedResponsesObject.class' import * as UserService from '../../../user/user.service' import { ForbiddenFormError, @@ -128,8 +127,6 @@ jest.mock('src/app/modules/submission/submission.utils') const MockSubmissionUtils = mocked(SubmissionUtils) jest.mock('../admin-form.service') const MockAdminFormService = mocked(AdminFormService) -jest.mock('../../../submission/email-submission/ParsedResponsesObject.class') -const MockParsedResponsesObject = mocked(ParsedResponsesObject) jest.mock('../../form.service') const MockFormService = mocked(FormService) jest.mock('../../../user/user.service') @@ -5133,12 +5130,8 @@ describe('admin-form.controller', () => { MockEmailSubmissionService.validateAttachments.mockReturnValue( okAsync(true), ) - MockParsedResponsesObject.mockClear() - MockParsedResponsesObject.parseResponses.mockReturnValue( - ok(new ParsedResponsesObject(MOCK_PARSED_RESPONSES)), - ) - MockParsedResponsesObject.mock.instances[0].getAllResponses.mockReturnValue( - MOCK_PARSED_RESPONSES, + MockSubmissionService.getProcessedResponses.mockReturnValue( + ok(MOCK_PARSED_RESPONSES), ) MockEmailSubmissionService.createEmailSubmissionWithoutSave.mockReturnValue( MOCK_SUBMISSION, @@ -5193,7 +5186,7 @@ describe('admin-form.controller', () => { expect( MockEmailSubmissionService.validateAttachments, ).toHaveBeenCalledWith(MOCK_RESPONSES) - expect(MockParsedResponsesObject.parseResponses).toHaveBeenCalledWith( + expect(MockSubmissionService.getProcessedResponses).toHaveBeenCalledWith( MOCK_FORM, MOCK_RESPONSES, ) @@ -5260,7 +5253,7 @@ describe('admin-form.controller', () => { expect( MockEmailSubmissionService.validateAttachments, ).not.toHaveBeenCalled() - expect(MockParsedResponsesObject.parseResponses).not.toHaveBeenCalled() + expect(MockSubmissionService.getProcessedResponses).not.toHaveBeenCalled() expect(MockAdminFormService.extractMyInfoFieldIds).not.toHaveBeenCalled() expect( MockEmailSubmissionService.createEmailSubmissionWithoutSave, @@ -5309,7 +5302,7 @@ describe('admin-form.controller', () => { expect( MockEmailSubmissionService.validateAttachments, ).not.toHaveBeenCalled() - expect(MockParsedResponsesObject.parseResponses).not.toHaveBeenCalled() + expect(MockSubmissionService.getProcessedResponses).not.toHaveBeenCalled() expect(MockAdminFormService.extractMyInfoFieldIds).not.toHaveBeenCalled() expect( MockEmailSubmissionService.createEmailSubmissionWithoutSave, @@ -5362,7 +5355,7 @@ describe('admin-form.controller', () => { expect( MockEmailSubmissionService.validateAttachments, ).not.toHaveBeenCalled() - expect(MockParsedResponsesObject.parseResponses).not.toHaveBeenCalled() + expect(MockSubmissionService.getProcessedResponses).not.toHaveBeenCalled() expect(MockAdminFormService.extractMyInfoFieldIds).not.toHaveBeenCalled() expect( MockEmailSubmissionService.createEmailSubmissionWithoutSave, @@ -5415,7 +5408,7 @@ describe('admin-form.controller', () => { expect( MockEmailSubmissionService.validateAttachments, ).not.toHaveBeenCalled() - expect(MockParsedResponsesObject.parseResponses).not.toHaveBeenCalled() + expect(MockSubmissionService.getProcessedResponses).not.toHaveBeenCalled() expect(MockAdminFormService.extractMyInfoFieldIds).not.toHaveBeenCalled() expect( MockEmailSubmissionService.createEmailSubmissionWithoutSave, @@ -5468,7 +5461,7 @@ describe('admin-form.controller', () => { expect( MockEmailSubmissionService.validateAttachments, ).not.toHaveBeenCalled() - expect(MockParsedResponsesObject.parseResponses).not.toHaveBeenCalled() + expect(MockSubmissionService.getProcessedResponses).not.toHaveBeenCalled() expect(MockAdminFormService.extractMyInfoFieldIds).not.toHaveBeenCalled() expect( MockEmailSubmissionService.createEmailSubmissionWithoutSave, @@ -5521,7 +5514,7 @@ describe('admin-form.controller', () => { expect( MockEmailSubmissionService.validateAttachments, ).not.toHaveBeenCalled() - expect(MockParsedResponsesObject.parseResponses).not.toHaveBeenCalled() + expect(MockSubmissionService.getProcessedResponses).not.toHaveBeenCalled() expect(MockAdminFormService.extractMyInfoFieldIds).not.toHaveBeenCalled() expect( MockEmailSubmissionService.createEmailSubmissionWithoutSave, @@ -5574,7 +5567,7 @@ describe('admin-form.controller', () => { expect( MockEmailSubmissionService.validateAttachments, ).not.toHaveBeenCalled() - expect(MockParsedResponsesObject.parseResponses).not.toHaveBeenCalled() + expect(MockSubmissionService.getProcessedResponses).not.toHaveBeenCalled() expect(MockAdminFormService.extractMyInfoFieldIds).not.toHaveBeenCalled() expect( MockEmailSubmissionService.createEmailSubmissionWithoutSave, @@ -5627,7 +5620,7 @@ describe('admin-form.controller', () => { expect( MockEmailSubmissionService.validateAttachments, ).toHaveBeenCalledWith(MOCK_RESPONSES) - expect(MockParsedResponsesObject.parseResponses).not.toHaveBeenCalled() + expect(MockSubmissionService.getProcessedResponses).not.toHaveBeenCalled() expect(MockAdminFormService.extractMyInfoFieldIds).not.toHaveBeenCalled() expect( MockEmailSubmissionService.createEmailSubmissionWithoutSave, @@ -5680,7 +5673,7 @@ describe('admin-form.controller', () => { expect( MockEmailSubmissionService.validateAttachments, ).toHaveBeenCalledWith(MOCK_RESPONSES) - expect(MockParsedResponsesObject.parseResponses).not.toHaveBeenCalled() + expect(MockSubmissionService.getProcessedResponses).not.toHaveBeenCalled() expect(MockAdminFormService.extractMyInfoFieldIds).not.toHaveBeenCalled() expect( MockEmailSubmissionService.createEmailSubmissionWithoutSave, @@ -5699,7 +5692,7 @@ describe('admin-form.controller', () => { }) it('should return 400 when responses cannot be processed', async () => { - MockParsedResponsesObject.parseResponses.mockReturnValueOnce( + MockSubmissionService.getProcessedResponses.mockReturnValueOnce( err(new ProcessingError()), ) const mockReq = expressHandler.mockRequest({ @@ -5733,7 +5726,7 @@ describe('admin-form.controller', () => { expect( MockEmailSubmissionService.validateAttachments, ).toHaveBeenCalledWith(MOCK_RESPONSES) - expect(MockParsedResponsesObject.parseResponses).toHaveBeenCalledWith( + expect(MockSubmissionService.getProcessedResponses).toHaveBeenCalledWith( MOCK_FORM, MOCK_RESPONSES, ) @@ -5755,7 +5748,7 @@ describe('admin-form.controller', () => { }) it('should return 409 when the submitted field IDs do not match the form field IDs', async () => { - MockParsedResponsesObject.parseResponses.mockReturnValueOnce( + MockSubmissionService.getProcessedResponses.mockReturnValueOnce( err(new ConflictError('')), ) const mockReq = expressHandler.mockRequest({ @@ -5789,7 +5782,7 @@ describe('admin-form.controller', () => { expect( MockEmailSubmissionService.validateAttachments, ).toHaveBeenCalledWith(MOCK_RESPONSES) - expect(MockParsedResponsesObject.parseResponses).toHaveBeenCalledWith( + expect(MockSubmissionService.getProcessedResponses).toHaveBeenCalledWith( MOCK_FORM, MOCK_RESPONSES, ) @@ -5811,7 +5804,7 @@ describe('admin-form.controller', () => { }) it('should return 400 when any answer is invalid', async () => { - MockParsedResponsesObject.parseResponses.mockReturnValueOnce( + MockSubmissionService.getProcessedResponses.mockReturnValueOnce( err(new ValidateFieldError()), ) const mockReq = expressHandler.mockRequest({ @@ -5845,7 +5838,7 @@ describe('admin-form.controller', () => { expect( MockEmailSubmissionService.validateAttachments, ).toHaveBeenCalledWith(MOCK_RESPONSES) - expect(MockParsedResponsesObject.parseResponses).toHaveBeenCalledWith( + expect(MockSubmissionService.getProcessedResponses).toHaveBeenCalledWith( MOCK_FORM, MOCK_RESPONSES, ) @@ -5901,7 +5894,7 @@ describe('admin-form.controller', () => { expect( MockEmailSubmissionService.validateAttachments, ).toHaveBeenCalledWith(MOCK_RESPONSES) - expect(MockParsedResponsesObject.parseResponses).toHaveBeenCalledWith( + expect(MockSubmissionService.getProcessedResponses).toHaveBeenCalledWith( MOCK_FORM, MOCK_RESPONSES, ) @@ -5966,7 +5959,7 @@ describe('admin-form.controller', () => { expect( MockEmailSubmissionService.validateAttachments, ).toHaveBeenCalledWith(MOCK_RESPONSES) - expect(MockParsedResponsesObject.parseResponses).toHaveBeenCalledWith( + expect(MockSubmissionService.getProcessedResponses).toHaveBeenCalledWith( MOCK_FORM, MOCK_RESPONSES, ) @@ -6031,7 +6024,7 @@ describe('admin-form.controller', () => { expect( MockEmailSubmissionService.validateAttachments, ).toHaveBeenCalledWith(MOCK_RESPONSES) - expect(MockParsedResponsesObject.parseResponses).toHaveBeenCalledWith( + expect(MockSubmissionService.getProcessedResponses).toHaveBeenCalledWith( MOCK_FORM, MOCK_RESPONSES, ) diff --git a/src/app/modules/form/admin-form/admin-form.controller.ts b/src/app/modules/form/admin-form/admin-form.controller.ts index 311deac41a..3be70728b3 100644 --- a/src/app/modules/form/admin-form/admin-form.controller.ts +++ b/src/app/modules/form/admin-form/admin-form.controller.ts @@ -50,6 +50,10 @@ import { } from '../../core/core.errors' import { ControllerHandler } from '../../core/core.types' import * as FeedbackService from '../../feedback/feedback.service' +import { + createCorppassParsedResponses, + createSingpassParsedResponses, +} from '../../spcp/spcp.util' import * as EmailSubmissionMiddleware from '../../submission/email-submission/email-submission.middleware' import * as EmailSubmissionService from '../../submission/email-submission/email-submission.service' import { @@ -57,7 +61,6 @@ import { mapRouteError as mapEmailSubmissionError, SubmissionEmailObj, } from '../../submission/email-submission/email-submission.util' -import ParsedResponsesObject from '../../submission/email-submission/ParsedResponsesObject.class' import * as EncryptSubmissionMiddleware from '../../submission/encrypt-submission/encrypt-submission.middleware' import * as EncryptSubmissionService from '../../submission/encrypt-submission/encrypt-submission.service' import { mapRouteError as mapEncryptSubmissionError } from '../../submission/encrypt-submission/encrypt-submission.utils' @@ -1509,7 +1512,7 @@ export const submitEmailPreview: ControllerHandler< const parsedResponsesResult = await EmailSubmissionService.validateAttachments(responses).andThen(() => - ParsedResponsesObject.parseResponses(form, responses), + SubmissionService.getProcessedResponses(form, responses), ) if (parsedResponsesResult.isErr()) { logger.error({ @@ -1526,22 +1529,21 @@ export const submitEmailPreview: ControllerHandler< const attachments = mapAttachmentsFromResponses(req.body.responses) // Handle SingPass, CorpPass and MyInfo authentication and validation - const { authType } = form - if (authType === AuthType.SP || authType === AuthType.MyInfo) { - parsedResponses.addNdiResponses({ - authType, - uinFin: PREVIEW_SINGPASS_UINFIN, - }) - } else if (authType === AuthType.CP) { - parsedResponses.addNdiResponses({ - authType, - uinFin: PREVIEW_CORPPASS_UINFIN, - userInfo: PREVIEW_CORPPASS_UID, - }) + if (form.authType === AuthType.SP || form.authType === AuthType.MyInfo) { + parsedResponses.push( + ...createSingpassParsedResponses(PREVIEW_SINGPASS_UINFIN), + ) + } else if (form.authType === AuthType.CP) { + parsedResponses.push( + ...createCorppassParsedResponses( + PREVIEW_CORPPASS_UINFIN, + PREVIEW_CORPPASS_UID, + ), + ) } const emailData = new SubmissionEmailObj( - parsedResponses.getAllResponses(), + parsedResponses, // All MyInfo fields are verified in preview new Set(AdminFormService.extractMyInfoFieldIds(form.form_fields)), form.authType, @@ -1554,9 +1556,7 @@ export const submitEmailPreview: ControllerHandler< ) const sendAdminEmailResult = await MailService.sendSubmissionToAdmin({ - replyToEmails: EmailSubmissionService.extractEmailAnswers( - parsedResponses.getAllResponses(), - ), + replyToEmails: EmailSubmissionService.extractEmailAnswers(parsedResponses), form, submission, attachments, @@ -1585,7 +1585,7 @@ export const submitEmailPreview: ControllerHandler< attachments, responsesData: emailData.autoReplyData, recipientData: extractEmailConfirmationData( - parsedResponses.getAllResponses(), + parsedResponses, form.form_fields, ), }).mapErr((error) => { diff --git a/src/app/modules/submission/__tests__/submission/submission.service.spec.ts b/src/app/modules/submission/__tests__/submission/submission.service.spec.ts index 93dd37a40d..db1cf95366 100644 --- a/src/app/modules/submission/__tests__/submission/submission.service.spec.ts +++ b/src/app/modules/submission/__tests__/submission/submission.service.spec.ts @@ -9,26 +9,40 @@ import { MalformedParametersError, } from 'src/app/modules/core/core.errors' import * as SubmissionService from 'src/app/modules/submission/submission.service' +import { ProcessedFieldResponse } from 'src/app/modules/submission/submission.types' import MailService from 'src/app/services/mail/mail.service' import { createQueryWithDateParam } from 'src/app/utils/date' +import * as LogicUtil from 'src/shared/util/logic' import { AutoReplyOptions, BasicField, IAttachmentInfo, + IEmailFormSchema, IEmailSubmissionSchema, + IEncryptedFormSchema, IEncryptedSubmissionSchema, IFormSchema, + IPreventSubmitLogicSchema, ISubmissionSchema, + LogicType, + ResponseMode, SubmissionType, } from 'src/types' import { generateDefaultField, generateNewSingleAnswerResponse, + generateProcessedSingleAnswerResponse, + generateSingleAnswerResponse, } from 'tests/unit/backend/helpers/generate-form-data' import dbHandler from 'tests/unit/backend/helpers/jest-db' -import { SendEmailConfirmationError } from '../../submission.errors' +import { + ConflictError, + ProcessingError, + SendEmailConfirmationError, + ValidateFieldError, +} from '../../submission.errors' import { extractEmailConfirmationData } from '../../submission.utils' jest.mock('src/app/services/mail/mail.service') @@ -95,6 +109,285 @@ describe('submission.service', () => { afterAll(async () => await dbHandler.closeDatabase()) beforeEach(() => jest.clearAllMocks()) + describe('getProcessedResponses', () => { + it('should return list of parsed responses for encrypted form submission successfully', async () => { + // Arrange + // Only mobile and email fields are parsed, since the other fields are + // e2e encrypted from the browser. + const mobileField = generateDefaultField(BasicField.Mobile) + const emailField = generateDefaultField(BasicField.Email) + // Add answers to both mobile and email fields + const mobileResponse = generateSingleAnswerResponse( + mobileField, + '+6587654321', + ) + const emailResponse = generateSingleAnswerResponse( + emailField, + 'test@example.com', + ) + + const mobileProcessedResponse = generateProcessedSingleAnswerResponse( + mobileField, + '+6587654321', + ) + const emailProcessedResponse = generateProcessedSingleAnswerResponse( + emailField, + 'test@example.com', + ) + + // Act + const actualResult = SubmissionService.getProcessedResponses( + { + responseMode: ResponseMode.Encrypt, + form_fields: [mobileField, emailField], + } as unknown as IFormSchema, + [mobileResponse, emailResponse], + ) + + // Assert + const expectedParsed: ProcessedFieldResponse[] = [ + { ...mobileProcessedResponse, isVisible: true }, + { ...emailProcessedResponse, isVisible: true }, + ] + // Should only have email and mobile fields for encrypted forms. + expect(actualResult.isOk()).toEqual(true) + expect(actualResult._unsafeUnwrap()).toEqual(expectedParsed) + }) + + it('should return list of parsed responses for email form submission successfully', async () => { + // Arrange + // Add answer to subset of field types + const shortTextField = generateDefaultField(BasicField.ShortText) + const decimalField = generateDefaultField(BasicField.Decimal) + + // Add answers to both mobile and email fields + const shortTextResponse = generateSingleAnswerResponse( + shortTextField, + 'the quick brown fox jumps over the lazy dog', + ) + const decimalResponse = generateSingleAnswerResponse( + decimalField, + '3.142', + ) + + const shortTextProcessedResponse = generateProcessedSingleAnswerResponse( + shortTextField, + 'the quick brown fox jumps over the lazy dog', + ) + const decimalProcessedResponse = generateProcessedSingleAnswerResponse( + decimalField, + '3.142', + ) + + // Act + const actualResult = SubmissionService.getProcessedResponses( + { + responseMode: ResponseMode.Email, + form_fields: [shortTextField, decimalField], + } as unknown as IFormSchema, + [shortTextResponse, decimalResponse], + ) + + // Assert + const expectedParsed: ProcessedFieldResponse[] = [ + { ...shortTextProcessedResponse, isVisible: true }, + { ...decimalProcessedResponse, isVisible: true }, + ] + + expect(actualResult.isOk()).toEqual(true) + expect(actualResult._unsafeUnwrap()).toEqual(expectedParsed) + }) + + it('should return error when email form has more fields than responses', async () => { + // Arrange + const extraField = generateDefaultField(BasicField.Mobile) + + // Act + Assert + + const actualResult = SubmissionService.getProcessedResponses( + { + responseMode: ResponseMode.Email, + form_fields: [extraField], + } as unknown as IEmailFormSchema, + [], + ) + + expect(actualResult.isErr()).toEqual(true) + expect(actualResult._unsafeUnwrapErr()).toEqual( + new ConflictError('Some form fields are missing'), + ) + }) + + it('should return error when encrypt form has more fields than responses', async () => { + // Arrange + const extraField = generateDefaultField(BasicField.Mobile) + + // Act + Assert + + const actualResult = SubmissionService.getProcessedResponses( + { + responseMode: ResponseMode.Encrypt, + form_fields: [extraField], + } as unknown as IEncryptedFormSchema, + [], + ) + + expect(actualResult.isErr()).toEqual(true) + expect(actualResult._unsafeUnwrapErr()).toEqual( + new ConflictError('Some form fields are missing'), + ) + }) + it('should allow responses for encrypt mode hidden fields', async () => { + // Arrange + // Only check for mobile and email fields, since the other fields are + // e2e encrypted from the browser. + const mobileField = generateDefaultField(BasicField.Mobile) + const emailField = generateDefaultField(BasicField.Email) + // Add answers to both mobile and email fields + const mobileResponse = generateSingleAnswerResponse( + mobileField, + '+6587654321', + ) + + const emailResponse = generateSingleAnswerResponse( + emailField, + 'test@example.com', + ) + + const mobileProcessedResponse = generateProcessedSingleAnswerResponse( + mobileField, + '+6587654321', + ) + mobileProcessedResponse.isVisible = false + + const emailProcessedResponse = generateProcessedSingleAnswerResponse( + emailField, + 'test@example.com', + ) + emailProcessedResponse.isVisible = false + + // Act + const actualResult = SubmissionService.getProcessedResponses( + { + responseMode: ResponseMode.Encrypt, + form_fields: [mobileField, emailField], + } as unknown as IFormSchema, + [mobileResponse, emailResponse], + ) + + // Assert + const expectedParsed: ProcessedFieldResponse[] = [ + { ...mobileProcessedResponse, isVisible: true }, //getProcessedResponses sets isVisible to be true for encrypt mode + { ...emailProcessedResponse, isVisible: true }, + ] + // Should only have email and mobile fields for encrypted forms. + expect(actualResult.isOk()).toEqual(true) + expect(actualResult._unsafeUnwrap()).toEqual(expectedParsed) + }) + + it('should return error when any responses are not valid for encrypted form submission', async () => { + // Arrange + // Only mobile and email fields are parsed, since the other fields are + // e2e encrypted from the browser. + const mobileField = generateDefaultField(BasicField.Mobile) + const mobileResponse = generateSingleAnswerResponse( + mobileField, + 'invalid', + ) + + // Act + Assert + const actualResult = SubmissionService.getProcessedResponses( + { + responseMode: ResponseMode.Encrypt, + form_fields: [mobileField], + } as unknown as IEncryptedFormSchema, + [mobileResponse], + ) + + expect(actualResult.isErr()).toEqual(true) + expect(actualResult._unsafeUnwrapErr()).toEqual( + new ValidateFieldError('Invalid answer submitted'), + ) + }) + + it('should return error when any responses are not valid for email form submission', async () => { + // Arrange + // Set NRIC field in form as required. + const nricField = generateDefaultField(BasicField.Nric) + const nricResponse = generateSingleAnswerResponse(nricField, 'invalid') + + // Act + Assert + const actualResult = SubmissionService.getProcessedResponses( + { + responseMode: ResponseMode.Email, + form_fields: [nricField], + } as unknown as IEmailFormSchema, + [nricResponse], + ) + + expect(actualResult.isErr()).toEqual(true) + expect(actualResult._unsafeUnwrapErr()).toEqual( + new ValidateFieldError('Invalid answer submitted'), + ) + }) + + it('should return error when encrypted form submission is prevented by logic', async () => { + // Arrange + // Mock logic util to return non-empty to check if error is thrown + jest + .spyOn(LogicUtil, 'getLogicUnitPreventingSubmit') + .mockReturnValueOnce({ + preventSubmitMessage: 'mock prevent submit', + conditions: [], + logicType: LogicType.PreventSubmit, + _id: 'some id', + } as unknown as IPreventSubmitLogicSchema) + + // Act + Assert + const actualResult = SubmissionService.getProcessedResponses( + { + responseMode: ResponseMode.Encrypt, + form_fields: [], + } as unknown as IEncryptedFormSchema, + [], + ) + + expect(actualResult.isErr()).toEqual(true) + expect(actualResult._unsafeUnwrapErr()).toEqual( + new ProcessingError('Submission prevented by form logic'), + ) + }) + + it('should return error when email form submission is prevented by logic', async () => { + // Arrange + // Mock logic util to return non-empty to check if error is thrown. + const mockReturnLogicUnit = { + preventSubmitMessage: 'mock prevent submit', + conditions: [], + logicType: LogicType.PreventSubmit, + _id: 'some id', + } as unknown as IPreventSubmitLogicSchema + + jest + .spyOn(LogicUtil, 'getLogicUnitPreventingSubmit') + .mockReturnValueOnce(mockReturnLogicUnit) + + // Act + Assert + const actualResult = SubmissionService.getProcessedResponses( + { + responseMode: ResponseMode.Email, + form_fields: [], + } as unknown as IEmailFormSchema, + [], + ) + + expect(actualResult.isErr()).toEqual(true) + expect(actualResult._unsafeUnwrapErr()).toEqual( + new ProcessingError('Submission prevented by form logic'), + ) + }) + }) + describe('getFormSubmissionsCount', () => { const countSpy = jest.spyOn(Submission, 'countDocuments') const MOCK_FORM_ID = new ObjectId() diff --git a/src/app/modules/submission/email-submission/ParsedResponsesObject.class.ts b/src/app/modules/submission/email-submission/ParsedResponsesObject.class.ts deleted file mode 100644 index 0a3d899220..0000000000 --- a/src/app/modules/submission/email-submission/ParsedResponsesObject.class.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { err, ok, Result } from 'neverthrow' - -import { - getLogicUnitPreventingSubmit, - getVisibleFieldIds, -} from '../../../../shared/util/logic' -import { - AuthType, - FieldResponse, - IFieldSchema, - IFormDocument, - ResponseMode, -} from '../../../../types' -import { validateField } from '../../../utils/field-validation' -import { - createCorppassParsedResponses, - createSingpassParsedResponses, -} from '../../spcp/spcp.util' -import { - ConflictError, - ProcessingError, - ValidateFieldError, -} from '../submission.errors' -import { ProcessedFieldResponse } from '../submission.types' -import { getFilteredResponses } from '../submission.utils' - -type NdiUserInfo = - | { authType: AuthType.SP | AuthType.MyInfo; uinFin: string } - | { authType: AuthType.CP; uinFin: string; userInfo: string } - -export default class ParsedResponsesObject { - public ndiResponses: ProcessedFieldResponse[] = [] - private constructor(public responses: ProcessedFieldResponse[]) {} - - addNdiResponses(info: NdiUserInfo): ParsedResponsesObject { - /** - * No typescript destructuring being done in switch statement - * because typescript isn't smart enough to do narrowing with - * destructured variable switch cases. - */ - switch (info.authType) { - case AuthType.SP: - case AuthType.MyInfo: - this.ndiResponses = createSingpassParsedResponses(info.uinFin) - break - case AuthType.CP: - this.ndiResponses = createCorppassParsedResponses( - info.uinFin, - info.userInfo, - ) - break - } - return this - } - - getAllResponses(): ProcessedFieldResponse[] { - return [...this.responses, ...this.ndiResponses] - } - - /** - * Injects response metadata such as the question, visibility state. In - * addition, validation such as input validation or signature validation on - * verified fields are also performed on the response. - * @param form The form document corresponding to the responses - * @param responses The responses to process and validate - * @returns neverthrow ok() with field responses with additional metadata injected. - * @returns neverthrow err() if response validation fails - */ - static parseResponses( - form: IFormDocument, - responses: FieldResponse[], - ): Result< - ParsedResponsesObject, - ProcessingError | ConflictError | ValidateFieldError - > { - const filteredResponsesResult = getFilteredResponses(form, responses) - if (filteredResponsesResult.isErr()) { - return err(filteredResponsesResult.error) - } - - const filteredResponses = filteredResponsesResult.value - - // Set of all visible fields - const visibleFieldIds = getVisibleFieldIds(filteredResponses, form) - - // Guard against invalid form submissions that should have been prevented by - // logic. - if ( - getLogicUnitPreventingSubmit(filteredResponses, form, visibleFieldIds) - ) { - return err(new ProcessingError('Submission prevented by form logic')) - } - - // Create a map keyed by field._id for easier access - - if (!form.form_fields) { - return err(new ProcessingError('Form fields are undefined')) - } - - const fieldMap = form.form_fields.reduce<{ - [fieldId: string]: IFieldSchema - }>((acc, field) => { - acc[field._id] = field - return acc - }, {}) - - // Validate each field in the form and inject metadata into the responses. - const processedResponses = [] - for (const response of filteredResponses) { - const responseId = response._id - const formField = fieldMap[responseId] - if (!formField) { - return err( - new ProcessingError('Response ID does not match form field IDs'), - ) - } - - const processingResponse: ProcessedFieldResponse = { - ...response, - isVisible: - // Set isVisible as true for Encrypt mode if there is a response for mobile and email field - // Because we cannot tell if the field is unhidden by logic - // This prevents downstream validateField from incorrectly preventing - // encrypt mode submissions with responses on unhidden fields - // TODO(#780): Remove this once submission service is separated into - // Email and Encrypted services - form.responseMode === ResponseMode.Encrypt - ? 'answer' in response && - typeof response.answer === 'string' && - response.answer.trim() !== '' - : visibleFieldIds.has(responseId), - question: formField.getQuestion(), - } - - if (formField.isVerifiable) { - processingResponse.isUserVerified = formField.isVerifiable - } - - // Error will be returned if the processed response is not valid. - const validateFieldResult = validateField( - form._id, - formField, - processingResponse, - ) - if (validateFieldResult.isErr()) { - return err(validateFieldResult.error) - } - processedResponses.push(processingResponse) - } - - return ok(new ParsedResponsesObject(processedResponses)) - } -} diff --git a/src/app/modules/submission/email-submission/__tests__/ParsedResponsesObject.class.spec.ts b/src/app/modules/submission/email-submission/__tests__/ParsedResponsesObject.class.spec.ts deleted file mode 100644 index b0a8bb01ae..0000000000 --- a/src/app/modules/submission/email-submission/__tests__/ParsedResponsesObject.class.spec.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { - generateDefaultField, - generateProcessedSingleAnswerResponse, - generateSingleAnswerResponse, -} from '../../../../../../tests/unit/backend/helpers/generate-form-data' -import * as LogicUtil from '../../../../../shared/util/logic' -import { - BasicField, - IEmailFormSchema, - IFormSchema, - IPreventSubmitLogicSchema, - LogicType, - ResponseMode, -} from '../../../../../types' -import { - ConflictError, - ProcessingError, - ValidateFieldError, -} from '../../submission.errors' -import { ProcessedFieldResponse } from '../../submission.types' -import ParsedResponsesObject from '../ParsedResponsesObject.class' - -describe('ParsedResponsesObject', () => { - it('should return list of parsed responses for email form submission successfully', async () => { - // Add answer to subset of field types - const shortTextField = generateDefaultField(BasicField.ShortText) - const decimalField = generateDefaultField(BasicField.Decimal) - - // Add answers to both mobile and email fields - const shortTextResponse = generateSingleAnswerResponse( - shortTextField, - 'the quick brown fox jumps over the lazy dog', - ) - const decimalResponse = generateSingleAnswerResponse(decimalField, '3.142') - - const shortTextProcessedResponse = generateProcessedSingleAnswerResponse( - shortTextField, - 'the quick brown fox jumps over the lazy dog', - ) - const decimalProcessedResponse = generateProcessedSingleAnswerResponse( - decimalField, - '3.142', - ) - - const result = ParsedResponsesObject.parseResponses( - { - responseMode: ResponseMode.Email, - form_fields: [shortTextField, decimalField], - } as unknown as IFormSchema, - [shortTextResponse, decimalResponse], - ) - - const expectedParsed: ProcessedFieldResponse[] = [ - { ...shortTextProcessedResponse, isVisible: true }, - { ...decimalProcessedResponse, isVisible: true }, - ] - - expect(result.isOk()).toEqual(true) - expect(result._unsafeUnwrap().getAllResponses()).toEqual(expectedParsed) - }) - - it('should return error when email form has more fields than responses', async () => { - const extraField = generateDefaultField(BasicField.Mobile) - - const result = ParsedResponsesObject.parseResponses( - { - responseMode: ResponseMode.Email, - form_fields: [extraField], - } as unknown as IEmailFormSchema, - [], - ) - - expect(result.isErr()).toEqual(true) - expect(result._unsafeUnwrapErr()).toEqual( - new ConflictError('Some form fields are missing'), - ) - }) - - it('should return error when any responses are not valid for email form submission', async () => { - // Set NRIC field in form as required. - const nricField = generateDefaultField(BasicField.Nric) - const nricResponse = generateSingleAnswerResponse(nricField, 'invalid') - - const result = ParsedResponsesObject.parseResponses( - { - responseMode: ResponseMode.Email, - form_fields: [nricField], - } as unknown as IEmailFormSchema, - [nricResponse], - ) - - expect(result.isErr()).toEqual(true) - expect(result._unsafeUnwrapErr()).toEqual( - new ValidateFieldError('Invalid answer submitted'), - ) - }) - - it('should return error when email form submission is prevented by logic', async () => { - // Mock logic util to return non-empty to check if error is thrown. - const mockReturnLogicUnit = { - preventSubmitMessage: 'mock prevent submit', - conditions: [], - logicType: LogicType.PreventSubmit, - _id: 'some id', - } as unknown as IPreventSubmitLogicSchema - - jest - .spyOn(LogicUtil, 'getLogicUnitPreventingSubmit') - .mockReturnValueOnce(mockReturnLogicUnit) - - const result = ParsedResponsesObject.parseResponses( - { - responseMode: ResponseMode.Email, - form_fields: [], - } as unknown as IEmailFormSchema, - [], - ) - - expect(result.isErr()).toEqual(true) - expect(result._unsafeUnwrapErr()).toEqual( - new ProcessingError('Submission prevented by form logic'), - ) - }) -}) diff --git a/src/app/modules/submission/email-submission/email-submission.controller.ts b/src/app/modules/submission/email-submission/email-submission.controller.ts index 2e94c14898..734641a093 100644 --- a/src/app/modules/submission/email-submission/email-submission.controller.ts +++ b/src/app/modules/submission/email-submission/email-submission.controller.ts @@ -20,6 +20,10 @@ import { import { MyInfoService } from '../../myinfo/myinfo.service' import * as MyInfoUtil from '../../myinfo/myinfo.util' import { SpcpService } from '../../spcp/spcp.service' +import { + createCorppassParsedResponses, + createSingpassParsedResponses, +} from '../../spcp/spcp.util' import * as EmailSubmissionMiddleware from '../email-submission/email-submission.middleware' import * as SubmissionService from '../submission.service' import { extractEmailConfirmationData } from '../submission.utils' @@ -31,7 +35,6 @@ import { mapRouteError, SubmissionEmailObj, } from './email-submission.util' -import ParsedResponsesObject from './ParsedResponsesObject.class' const logger = createLoggerWithLabel(module) @@ -134,7 +137,7 @@ const submitEmailModeForm: ControllerHandler< // Validate responses EmailSubmissionService.validateAttachments(req.body.responses) .andThen(() => - ParsedResponsesObject.parseResponses(form, req.body.responses), + SubmissionService.getProcessedResponses(form, req.body.responses), ) .map((parsedResponses) => ({ parsedResponses, form })) .mapErr((error) => { @@ -154,11 +157,10 @@ const submitEmailModeForm: ControllerHandler< .asyncAndThen((jwt) => SpcpService.extractCorppassJwtPayload(jwt)) .map((jwt) => ({ form, - parsedResponses: parsedResponses.addNdiResponses({ - authType, - uinFin: jwt.userName, - userInfo: jwt.userInfo, - }), + parsedResponses: [ + ...parsedResponses, + ...createCorppassParsedResponses(jwt.userName, jwt.userInfo), + ], })) .mapErr((error) => { spcpSubmissionFailure = true @@ -174,10 +176,10 @@ const submitEmailModeForm: ControllerHandler< .asyncAndThen((jwt) => SpcpService.extractSingpassJwtPayload(jwt)) .map((jwt) => ({ form, - parsedResponses: parsedResponses.addNdiResponses({ - authType, - uinFin: jwt.userName, - }), + parsedResponses: [ + ...parsedResponses, + ...createSingpassParsedResponses(jwt.userName), + ], })) .mapErr((error) => { spcpSubmissionFailure = true @@ -197,19 +199,16 @@ const submitEmailModeForm: ControllerHandler< .asyncAndThen((uinFin) => MyInfoService.fetchMyInfoHashes(uinFin, formId) .andThen((hashes) => - MyInfoService.checkMyInfoHashes( - parsedResponses.responses, - hashes, - ), + MyInfoService.checkMyInfoHashes(parsedResponses, hashes), ) .map( (hashedFields) => ({ form, hashedFields, - parsedResponses: parsedResponses.addNdiResponses({ - authType, - uinFin, - }), + parsedResponses: [ + ...parsedResponses, + ...createSingpassParsedResponses(uinFin), + ], }), ), ) @@ -232,7 +231,7 @@ const submitEmailModeForm: ControllerHandler< .andThen(({ form, parsedResponses, hashedFields }) => { // Create data for response email as well as email confirmation const emailData = new SubmissionEmailObj( - parsedResponses.getAllResponses(), + parsedResponses, hashedFields, form.authType, ) @@ -275,9 +274,8 @@ const submitEmailModeForm: ControllerHandler< // NOTE: This should short circuit in the event of an error. // This is why sendSubmissionToAdmin is separated from sendEmailConfirmations in 2 blocks return MailService.sendSubmissionToAdmin({ - replyToEmails: EmailSubmissionService.extractEmailAnswers( - parsedResponses.getAllResponses(), - ), + replyToEmails: + EmailSubmissionService.extractEmailAnswers(parsedResponses), form, submission, attachments, @@ -315,7 +313,7 @@ const submitEmailModeForm: ControllerHandler< attachments, responsesData: emailData.autoReplyData, recipientData: extractEmailConfirmationData( - parsedResponses.getAllResponses(), + parsedResponses, form.form_fields, ), }).mapErr((error) => { diff --git a/src/app/modules/submission/email-submission/email-submission.types.ts b/src/app/modules/submission/email-submission/email-submission.types.ts index d142ed7b60..eae1936c14 100644 --- a/src/app/modules/submission/email-submission/email-submission.types.ts +++ b/src/app/modules/submission/email-submission/email-submission.types.ts @@ -4,9 +4,7 @@ import { IBaseResponse, IPopulatedEmailForm, } from '../../../../types' -import { ProcessedResponse } from '../submission.types' - -import ParsedResponsesObject from './ParsedResponsesObject.class' +import { ProcessedFieldResponse, ProcessedResponse } from '../submission.types' // When a response has been formatted for email, all answerArray // should have been converted to answer @@ -30,6 +28,6 @@ export interface SubmissionHash { export interface IPopulatedEmailFormWithResponsesAndHash { form: IPopulatedEmailForm - parsedResponses: ParsedResponsesObject + parsedResponses: ProcessedFieldResponse[] hashedFields?: Set } diff --git a/src/app/modules/submission/submission.service.ts b/src/app/modules/submission/submission.service.ts index 281719ba1c..1dedd9b20f 100644 --- a/src/app/modules/submission/submission.service.ts +++ b/src/app/modules/submission/submission.service.ts @@ -1,24 +1,132 @@ import mongoose from 'mongoose' -import { errAsync, okAsync, ResultAsync } from 'neverthrow' +import { err, errAsync, ok, okAsync, Result, ResultAsync } from 'neverthrow' +import { + getLogicUnitPreventingSubmit, + getVisibleFieldIds, +} from '../../../shared/util/logic' import { EmailRespondentConfirmationField, + FieldResponse, IAttachmentInfo, + IFieldSchema, + IFormDocument, IPopulatedForm, ISubmissionSchema, + ResponseMode, } from '../../../types' import { createLoggerWithLabel } from '../../config/logger' import getSubmissionModel from '../../models/submission.server.model' import MailService from '../../services/mail/mail.service' import { AutoReplyMailData } from '../../services/mail/mail.types' import { createQueryWithDateParam, isMalformedDate } from '../../utils/date' +import { validateField } from '../../utils/field-validation' import { DatabaseError, MalformedParametersError } from '../core/core.errors' -import { SendEmailConfirmationError } from './submission.errors' +import { + ConflictError, + ProcessingError, + SendEmailConfirmationError, + ValidateFieldError, +} from './submission.errors' +import { ProcessedFieldResponse } from './submission.types' +import { getFilteredResponses } from './submission.utils' const logger = createLoggerWithLabel(module) const SubmissionModel = getSubmissionModel(mongoose) +/** + * Injects response metadata such as the question, visibility state. In + * addition, validation such as input validation or signature validation on + * verified fields are also performed on the response. + * @param form The form document corresponding to the responses + * @param responses The responses to process and validate + * @returns neverthrow ok() with field responses with additional metadata injected. + * @returns neverthrow err() if response validation fails + */ +export const getProcessedResponses = ( + form: IFormDocument, + originalResponses: FieldResponse[], +): Result< + ProcessedFieldResponse[], + ProcessingError | ConflictError | ValidateFieldError +> => { + const filteredResponsesResult = getFilteredResponses(form, originalResponses) + if (filteredResponsesResult.isErr()) { + return err(filteredResponsesResult.error) + } + + const filteredResponses = filteredResponsesResult.value + + // Set of all visible fields + const visibleFieldIds = getVisibleFieldIds(filteredResponses, form) + + // Guard against invalid form submissions that should have been prevented by + // logic. + if (getLogicUnitPreventingSubmit(filteredResponses, form, visibleFieldIds)) { + return err(new ProcessingError('Submission prevented by form logic')) + } + + // Create a map keyed by field._id for easier access + + if (!form.form_fields) { + return err(new ProcessingError('Form fields are undefined')) + } + + const fieldMap = form.form_fields.reduce<{ + [fieldId: string]: IFieldSchema + }>((acc, field) => { + acc[field._id] = field + return acc + }, {}) + + // Validate each field in the form and inject metadata into the responses. + const processedResponses = [] + for (const response of filteredResponses) { + const responseId = response._id + const formField = fieldMap[responseId] + if (!formField) { + return err( + new ProcessingError('Response ID does not match form field IDs'), + ) + } + + const processingResponse: ProcessedFieldResponse = { + ...response, + isVisible: + // Set isVisible as true for Encrypt mode if there is a response for mobile and email field + // Because we cannot tell if the field is unhidden by logic + // This prevents downstream validateField from incorrectly preventing + // encrypt mode submissions with responses on unhidden fields + // TODO(#780): Remove this once submission service is separated into + // Email and Encrypted services + form.responseMode === ResponseMode.Encrypt + ? 'answer' in response && + typeof response.answer === 'string' && + response.answer.trim() !== '' + : visibleFieldIds.has(responseId), + question: formField.getQuestion(), + } + + if (formField.isVerifiable) { + processingResponse.isUserVerified = formField.isVerifiable + } + + // Error will be returned if the processed response is not valid. + const validateFieldResult = validateField( + form._id, + formField, + processingResponse, + ) + if (validateFieldResult.isErr()) { + return err(validateFieldResult.error) + } + processedResponses.push(processingResponse) + } + + return ok(processedResponses) +} + /** * Returns number of form submissions of given form id in the given date range. * From c01e980e544695743985553e67b8dc66e065e2a3 Mon Sep 17 00:00:00 2001 From: Kar Rui Lau Date: Thu, 24 Jun 2021 15:43:42 +0800 Subject: [PATCH 21/21] chore: bump version to v5.17.0 --- CHANGELOG.md | 74 ++++++++++++++++++++++++++++++++--------------- package-lock.json | 2 +- package.json | 4 +-- 3 files changed, 54 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3764fe3ac7..929387c795 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,34 @@ All notable changes to this project will be documented in this file. Dates are d Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). +#### [v5.17.0](https://github.com/opengovsg/FormSG/compare/v5.16.0...v5.17.0) + +- refactor: revert "refactor(email-submission): encapsulate parsedResponses (#2206)" [`#2248`](https://github.com/opengovsg/FormSG/pull/2248) +- feat(config): support config via dotenv, use EB to create file [`#2194`](https://github.com/opengovsg/FormSG/pull/2194) +- chore(deps-dev): bump @types/node from 14.17.3 to 14.17.4 [`#2245`](https://github.com/opengovsg/FormSG/pull/2245) +- chore(deps-dev): bump core-js from 3.15.0 to 3.15.1 [`#2243`](https://github.com/opengovsg/FormSG/pull/2243) +- docs(script): add script to sync (has)AllowedEmailDomains [`#2234`](https://github.com/opengovsg/FormSG/pull/2234) +- test: fix flaky form feedback test [`#2241`](https://github.com/opengovsg/FormSG/pull/2241) +- chore(deps-dev): bump @typescript-eslint/eslint-plugin [`#2239`](https://github.com/opengovsg/FormSG/pull/2239) +- fix(deps): bump nocache from 3.0.0 to 3.0.1 [`#2236`](https://github.com/opengovsg/FormSG/pull/2236) +- chore(deps-dev): bump @babel/preset-env from 7.14.5 to 7.14.7 [`#2238`](https://github.com/opengovsg/FormSG/pull/2238) +- fix(deps): bump aws-sdk from 2.931.0 to 2.932.0 [`#2237`](https://github.com/opengovsg/FormSG/pull/2237) +- refactor(feature-manager): delete remaining unused code [`#2223`](https://github.com/opengovsg/FormSG/pull/2223) +- feat(feature-manager): remove spcp-myinfo from feature manager [`#2222`](https://github.com/opengovsg/FormSG/pull/2222) +- feat(feature-manager): remove sms from feature manager [`#2218`](https://github.com/opengovsg/FormSG/pull/2218) +- refactor: convert CsvMergedHeadersGenerator to typescript [`#2080`](https://github.com/opengovsg/FormSG/pull/2080) +- refactor(email-submission): encapsulate parsedResponses [`#2206`](https://github.com/opengovsg/FormSG/pull/2206) +- build: merge release 5.16.0 into develop [`#2230`](https://github.com/opengovsg/FormSG/pull/2230) +- chore(deps-dev): bump @typescript-eslint/parser from 4.27.0 to 4.28.0 [`#2226`](https://github.com/opengovsg/FormSG/pull/2226) +- chore(deps-dev): bump core-js from 3.14.0 to 3.15.0 [`#2225`](https://github.com/opengovsg/FormSG/pull/2225) +- build(ci): create .env files in EB with Param Store [`ac27242`](https://github.com/opengovsg/FormSG/commit/ac2724276bef8e4bd47211efa32b24a5a8cb1738) +- docs(deploy): add information concerning SSM params [`ef7d79f`](https://github.com/opengovsg/FormSG/commit/ef7d79fdfac7c482504aa2e2621b4b190479f070) +- feat(config): support env var config via dotenv [`9fc8c9e`](https://github.com/opengovsg/FormSG/commit/9fc8c9e6562fe55e7e13c593c2bb1a4c96cbfe96) + #### [v5.16.0](https://github.com/opengovsg/FormSG/compare/v5.15.0...v5.16.0) +> 22 June 2021 + - fix(deps): bump @sentry/browser from 6.7.1 to 6.7.2 [`#2228`](https://github.com/opengovsg/FormSG/pull/2228) - fix(deps): bump @sentry/integrations from 6.7.1 to 6.7.2 [`#2227`](https://github.com/opengovsg/FormSG/pull/2227) - fix(deps): bump libphonenumber-js from 1.9.19 to 1.9.20 [`#2224`](https://github.com/opengovsg/FormSG/pull/2224) @@ -39,6 +65,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - fix(deps): bump aws-sdk from 2.927.0 to 2.928.0 [`#2182`](https://github.com/opengovsg/FormSG/pull/2182) - chore: merge v5.14.0 into develop [`#2174`](https://github.com/opengovsg/FormSG/pull/2174) - feat: update landing spcp image, minify app images [`#2173`](https://github.com/opengovsg/FormSG/pull/2173) +- chore: bump version to 5.16.0 [`1288985`](https://github.com/opengovsg/FormSG/commit/128898523c9fe31ddf970f2e175074bfecb80163) #### [v5.15.0](https://github.com/opengovsg/FormSG/compare/v5.14.1...v5.15.0) @@ -122,8 +149,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). > 11 June 2021 -- fix: use correct argument key when counting form submissions [`#2101`](https://github.com/opengovsg/FormSG/pull/2101) -- chore: bump version to v5.13.0 [`4516bbc`](https://github.com/opengovsg/FormSG/commit/4516bbcaf2ef2d99830cb5abb0ee26e2d53b31c2) - chore: bump version to 5.13.1 [`7c87bf3`](https://github.com/opengovsg/FormSG/commit/7c87bf3af16347b630581427bc9223a84846dea3) - feat(verification): up expiry time to 30min [`34b28c8`](https://github.com/opengovsg/FormSG/commit/34b28c87c14e8e0b55276e5a21b6f5473c436e24) @@ -131,6 +156,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). > 8 June 2021 +- fix: use correct argument key when counting form submissions [`#2101`](https://github.com/opengovsg/FormSG/pull/2101) - chore(adminsubmissionsservice): renamed form to submissions to reflect context [`#2098`](https://github.com/opengovsg/FormSG/pull/2098) - feat: enable retries for webhooks [`#2093`](https://github.com/opengovsg/FormSG/pull/2093) - feat: log form updates [`#2063`](https://github.com/opengovsg/FormSG/pull/2063) @@ -162,7 +188,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - fix(deps): update mongoose to 5.12.12, update model types [`#2046`](https://github.com/opengovsg/FormSG/pull/2046) - chore(deps-dev): bump type-fest from 0.20.2 to 1.2.0 [`#2049`](https://github.com/opengovsg/FormSG/pull/2049) - test(betas): provide coverage [`23f9a9f`](https://github.com/opengovsg/FormSG/commit/23f9a9fe9675eab1d25c1983a08a7c76e0139d52) -- chore: bump version to v5.13.0 [`6fa6c88`](https://github.com/opengovsg/FormSG/commit/6fa6c888fd381ae2fef0a83ff2fc0f1d89a6635a) +- chore: bump version to v5.13.0 [`4516bbc`](https://github.com/opengovsg/FormSG/commit/4516bbcaf2ef2d99830cb5abb0ee26e2d53b31c2) #### [v5.12.1](https://github.com/opengovsg/FormSG/compare/v5.12.0...v5.12.1) @@ -211,6 +237,12 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - fix: add types to RequestHandler in submitEncryptModeForm [`#1889`](https://github.com/opengovsg/FormSG/pull/1889) - chore: merge v5.11.0 into develop [`#1969`](https://github.com/opengovsg/FormSG/pull/1969) - fix(verification): loosen OTP waiting time by 2 seconds [`#1957`](https://github.com/opengovsg/FormSG/pull/1957) +- chore: bump version to 5.12.0 [`85759bc`](https://github.com/opengovsg/FormSG/commit/85759bc9dc01f73da3cbd0ec73c636e58e983948) + +#### [v5.11.0](https://github.com/opengovsg/FormSG/compare/v5.10.1...v5.11.0) + +> 25 May 2021 + - fix: set form logo default value when creating form document [`#1966`](https://github.com/opengovsg/FormSG/pull/1966) - chore(deps-dev): bump ts-node from 9.1.1 to 10.0.0 [`#1964`](https://github.com/opengovsg/FormSG/pull/1964) - feat: extract public form submission flow (and preview) to specific Typescript services [`#1917`](https://github.com/opengovsg/FormSG/pull/1917) @@ -243,13 +275,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - feat: upgrade myinfo-gov-client to 4.0.0 [`#1925`](https://github.com/opengovsg/FormSG/pull/1925) - feat: add response ID to thank you page [`#1855`](https://github.com/opengovsg/FormSG/pull/1855) - refactor(verification): reset field verification state when field changes [`#1900`](https://github.com/opengovsg/FormSG/pull/1900) -- chore: bump version to 5.11.0 [`54b1958`](https://github.com/opengovsg/FormSG/commit/54b1958d0968e670ef145461d9d7859384d573ef) -- chore: bump version to 5.12.0 [`85759bc`](https://github.com/opengovsg/FormSG/commit/85759bc9dc01f73da3cbd0ec73c636e58e983948) - -#### [v5.11.0](https://github.com/opengovsg/FormSG/compare/v5.10.0...v5.11.0) - -> 19 May 2021 - - chore(deps-dev): bump @babel/core from 7.14.2 to 7.14.3 [`#1920`](https://github.com/opengovsg/FormSG/pull/1920) - fix(deps): bump aws-sdk from 2.907.0 to 2.908.0 [`#1922`](https://github.com/opengovsg/FormSG/pull/1922) - chore(deps-dev): bump @types/node from 14.14.45 to 14.17.0 [`#1921`](https://github.com/opengovsg/FormSG/pull/1921) @@ -281,7 +306,12 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - fix(deps): bump aws-sdk from 2.903.0 to 2.904.0 [`#1869`](https://github.com/opengovsg/FormSG/pull/1869) - chore(deps-dev): bump @typescript-eslint/eslint-plugin [`#1868`](https://github.com/opengovsg/FormSG/pull/1868) - fix(deps): bump @sentry/integrations from 6.3.5 to 6.3.6 [`#1850`](https://github.com/opengovsg/FormSG/pull/1850) -- chore: bump version to 5.11.0 [`971eb29`](https://github.com/opengovsg/FormSG/commit/971eb2948f01461d884c1ba504d421ed5be189a2) +- chore: bump version to 5.11.0 [`54b1958`](https://github.com/opengovsg/FormSG/commit/54b1958d0968e670ef145461d9d7859384d573ef) + +#### [v5.10.1](https://github.com/opengovsg/FormSG/compare/v5.10.0...v5.10.1) + +> 17 May 2021 + - chore: bump version to v5.10.1 [`0442cd7`](https://github.com/opengovsg/FormSG/commit/0442cd72637019fb1e43bce5f8f5abe14ee79f8c) - fix: allow for unknown keys in updateEndPage validator [`617d86a`](https://github.com/opengovsg/FormSG/commit/617d86a28910eec6ebd3249a2de636086429d6a6) @@ -328,14 +358,13 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - chore(deps-dev): bump @typescript-eslint/eslint-plugin [`#1790`](https://github.com/opengovsg/FormSG/pull/1790) - feat(api-refactor): add specific update end page endpoint in server [`#1760`](https://github.com/opengovsg/FormSG/pull/1760) - feat: move server.ts into src/app [`#1785`](https://github.com/opengovsg/FormSG/pull/1785) -- fix: trigger digest cycle for delete logic [`#1787`](https://github.com/opengovsg/FormSG/pull/1787) -- chore: bump version to 5.9.0 [`6d6e475`](https://github.com/opengovsg/FormSG/commit/6d6e475c417cfb5efacb203888b0f296159d8ac1) - chore: bump version to v5.10.0 [`0615ce5`](https://github.com/opengovsg/FormSG/commit/0615ce5262fcdb65932ad6c9be9ee66503b0e949) #### [v5.9.0](https://github.com/opengovsg/FormSG/compare/v5.8.0...v5.9.0) > 4 May 2021 +- fix: trigger digest cycle for delete logic [`#1787`](https://github.com/opengovsg/FormSG/pull/1787) - fix: allow commas in email confirmation sender [`#1782`](https://github.com/opengovsg/FormSG/pull/1782) - chore(deps-dev): bump core-js from 3.11.1 to 3.11.2 [`#1780`](https://github.com/opengovsg/FormSG/pull/1780) - fix(deps): bump fp-ts from 2.10.4 to 2.10.5 [`#1781`](https://github.com/opengovsg/FormSG/pull/1781) @@ -360,7 +389,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - fix(deps): bump aws-sdk from 2.893.0 to 2.894.0 [`#1756`](https://github.com/opengovsg/FormSG/pull/1756) - fix(deps): bump @sentry/integrations from 6.3.1 to 6.3.3 [`#1755`](https://github.com/opengovsg/FormSG/pull/1755) - chore: merge v5.8.0 into develop [`#1751`](https://github.com/opengovsg/FormSG/pull/1751) -- chore: bump version to 5.9.0 [`902fd6a`](https://github.com/opengovsg/FormSG/commit/902fd6a764e94bd0882ca1f7bebb3e79f916c9f3) +- chore: bump version to 5.9.0 [`6d6e475`](https://github.com/opengovsg/FormSG/commit/6d6e475c417cfb5efacb203888b0f296159d8ac1) #### [v5.8.0](https://github.com/opengovsg/FormSG/compare/v5.7.1...v5.8.0) @@ -469,15 +498,15 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). > 13 April 2021 -- fix: call correct user update emergency contact endpoint [`#1631`](https://github.com/opengovsg/FormSG/pull/1631) -- chore: bump version to v5.6.0 [`c4724a9`](https://github.com/opengovsg/FormSG/commit/c4724a9f64c6dd5cb134dd46ceae8ab6a736dbec) - test(AdminFormRoutes): test for equal start and end dates validation [`2d81ff4`](https://github.com/opengovsg/FormSG/commit/2d81ff4a4573a70a698596f5ac2ec1a4c5139b27) - chore: bump version to v5.6.1 [`c640dd1`](https://github.com/opengovsg/FormSG/commit/c640dd1e8c219f5293f36fff18f57881ae1ded73) +- fix: use Joi.date.min() instead of Joi.date.greater() for date range [`864561c`](https://github.com/opengovsg/FormSG/commit/864561c129bebd24cfd7dca2851106066141a780) #### [v5.6.0](https://github.com/opengovsg/FormSG/compare/v5.5.1...v5.6.0) > 13 April 2021 +- fix: call correct user update emergency contact endpoint [`#1631`](https://github.com/opengovsg/FormSG/pull/1631) - fix(deps): bump winston-cloudwatch from 2.5.1 to 2.5.2 [`#1626`](https://github.com/opengovsg/FormSG/pull/1626) - chore(deps-dev): bump @typescript-eslint/parser from 4.21.0 to 4.22.0 [`#1627`](https://github.com/opengovsg/FormSG/pull/1627) - chore(deps-dev): bump date-fns from 2.20.1 to 2.20.2 [`#1628`](https://github.com/opengovsg/FormSG/pull/1628) @@ -542,7 +571,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - chore(deps-dev): bump @typescript-eslint/eslint-plugin [`#1556`](https://github.com/opengovsg/FormSG/pull/1556) - fix(deps): bump @opengovsg/spcp-auth-client from 1.4.4 to 1.4.5 [`#1555`](https://github.com/opengovsg/FormSG/pull/1555) - refactor(corppass-ui): make ui changes for corppass [`#1533`](https://github.com/opengovsg/FormSG/pull/1533) -- chore: bump version to v5.6.0 [`bf1874d`](https://github.com/opengovsg/FormSG/commit/bf1874d1aedec79f416cf0db3eec02eb1bcf017b) +- chore: bump version to v5.6.0 [`c4724a9`](https://github.com/opengovsg/FormSG/commit/c4724a9f64c6dd5cb134dd46ceae8ab6a736dbec) #### [v5.5.1](https://github.com/opengovsg/FormSG/compare/v5.5.0...v5.5.1) @@ -858,15 +887,14 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). > 23 February 2021 -- fix: add _id key in permissionList object for updateForm validator [`#1224`](https://github.com/opengovsg/FormSG/pull/1224) - feat: remove updateFormValidator [`92f3f75`](https://github.com/opengovsg/FormSG/commit/92f3f75bc760f32bdb495e27eb880d53f1562093) -- chore: bump version to v4.59.0 [`21bee76`](https://github.com/opengovsg/FormSG/commit/21bee768bb40e9eae57fe25b8a3c7b2ea3ccc130) - chore: bump version to v4.59.1 [`a712594`](https://github.com/opengovsg/FormSG/commit/a712594146e294a01cea13e429e4ed03109a1f70) #### [v4.59.0](https://github.com/opengovsg/FormSG/compare/v4.58.2...v4.59.0) > 23 February 2021 +- fix: add _id key in permissionList object for updateForm validator [`#1224`](https://github.com/opengovsg/FormSG/pull/1224) - chore: use formsg-sdk beta release [`#1219`](https://github.com/opengovsg/FormSG/pull/1219) - chore(deps-dev): bump csv-parse from 4.15.1 to 4.15.3 [`#1213`](https://github.com/opengovsg/FormSG/pull/1213) - refactor: move addLogin method to Billing module [`#1195`](https://github.com/opengovsg/FormSG/pull/1195) @@ -923,7 +951,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - fix(deps): bump nodemailer from 6.4.17 to 6.4.18 [`#1143`](https://github.com/opengovsg/FormSG/pull/1143) - fix(deps): bump @sentry/integrations from 5.30.0 to 6.1.0 [`#1142`](https://github.com/opengovsg/FormSG/pull/1142) - fix(deps): bump libphonenumber-js from 1.9.8 to 1.9.11 [`#1138`](https://github.com/opengovsg/FormSG/pull/1138) -- chore: bump version to v4.59.0 [`53bd033`](https://github.com/opengovsg/FormSG/commit/53bd03318be09e83dc549d93515830e3b7efc365) +- chore: bump version to v4.59.0 [`21bee76`](https://github.com/opengovsg/FormSG/commit/21bee768bb40e9eae57fe25b8a3c7b2ea3ccc130) #### [v4.58.2](https://github.com/opengovsg/FormSG/compare/v4.58.1...v4.58.2) @@ -1233,16 +1261,16 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - build: Release 4.30.1 - Fix field creation on old clients [`#74`](https://github.com/opengovsg/FormSG/pull/74) - Release 4.30.0 - acknowledgement for secret key backup, TypeScript migrations [`#67`](https://github.com/opengovsg/FormSG/pull/67) - build: empty commit to trigger PR build [`d0c6583`](https://github.com/opengovsg/FormSG/commit/d0c65838efa6731a0f10e89ff954c132b9f8b854) -- feat: update table field styling to not rely on multiple divs [`db03da3`](https://github.com/opengovsg/FormSG/commit/db03da33eca2fc0a6174ca58d7dd8b3cfadadcea) - fix: return 401 for missing JWT [`e6c1947`](https://github.com/opengovsg/FormSG/commit/e6c19477b05fc4aed90b2db42916220aea2a263c) +- test: add tests for extractJwt [`16191a9`](https://github.com/opengovsg/FormSG/commit/16191a957be0dc7f63dc4221569fac2794b7d063) #### [v4.50.2](https://github.com/opengovsg/FormSG/compare/v4.50.1...v4.50.2) > 16 December 2020 -- feat: update table field styling to not rely on multiple divs [`e857aed`](https://github.com/opengovsg/FormSG/commit/e857aed667145441758f09f311c2b9f16f57645b) +- feat: update table field styling to not rely on multiple divs [`db03da3`](https://github.com/opengovsg/FormSG/commit/db03da33eca2fc0a6174ca58d7dd8b3cfadadcea) - fix: email format validation should allow 126/163.com, align frontend and backend validation [`be35522`](https://github.com/opengovsg/FormSG/commit/be35522e15d20521f58fada7ed3164e6c0c0894d) -- chore: bump version to v4.50.2 [`fad62ec`](https://github.com/opengovsg/FormSG/commit/fad62ec5730412201e208332a91286fd53e2b36a) +- chore: bump version to v4.50.2 [`1ac7be6`](https://github.com/opengovsg/FormSG/commit/1ac7be6cb69553d186ec194682ae25bd98318a8b) #### [v4.50.1](https://github.com/opengovsg/FormSG/compare/v4.50.0...v4.50.1) diff --git a/package-lock.json b/package-lock.json index ec02e4bb9d..43eedaf2e4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "FormSG", - "version": "5.16.0", + "version": "5.17.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 63700a229d..18878deb69 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "FormSG", "description": "Form Manager for Government", - "version": "5.16.0", + "version": "5.17.0", "homepage": "https://form.gov.sg", "authors": [ "FormSG " @@ -254,4 +254,4 @@ "webpack-merge": "^4.1.3", "worker-loader": "^2.0.0" } -} \ No newline at end of file +}