diff --git a/__tests__/unit/backend/helpers/jest-db.ts b/__tests__/unit/backend/helpers/jest-db.ts index a712f973d1..373aec4adf 100644 --- a/__tests__/unit/backend/helpers/jest-db.ts +++ b/__tests__/unit/backend/helpers/jest-db.ts @@ -116,7 +116,7 @@ const insertFormCollectionReqs = async ({ betaFlags, apiToken, }: { - userId?: Schema.Types.ObjectId + userId?: Types.ObjectId mailName?: string mailDomain?: string shortName?: string 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 606fd5806f..a11630bccb 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 @@ -36,7 +36,6 @@ import { import * as SubmissionService from 'src/app/modules/submission/submission.service' import * as SubmissionUtils from 'src/app/modules/submission/submission.utils' import { MissingUserError } from 'src/app/modules/user/user.errors' -import { SmsLimitExceededError } from 'src/app/modules/verification/verification.errors' import * as WorkspaceService from 'src/app/modules/workspace/workspace.service' import { MailGenerationError, @@ -6937,9 +6936,6 @@ describe('admin-form.controller', () => { MockAuthService.getFormAfterPermissionChecks.mockReturnValue( okAsync(MOCK_FORM), ) - MockAdminFormService.shouldUpdateFormField.mockReturnValue( - okAsync(MOCK_FORM), - ) MockAdminFormService.updateFormField.mockReturnValue( okAsync(MOCK_UPDATED_FIELD as FormFieldSchema), ) @@ -7038,29 +7034,6 @@ describe('admin-form.controller', () => { expect(MockAdminFormService.updateFormField).not.toHaveBeenCalled() }) - it('should return 409 when the field could not be updated due to a sms limit exceeded error', async () => { - // Arrange - MockAdminFormService.shouldUpdateFormField.mockReturnValueOnce( - errAsync(new SmsLimitExceededError()), - ) - const expected = { - message: - 'You have exceeded the free sms limit. Please refresh and try again.', - } - const mockRes = expressHandler.mockResponse() - - // Act - await AdminFormController._handleUpdateFormField( - MOCK_REQ, - mockRes, - jest.fn(), - ) - - // Assert - expect(mockRes.status).toHaveBeenCalledWith(409) - expect(mockRes.json).toHaveBeenCalledWith(expected) - }) - it('should return 410 when form to update form field for is already archived', async () => { // Arrange const mockRes = expressHandler.mockResponse() @@ -7233,9 +7206,6 @@ describe('admin-form.controller', () => { MockAuthService.getFormAfterPermissionChecks.mockReturnValue( okAsync(MOCK_FORM), ) - MockAdminFormService.shouldUpdateFormField.mockReturnValue( - okAsync(MOCK_FORM), - ) MockAdminFormService.createFormField.mockReturnValue( okAsync(MOCK_RETURNED_FIELD), ) @@ -7345,29 +7315,6 @@ describe('admin-form.controller', () => { expect(MockAdminFormService.createFormField).not.toHaveBeenCalled() }) - it('should return 409 when the field could not be created due to a sms limit exceeded error', async () => { - // Arrange - MockAdminFormService.shouldUpdateFormField.mockReturnValueOnce( - errAsync(new SmsLimitExceededError()), - ) - const expected = { - message: - 'You have exceeded the free sms limit. Please refresh and try again.', - } - const mockRes = expressHandler.mockResponse() - - // Act - await AdminFormController._handleCreateFormField( - MOCK_REQ, - mockRes, - jest.fn(), - ) - - // Assert - expect(mockRes.status).toHaveBeenCalledWith(409) - expect(mockRes.json).toHaveBeenCalledWith(expected) - }) - it('should return 410 when attempting to create a form field for an archived form', async () => { // Arrange const expectedErrorString = 'form gone pls' @@ -7594,9 +7541,6 @@ describe('admin-form.controller', () => { okAsync(MOCK_FORM), ) MockAdminFormService.getFormField.mockReturnValue(ok(MOCK_FIELDS[0])) - MockAdminFormService.shouldUpdateFormField.mockReturnValue( - okAsync(MOCK_FORM), - ) MockAdminFormService.duplicateFormField.mockReturnValue( okAsync(MOCK_DUPLICATED_FIELD), ) @@ -7734,28 +7678,6 @@ describe('admin-form.controller', () => { ) }) - it('should return 409 when the field could not be duplicated due to a sms limit exceeded error', async () => { - // Arrange - MockAdminFormService.shouldUpdateFormField.mockReturnValueOnce( - errAsync(new SmsLimitExceededError()), - ) - const expected = { - message: - 'You have exceeded the free sms limit. Please refresh and try again.', - } - const mockRes = expressHandler.mockResponse() - - // Act - await AdminFormController.handleDuplicateFormField( - MOCK_REQ, - mockRes, - jest.fn(), - ) - - // Assert - expect(mockRes.status).toHaveBeenCalledWith(409) - expect(mockRes.json).toHaveBeenCalledWith(expected) - }) it('should return 410 when form to duplicate form field for is already archived', async () => { // Arrange const mockRes = expressHandler.mockResponse() diff --git a/src/app/modules/form/admin-form/__tests__/admin-form.service.spec.ts b/src/app/modules/form/admin-form/__tests__/admin-form.service.spec.ts index 8434a7686b..406840c231 100644 --- a/src/app/modules/form/admin-form/__tests__/admin-form.service.spec.ts +++ b/src/app/modules/form/admin-form/__tests__/admin-form.service.spec.ts @@ -24,7 +24,6 @@ import { } from 'src/app/modules/core/core.errors' import { MissingUserError } from 'src/app/modules/user/user.errors' import * as UserService from 'src/app/modules/user/user.service' -import { SmsLimitExceededError } from 'src/app/modules/verification/verification.errors' import { TwilioCredentials } from 'src/app/services/sms/sms.types' import { CreatePresignedPostError } from 'src/app/utils/aws-s3' import { formatErrorRecoveryMessage } from 'src/app/utils/handle-mongo-error' @@ -60,7 +59,6 @@ import { LogicType, SettingsUpdateDto, } from '../../../../../../shared/types' -import { smsConfig } from '../../../../config/features/sms.config' import * as SmsService from '../../../../services/sms/sms.service' import { FormNotFoundError, @@ -2317,6 +2315,8 @@ describe('admin-form.service', () => { buttonLink: 'https://some-button-link.example.com', buttonText: 'expected button text', paragraph: 'some paragraph', + paymentTitle: '', + paymentParagraph: '', } it('should return updated end page when update is successful', async () => { @@ -2609,113 +2609,6 @@ describe('admin-form.service', () => { }) }) - describe('shouldUpdateFormField', () => { - const MOCK_FORM = { - admin: { - _id: new ObjectId(), - }, - } as unknown as IPopulatedForm - - const countSpy = jest.spyOn(SmsService, 'retrieveFreeSmsCounts') - - describe('when update form field is not a BasicField.Mobile type', () => { - const MOCK_RATING_FIELD = generateDefaultField(BasicField.Rating) - it('should return the form without doing anything', async () => { - // Act - const actual = await AdminFormService.shouldUpdateFormField( - MOCK_FORM, - MOCK_RATING_FIELD, - ) - - // Assert - expect(actual._unsafeUnwrap()).toEqual(MOCK_FORM) - expect(countSpy).not.toHaveBeenCalled() - }) - }) - - describe('when update form field is a BasicField.Mobile type', () => { - const MOCK_UNVERIFIABLE_MOBILE_FIELD = generateDefaultField( - BasicField.Mobile, - ) - const MOCK_VERIFIABLE_MOBILE_FIELD = generateDefaultField( - BasicField.Mobile, - { isVerifiable: true }, - ) - - it('should return the given form when the admin is under the free sms limit', async () => { - // Arrange - countSpy.mockReturnValueOnce(okAsync(smsConfig.smsVerificationLimit)) - - // Act - const actual = await AdminFormService.shouldUpdateFormField( - MOCK_FORM, - MOCK_VERIFIABLE_MOBILE_FIELD, - ) - - // Assert - expect(actual._unsafeUnwrap()).toBe(MOCK_FORM) - }) - - it('should return the given form when the form is onboarded with its own credentials', async () => { - // Arrange - const MOCK_ONBOARDED_FORM = { ...MOCK_FORM, msgSrvcName: 'form a form' } - - // Act - const actual = await AdminFormService.shouldUpdateFormField( - MOCK_ONBOARDED_FORM, - MOCK_VERIFIABLE_MOBILE_FIELD, - ) - - // Assert - expect(countSpy).not.toHaveBeenCalled() - expect(actual._unsafeUnwrap()).toEqual(MOCK_ONBOARDED_FORM) - }) - - it('should return the given form when mobile field is not verifiable', async () => { - // Act - const actual = await AdminFormService.shouldUpdateFormField( - MOCK_FORM, - MOCK_UNVERIFIABLE_MOBILE_FIELD, - ) - - // Assert - expect(countSpy).not.toHaveBeenCalled() - expect(actual._unsafeUnwrap()).toEqual(MOCK_FORM) - }) - - it('should return sms retrieval error when sms limit exceeded and the given form has not been onboarded', async () => { - // Arrange - countSpy.mockReturnValueOnce( - okAsync(smsConfig.smsVerificationLimit + 1), - ) - - // Act - const actual = await AdminFormService.shouldUpdateFormField( - MOCK_FORM, - MOCK_VERIFIABLE_MOBILE_FIELD, - ) - - // Assert - expect(actual._unsafeUnwrapErr()).toEqual(new SmsLimitExceededError()) - }) - - it('should propagate any database errors encountered during retrieval', async () => { - // Arrange - const MOCK_ERROR_STRING = 'something went oopsie' - const expectedError = new DatabaseError(MOCK_ERROR_STRING) - countSpy.mockReturnValueOnce(errAsync(expectedError)) - - // Act - const actual = await AdminFormService.shouldUpdateFormField( - MOCK_FORM, - MOCK_VERIFIABLE_MOBILE_FIELD, - ) - - // Assert - expect(actual._unsafeUnwrapErr()).toBe(expectedError) - }) - }) - }) describe('createTwilioCredentials', () => { const MOCK_FORM_ID = new mongoose.Types.ObjectId() const MOCK_ADMIN_ID = new mongoose.Types.ObjectId() diff --git a/src/app/modules/verification/__tests__/verification.service.spec.ts b/src/app/modules/verification/__tests__/verification.service.spec.ts index abe076848a..086636df37 100644 --- a/src/app/modules/verification/__tests__/verification.service.spec.ts +++ b/src/app/modules/verification/__tests__/verification.service.spec.ts @@ -18,7 +18,6 @@ import dbHandler from '__tests__/unit/backend/helpers/jest-db' import { jsonParseStringify } from '__tests__/unit/backend/helpers/serialize-data' import { PAYMENT_CONTACT_FIELD_ID } from 'shared/constants' -import { smsConfig } from 'src/app/config/features/sms.config' import formsgSdk from 'src/app/config/formsg-sdk' import * as FormService from 'src/app/modules/form/form.service' import { @@ -29,28 +28,16 @@ import { OtpRetryExceededError, WrongOtpError, } from 'src/app/modules/verification/verification.errors' -import { - MailGenerationError, - MailSendError, -} from 'src/app/services/mail/mail.errors' +import { MailSendError } from 'src/app/services/mail/mail.errors' import MailService from 'src/app/services/mail/mail.service' import { SmsSendError } from 'src/app/services/sms/sms.errors' import { SmsFactory } from 'src/app/services/sms/sms.factory' -import * as SmsService from 'src/app/services/sms/sms.service' import * as HashUtils from 'src/app/utils/hash' -import { - IFormSchema, - IPopulatedForm, - IVerificationSchema, - UpdateFieldData, -} from 'src/types' +import { IFormSchema, IVerificationSchema, UpdateFieldData } from 'src/types' import { BasicField } from '../../../../../shared/types' -import { SMS_WARNING_TIERS } from '../../../../../shared/utils/verification' import { DatabaseError } from '../../core/core.errors' -import * as AdminFormService from '../../form/admin-form/admin-form.service' import { FormNotFoundError } from '../../form/form.errors' -import * as FormUtils from '../../form/form.utils' import { FieldNotFoundInTransactionError, TransactionExpiredError, @@ -1152,226 +1139,6 @@ describe('Verification service', () => { }) }) - describe('disableVerifiedFieldsIfRequired', () => { - const MOCK_FORM = { - title: 'some mock form', - _id: new ObjectId(), - admin: { - _id: new ObjectId(), - }, - permissionList: [{ email: 'some@user.gov.sg' }], - } as IPopulatedForm - let mobileTransaction: IVerificationSchema - const EMAIL_FIELD = generateFieldParams({ fieldType: BasicField.Email }) - const MOBILE_FIELD = generateFieldParams({ fieldType: BasicField.Mobile }) - const onboardSpy = jest.spyOn(FormUtils, 'isFormOnboarded') - const retrievalSpy = jest.spyOn(SmsService, 'retrieveFreeSmsCounts') - const disableSpy = jest.spyOn( - AdminFormService, - 'disableSmsVerificationsForUser', - ) - - beforeEach(async () => { - mobileTransaction = await VerificationModel.create({ - formId: MOCK_FORM._id, - fields: [MOBILE_FIELD], - // Expire 1 hour in future - expireAt: addHours(new Date(), 1), - }) - }) - - it('should not do anything when the transaction is not for a BasicField.Mobile field', async () => { - // Arrange - const nonMobileTransaction = await VerificationModel.create({ - formId: MOCK_FORM._id, - fields: [EMAIL_FIELD], - // Expire 1 hour in future - expireAt: addHours(new Date(), 1), - }) - - // Act - const actualResult = - await VerificationService.disableVerifiedFieldsIfRequired( - MOCK_FORM, - nonMobileTransaction, - EMAIL_FIELD._id, - ) - - // Assert - expect(actualResult._unsafeUnwrap()).toEqual(false) - expect(retrievalSpy).not.toHaveBeenCalled() - }) - - it('should not do anything when the form is onboarded even with Mobile field', async () => { - // Arrange - onboardSpy.mockReturnValueOnce(true) - - // Act - const actualResult = - await VerificationService.disableVerifiedFieldsIfRequired( - MOCK_FORM, - mobileTransaction, - MOBILE_FIELD._id, - ) - - // Assert - expect(actualResult._unsafeUnwrap()).toBe(false) - expect(retrievalSpy).not.toHaveBeenCalled() - }) - - it('should disable sms verifications and send email when sms limit is exceeded', async () => { - // Arrange - MockMailService.sendSmsVerificationDisabledEmail.mockReturnValueOnce( - okAsync(true), - ) - - disableSpy.mockReturnValueOnce(okAsync(true)) - retrievalSpy.mockReturnValueOnce( - okAsync(smsConfig.smsVerificationLimit + 1), - ) - - // Act - const actualResult = - await VerificationService.disableVerifiedFieldsIfRequired( - MOCK_FORM, - mobileTransaction, - MOBILE_FIELD._id, - ) - - // Assert - expect(actualResult._unsafeUnwrap()).toBe(true) - expect( - MockMailService.sendSmsVerificationDisabledEmail, - ).toHaveBeenCalledWith(MOCK_FORM) - // NOTE: String casting is required so that the test recognises them as equal - expect(disableSpy).toHaveBeenCalledWith(String(MOCK_FORM.admin._id)) - }) - - it('should send a warning when the admin has sent out a certain number of sms', async () => { - // Arrange - MockMailService.sendSmsVerificationWarningEmail.mockReturnValueOnce( - okAsync(true), - ) - retrievalSpy.mockReturnValueOnce(okAsync(SMS_WARNING_TIERS.LOW)) - - // Act - const actualResult = - await VerificationService.disableVerifiedFieldsIfRequired( - MOCK_FORM, - mobileTransaction, - MOBILE_FIELD._id, - ) - // Assert - expect(actualResult._unsafeUnwrap()).toBe(true) - expect(disableSpy).not.toHaveBeenCalled() - expect( - MockMailService.sendSmsVerificationWarningEmail, - ).toHaveBeenCalledWith(MOCK_FORM, SMS_WARNING_TIERS.LOW) - }) - - it('should not do anything when the sms sent by admin is not at any limit', async () => { - // Arrange - retrievalSpy.mockReturnValueOnce(okAsync(SMS_WARNING_TIERS.LOW - 1)) - - // Act - const actualResult = - await VerificationService.disableVerifiedFieldsIfRequired( - MOCK_FORM, - mobileTransaction, - MOBILE_FIELD._id, - ) - // Assert - expect(actualResult._unsafeUnwrap()).toBe(false) - expect( - MockMailService.sendSmsVerificationDisabledEmail, - ).not.toHaveBeenCalled() - expect( - MockMailService.sendSmsVerificationWarningEmail, - ).not.toHaveBeenCalled() - expect(disableSpy).not.toHaveBeenCalled() - }) - - it('should log any errors encountered during warning mail sending', async () => { - // Arrange - const expected = new MailGenerationError('big ded') - MockMailService.sendSmsVerificationWarningEmail.mockReturnValueOnce( - errAsync(expected), - ) - retrievalSpy.mockReturnValueOnce(okAsync(SMS_WARNING_TIERS.LOW)) - - // Act - const actualResult = - await VerificationService.disableVerifiedFieldsIfRequired( - MOCK_FORM, - mobileTransaction, - MOBILE_FIELD._id, - ) - // Assert - expect(actualResult._unsafeUnwrapErr()).toBe(undefined) - expect(disableSpy).not.toHaveBeenCalled() - expect( - MockMailService.sendSmsVerificationWarningEmail, - ).toHaveBeenCalledWith(MOCK_FORM, SMS_WARNING_TIERS.LOW) - expect(mockLogger.error).toHaveBeenCalledWith( - expect.objectContaining({ error: expected }), - ) - }) - - it('should propagate any errors encountered during disabled mail sending', async () => { - // Arrange - const expected = new MailGenerationError('big ded') - MockMailService.sendSmsVerificationDisabledEmail.mockReturnValueOnce( - errAsync(expected), - ) - retrievalSpy.mockReturnValueOnce( - okAsync(smsConfig.smsVerificationLimit + 1), - ) - - // Act - const actualResult = - await VerificationService.disableVerifiedFieldsIfRequired( - MOCK_FORM, - mobileTransaction, - MOBILE_FIELD._id, - ) - - // Assert - expect(disableSpy).not.toHaveBeenCalled() - expect( - MockMailService.sendSmsVerificationWarningEmail, - ).not.toHaveBeenCalled() - expect(actualResult._unsafeUnwrapErr()).toBe(undefined) - expect( - MockMailService.sendSmsVerificationDisabledEmail, - ).toHaveBeenCalledWith(MOCK_FORM) - expect(mockLogger.error).toHaveBeenCalledWith( - expect.objectContaining({ error: expected }), - ) - }) - - it('should log the error received when retrieval of sms counts fails', async () => { - // Arrange - const expected = new DatabaseError() - onboardSpy.mockReturnValueOnce(false) - retrievalSpy.mockReturnValueOnce(errAsync(expected)) - - // Act - const actualResult = - await VerificationService.disableVerifiedFieldsIfRequired( - MOCK_FORM, - mobileTransaction, - MOBILE_FIELD._id, - ) - - // Assert - expect(actualResult._unsafeUnwrapErr()).toBe(undefined) - expect(mockLogger.error).toHaveBeenCalledWith( - expect.objectContaining({ error: expected }), - ) - expect(retrievalSpy).toHaveBeenCalledWith(String(MOCK_FORM.admin._id)) - }) - }) - describe('shouldGenerateMobileOtp', () => { it('should return true when the fieldId is valid and verifiable', async () => { // Arrange