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: migrate GET /adminform endpoint to Typescript #575

Merged
merged 14 commits into from
Nov 9, 2020
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
34 changes: 0 additions & 34 deletions src/app/controllers/admin-forms.server.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -422,40 +422,6 @@ function makeModule(connection) {
}
})
},
/**
* List of all of user-created (and collaborated-on) forms
* @param {Object} req - Express request object
* @param {Object} res - Express response object
*/
list: function (req, res) {
let Form = getFormModel(connection)
// List forms when either the user is an admin or collaborator
let searchFields = [
{ 'permissionList.email': req.session.user.email },
{ admin: req.session.user },
]
let returnedFields = '_id title admin lastModified status form_fields'

Form.find({ $or: searchFields }, returnedFields)
.sort('-lastModified')
.populate({
path: 'admin',
populate: {
path: 'agency',
},
})
.exec(function (err, forms) {
if (err) {
return respondOnMongoError(req, res, err)
} else if (!forms) {
return res.status(StatusCodes.NOT_FOUND).json({
message: 'No user-created and collaborated-on forms found',
})
}
let activeForms = forms.filter((form) => form.status !== 'ARCHIVED')
return res.json(activeForms)
})
},
/**
* Return form feedback matching query
* @param {Object} req - Express request object
Expand Down
42 changes: 31 additions & 11 deletions src/app/models/form.server.model.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
import BSON from 'bson-ext'
import { compact, filter, pick, uniq } from 'lodash'
import { Model, Mongoose, Schema, SchemaOptions } from 'mongoose'
import { Mongoose, Schema, SchemaOptions } from 'mongoose'
import validator from 'validator'

import { FORM_DUPLICATE_KEYS } from '../../shared/constants'
import {
AuthType,
BasicField,
Colors,
DashboardFormView,
FormLogoState,
FormOtpData,
IEmailFormModel,
IEmailFormSchema,
IEncryptedFormModel,
IEncryptedFormSchema,
IForm,
IFormModel,
IFormSchema,
IPopulatedForm,
LogicType,
Expand Down Expand Up @@ -86,23 +90,13 @@ const formSchemaOptions: SchemaOptions = {
},
}

export interface IFormModel extends Model<IFormSchema> {
getOtpData(formId: string): Promise<FormOtpData | null>
getFullFormById(formId: string): Promise<IPopulatedForm | null>
deactivateById(formId: string): Promise<IFormSchema | null>
}

type IEncryptedFormModel = Model<IEncryptedFormSchema> & IFormModel

const EncryptedFormSchema = new Schema<IEncryptedFormSchema>({
publicKey: {
type: String,
required: true,
},
})

type IEmailFormModel = Model<IEmailFormSchema> & IFormModel

// Converts '[email protected], [email protected]' to ['[email protected]', '[email protected]']
function transformEmailString(v: string): string[] {
return v
Expand Down Expand Up @@ -501,6 +495,32 @@ const compileFormModel = (db: Mongoose): IFormModel => {
return form.save()
}

FormSchema.statics.getDashboardForms = async function (
this: IFormModel,
userId: IUserSchema['_id'],
userEmail: IUserSchema['email'],
): Promise<DashboardFormView[]> {
return (
this.find()
// List forms when either the user is an admin or collaborator.
.or([{ 'permissionList.email': userEmail }, { admin: userId }])
// Filter out archived forms.
.where('status')
.ne(Status.Archived)
// Project selected fields.
.select('_id title admin lastModified status form_fields')
Copy link
Contributor

Choose a reason for hiding this comment

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

might be cleaner to extract this into a constants file somewhere

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Only used here, no need to extract for now

.sort('-lastModified')
.populate({
path: 'admin',
populate: {
path: 'agency',
},
})
.lean()
.exec()
)
}

// Hooks
FormSchema.pre<IFormSchema>('validate', async function (next) {
// Reject save if form document is too large
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { errAsync, okAsync } from 'neverthrow'
import { mocked } from 'ts-jest/utils'

import { DatabaseError } from 'src/app/modules/core/core.errors'
import { MissingUserError } from 'src/app/modules/user/user.errors'

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

import { handleListDashboardForms } from '../admin-form.controller'
import * as AdminFormService from '../admin-form.service'

jest.mock('../admin-form.service')
const MockAdminFormService = mocked(AdminFormService)

describe('admin-form.controller', () => {
describe('handleListDashboardForms', () => {
const MOCK_REQ = expressHandler.mockRequest({
session: {
user: {
_id: 'exists',
},
},
})
it('should return 200 with list of forms', async () => {
// Arrange
const mockRes = expressHandler.mockResponse()
// Mock return array.
MockAdminFormService.getDashboardForms.mockReturnValueOnce(okAsync([]))

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

// Assert
expect(mockRes.json).toHaveBeenCalledWith([])
})

it('should return 422 on MissingUserError', async () => {
// Arrange
const mockRes = expressHandler.mockResponse()
MockAdminFormService.getDashboardForms.mockReturnValueOnce(
errAsync(new MissingUserError()),
)

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

// Assert
expect(mockRes.status).toBeCalledWith(422)
expect(mockRes.json).toBeCalledWith({ message: 'User not found' })
})

it('should return 500 when database error occurs', async () => {
// Arrange
const mockRes = expressHandler.mockResponse()
const mockErrorString = 'something went wrong'
MockAdminFormService.getDashboardForms.mockReturnValueOnce(
errAsync(new DatabaseError(mockErrorString)),
)

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

// Assert
expect(mockRes.status).toBeCalledWith(500)
expect(mockRes.json).toBeCalledWith({ message: mockErrorString })
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import mongoose from 'mongoose'
import { errAsync, okAsync } from 'neverthrow'
import { mocked } from 'ts-jest/utils'

import getFormModel from 'src/app/models/form.server.model'
import { DatabaseError } from 'src/app/modules/core/core.errors'
import { MissingUserError } from 'src/app/modules/user/user.errors'
import * as UserService from 'src/app/modules/user/user.service'
import { DashboardFormView, IPopulatedUser, IUserSchema } from 'src/types'

import { getDashboardForms } from '../admin-form.service'

const FormModel = getFormModel(mongoose)

jest.mock('src/app/modules/user/user.service')
const MockUserService = mocked(UserService)

describe('admin-form.service', () => {
describe('getDashboardForms', () => {
it('should return list of forms user is authorized to view', async () => {
// Arrange
const mockUserId = 'mockUserId'
const mockUser: Partial<IUserSchema> = {
email: '[email protected]',
_id: mockUserId,
}
const mockDashboardForms: DashboardFormView[] = [
{
admin: {} as IPopulatedUser,
title: 'test form 1',
},
{
admin: {} as IPopulatedUser,
title: 'test form 2',
},
]
// Mock user admin success.
MockUserService.findAdminById.mockReturnValueOnce(
okAsync(mockUser as IUserSchema),
)
const getSpy = jest
.spyOn(FormModel, 'getDashboardForms')
.mockResolvedValueOnce(mockDashboardForms)

// Act
const actualResult = await getDashboardForms(mockUserId)

// Assert
expect(getSpy).toHaveBeenCalledWith(mockUserId, mockUser.email)
expect(actualResult.isOk()).toEqual(true)
expect(actualResult._unsafeUnwrap()).toEqual(mockDashboardForms)
})

it('should return MissingUserError when user with userId does not exist', async () => {
// Arrange
const expectedError = new MissingUserError('not found')
MockUserService.findAdminById.mockReturnValueOnce(errAsync(expectedError))

// Act
const actualResult = await getDashboardForms('any')

// Assert
expect(actualResult.isErr()).toEqual(true)
// Error should passthrough.
expect(actualResult._unsafeUnwrapErr()).toEqual(expectedError)
})

it('should return DatabaseError when error occurs whilst querying the database', async () => {
// Arrange
const mockUserId = 'mockUserId'
const mockUser: Partial<IUserSchema> = {
email: '[email protected]',
_id: mockUserId,
}
// Mock user admin success.
MockUserService.findAdminById.mockReturnValueOnce(
okAsync(mockUser as IUserSchema),
)
const getSpy = jest
.spyOn(FormModel, 'getDashboardForms')
.mockRejectedValueOnce(new Error('some error'))

// Act
const actualResult = await getDashboardForms(mockUserId)

// Assert
expect(getSpy).toHaveBeenCalledWith(mockUserId, mockUser.email)
expect(actualResult.isErr()).toEqual(true)
expect(actualResult._unsafeUnwrapErr()).toEqual(new DatabaseError())
})
})
})
38 changes: 38 additions & 0 deletions src/app/modules/form/admin-form/admin-form.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { RequestHandler } from 'express'

import { createLoggerWithLabel } from '../../../../config/logger'

import { getDashboardForms } from './admin-form.service'
import { mapRouteError } from './admin-form.utils'

const logger = createLoggerWithLabel(module)

/**
* Handler for GET /adminform endpoint.
* @security session
*
* @returns 200 with list of forms user can access when list is retrieved successfully
* @returns 422 when user of given id cannnot be found in the database
* @returns 500 when database errors occur
*/
export const handleListDashboardForms: RequestHandler = async (req, res) => {
const authedUserId = (req.session as Express.AuthedSession).user._id
const dashboardResult = await getDashboardForms(authedUserId)

if (dashboardResult.isErr()) {
const { error } = dashboardResult
logger.error({
message: 'Error listing dashboard forms',
meta: {
action: 'handleListDashboardForms',
karrui marked this conversation as resolved.
Show resolved Hide resolved
userId: authedUserId,
},
error,
})
const { errorMessage, statusCode } = mapRouteError(error)
return res.status(statusCode).json({ message: errorMessage })
}

// Success.
return res.json(dashboardResult.value)
}
47 changes: 47 additions & 0 deletions src/app/modules/form/admin-form/admin-form.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import mongoose from 'mongoose'
import { ResultAsync } from 'neverthrow'

import { createLoggerWithLabel } from '../../../../config/logger'
import { DashboardFormView } from '../../../../types'
import getFormModel from '../../../models/form.server.model'
import { DatabaseError } from '../../core/core.errors'
import { MissingUserError } from '../../user/user.errors'
import { findAdminById } from '../../user/user.service'

const logger = createLoggerWithLabel(module)
const FormModel = getFormModel(mongoose)

/**
* Retrieves a list of forms that the user of the given userId can access in
* their dashboard.
* @param userId the id of the user to retrieve accessible forms for.
* @returns ok(DashboardFormViewList)
* @returns err(MissingUserError) if user with userId does not exist in the database
* @returns err(DatabaseError) if error occurs whilst querying the database
*/
export const getDashboardForms = (
userId: string,
): ResultAsync<DashboardFormView[], MissingUserError | DatabaseError> => {
// Step 1: Verify user exists.
return (
findAdminById(userId)
// Step 2: Retrieve lists users are authorized to see.
.andThen((admin) => {
return ResultAsync.fromPromise(
FormModel.getDashboardForms(userId, admin.email),
(error) => {
logger.error({
message: 'Database error when retrieving admin dashboard forms',
meta: {
action: 'getDashboardForms',
userId,
},
error,
})

return new DatabaseError()
},
)
})
)
}
Loading