Skip to content

Commit

Permalink
refactor: validateAndProcessEncryptSubmission to typescript (#887)
Browse files Browse the repository at this point in the history
* chore: use correct type for validators

* refactor: migrate validateAndProcessEncryptSubmission to ts

* refactor: update tests
  • Loading branch information
tshuli authored Dec 15, 2020
1 parent ef5d6c6 commit afc73ab
Show file tree
Hide file tree
Showing 18 changed files with 131 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export interface EmailDataForOneField {
// When a response has been formatted for email, all answerArray
// should have been converted to answer
interface IResponseFormattedForEmail extends IBaseResponse {
question: string
fieldType: BasicField
answer: string
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { RequestHandler } from 'express'
import { SetOptional } from 'type-fest'

import { createReqMeta } from '../../../../app/utils/request'
import { createLoggerWithLabel } from '../../../../config/logger'
import { FieldResponse, WithForm, WithParsedResponses } from '../../../../types'
import { checkIsEncryptedEncoding } from '../../../utils/encryption'
import { getProcessedResponses } from '../submission.service'

import { mapRouteError } from './encrypt-submission.utils'

const logger = createLoggerWithLabel(module)

type EncryptSubmissionBody = {
responses: FieldResponse[]
encryptedContent: string
attachments?: {
encryptedFile?: {
binary: string
nonce: string
submissionPublicKey: string
}
}
isPreview: boolean
version: number
}

export const validateAndProcessEncryptSubmission: RequestHandler<
{ formId: string },
unknown,
EncryptSubmissionBody
> = (req, res, next) => {
const { form } = req as WithForm<typeof req>
const { encryptedContent, responses } = req.body

// Step 1: Check whether submitted encryption is valid.
return (
checkIsEncryptedEncoding(encryptedContent)
// Step 2: Encryption is valid, process given responses.
.andThen(() => getProcessedResponses(form, responses))
// If pass, then set parsedResponses and delete responses.
.map((processedResponses) => {
// eslint-disable-next-line @typescript-eslint/no-extra-semi
;(req.body as WithParsedResponses<
typeof req.body
>).parsedResponses = processedResponses
// Prevent downstream functions from using responses by deleting it.
delete (req.body as SetOptional<EncryptSubmissionBody, 'responses'>)
.responses
return next()
})
// If error, log and return res error.
.mapErr((error) => {
logger.error({
message: 'Error validating encrypt submission responses',
meta: {
action: 'validateEncryptSubmission',
...createReqMeta(req),
formId: form._id,
},
error,
})

const { statusCode, errorMessage } = mapRouteError(error)
return res.status(statusCode).json({
message: errorMessage,
})
})
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ import { createLoggerWithLabel } from '../../../../config/logger'
import { MapRouteError } from '../../../../types/routing'
import { DatabaseError, MalformedParametersError } from '../../core/core.errors'
import { CreatePresignedUrlError } from '../../form/admin-form/admin-form.errors'
import { SubmissionNotFoundError } from '../submission.errors'
import {
ConflictError,
InvalidEncodingError,
ProcessingError,
SubmissionNotFoundError,
ValidateFieldError,
} from '../submission.errors'

const logger = createLoggerWithLabel(module)

Expand All @@ -25,6 +31,25 @@ export const mapRouteError: MapRouteError = (error) => {
statusCode: StatusCodes.NOT_FOUND,
errorMessage: error.message,
}
case InvalidEncodingError:
return {
statusCode: StatusCodes.BAD_REQUEST,
errorMessage:
'Invalid data was found. Please check your responses and submit again.',
}
case ValidateFieldError:
case ProcessingError:
return {
statusCode: StatusCodes.BAD_REQUEST,
errorMessage:
'There is something wrong with your form submission. Please check your responses and try again. If the problem persists, please refresh the page.',
}
case ConflictError:
return {
statusCode: StatusCodes.CONFLICT,
errorMessage:
'The form has been updated. Please refresh and submit again.',
}
case CreatePresignedUrlError:
case DatabaseError:
return {
Expand Down
1 change: 1 addition & 0 deletions src/app/modules/submission/submission.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
} from 'src/types/response'

export type ProcessedResponse = {
question: string
isVisible?: boolean
isUserVerified?: boolean
}
Expand Down
5 changes: 3 additions & 2 deletions src/app/routes/admin-forms.server.routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const EncryptSubmissionController = require('../modules/submission/encrypt-submi
const {
PermissionLevel,
} = require('../modules/form/admin-form/admin-form.types')
const EncryptSubmissionMiddleware = require('../modules/submission/encrypt-submission/encrypt-submission.middleware')
const SpcpController = require('../modules/spcp/spcp.controller')
const { BasicField, ResponseMode } = require('../../types')

Expand Down Expand Up @@ -476,8 +477,6 @@ module.exports = function (app) {
* @security OTP
*/
app.route('/v2/submissions/encrypt/preview/:formId([a-fA-F0-9]{24})').post(
authActiveForm(PermissionLevel.Read),
encryptSubmissions.validateEncryptSubmission,
celebrate({
[Segments.BODY]: Joi.object({
responses: Joi.array()
Expand Down Expand Up @@ -522,6 +521,8 @@ module.exports = function (app) {
version: Joi.number().required(),
}),
}),
authActiveForm(PermissionLevel.Read),
EncryptSubmissionMiddleware.validateAndProcessEncryptSubmission,
AdminFormController.passThroughSpcp,
submissions.injectAutoReplyInfo,
webhookVerifiedContentFactory.encryptedVerifiedFields,
Expand Down
2 changes: 1 addition & 1 deletion src/app/utils/encryption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { InvalidEncodingError } from '../modules/submission/submission.errors'

export const checkIsEncryptedEncoding = (
encryptedStr: string,
): Result<boolean, Error> => {
): Result<true, InvalidEncodingError> => {
// TODO (#42): Remove this type check once whole backend is in TypeScript.
if (typeof encryptedStr !== 'string') {
return err(new InvalidEncodingError('encryptedStr is not of type `string`'))
Expand Down
4 changes: 2 additions & 2 deletions src/app/utils/field-validation/validators/common.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { left, right } from 'fp-ts/lib/Either'

import { ProcessedSingleAnswerResponse } from 'src/app/modules/submission/submission.types'
import { ResponseValidator } from 'src/types/field/utils/validation'
import { ISingleAnswerResponse } from 'src/types/response'

export const notEmptySingleAnswerResponse: ResponseValidator<ISingleAnswerResponse> = (
export const notEmptySingleAnswerResponse: ResponseValidator<ProcessedSingleAnswerResponse> = (
response,
) => {
if (response.answer.trim().length === 0)
Expand Down
4 changes: 2 additions & 2 deletions src/app/utils/field-validation/validators/dateValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ import { chain, left, right } from 'fp-ts/lib/Either'
import { flow } from 'fp-ts/lib/function'
import moment from 'moment-timezone'

import { ProcessedSingleAnswerResponse } from 'src/app/modules/submission/submission.types'
import { IDateField } from 'src/types/field'
import { ResponseValidator } from 'src/types/field/utils/validation'
import { ISingleAnswerResponse } from 'src/types/response'

import { DateSelectedValidation } from '../../../../shared/constants'

import { notEmptySingleAnswerResponse } from './common'

type DateValidator = ResponseValidator<ISingleAnswerResponse>
type DateValidator = ResponseValidator<ProcessedSingleAnswerResponse>
type DateValidatorConstructor = (dateField: IDateField) => DateValidator

/**
Expand Down
4 changes: 2 additions & 2 deletions src/app/utils/field-validation/validators/decimalValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import { flow } from 'fp-ts/lib/function'
import isFloat from 'validator/lib/isFloat'
import isInt from 'validator/lib/isInt'

import { ProcessedSingleAnswerResponse } from 'src/app/modules/submission/submission.types'
import { IDecimalField } from 'src/types/field'
import { ResponseValidator } from 'src/types/field/utils/validation'
import { ISingleAnswerResponse } from 'src/types/response'

import { notEmptySingleAnswerResponse } from './common'

type DecimalValidator = ResponseValidator<ISingleAnswerResponse>
type DecimalValidator = ResponseValidator<ProcessedSingleAnswerResponse>
type DecimalValidatorConstructor = (
decimalField: IDecimalField,
) => DecimalValidator
Expand Down
4 changes: 2 additions & 2 deletions src/app/utils/field-validation/validators/homeNoValidator.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { chain, left, right } from 'fp-ts/lib/Either'
import { flow } from 'fp-ts/lib/function'

import { ProcessedSingleAnswerResponse } from 'src/app/modules/submission/submission.types'
import { IHomenoField } from 'src/types/field'
import { ResponseValidator } from 'src/types/field/utils/validation'
import { ISingleAnswerResponse } from 'src/types/response'

import {
isHomePhoneNumber,
Expand All @@ -12,7 +12,7 @@ import {

import { notEmptySingleAnswerResponse } from './common'

type HomeNoValidator = ResponseValidator<ISingleAnswerResponse>
type HomeNoValidator = ResponseValidator<ProcessedSingleAnswerResponse>
type HomeNoValidatorConstructor = (
homeNumberField: IHomenoField,
) => HomeNoValidator
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { chain, left, right } from 'fp-ts/lib/Either'
import { flow } from 'fp-ts/lib/function'

import { ProcessedSingleAnswerResponse } from 'src/app/modules/submission/submission.types'
import { IMobileField } from 'src/types/field'
import { ResponseValidator } from 'src/types/field/utils/validation'
import { ISingleAnswerResponse } from 'src/types/response'

import {
isMobilePhoneNumber,
Expand All @@ -12,7 +12,7 @@ import {

import { notEmptySingleAnswerResponse } from './common'

type MobileNoValidator = ResponseValidator<ISingleAnswerResponse>
type MobileNoValidator = ResponseValidator<ProcessedSingleAnswerResponse>
type MobileNoValidatorConstructor = (
mobileNumberField: IMobileField,
) => MobileNoValidator
Expand Down
4 changes: 2 additions & 2 deletions src/app/utils/field-validation/validators/nricValidator.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { chain, left, right } from 'fp-ts/lib/Either'
import { flow } from 'fp-ts/lib/function'

import { ProcessedSingleAnswerResponse } from 'src/app/modules/submission/submission.types'
import { ResponseValidator } from 'src/types/field/utils/validation'
import { ISingleAnswerResponse } from 'src/types/response'

import { isNricValid } from '../../../../shared/util/nric-validation'

import { notEmptySingleAnswerResponse } from './common'

type NricValidator = ResponseValidator<ISingleAnswerResponse>
type NricValidator = ResponseValidator<ProcessedSingleAnswerResponse>
type NricValidatorConstructor = () => NricValidator

const nricValidator: NricValidator = (response) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { chain, left, right } from 'fp-ts/lib/Either'
import { flow } from 'fp-ts/lib/function'

import { ProcessedSingleAnswerResponse } from 'src/app/modules/submission/submission.types'
import { IRadioField } from 'src/types/field'
import { ResponseValidator } from 'src/types/field/utils/validation'
import { ISingleAnswerResponse } from 'src/types/response'

import { notEmptySingleAnswerResponse } from './common'
import { isOneOfOptions, isOtherOption } from './options'

type RadioButtonValidator = ResponseValidator<ISingleAnswerResponse>
type RadioButtonValidator = ResponseValidator<ProcessedSingleAnswerResponse>
type RadioButtonValidatorConstructor = (
radioButtonField: IRadioField,
) => RadioButtonValidator
Expand Down
4 changes: 2 additions & 2 deletions src/app/utils/field-validation/validators/ratingValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import { chain, left, right } from 'fp-ts/lib/Either'
import { flow } from 'fp-ts/lib/function'
import isInt from 'validator/lib/isInt'

import { ProcessedSingleAnswerResponse } from 'src/app/modules/submission/submission.types'
import { IRatingField } from 'src/types/field'
import { ResponseValidator } from 'src/types/field/utils/validation'
import { ISingleAnswerResponse } from 'src/types/response'

import { notEmptySingleAnswerResponse } from './common'

type RatingValidator = ResponseValidator<ISingleAnswerResponse>
type RatingValidator = ResponseValidator<ProcessedSingleAnswerResponse>
type RatingValidatorConstructor = (ratingField: IRatingField) => RatingValidator

const makeRatingLimitsValidator: RatingValidatorConstructor = (ratingField) => (
Expand Down
4 changes: 2 additions & 2 deletions src/app/utils/field-validation/validators/textValidator.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { chain, left, right } from 'fp-ts/lib/Either'
import { flow } from 'fp-ts/lib/function'

import { ProcessedSingleAnswerResponse } from 'src/app/modules/submission/submission.types'
import { ILongTextField, IShortTextField } from 'src/types/field'
import { ResponseValidator } from 'src/types/field/utils/validation'
import { ISingleAnswerResponse } from 'src/types/response'

import { TextSelectedValidation } from '../../../../types/field/baseField'

import { notEmptySingleAnswerResponse } from './common'

type TextFieldValidatorConstructor = (
textField: IShortTextField | ILongTextField,
) => ResponseValidator<ISingleAnswerResponse>
) => ResponseValidator<ProcessedSingleAnswerResponse>

const minLengthValidator: TextFieldValidatorConstructor = (textField) => (
response,
Expand Down
6 changes: 6 additions & 0 deletions src/types/express.locals.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
// TODO (#42): remove these types when migrating away from middleware pattern

import { ProcessedFieldResponse } from '../app/modules/submission/submission.types'

import { IPopulatedForm } from './form'
import { SpcpSession } from './spcp'

export type WithForm<T> = T & {
form: IPopulatedForm
}

export type WithParsedResponses<T> = T & {
parsedResponses: ProcessedFieldResponse[]
}

export type ResWithSpcpSession<T> = T & {
locals: { spcpSession?: SpcpSession }
}
Expand Down
3 changes: 2 additions & 1 deletion src/types/response/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ export type AttachmentsMap = Record<IFieldSchema['_id'], File>
export interface IBaseResponse {
_id: IFieldSchema['_id']
fieldType: BasicField
question: string
myInfo?: IMyInfo
// Signature exists for verifiable fields if the answer is verified.
signature?: string
}

export interface ISingleAnswerResponse extends IBaseResponse {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const express = require('express')
const request = require('supertest')

const dbHandler = require('../helpers/db-handler')
const EncryptSubmissionsMiddleware = require('../../../../dist/backend/app/modules/submission/encrypt-submission/encrypt-submission.middleware')

const User = dbHandler.makeModel('user.server.model', 'User')
const Agency = dbHandler.makeModel('agency.server.model', 'Agency')
Expand Down Expand Up @@ -141,15 +142,15 @@ describe('Encrypt Submissions Controller', () => {
})
}

describe('validateEncryptSubmission', () => {
describe('validateAndProcessEncryptSubmission', () => {
const app = express()

beforeAll(() => {
app
.route(endpointPath)
.post(
injectFixtures,
Controller.validateEncryptSubmission,
EncryptSubmissionsMiddleware.validateAndProcessEncryptSubmission,
sendSubmissionBack,
)
})
Expand Down

0 comments on commit afc73ab

Please sign in to comment.