Skip to content

Commit

Permalink
fix: enable pdf attachment for encrypt mode forms (#7523)
Browse files Browse the repository at this point in the history
* fix: enable pdf attachment for encrypt mode forms

* fix: remove unnecessary import

* fix: fix wrong condition

* fix: remove unnecessary comment

* fix: fix failing test case

* fix: do not toggle off if payment enabled form

* fix: remove includeFormSummary watch and remove useMemo

* docs: add comment for why isVisible is stripped out

* tests: add jest test for includeFormSummary toggle on mrf

* tests: should not send pdf response if encrypt form with payment activated

* fix: remove focused jest tests

* fix: update mail service to check for active payment field

* fix: update mail service to not render email if active payment field

* fix: remove unused var

* tests: add tests for autoReplyData

* fix: should parse responses using the form def

* tests: add test to ensure isVisible is still present

* Revert "fix: should parse responses using the form def"

This reverts commit 1762b25.
  • Loading branch information
g-tejas authored Aug 5, 2024
1 parent 914e1ba commit 10f4d36
Show file tree
Hide file tree
Showing 12 changed files with 338 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { RegisterOptions } from 'react-hook-form'
import { Box, FormControl, useMergeRefs } from '@chakra-ui/react'
import { extend, pick } from 'lodash'

import { PaymentChannel } from '~shared/types'
import { EmailFieldBase } from '~shared/types/field'
import { FormResponseMode } from '~shared/types/form'
import { validateEmailDomains } from '~shared/utils/email-domain-validation'
Expand Down Expand Up @@ -83,7 +84,6 @@ export const EditEmail = ({ field }: EditEmailProps): JSX.Element => {

const watchedHasAllowedEmailDomains = watch('hasAllowedEmailDomains')
const watchedHasAutoReply = watch('autoReplyOptions.hasAutoReply')
const includeFormSummary = watch('autoReplyOptions.includeFormSummary')

const requiredValidationRule = useMemo(
() => createBaseValidationRules({ required: true }),
Expand Down Expand Up @@ -130,26 +130,23 @@ export const EditEmail = ({ field }: EditEmailProps): JSX.Element => {
)

const { data: form } = useCreateTabForm()
const isPdfResponseEnabled = useMemo(
() =>
form?.responseMode === FormResponseMode.Email ||
(form?.responseMode === FormResponseMode.Encrypt && includeFormSummary),
[form, includeFormSummary],
)

const pdfResponseToggleDescription = useMemo(() => {
if (!isPdfResponseEnabled) {
return 'For security reasons, PDF responses are not included in email confirmations for Storage mode forms'
}
}, [isPdfResponseEnabled])
const isEncryptMode = form?.responseMode === FormResponseMode.Encrypt
const isPaymentDisabledForm =
isEncryptMode &&
form.payments_channel.channel === PaymentChannel.Unconnected

const isPdfResponseEnabled =
form?.responseMode === FormResponseMode.Email || isPaymentDisabledForm

const pdfResponseToggleDescription = isPdfResponseEnabled
? undefined
: 'PDF responses are not supported for encrypt forms with active payment fields'

// email confirmation is not supported on MRF
const isToggleEmailConfirmationDisabled = useMemo(
() =>
form?.responseMode === FormResponseMode.Multirespondent &&
!field.autoReplyOptions.hasAutoReply,
[field.autoReplyOptions.hasAutoReply, form?.responseMode],
)
const isToggleEmailConfirmationDisabled =
form?.responseMode === FormResponseMode.Multirespondent &&
!field.autoReplyOptions.hasAutoReply

return (
<CreatePageDrawerContentContainer>
Expand Down
29 changes: 29 additions & 0 deletions src/app/models/field/__tests__/emailField.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,33 @@ describe('models.fields.emailField', () => {
})
expect(actual.field.toObject()).toEqual(expected)
})

it('should set includeFormSummary to false on ResponseMode.Multirespondent forms', async () => {
// Arrange
const mockEmailField = {
autoReplyOptions: {
hasAutoReply: true,
autoReplySubject: 'some subject',
autoReplySender: 'some sender',
autoReplyMessage: 'This is a test message',
// Set includeFormSummary to true.
includeFormSummary: true,
},
}
// Act
const actual = await MockParent.create({
responseMode: FormResponseMode.Multirespondent,
field: mockEmailField,
})

// Assert
const expected = merge(EMAIL_FIELD_DEFAULTS, mockEmailField, {
_id: expect.anything(),
autoReplyOptions: {
// Should be always set to false for MRF forms
includeFormSummary: false,
},
})
expect(actual.field.toObject()).toEqual(expected)
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,56 @@ describe('email-submission.util', () => {
])
})

it('should include undefined isVisible fields from autoreply data', async () => {
// Arrange
const response = generateNewSingleAnswerResponse(BasicField.ShortText, {
isVisible: undefined,
})

// Assert
const emailData = new SubmissionEmailObj(
[response],
new Set(),
FormAuthType.NIL,
)

// Assert
expect(emailData.dataCollationData).toEqual([
generateSingleAnswerJson(response),
])
expect(emailData.autoReplyData).toEqual([
generateSingleAnswerAutoreply(response),
])
expect(emailData.formData).toEqual([
generateSingleAnswerFormData(response),
])
})

it('should include isVisible true fields from autoreply data', async () => {
// Arrange
const response = generateNewSingleAnswerResponse(BasicField.ShortText, {
isVisible: true,
})

// Assert
const emailData = new SubmissionEmailObj(
[response],
new Set(),
FormAuthType.NIL,
)

// Assert
expect(emailData.dataCollationData).toEqual([
generateSingleAnswerJson(response),
])
expect(emailData.autoReplyData).toEqual([
generateSingleAnswerAutoreply(response),
])
expect(emailData.formData).toEqual([
generateSingleAnswerFormData(response),
])
})

it('should generate table answers with [table] prefix in form and JSON data', () => {
const response = generateNewTableResponse()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -542,7 +542,7 @@ const getAutoReplyFormattedResponse = (
const { question, answer, isVisible } = response
const answerSplitByNewLine = answer.split('\n')
// Auto reply email will contain only visible fields
if (isVisible) {
if (isVisible !== false) {
return {
question, // No prefixes for autoreply
answerTemplate: answerSplitByNewLine,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { ObjectId } from 'bson'
import { merge } from 'lodash'
import mongoose from 'mongoose'
import { ok, okAsync } from 'neverthrow'
import { FormAuthType, MyInfoAttribute } from 'shared/types'
import { BasicField, FormAuthType, MyInfoAttribute } from 'shared/types'

import { getEncryptSubmissionModel } from 'src/app/models/submission.server.model'
import * as OidcService from 'src/app/modules/spcp/spcp.oidc.service/index'
Expand All @@ -19,6 +19,8 @@ import MailService from 'src/app/services/mail/mail.service'
import { FormFieldSchema, IPopulatedEncryptedForm } from 'src/types'
import { EncryptSubmissionDto, FormCompleteDto } from 'src/types/api'

import { SubmissionEmailObj } from '../../email-submission/email-submission.util'
import { ProcessedFieldResponse } from '../../submission.types'
import {
generateHashedSubmitterId,
getCookieNameByAuthType,
Expand Down Expand Up @@ -756,4 +758,78 @@ describe('encrypt-submission.controller', () => {
).toEqual(MOCK_MASKED_NRIC)
})
})

describe('emailData', () => {
it('should have the isVisible field set to true for form fields', async () => {
// Arrange
const performEncryptPostSubmissionActionsSpy = jest.spyOn(
EncryptSubmissionService,
'performEncryptPostSubmissionActions',
)
const mockFormId = new ObjectId()
const mockEncryptForm = {
_id: mockFormId,
title: 'some form',
authType: FormAuthType.NIL,
isNricMaskEnabled: false,
form_fields: [
{
_id: new ObjectId(),
fieldType: BasicField.ShortText,
title: 'Long answer',
description: '',
required: false,
disabled: false,
},
] as FormFieldSchema[],
emails: ['[email protected]'],
getUniqueMyInfoAttrs: () => [] as MyInfoAttribute[],
} as IPopulatedEncryptedForm

const mockResponses = [
{
_id: new ObjectId(),
question: 'Long answer',
answer: 'this is an answer',
fieldType: 'textarea',
isVisible: true,
},
]

const mockReq = merge(
expressHandler.mockRequest({
params: { formId: 'some id' },
body: {
responses: mockResponses,
},
}),
{
formsg: {
encryptedPayload: {
encryptedContent: 'encryptedContent',
version: 1,
},
formDef: {},
encryptedFormDef: mockEncryptForm,
} as unknown as EncryptSubmissionDto,
} as unknown as FormCompleteDto,
) as unknown as SubmitEncryptModeFormHandlerRequest
const mockRes = expressHandler.mockResponse()

// Setup the SubmissionEmailObj
const emailData = new SubmissionEmailObj(
mockResponses as any as ProcessedFieldResponse[],
new Set(),
FormAuthType.NIL,
)

// Act
await submitEncryptModeFormForTest(mockReq, mockRes)

// Assert
expect(performEncryptPostSubmissionActionsSpy.mock.calls[0][2]).toEqual(
emailData,
)
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
} from '../../../../../shared/types'
import { maskNric } from '../../../../../shared/utils/nric-mask'
import {
IAttachmentInfo,
IEncryptedForm,
IEncryptedSubmissionSchema,
IPopulatedEncryptedForm,
Expand Down Expand Up @@ -393,6 +394,7 @@ const submitEncryptModeForm = async (
formId,
form,
responses: req.formsg.filteredResponses,
unencryptedAttachments: req.formsg.unencryptedAttachments,
emailFields: parsedResponses.getAllResponses(),
responseMetadata,
submissionContent,
Expand Down Expand Up @@ -656,12 +658,14 @@ const _createSubmission = async ({
form,
responseMetadata,
responses,
unencryptedAttachments,
emailFields,
}: {
req: Parameters<SubmitEncryptModeFormHandlerType>[0]
res: Parameters<SubmitEncryptModeFormHandlerType>[1]
responseMetadata: EncryptSubmissionDto['responseMetadata']
responses: ParsedClearFormFieldResponse[]
unencryptedAttachments?: IAttachmentInfo[]
emailFields: ProcessedFieldResponse[]
formId: string
form: IPopulatedEncryptedForm
Expand Down Expand Up @@ -775,7 +779,12 @@ const _createSubmission = async ({
timestamp: createdTime.getTime(),
})

return await performEncryptPostSubmissionActions(submission, responses)
return await performEncryptPostSubmissionActions(
submission,
responses,
emailData,
unencryptedAttachments,
)
}

export const handleStorageSubmission = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,7 @@ export const validateStorageSubmission = async (
.map((parsedResponses) => {
const responses = [] as EncryptFormFieldResponse[]
for (const response of parsedResponses.getAllResponses()) {
// `isVisible` is being stripped out here. Why: https://github.com/opengovsg/FormSG/pull/6907
if (response.isVisible) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { isVisible: _, ...rest } = response
Expand Down Expand Up @@ -463,6 +464,21 @@ export const encryptSubmission = async (
req.body.version,
)

// Autoreplies are sent after the submission has been saved in the DB,
// but attachments are stripped here. To ensure that users receive their
// attachments in the autoreply we keep the attachments in req.formsg
if (req.formsg) {
req.formsg.unencryptedAttachments = req.body.responses
.filter(isAttachmentResponse)
.map((response) => {
return {
filename: response.filename,
content: response.content,
fieldId: response._id,
}
})
}

const strippedBodyResponses = req.body.responses.map((response) => {
if (isAttachmentResponse(response)) {
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from '../../../../../shared/types'
import {
FieldResponse,
IAttachmentInfo,
IEncryptedSubmissionSchema,
IPopulatedEncryptedForm,
IPopulatedForm,
Expand All @@ -25,6 +26,7 @@ import {
WebhookValidationError,
} from '../../webhook/webhook.errors'
import { WebhookFactory } from '../../webhook/webhook.factory'
import { SubmissionEmailObj } from '../email-submission/email-submission.util'
import {
ResponseModeError,
SendEmailConfirmationError,
Expand Down Expand Up @@ -141,6 +143,8 @@ export const createEncryptSubmissionWithoutSave = ({
export const performEncryptPostSubmissionActions = (
submission: IEncryptedSubmissionSchema,
responses: FieldResponse[],
emailData?: SubmissionEmailObj,
attachments?: IAttachmentInfo[],
): ResultAsync<
true,
| FormNotFoundError
Expand Down Expand Up @@ -171,6 +175,8 @@ export const performEncryptPostSubmissionActions = (
return sendEmailConfirmations({
form,
submission,
attachments,
responsesData: emailData?.autoReplyData,
recipientData: extractEmailConfirmationData(
responses,
form.form_fields,
Expand Down
Loading

0 comments on commit 10f4d36

Please sign in to comment.