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

Test: publicform integration tests #1572

Merged
merged 5 commits into from
Apr 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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({
Copy link
Contributor

Choose a reason for hiding this comment

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

noticed you fixed a regression here. in future it would be good to separate this kind of change into a different PR which can be reviewed with greater urgency. if you name your PR as you did here, other devs may (rightly) assume (as I did) that this PR only contains tests.

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 {
Copy link
Contributor

Choose a reason for hiding this comment

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

it doesn't make a difference but for the record, it wasn't necessary to change this to an interface, since you can use & to achieve the same thing

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