Skip to content

Commit

Permalink
refactor: encapsulate parsed responses (part 1) (#1140)
Browse files Browse the repository at this point in the history
* refactor: define EmailDataObj class

* refactor: use lodash compact

* docs: improve comments

* chore: shift business logic of returning masked autoreplydata to class

* chore: update tests

* chore: update types for response emails

* chore: rename jsonData to dataCollationData

* refactor: unnest compact

* refactor: convert maskField to generic maskStringHead function

* refactor: separate checks in tests

* chore: add defensive check that charsToReveal is nonnegative

* refactor: simplify use of state and run maskUidOnLastField only if necessary

Co-authored-by: Yuan Ruo <[email protected]>

* style: lint

* refactor: use type instead of interface, constraint generic T on createFormattedDataForOneField

* refactor: avoid state

* build: resolve merge conflicts

* doc: remove comment

* docs: createFormattedDataForOneField

* chore: rename Submission Email Object, add unit tests

Co-authored-by: Yuan Ruo <[email protected]>
  • Loading branch information
tshuli and liangyuanruo authored Mar 8, 2021
1 parent 7f68e9c commit 147e5ff
Show file tree
Hide file tree
Showing 14 changed files with 604 additions and 330 deletions.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,25 @@ import { readFileSync } from 'fs'
import { cloneDeep, merge } from 'lodash'

import {
AuthType,
BasicField,
EmailAutoReplyField,
FieldResponse,
IAttachmentResponse,
ISingleAnswerResponse,
SPCPFieldTitle,
} from 'src/types'

import { ProcessedFieldResponse } from '../../submission.types'
import { createEmailData } from '../email-submission.service'
import { ResponseFormattedForEmail } from '../email-submission.types'
import {
addAttachmentToResponses,
areAttachmentsMoreThan7MB,
getFormDataPrefixedQuestion,
getInvalidFileExtensions,
getJsonPrefixedQuestion,
handleDuplicatesInAttachments,
mapAttachmentsFromResponses,
maskUidOnLastField,
} from '../email-submission.util'

const validSingleFile = {
Expand Down Expand Up @@ -305,30 +309,120 @@ describe('email-submission.util', () => {
})
})

describe('maskUidOnLastField', () => {
it('should mask UID on SPCPFieldTitle.CpUid if it is the last field of autoReplyData', () => {
const autoReplyData: EmailAutoReplyField[] = [
{ question: 'question 1', answerTemplate: ['answer1'] },
{ question: SPCPFieldTitle.CpUid, answerTemplate: ['S1234567A'] },
describe('Submission Email Object', () => {
const response1 = getResponse(
String(new ObjectId()),
MOCK_ANSWER,
) as ProcessedFieldResponse

const response2 = getResponse(
String(new ObjectId()),
'',
) as ProcessedFieldResponse

response1.fieldType = BasicField.YesNo
response2.fieldType = BasicField.YesNo
response1.isVisible = true
response2.isVisible = false

const hashedFields = new Set([
new ObjectId().toHexString(),
new ObjectId().toHexString(),
])
const authType = AuthType.NIL
const submissionEmailObj = createEmailData(
[response1, response2],
hashedFields,
authType,
)

it('should return the response in correct json format when dataCollationData() method is called', () => {
const correctJson = [
{
question: getJsonPrefixedQuestion(
response1 as ResponseFormattedForEmail,
),
answer: (response1 as ResponseFormattedForEmail).answer,
},
{
question: getJsonPrefixedQuestion(
response2 as ResponseFormattedForEmail,
),
answer: (response2 as ResponseFormattedForEmail).answer,
},
]
const maskedAutoReplyData: EmailAutoReplyField[] = [
{ question: 'question 1', answerTemplate: ['answer1'] },
{ question: SPCPFieldTitle.CpUid, answerTemplate: ['*****567A'] },
expect(submissionEmailObj.dataCollationData).toEqual(correctJson)
})

it('should return the response in correct admin response format when formData() method is called', () => {
const correctFormData = [
{
question: getFormDataPrefixedQuestion(
response1 as ResponseFormattedForEmail,
hashedFields,
),
answerTemplate: (response1 as ResponseFormattedForEmail).answer.split(
'\n',
),
answer: (response1 as ResponseFormattedForEmail).answer,
fieldType: response1.fieldType,
},
{
question: getFormDataPrefixedQuestion(
response2 as ResponseFormattedForEmail,
hashedFields,
),
answerTemplate: (response2 as ResponseFormattedForEmail).answer.split(
'\n',
),
answer: (response2 as ResponseFormattedForEmail).answer,
fieldType: response2.fieldType,
},
]
expect(maskUidOnLastField(autoReplyData)).toEqual(maskedAutoReplyData)
expect(submissionEmailObj.formData).toEqual(correctFormData)
})
it('should not mask UID on form field with same name as SPCPFieldTitle.CpUid if it is not the last field of autoReplyData', () => {
const autoReplyData: EmailAutoReplyField[] = [
{ question: 'question 1', answerTemplate: ['answer1'] },
{ question: 'CorpPass Validated UID', answerTemplate: ['S9999999Z'] },
{ question: SPCPFieldTitle.CpUid, answerTemplate: ['S1234567A'] },

it('should return the response in correct email confirmation format when autoReplyData() method is called', () => {
const correctConfirmation = [
{
question: response1.question,
answerTemplate: (response1 as ResponseFormattedForEmail).answer.split(
'\n',
),
},
// Note that response2 is not shown in Email Confirmation as isVisible is false
]
const maskedAutoReplyData: EmailAutoReplyField[] = [
{ question: 'question 1', answerTemplate: ['answer1'] },
{ question: 'CorpPass Validated UID', answerTemplate: ['S9999999Z'] },
{ question: SPCPFieldTitle.CpUid, answerTemplate: ['*****567A'] },
expect(submissionEmailObj.autoReplyData).toEqual(correctConfirmation)
})

it('should mask corppass UID if AuthType is Corppass and autoReplyData() method is called', () => {
const responseCPUID = getResponse(
String(new ObjectId()),
'S1234567A',
) as ProcessedFieldResponse

responseCPUID.question = SPCPFieldTitle.CpUid
responseCPUID.isVisible = true

const submissionEmailObjCP = createEmailData(
[response1, response2, responseCPUID],
hashedFields,
AuthType.CP,
)
const correctConfirmation = [
{
question: response1.question,
answerTemplate: (response1 as ResponseFormattedForEmail).answer.split(
'\n',
),
},
// Note that response2 is not shown in Email Confirmation as isVisible is false
{
question: responseCPUID.question,
answerTemplate: ['*****567A'],
},
]
expect(maskUidOnLastField(autoReplyData)).toEqual(maskedAutoReplyData)
expect(submissionEmailObjCP.autoReplyData).toEqual(correctConfirmation)
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import * as EmailSubmissionService from './email-submission.service'
import {
mapAttachmentsFromResponses,
mapRouteError,
maskUidOnLastField,
} from './email-submission.util'

const logger = createLoggerWithLabel(module)
Expand Down Expand Up @@ -189,6 +188,7 @@ export const handleEmailSubmission: RequestHandler<
const emailData = EmailSubmissionService.createEmailData(
parsedResponses,
hashedFields,
authType,
)

// Save submission to database
Expand Down Expand Up @@ -226,7 +226,7 @@ export const handleEmailSubmission: RequestHandler<
form,
submission,
attachments,
jsonData: emailData.jsonData,
dataCollationData: emailData.dataCollationData,
formData: emailData.formData,
},
)
Expand Down Expand Up @@ -259,10 +259,7 @@ export const handleEmailSubmission: RequestHandler<
parsedResponses,
submission,
attachments,
autoReplyData:
authType === AuthType.CP
? maskUidOnLastField(emailData.autoReplyData)
: emailData.autoReplyData,
autoReplyData: emailData.autoReplyData,
}).mapErr((error) => {
logger.error({
message: 'Error while sending email confirmations',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,12 @@ export const prepareEmailSubmission: RequestHandler<
(res as ResWithHashedFields<typeof res>).locals.hashedFields || new Set()
let emailData: EmailData
// TODO (#847): remove when we are sure of the shape of responses
const { form } = req as WithForm<typeof req>
try {
emailData = EmailSubmissionService.createEmailData(
req.body.parsedResponses,
hashedFields,
form.authType,
)
} catch (error) {
logger.error({
Expand Down Expand Up @@ -83,7 +85,8 @@ export const prepareEmailSubmission: RequestHandler<
}
// eslint-disable-next-line @typescript-eslint/no-extra-semi
;(req as WithEmailData<typeof req>).autoReplyData = emailData.autoReplyData
;(req as WithEmailData<typeof req>).jsonData = emailData.jsonData
;(req as WithEmailData<typeof req>).dataCollationData =
emailData.dataCollationData
;(req as WithEmailData<typeof req>).formData = emailData.formData
return next()
}
Expand Down Expand Up @@ -182,7 +185,7 @@ export const validateEmailSubmission: RequestHandler<
* @param req - Express request object
* @param req.form - form object from req
* @param req.formData - the submission for the form
* @param req.jsonData - data to be included in JSON section of email
* @param req.dataCollationData - data to be included in JSON section of email
* @param req.submission - submission which was saved to database
* @param req.attachments - submitted attachments, parsed by
* exports.receiveSubmission
Expand All @@ -198,7 +201,7 @@ export const sendAdminEmail: RequestHandler<
const {
form,
formData,
jsonData,
dataCollationData,
submission,
attachments,
} = req as WithAdminEmailData<typeof req>
Expand All @@ -220,7 +223,7 @@ export const sendAdminEmail: RequestHandler<
form,
submission,
attachments,
jsonData,
dataCollationData,
formData,
})
.map(() => next())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import { err, errAsync, ok, okAsync, Result, ResultAsync } from 'neverthrow'

import { createLoggerWithLabel } from '../../../../config/logger'
import {
AuthType,
BasicField,
EmailAdminDataField,
EmailData,
EmailDataForOneField,
EmailFormField,
FieldResponse,
IAttachmentInfo,
IEmailSubmissionSchema,
Expand All @@ -19,10 +19,6 @@ import {
} from '../../../../types'
import { getEmailSubmissionModel } from '../../../models/submission.server.model'
import MailService from '../../../services/mail/mail.service'
import {
isProcessedCheckboxResponse,
isProcessedTableResponse,
} from '../../../utils/field-validation/field-validation.guards'
import { DatabaseError } from '../../core/core.errors'
import { transformEmails } from '../../form/form.utils'
import { ResponseModeError, SendAdminEmailError } from '../submission.errors'
Expand All @@ -44,39 +40,14 @@ import { SubmissionHash } from './email-submission.types'
import {
areAttachmentsMoreThan7MB,
concatAttachmentsAndResponses,
getAnswerForCheckbox,
getAnswerRowsForTable,
getFormattedResponse,
getInvalidFileExtensions,
mapAttachmentsFromResponses,
SubmissionEmailObj,
} from './email-submission.util'

const EmailSubmissionModel = getEmailSubmissionModel(mongoose)
const logger = createLoggerWithLabel(module)

/**
* Creates response and autoreply email data for a single response.
* Helper function for createEmailData.
* @param response Processed and validated response for one field
* @param hashedFields IDs of fields whose responses have been verified by MyInfo hashes
* @returns email data for one form field
*/
const createEmailDataForOneField = (
response: ProcessedFieldResponse,
hashedFields: Set<string>,
): EmailDataForOneField[] => {
if (isProcessedTableResponse(response)) {
return getAnswerRowsForTable(response).map((row) =>
getFormattedResponse(row, hashedFields),
)
} else if (isProcessedCheckboxResponse(response)) {
const checkbox = getAnswerForCheckbox(response)
return [getFormattedResponse(checkbox, hashedFields)]
} else {
return [getFormattedResponse(response, hashedFields)]
}
}

/**
* Creates data to be included in the response and autoreply emails.
* @param parsedResponses Processed and validated responses
Expand All @@ -86,33 +57,9 @@ const createEmailDataForOneField = (
export const createEmailData = (
parsedResponses: ProcessedFieldResponse[],
hashedFields: Set<string>,
authType: AuthType = AuthType.NIL,
): EmailData => {
// First, get an array of email data for each response
// Each field has an array of email data to accommodate table fields,
// which have multiple rows of data per field. Hence flatten and maintain
// the order of responses.
return (
parsedResponses
.flatMap((response) => createEmailDataForOneField(response, hashedFields))
// Then reshape such that autoReplyData, jsonData and formData are each arrays
.reduce<EmailData>(
(acc, dataForOneField) => {
if (dataForOneField.autoReplyData) {
acc.autoReplyData.push(dataForOneField.autoReplyData)
}
if (dataForOneField.jsonData) {
acc.jsonData.push(dataForOneField.jsonData)
}
acc.formData.push(dataForOneField.formData)
return acc
},
{
autoReplyData: [],
jsonData: [],
formData: [],
},
)
)
return new SubmissionEmailObj(parsedResponses, hashedFields, authType)
}

/**
Expand Down Expand Up @@ -169,7 +116,7 @@ export const validateAttachments = (
* @returns errAsync(ConcatSubmissionError) if error occurred while concatenating attachments
*/
export const hashSubmission = (
formData: EmailFormField[],
formData: EmailAdminDataField[],
attachments: IAttachmentInfo[],
): ResultAsync<SubmissionHash, SubmissionHashError | ConcatSubmissionError> => {
// TODO (#847): remove this try-catch when we are sure that the shape of formData is correct
Expand Down
Loading

0 comments on commit 147e5ff

Please sign in to comment.