Skip to content

Commit

Permalink
ref: migrate get single encrypt submission flow to TypeScript (#670)
Browse files Browse the repository at this point in the history
* feat(EncryptedSubModel): add findEncryptedSubmissionById static method

* test(EncryptSubModel): add tests for findEncryptedSubmissionById

* feat: update getMongoErrorMessage to accept unknown type

* feat(EncryptSubSvc): add getEncryptedSubmissionData service fn

* feat(EncryptSubSvc): add transformAttachmentMetasToSignedUrls fn

* test(EncryptSubSvc): add tests for getEncryptedSubmissionData

* test(EncryptSubSvc): add test for transformAttachmentMetasToSignedUrls

* feat: add logging when error occurs in handleStreamEncryptedResponses

* feat(EncryptSubSvc): allow undefined attachmentMeta when transforming

* feat(EncryptSubCtl): add handleGetEncryptedResponse

* ref(AdminFormsRoutes): replace route handler and remove unused fn

* test(EncryptSubCtl: add tests for handleGetEncryptedResponse
  • Loading branch information
karrui authored Nov 23, 2020
1 parent 178331e commit 6668580
Show file tree
Hide file tree
Showing 12 changed files with 674 additions and 84 deletions.
66 changes: 0 additions & 66 deletions src/app/controllers/encrypt-submissions.server.controller.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
'use strict'
const crypto = require('crypto')
const moment = require('moment-timezone')
const { StatusCodes } = require('http-status-codes')

const mongoose = require('mongoose')
const errorHandler = require('../utils/handle-mongo-error')
const {
getEncryptSubmissionModel,
} = require('../models/submission.server.model')
const getSubmissionModel = require('../models/submission.server.model').default
const Submission = getSubmissionModel(mongoose)
const EncryptSubmission = getEncryptSubmissionModel(mongoose)

const { checkIsEncryptedEncoding } = require('../utils/encryption')
Expand Down Expand Up @@ -268,66 +265,3 @@ exports.getMetadata = function (req, res) {
})
}
}

/**
* Return actual encrypted form responses matching submission id
* @param {Object} req - Express request object
* @param {String} req.query.submissionId - submission to return data for
* @param {Object} req.form - the form
* @param {Object} res - Express response object
*/
exports.getEncryptedResponse = function (req, res) {
let { submissionId } = req.query || {}

Submission.findOne(
{
form: req.form._id,
_id: submissionId,
submissionType: 'encryptSubmission',
},
{
encryptedContent: 1,
verifiedContent: 1,
attachmentMetadata: 1,
created: 1,
},
).exec(async (err, response) => {
if (err || !response) {
logger.error({
message: 'Failure retrieving encrypted submission from database',
meta: {
action: 'getEncryptedResponse',
...createReqMeta(req),
},
error: err,
})
return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
message: errorHandler.getMongoErrorMessage(err),
})
} else {
const entry = {
refNo: response._id,
submissionTime: moment(response.created)
.tz('Asia/Singapore')
.format('ddd, D MMM YYYY, hh:mm:ss A'),
content: response.encryptedContent,
verified: response.verifiedContent,
}
// make sure client obtains S3 presigned URLs to download attachments
if (response.attachmentMetadata) {
const attachmentMetadata = {}
for (let [key, objectPath] of response.attachmentMetadata) {
attachmentMetadata[key] = await s3.getSignedUrlPromise('getObject', {
Bucket: attachmentS3Bucket,
Key: objectPath,
Expires: req.session.cookie.maxAge / 1000, // Remaining login duration in seconds
})
}
entry.attachmentMetadata = attachmentMetadata
} else {
entry.attachmentMetadata = {}
}
return res.json(entry)
}
})
}
19 changes: 19 additions & 0 deletions src/app/models/submission.server.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,25 @@ const getSubmissionCursorByFormId: IEncryptSubmissionModel['getSubmissionCursorB

EncryptSubmissionSchema.statics.getSubmissionCursorByFormId = getSubmissionCursorByFormId

EncryptSubmissionSchema.statics.findEncryptedSubmissionById = function (
this: IEncryptSubmissionModel,
formId: string,
submissionId: string,
) {
return this.findOne({
_id: submissionId,
form: formId,
submissionType: SubmissionType.Encrypt,
})
.select({
encryptedContent: 1,
verifiedContent: 1,
attachmentMetadata: 1,
created: 1,
})
.exec()
}

const compileSubmissionModel = (db: Mongoose): ISubmissionModel => {
const Submission = db.model('Submission', SubmissionSchema)
Submission.discriminator(SubmissionType.Email, EmailSubmissionSchema)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { errAsync, okAsync } from 'neverthrow'
import { mocked } from 'ts-jest/utils'

import { DatabaseError } from 'src/app/modules/core/core.errors'
import { CreatePresignedUrlError } from 'src/app/modules/form/admin-form/admin-form.errors'
import { SubmissionData } from 'src/types'

import expressHandler from 'tests/unit/backend/helpers/jest-express'

import { SubmissionNotFoundError } from '../../submission.errors'
import { handleGetEncryptedResponse } from '../encrypt-submission.controller'
import * as EncryptSubmissionService from '../encrypt-submission.service'

jest.mock('../encrypt-submission.service')
const MockEncryptSubService = mocked(EncryptSubmissionService)

describe('encrypt-submission.controller', () => {
describe('handleGetEncryptedResponse', () => {
const MOCK_REQ = expressHandler.mockRequest({
params: { formId: 'mockFormId' },
query: { submissionId: 'mockSubmissionId' },
session: {
cookie: {
maxAge: 20000,
},
},
})

it('should return 200 with encrypted response', async () => {
// Arrange
const mockSubData: SubmissionData = {
_id: 'some id',
encryptedContent: 'some encrypted content',
verifiedContent: 'some verified content',
created: new Date('2020-10-10'),
} as SubmissionData
const mockSignedUrls = {
someKey1: 'some-signed-url',
someKey2: 'another-signed-url',
}
const mockRes = expressHandler.mockResponse()

// Mock service responses.
MockEncryptSubService.getEncryptedSubmissionData.mockReturnValueOnce(
okAsync(mockSubData),
)
MockEncryptSubService.transformAttachmentMetasToSignedUrls.mockReturnValueOnce(
okAsync(mockSignedUrls),
)

// Act
await handleGetEncryptedResponse(MOCK_REQ, mockRes, jest.fn())

// Assert
const expected = {
refNo: mockSubData._id,
submissionTime: 'Sat, 10 Oct 2020, 08:00:00 AM',
content: mockSubData.encryptedContent,
verified: mockSubData.verifiedContent,
attachmentMetadata: mockSignedUrls,
}
expect(mockRes.json).toHaveBeenCalledWith(expected)
})

it('should return 404 when submissionId cannot be found in the database', async () => {
// Arrange
const mockErrorString = 'not found'
MockEncryptSubService.getEncryptedSubmissionData.mockReturnValueOnce(
errAsync(new SubmissionNotFoundError(mockErrorString)),
)
const mockRes = expressHandler.mockResponse()

// Act
await handleGetEncryptedResponse(MOCK_REQ, mockRes, jest.fn())

// Assert
expect(mockRes.status).toHaveBeenCalledWith(404)
expect(mockRes.json).toHaveBeenCalledWith({ message: mockErrorString })
})

it('should return 500 when database error occurs', async () => {
// Arrange
const mockErrorString = 'database error occurred'
MockEncryptSubService.getEncryptedSubmissionData.mockReturnValueOnce(
errAsync(new DatabaseError(mockErrorString)),
)
const mockRes = expressHandler.mockResponse()

// Act
await handleGetEncryptedResponse(MOCK_REQ, mockRes, jest.fn())

// Assert
expect(mockRes.status).toHaveBeenCalledWith(500)
expect(mockRes.json).toHaveBeenCalledWith({ message: mockErrorString })
})

it('should return 500 when error occurs when generating presigned URLs', async () => {
// Arrange
const mockErrorString = 'presigned url error occured'
MockEncryptSubService.getEncryptedSubmissionData.mockReturnValueOnce(
okAsync({} as SubmissionData),
)
MockEncryptSubService.transformAttachmentMetasToSignedUrls.mockReturnValueOnce(
errAsync(new CreatePresignedUrlError(mockErrorString)),
)

const mockRes = expressHandler.mockResponse()

// Act
await handleGetEncryptedResponse(MOCK_REQ, mockRes, jest.fn())

// Assert
expect(mockRes.status).toHaveBeenCalledWith(500)
expect(mockRes.json).toHaveBeenCalledWith({ message: mockErrorString })
})
})
})
Loading

0 comments on commit 6668580

Please sign in to comment.