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

feat(admin-form): individual form field api #1799

Merged
merged 9 commits into from
May 6, 2021
Original file line number Diff line number Diff line change
Expand Up @@ -8878,4 +8878,185 @@ describe('admin-form.controller', () => {
expect(mockRes.json).toBeCalledWith(expectedResponse)
})
})

describe('handleGetFormField', () => {
const MOCK_USER_ID = new ObjectId().toHexString()
const MOCK_FORM_ID = new ObjectId().toHexString()
const MOCK_USER = {
_id: MOCK_USER_ID,
email: '[email protected]',
} as IPopulatedUser
const MOCK_FIELDS = [
generateDefaultField(BasicField.Rating),
generateDefaultField(BasicField.Table),
]
const MOCK_FIELD_ID = String(MOCK_FIELDS[1]._id)

const MOCK_FORM = {
admin: MOCK_USER,
_id: MOCK_FORM_ID,
form_fields: MOCK_FIELDS,
title: 'mock title',
} as IPopulatedForm

const MOCK_REQ = expressHandler.mockRequest({
params: {
formId: MOCK_FORM_ID,
fieldId: MOCK_FIELD_ID,
},
session: {
user: {
_id: MOCK_USER_ID,
},
},
})

beforeEach(() => {
// Mock various services to return expected results.
MockUserService.getPopulatedUserById.mockReturnValue(okAsync(MOCK_USER))
MockAuthService.getFormAfterPermissionChecks.mockReturnValue(
okAsync(MOCK_FORM),
)
})

it('should return 200 when deletion is successful', async () => {
// Arrange
const mockRes = expressHandler.mockResponse()
MockAdminFormService.getFormField.mockReturnValueOnce(ok(MOCK_FIELDS[1]))

// Act
await AdminFormController.handleGetFormField(MOCK_REQ, mockRes, jest.fn())

// Assert
expect(mockRes.status).toHaveBeenCalledWith(200)
expect(mockRes.json).toHaveBeenCalledWith(MOCK_FIELDS[1])
expect(MockAdminFormService.getFormField).toHaveBeenCalledWith(
MOCK_FORM,
MOCK_FIELD_ID,
)
})

it('should return 403 when current user does not have permissions to retrieve form fields', async () => {
// Arrange
const mockRes = expressHandler.mockResponse()
const expectedErrorString = 'no write permissions'
MockAuthService.getFormAfterPermissionChecks.mockReturnValueOnce(
errAsync(new ForbiddenFormError(expectedErrorString)),
)

// Act
await AdminFormController.handleGetFormField(MOCK_REQ, mockRes, jest.fn())

// Assert
expect(mockRes.status).toHaveBeenCalledWith(403)
expect(mockRes.json).toHaveBeenCalledWith({
message: expectedErrorString,
})
expect(MockAdminFormService.deleteFormField).not.toHaveBeenCalled()
})

it('should return 404 when field to retrieve cannot be found', async () => {
// Arrange
const mockRes = expressHandler.mockResponse()
MockAdminFormService.getFormField.mockReturnValueOnce(
err(new FieldNotFoundError('Field to retrieve not found')),
)

// Act
await AdminFormController.handleGetFormField(MOCK_REQ, mockRes, jest.fn())

// Assert
expect(mockRes.status).toHaveBeenCalledWith(404)
expect(mockRes.json).toHaveBeenCalledWith({
message: 'Field to retrieve not found',
})
expect(MockAdminFormService.getFormField).toHaveBeenCalledWith(
MOCK_FORM,
MOCK_FIELD_ID,
)
})

it('should return 404 when form to retrieve form field for cannot be found', async () => {
// Arrange
const mockRes = expressHandler.mockResponse()

const expectedErrorString = 'nope'
MockAuthService.getFormAfterPermissionChecks.mockReturnValueOnce(
errAsync(new FormNotFoundError(expectedErrorString)),
)

// Act
await AdminFormController.handleGetFormField(MOCK_REQ, mockRes, jest.fn())

// Assert
expect(mockRes.status).toHaveBeenCalledWith(404)
expect(mockRes.json).toHaveBeenCalledWith({
message: expectedErrorString,
})
expect(MockAdminFormService.getFormField).not.toHaveBeenCalled()
})

it('should return 410 when form to retrieve form field for is already archived', async () => {
// Arrange
const mockRes = expressHandler.mockResponse()

const expectedErrorString = 'already deleted'
MockAuthService.getFormAfterPermissionChecks.mockReturnValueOnce(
errAsync(new FormDeletedError(expectedErrorString)),
)

// Act
await AdminFormController.handleGetFormField(MOCK_REQ, mockRes, jest.fn())

// Assert
expect(mockRes.status).toHaveBeenCalledWith(410)
expect(mockRes.json).toHaveBeenCalledWith({
message: expectedErrorString,
})
expect(MockAdminFormService.getFormField).not.toHaveBeenCalled()
})

it('should return 422 when user in session cannot be retrieved from the database', async () => {
// Arrange
const mockRes = expressHandler.mockResponse()

const expectedErrorString = 'user not in session??!!'
MockUserService.getPopulatedUserById.mockReturnValueOnce(
errAsync(new MissingUserError(expectedErrorString)),
)

// Act
await AdminFormController.handleGetFormField(MOCK_REQ, mockRes, jest.fn())

// Assert
expect(mockRes.status).toHaveBeenCalledWith(422)
expect(mockRes.json).toHaveBeenCalledWith({
message: expectedErrorString,
})
expect(
MockAuthService.getFormAfterPermissionChecks,
).not.toHaveBeenCalled()
expect(MockAdminFormService.getFormField).not.toHaveBeenCalled()
})

it('should return 500 when generic database error occurs during form field retrieval', async () => {
// Arrange
const mockRes = expressHandler.mockResponse()

const expectedErrorString = 'some database error bam'
MockUserService.getPopulatedUserById.mockReturnValueOnce(
errAsync(new DatabaseError(expectedErrorString)),
)

// Act
await AdminFormController.handleGetFormField(MOCK_REQ, mockRes, jest.fn())

// Assert
expect(mockRes.status).toHaveBeenCalledWith(500)
expect(mockRes.json).toHaveBeenCalledWith({
message: expectedErrorString,
})
expect(MockAdminFormService.getFormField).not.toHaveBeenCalled()
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ import {
duplicateForm,
editFormFields,
getDashboardForms,
getFormField,
reorderFormField,
transferFormOwnership,
updateEndPage,
Expand Down Expand Up @@ -1861,4 +1862,43 @@ describe('admin-form.service', () => {
expect(UPDATE_SPY).not.toHaveBeenCalled()
})
})

describe('getFormField', () => {
it('should return the form field when retrieval is successful', async () => {
// Arrange
const MOCK_FIELD = generateDefaultField(BasicField.Image)
const MOCK_FORM = {
title: 'some mock form',
// Append created field to end of form_fields.
form_fields: [MOCK_FIELD],
_id: new ObjectId(),
} as IFormSchema

// Act
const actual = await getFormField(MOCK_FORM, String(MOCK_FIELD._id))

// Assert
expect(actual._unsafeUnwrap()).toEqual(MOCK_FIELD)
})

it("should return FieldNotFoundError when the fieldId does not exist in the form's fields", async () => {
// Arrange
const MOCK_ID = new ObjectId().toHexString()
const MOCK_FORM = ({
title: 'some mock form',
// Append created field to end of form_fields.
form_fields: [],
_id: new ObjectId(),
} as unknown) as IFormSchema
const expectedError = new FieldNotFoundError(
`Attempted to retrieve field ${MOCK_ID} from ${MOCK_FORM._id} but field was not present`,
)

// Act
const actual = await getFormField(MOCK_FORM, MOCK_ID)

// Assert
expect(actual._unsafeUnwrapErr()).toEqual(expectedError)
})
})
})
53 changes: 53 additions & 0 deletions src/app/modules/form/admin-form/admin-form.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2000,6 +2000,59 @@ export const handleUpdateEndPage = [
_handleUpdateEndPage,
] as RequestHandler[]

/**
* Handler for GET /admin/forms/:formId/fields/:fieldId
* @security session
*
* @returns 200 with form field when retrieval is successful
* @returns 403 when current user does not have permissions to retrieve form field
* @returns 404 when form cannot be found
* @returns 404 when form field cannot be found
* @returns 410 when retrieving form field of an archived form
* @returns 422 when user in session cannot be retrieved from the database
* @returns 500 when database error occurs
*/
export const handleGetFormField: RequestHandler<
{
formId: string
fieldId: string
},
ErrorDto | FormFieldDto
> = (req, res) => {
const { formId, fieldId } = req.params
const sessionUserId = (req.session as Express.AuthedSession).user._id

return (
// Step 1: Retrieve currently logged in user.
UserService.getPopulatedUserById(sessionUserId)
.andThen((user) =>
// Step 2: Retrieve form with read permission check.
AuthService.getFormAfterPermissionChecks({
user,
formId,
level: PermissionLevel.Read,
}),
)
.andThen((form) => AdminFormService.getFormField(form, fieldId))
.map((formField) => res.status(StatusCodes.OK).json(formField))
.mapErr((error) => {
logger.error({
message: 'Error occurred when retrieving form field',
meta: {
action: 'handleGetFormField',
...createReqMeta(req),
userId: sessionUserId,
formId,
fieldId,
},
error,
})
const { errorMessage, statusCode } = mapRouteError(error)
return res.status(statusCode).json({ message: errorMessage })
})
)
}

/**
* NOTE: Exported for testing.
* Private handler for PUT /api/v3/admin/forms/:formId/collaborators
Expand Down
23 changes: 22 additions & 1 deletion src/app/modules/form/admin-form/admin-form.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { PresignedPost } from 'aws-sdk/clients/s3'
import { assignIn, last, omit } from 'lodash'
import mongoose from 'mongoose'
import { errAsync, okAsync, ResultAsync } from 'neverthrow'
import { err, errAsync, ok, okAsync, Result, ResultAsync } from 'neverthrow'
import { Except, Merge } from 'type-fest'

import {
Expand Down Expand Up @@ -910,3 +910,24 @@ export const updateEndPage = (
return okAsync(updatedForm.endPage)
})
}

/**
* Retrieves a form field from the given form.
* @param form The form to retrieve the specified form field for
* @param fieldId the id of the form field
* @returns ok(form field) on success
* @returns err(FieldNotFoundError) if the fieldId does not exist in form's fields
*/
export const getFormField = (
form: IPopulatedForm,
fieldId: string,
): Result<IFieldSchema, FieldNotFoundError> => {
const formField = getFormFieldById(form.form_fields, fieldId)
if (!formField)
return err(
new FieldNotFoundError(
`Attempted to retrieve field ${fieldId} from ${form._id} but field was not present`,
),
)
return ok(formField)
}
2 changes: 1 addition & 1 deletion src/app/modules/form/form.errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export class AuthTypeMismatchError extends ApplicationError {
export class FormAuthNoEsrvcIdError extends ApplicationError {
constructor(formId: string) {
super(
`Attempted to validate form ${formId} whhich did not have an eServiceId`,
`Attempted to validate form ${formId} which did not have an eServiceId`,
)
}
}
Loading