Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: use neverthrow for exceptions #634

Merged
merged 24 commits into from
Nov 23, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
656bd26
refactor: neverthrow for checkIsEncryptedEncoding
tshuli Nov 23, 2020
c174935
refactor: remove try-catch for checkIsEncryptedEncoding
tshuli Nov 23, 2020
d282f6e
fix: add .value to access neverthrown Result
tshuli Nov 23, 2020
a97d120
fix: add check for nonceEncrypted
tshuli Nov 23, 2020
3962131
chore: define InvalidEncodingError error class, rename, include full …
tshuli Nov 18, 2020
d9a7a09
chore: remove convert try-catch for getProcessedResponses to neverthr…
tshuli Nov 18, 2020
ef7e00d
refactor: neverthrow for submission service
tshuli Nov 18, 2020
23dad3c
refactor: neverthrow for validateField
tshuli Nov 18, 2020
3d75d32
chore: update tests for validateField
tshuli Nov 18, 2020
00a2526
chore: limit Error class
tshuli Nov 18, 2020
dafb5ee
chore: update validateEmailSubmission in email submissions server con…
tshuli Nov 18, 2020
88227a0
fix: return error from neverthrow error object
tshuli Nov 18, 2020
4eddd4e
chore: add ValidateFieldError class
tshuli Nov 18, 2020
4cf9bf7
fix: ConflictError params
tshuli Nov 18, 2020
8e423e6
chore: update tests for submission.service.spec
tshuli Nov 18, 2020
6ba62c8
chore: remove unnecessary else
tshuli Nov 18, 2020
7d2d00b
refactor: check for err instead of ok
tshuli Nov 18, 2020
de26317
fix: res.sendStatus if no jsosn
tshuli Nov 18, 2020
5810072
docs: update error class JSDocs
tshuli Nov 18, 2020
6ec61c8
docs: update comments
tshuli Nov 18, 2020
7778008
refactor: return true instead of undefined if no error
tshuli Nov 18, 2020
6b25cb4
chore: clean up submission.service tests
tshuli Nov 18, 2020
cfdcc3f
chore: clean up tests
tshuli Nov 18, 2020
9e21a7d
chore: update tests
tshuli Nov 23, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 36 additions & 35 deletions src/app/controllers/email-submissions.server.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -233,44 +233,45 @@ exports.receiveEmailSubmissionUsingBusBoy = function (req, res, next) {
exports.validateEmailSubmission = function (req, res, next) {
const { form } = req

if (req.body.responses) {
try {
req.body.parsedResponses = getProcessedResponses(form, req.body.responses)
delete req.body.responses // Prevent downstream functions from using responses by deleting it
} catch (err) {
logger.error({
message:
err instanceof ConflictError
? 'Conflict - Form has been updated'
: 'Error processing responses',
meta: {
action: 'validateEmailSubmission',
...createReqMeta(req),
formId: req.form._id,
},
error: err,
if (!req.body.responses) {
return res.sendStatus(StatusCodes.BAD_REQUEST)
}

const getProcessedResponsesResult = getProcessedResponses(
form,
req.body.responses,
)

if (getProcessedResponsesResult.isErr()) {
const err = getProcessedResponsesResult.error
logger.error({
message:
err instanceof ConflictError
? 'Conflict - Form has been updated'
: 'Error processing responses',
meta: {
action: 'validateEmailSubmission',
...createReqMeta(req),
formId: req.form._id,
},
error: err,
})
if (err instanceof ConflictError) {
return res.status(err.status).json({
message: 'The form has been updated. Please refresh and submit again.',
})
if (err instanceof ConflictError) {
return res.status(err.status).json({
message:
'The form has been updated. Please refresh and submit again.',
})
} else {
return res.status(StatusCodes.BAD_REQUEST).json({
message:
'There is something wrong with your form submission. Please check your responses and try again. If the problem persists, please refresh the page.',
})
}
}

// Creates an array of attachments from the validated responses
req.attachments = mapAttachmentsFromParsedResponses(
req.body.parsedResponses,
)
return next()
} else {
return res.sendStatus(StatusCodes.BAD_REQUEST)
return res.status(StatusCodes.BAD_REQUEST).json({
message:
'There is something wrong with your form submission. Please check your responses and try again. If the problem persists, please refresh the page.',
})
}

req.body.parsedResponses = getProcessedResponsesResult.value
delete req.body.responses // Prevent downstream functions from using responses by deleting it
// Creates an array of attachments from the validated responses
req.attachments = mapAttachmentsFromParsedResponses(req.body.parsedResponses)
return next()
}

/**
Expand Down
65 changes: 33 additions & 32 deletions src/app/controllers/encrypt-submissions.server.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,54 +32,55 @@ const HttpStatus = require('http-status-codes')
*/
exports.validateEncryptSubmission = function (req, res, next) {
const { form } = req
try {
// Check if the encrypted content is base64
checkIsEncryptedEncoding(req.body.encryptedContent)
} catch (error) {

const isEncryptedResult = checkIsEncryptedEncoding(req.body.encryptedContent)
if (isEncryptedResult.isErr()) {
logger.error({
message: 'Invalid encryption',
meta: {
action: 'validateEncryptSubmission',
...createReqMeta(req),
formId: form._id,
},
error,
error: isEncryptedResult.error,
})
return res
.status(StatusCodes.BAD_REQUEST)
.json({ message: 'Invalid data was found. Please submit again.' })
}

if (req.body.responses) {
try {
req.body.parsedResponses = getProcessedResponses(form, req.body.responses)
delete req.body.responses // Prevent downstream functions from using responses by deleting it
} catch (err) {
logger.error({
message: 'Error processing responses',
meta: {
action: 'validateEncryptSubmission',
...createReqMeta(req),
formId: form._id,
},
error: err,
if (!req.body.responses) {
return res.sendStatus(StatusCodes.BAD_REQUEST)
}

const getProcessedResponsesResult = getProcessedResponses(
form,
req.body.responses,
)
if (getProcessedResponsesResult.isErr()) {
const err = getProcessedResponsesResult.error
logger.error({
message: 'Error processing responses',
meta: {
action: 'validateEncryptSubmission',
...createReqMeta(req),
formId: form._id,
},
error: err,
})
if (err instanceof ConflictError) {
return res.status(err.status).json({
message: 'The form has been updated. Please refresh and submit again.',
})
if (err instanceof ConflictError) {
return res.status(err.status).json({
message:
'The form has been updated. Please refresh and submit again.',
})
} else {
return res.status(StatusCodes.BAD_REQUEST).json({
message:
'There is something wrong with your form submission. Please check your responses and try again. If the problem persists, please refresh the page.',
})
}
}
return next()
} else {
return res.sendStatus(StatusCodes.BAD_REQUEST)
return res.status(StatusCodes.BAD_REQUEST).json({
message:
'There is something wrong with your form submission. Please check your responses and try again. If the problem persists, please refresh the page.',
})
}
req.body.parsedResponses = getProcessedResponsesResult.value
delete req.body.responses // Prevent downstream functions from using responses by deleting it
return next()
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ import {

import dbHandler from 'tests/unit/backend/helpers/jest-db'

import {
ConflictError,
ProcessingError,
ValidateFieldError,
} from '../../submission.errors'

const Form = getFormModel(mongoose)
const Submission = getSubmissionModel(mongoose)

Expand Down Expand Up @@ -126,7 +132,7 @@ describe('submission.service', () => {
updatedResponses[emailFieldIndex] = newEmailResponse

// Act
const actual = SubmissionService.getProcessedResponses(
const actualResult = SubmissionService.getProcessedResponses(
defaultEncryptForm,
updatedResponses,
)
Expand All @@ -137,7 +143,8 @@ describe('submission.service', () => {
{ ...newMobileResponse, isVisible: true },
]
// Should only have email and mobile fields for encrypted forms.
expect(actual).toEqual(expectedParsed)
expect(actualResult.isOk()).toEqual(true)
expect(actualResult._unsafeUnwrap()).toEqual(expectedParsed)
})

it('should return list of parsed responses for email form submission successfully', async () => {
Expand All @@ -160,7 +167,7 @@ describe('submission.service', () => {
updatedResponses[decimalFieldIndex] = newDecimalResponse

// Act
const actual = SubmissionService.getProcessedResponses(
const actualResult = SubmissionService.getProcessedResponses(
defaultEmailForm,
updatedResponses,
)
Expand All @@ -184,10 +191,11 @@ describe('submission.service', () => {
expectedParsed.push(expectedProcessed)
})

expect(actual).toEqual(expectedParsed)
expect(actualResult.isOk()).toEqual(true)
expect(actualResult._unsafeUnwrap()).toEqual(expectedParsed)
})

it('should throw error when email form has more fields than responses', async () => {
it('should return error when email form has more fields than responses', async () => {
// Arrange
const extraFieldForm = cloneDeep(defaultEmailForm)
const secondMobileField = cloneDeep(
Expand All @@ -197,15 +205,19 @@ describe('submission.service', () => {
extraFieldForm.form_fields!.push(secondMobileField)

// Act + Assert
expect(() =>
SubmissionService.getProcessedResponses(
extraFieldForm,
defaultEmailResponses,
),
).toThrowError('Some form fields are missing')

const actualResult = SubmissionService.getProcessedResponses(
extraFieldForm,
defaultEmailResponses,
)

expect(actualResult.isErr()).toEqual(true)
expect(actualResult._unsafeUnwrapErr()).toEqual(
new ConflictError('Some form fields are missing'),
)
})

it('should throw error when encrypt form has more fields than responses', async () => {
it('should return error when encrypt form has more fields than responses', async () => {
// Arrange
const extraFieldForm = cloneDeep(defaultEncryptForm)
const secondMobileField = cloneDeep(
Expand All @@ -215,15 +227,19 @@ describe('submission.service', () => {
extraFieldForm.form_fields!.push(secondMobileField)

// Act + Assert
expect(() =>
SubmissionService.getProcessedResponses(
extraFieldForm,
defaultEncryptResponses,
),
).toThrowError('Some form fields are missing')

const actualResult = SubmissionService.getProcessedResponses(
extraFieldForm,
defaultEncryptResponses,
)

expect(actualResult.isErr()).toEqual(true)
expect(actualResult._unsafeUnwrapErr()).toEqual(
new ConflictError('Some form fields are missing'),
)
})

it('should throw error when any responses are not valid for encrypted form submission', async () => {
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.
Expand All @@ -233,31 +249,39 @@ describe('submission.service', () => {
requireMobileEncryptForm.form_fields![mobileFieldIndex].required = true

// Act + Assert
expect(() =>
SubmissionService.getProcessedResponses(
requireMobileEncryptForm,
defaultEncryptResponses,
),
).toThrowError('Invalid answer submitted')

const actualResult = SubmissionService.getProcessedResponses(
requireMobileEncryptForm,
defaultEncryptResponses,
)

expect(actualResult.isErr()).toEqual(true)
expect(actualResult._unsafeUnwrapErr()).toEqual(
new ValidateFieldError('Invalid answer submitted'),
)
})

it('should throw error when any responses are not valid for email form submission', async () => {
it('should return error when any responses are not valid for email form submission', async () => {
// Arrange
// Set NRIC field in form as required.
const nricFieldIndex = TYPE_TO_INDEX_MAP[BasicField.Nric]
const requireNricEmailForm = cloneDeep(defaultEmailForm)
requireNricEmailForm.form_fields![nricFieldIndex].required = true

// Act + Assert
expect(() =>
SubmissionService.getProcessedResponses(
requireNricEmailForm,
defaultEmailResponses,
),
).toThrowError('Invalid answer submitted')

const actualResult = SubmissionService.getProcessedResponses(
requireNricEmailForm,
defaultEmailResponses,
)

expect(actualResult.isErr()).toEqual(true)
expect(actualResult._unsafeUnwrapErr()).toEqual(
new ValidateFieldError('Invalid answer submitted'),
)
})

it('should throw error when encrypted form submission is prevented by logic', async () => {
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
Expand All @@ -270,15 +294,19 @@ describe('submission.service', () => {
} as unknown) as IPreventSubmitLogicSchema)

// Act + Assert
expect(() =>
SubmissionService.getProcessedResponses(
defaultEncryptForm,
defaultEncryptResponses,
),
).toThrowError('Submission prevented by form logic')

const actualResult = SubmissionService.getProcessedResponses(
defaultEncryptForm,
defaultEncryptResponses,
)

expect(actualResult.isErr()).toEqual(true)
expect(actualResult._unsafeUnwrapErr()).toEqual(
new ProcessingError('Submission prevented by form logic'),
)
})

it('should throw error when email form submission is prevented by logic', async () => {
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 = ({
Expand All @@ -293,12 +321,16 @@ describe('submission.service', () => {
.mockReturnValueOnce(mockReturnLogicUnit)

// Act + Assert
expect(() =>
SubmissionService.getProcessedResponses(
defaultEmailForm,
defaultEmailResponses,
),
).toThrowError('Submission prevented by form logic')

const actualResult = SubmissionService.getProcessedResponses(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same for the rest of the tests here

defaultEmailForm,
defaultEmailResponses,
)

expect(actualResult.isErr()).toEqual(true)
expect(actualResult._unsafeUnwrapErr()).toEqual(
new ProcessingError('Submission prevented by form logic'),
)
})
})

Expand Down
Loading