Skip to content

Commit

Permalink
Test: publicform integration tests (#1572)
Browse files Browse the repository at this point in the history
* feat(helpers/jest-db): adds util methods to get/update form while testing

* test(public-form/routes): adds integration tests for public-form

* chore(helpers/jest-db): removed unused helper method

* fix(public-form/controller): fixed regression issues where certain fields were missed

* test(public-form): fixed tests so that privateFormError is checked correctly
  • Loading branch information
seaerchin authored Apr 12, 2021
1 parent 6748bba commit 6f6580e
Show file tree
Hide file tree
Showing 5 changed files with 312 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -952,10 +952,11 @@ describe('public-form.controller', () => {
// Arrange
// 1. Mock the response
const mockRes = expressHandler.mockResponse()
const MOCK_FORM_TITLE = 'private form'

// 2. Mock the call to retrieve the form
MockAuthService.getFormIfPublic.mockReturnValueOnce(
errAsync(new PrivateFormError(MOCK_ERROR_STRING, 'private form')),
errAsync(new PrivateFormError(MOCK_ERROR_STRING, MOCK_FORM_TITLE)),
)

// Act
Expand All @@ -977,6 +978,8 @@ describe('public-form.controller', () => {
expect(mockRes.status).toHaveBeenCalledWith(404)
expect(mockRes.json).toHaveBeenCalledWith({
message: MOCK_ERROR_STRING,
formTitle: MOCK_FORM_TITLE,
isPageFound: true,
})
})
})
Expand Down
280 changes: 280 additions & 0 deletions src/app/modules/form/public-form/__tests__/public-form.routes.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
import SPCPAuthClient from '@opengovsg/spcp-auth-client'
import { ObjectId } from 'bson-ext'
import { errAsync } from 'neverthrow'
import supertest, { Session } from 'supertest-session'
import { mocked } from 'ts-jest/utils'

import { DatabaseError } from 'src/app/modules/core/core.errors'
import { MYINFO_COOKIE_NAME } from 'src/app/modules/myinfo/myinfo.constants'
import { MyInfoCookieState } from 'src/app/modules/myinfo/myinfo.types'
import { AuthType, Status } from 'src/types'

import { setupApp } from 'tests/integration/helpers/express-setup'
import dbHandler from 'tests/unit/backend/helpers/jest-db'

import * as AuthService from '../../../auth/auth.service'
import { PublicFormRouter } from '../public-form.routes'

jest.mock('@opengovsg/myinfo-gov-client', () => ({
MyInfoGovClient: jest.fn().mockImplementation(() => ({
getPerson: jest.fn().mockReturnValue({ uinFin: 'S1234567A' }),
extractUinFin: jest.fn().mockReturnValue('S1234567A'),
})),
MyInfoMode: jest.requireActual('@opengovsg/myinfo-gov-client').MyInfoMode,
MyInfoSource: jest.requireActual('@opengovsg/myinfo-gov-client').MyInfoSource,
MyInfoAddressType: jest.requireActual('@opengovsg/myinfo-gov-client')
.MyInfoAddressType,
MyInfoAttribute: jest.requireActual('@opengovsg/myinfo-gov-client')
.MyInfoAttribute,
}))

jest.mock('@opengovsg/spcp-auth-client')
const MockSpcpAuthClient = mocked(SPCPAuthClient, true)

const app = setupApp('/', PublicFormRouter, {
setupWithAuth: false,
})

describe('public-form.routes', () => {
let request: Session
const mockSpClient = mocked(MockSpcpAuthClient.mock.instances[0], true)
const mockCpClient = mocked(MockSpcpAuthClient.mock.instances[1], true)

beforeAll(async () => await dbHandler.connect())
beforeEach(async () => {
request = supertest(app)
})
afterEach(async () => {
await dbHandler.clearDatabase()
jest.restoreAllMocks()
})
afterAll(async () => await dbHandler.closeDatabase())

describe('GET /:formId/publicform', () => {
const MOCK_COOKIE_PAYLOAD = {
userName: 'mock',
rememberMe: false,
}

it('should return 200 with public form when form has AuthType.NIL and valid formId', async () => {
// Arrange
const { form } = await dbHandler.insertEmailForm({
formOptions: { status: Status.Public },
})
// NOTE: This is needed to inject admin info into the form
const fullForm = await dbHandler.getFullFormById(form._id)
const expectedResponseBody = JSON.parse(
JSON.stringify({
form: fullForm.getPublicView(),
isIntranetUser: false,
}),
)

// Act
const actualResponse = await request.get(`/${form._id}/publicform`)

// Assert
expect(actualResponse.status).toEqual(200)
expect(actualResponse.body).toEqual(expectedResponseBody)
})

it('should return 200 with public form when form has AuthType.SP and valid formId', async () => {
// Arrange
mockSpClient.verifyJWT.mockImplementationOnce((_jwt, cb) =>
cb(null, {
userName: MOCK_COOKIE_PAYLOAD.userName,
}),
)
const { form } = await dbHandler.insertEmailForm({
formOptions: {
esrvcId: 'mockEsrvcId',
authType: AuthType.SP,
hasCaptcha: false,
status: Status.Public,
},
})
const formId = form._id
// NOTE: This is needed to inject admin info into the form
const fullForm = await dbHandler.getFullFormById(formId)
const expectedResponseBody = JSON.parse(
JSON.stringify({
form: fullForm?.getPublicView(),
spcpSession: { userName: MOCK_COOKIE_PAYLOAD.userName },
isIntranetUser: false,
}),
)

// Act
// Set cookie on request
const actualResponse = await request
.get(`/${formId}/publicform`)
.set('Cookie', ['jwtSp=mockJwt'])

// Assert
expect(actualResponse.status).toEqual(200)
expect(actualResponse.body).toEqual(expectedResponseBody)
})
it('should return 200 with public form when form has AuthType.CP and valid formId', async () => {
// Arrange
mockCpClient.verifyJWT.mockImplementationOnce((_jwt, cb) =>
cb(null, {
userName: MOCK_COOKIE_PAYLOAD.userName,
userInfo: 'MyCorpPassUEN',
}),
)
const { form } = await dbHandler.insertEmailForm({
formOptions: {
esrvcId: 'mockEsrvcId',
authType: AuthType.CP,
hasCaptcha: false,
status: Status.Public,
},
})
const formId = form._id
// NOTE: This is needed to inject admin info into the form
const fullForm = await dbHandler.getFullFormById(formId)
const expectedResponseBody = JSON.parse(
JSON.stringify({
form: fullForm?.getPublicView(),
spcpSession: { userName: MOCK_COOKIE_PAYLOAD.userName },
isIntranetUser: false,
}),
)

// Act
// Set cookie on request
const actualResponse = await request
.get(`/${formId}/publicform`)
.set('Cookie', ['jwtCp=mockJwt'])

// Assert
expect(actualResponse.status).toEqual(200)
expect(actualResponse.body).toEqual(expectedResponseBody)
})
it('should return 200 with public form when form has AuthType.MyInfo and valid formId', async () => {
// Arrange
const { form } = await dbHandler.insertEmailForm({
formOptions: {
esrvcId: 'mockEsrvcId',
authType: AuthType.MyInfo,
hasCaptcha: false,
status: Status.Public,
},
})
// NOTE: This is needed to inject admin info into the form
const fullForm = await dbHandler.getFullFormById(form._id)
const expectedResponseBody = JSON.parse(
JSON.stringify({
form: fullForm.getPublicView(),
spcpSession: { userName: 'S1234567A' },
isIntranetUser: false,
}),
)
const cookie = JSON.stringify({
accessToken: 'mockAccessToken',
usedCount: 0,
state: MyInfoCookieState.Success,
})

// Act
const actualResponse = await request
.get(`/${form._id}/publicform`)
.set('Cookie', [
// The j: indicates that the cookie is in JSON
`${MYINFO_COOKIE_NAME}=j:${encodeURIComponent(cookie)}`,
])

// Assert
expect(actualResponse.status).toEqual(200)
expect(actualResponse.body).toEqual(expectedResponseBody)
})

it('should return 404 if the form does not exist', async () => {
// Arrange
const cookie = JSON.stringify({
accessToken: 'mockAccessToken',
usedCount: 0,
state: MyInfoCookieState.Success,
})
const MOCK_FORM_ID = new ObjectId().toHexString()
const expectedResponseBody = JSON.parse(
JSON.stringify({
message: 'Form not found',
}),
)

// Act
const actualResponse = await request
.get(`/${MOCK_FORM_ID}/publicform`)
.set('Cookie', [
// The j: indicates that the cookie is in JSON
`${MYINFO_COOKIE_NAME}=j:${encodeURIComponent(cookie)}`,
])

// Assert
expect(actualResponse.status).toEqual(404)
expect(actualResponse.body).toEqual(expectedResponseBody)
})

it('should return 404 if the form is private', async () => {
// Arrange
const { form } = await dbHandler.insertEmailForm({
formOptions: { status: Status.Private },
})
const expectedResponseBody = JSON.parse(
JSON.stringify({
message: form.inactiveMessage,
formTitle: form.title,
isPageFound: true,
}),
)

// Act
const actualResponse = await request.get(`/${form._id}/publicform`)

// Assert
expect(actualResponse.status).toEqual(404)
expect(actualResponse.body).toEqual(expectedResponseBody)
})

it('should return 410 if the form has been archived', async () => {
// Arrange
const { form } = await dbHandler.insertEmailForm({
formOptions: { status: Status.Archived },
})
const expectedResponseBody = JSON.parse(
JSON.stringify({
message: 'Gone',
}),
)

// Act
const actualResponse = await request.get(`/${form._id}/publicform`)

// Assert
expect(actualResponse.status).toEqual(410)
expect(actualResponse.body).toEqual(expectedResponseBody)
})

it('should return 500 if a database error occurs', async () => {
// Arrange
const { form } = await dbHandler.insertEmailForm({
formOptions: { status: Status.Public },
})
const expectedError = new DatabaseError('all your base are belong to us')
const expectedResponseBody = JSON.parse(
JSON.stringify({ message: expectedError.message }),
)
jest
.spyOn(AuthService, 'getFormIfPublic')
.mockReturnValueOnce(errAsync(expectedError))

// Act
const actualResponse = await request.get(`/${form._id}/publicform`)

// Assert
expect(actualResponse.status).toEqual(500)
expect(actualResponse.body).toEqual(expectedResponseBody)
})
})
})
17 changes: 15 additions & 2 deletions src/app/modules/form/public-form/public-form.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import querystring from 'querystring'
import { UnreachableCaseError } from 'ts-essentials'

import { AuthType } from '../../../../types'
import { ErrorDto } from '../../../../types/api'
import { ErrorDto, PrivateFormErrorDto } from '../../../../types/api'
import { createLoggerWithLabel } from '../../../config/logger'
import { isMongoError } from '../../../utils/handle-mongo-error'
import { createReqMeta, getRequestIp } from '../../../utils/request'
Expand Down Expand Up @@ -188,7 +188,7 @@ export const handleRedirect: RequestHandler<
*/
export const handleGetPublicForm: RequestHandler<
{ formId: string },
PublicFormViewDto | ErrorDto
PublicFormViewDto | ErrorDto | PrivateFormErrorDto
> = async (req, res) => {
const { formId } = req.params
const logMeta = {
Expand All @@ -214,6 +214,19 @@ export const handleGetPublicForm: RequestHandler<
})
}
const { errorMessage, statusCode } = mapRouteError(error)

// Specialized error response for PrivateFormError.
// This is to maintain backwards compatibility with the middleware implementation
if (error instanceof PrivateFormError) {
return res.status(statusCode).json({
message: error.message,
// Flag to prevent default 404 subtext ("please check link") from
// showing.
isPageFound: true,
formTitle: error.formTitle,
})
}

return res.status(statusCode).json({ message: errorMessage })
}

Expand Down
7 changes: 6 additions & 1 deletion src/types/api/core.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
export type ErrorDto = {
export interface ErrorDto {
message: string
}

export interface PrivateFormErrorDto extends ErrorDto {
isPageFound: true
formTitle: string
}
8 changes: 7 additions & 1 deletion tests/unit/backend/helpers/jest-db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
IAgencySchema,
IEmailFormSchema,
IEncryptedFormSchema,
IPopulatedForm,
IUserSchema,
ResponseMode,
} from 'src/types'
Expand Down Expand Up @@ -153,7 +154,6 @@ const insertEmailForm = async ({
})

const EmailFormModel = getEmailFormModel(mongoose)

const form = await EmailFormModel.create({
title: 'example form title',
admin: user._id,
Expand Down Expand Up @@ -214,6 +214,11 @@ const insertEncryptForm = async ({
}
}

const getFullFormById = async (
formId: string,
): Promise<IPopulatedForm | null> =>
await getEmailFormModel(mongoose).getFullFormById(formId)

const dbHandler = {
connect,
closeDatabase,
Expand All @@ -224,6 +229,7 @@ const dbHandler = {
clearCollection,
insertEmailForm,
insertEncryptForm,
getFullFormById,
}

export default dbHandler

0 comments on commit 6f6580e

Please sign in to comment.