Skip to content

Commit

Permalink
feat: add sample-submission endpoint to retrieve sample submission da…
Browse files Browse the repository at this point in the history
…ta of a public form (#6325)

* feat: add createSampleSubmissionData function

* feat: add endpoint to get sample submission

* docs: add comments for GET sample submission endpoint

* feat: randomise response, add attachment type

* fix: remove form deactivation check

* tests: add unit tests for sample submission endpoint

* ref: split sample submission creation into smaller functions

* fix: return static fields in createSingleSampleSubmissionAnswer
  • Loading branch information
wanlingt authored May 18, 2023
1 parent 5e89d23 commit 9efd576
Show file tree
Hide file tree
Showing 4 changed files with 251 additions and 0 deletions.
65 changes: 65 additions & 0 deletions src/app/modules/form/form.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { err, errAsync, ok, okAsync, Result, ResultAsync } from 'neverthrow'

import {
FormAuthType,
FormField,
FormFieldDto,
FormResponseMode,
FormStatus,
} from '../../../../shared/types'
Expand Down Expand Up @@ -343,3 +345,66 @@ export const retrievePublicFormsWithSmsVerification = (
return okAsync(forms)
})
}

export const createSingleSampleSubmissionAnswer = (field: FormFieldDto) => {
let sampleValue = null
let noOfOptions = 0
let randomSelectedOption = 0
switch (field.fieldType) {
case 'textarea':
case 'textfield':
sampleValue =
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'
break
case 'radiobutton':
case 'dropdown':
noOfOptions = field.fieldOptions.length
randomSelectedOption = Math.floor(Math.random() * noOfOptions)
sampleValue = field.fieldOptions[randomSelectedOption]
break
case 'email':
sampleValue = '[email protected]'
break
case 'decimal':
sampleValue = 1.234
break
case 'number':
sampleValue = 1234
break
case 'mobile':
sampleValue = '+6598765432'
break
case 'homeno':
sampleValue = '+6567890123'
break
case 'yes_no':
sampleValue = 'yes'
break
case 'rating':
sampleValue = 1
break
case 'attachment':
sampleValue = 'attachmentFileName'
break
default:
break
}
return {
id: field._id,
question: field.title,
answer: sampleValue,
fieldType: field.fieldType,
}
}

export const createSampleSubmissionResponses = (
formFields: FormFieldDto<FormField>[],
) => {
const sampleData: Record<string, any> = {}
formFields.forEach((field) => {
const answer = createSingleSampleSubmissionAnswer(field)
if (!answer) return
sampleData[field._id] = answer
})
return sampleData
}
53 changes: 53 additions & 0 deletions src/app/modules/form/public-form/public-form.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,59 @@ export const handleGetPublicForm: ControllerHandler<
}
}

export const handleGetPublicFormSampleSubmission: ControllerHandler<
{ formId: string },
Record<string, any> | ErrorDto | PrivateFormErrorDto
> = async (req, res) => {
const { formId } = req.params
const logMeta = {
action: 'handleGetPublicFormSampleSubmission',
...createReqMeta(req),
formId,
}

const formResult = await getFormIfPublic(formId)
// Early return if form is not public or any error occurred.
if (formResult.isErr()) {
const { error } = formResult
// NOTE: Only log on possible database errors.
// This is because the other kinds of errors are expected errors and are not truly exceptional
if (isMongoError(error)) {
logger.error({
message: 'Error retrieving public form',
meta: logMeta,
error,
})
}
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 })
}
const form = formResult.value

const publicForm = form.getPublicView() as PublicFormDto

const formFields = publicForm.form_fields
if (!formFields) {
throw new Error('unable to get form fields')
}

const sampleData = FormService.createSampleSubmissionResponses(formFields)

return res.json({ responses: sampleData })
}
/**
* NOTE: This is exported only for testing
* Generates redirect URL to Official SingPass/CorpPass log in page
Expand Down
119 changes: 119 additions & 0 deletions src/app/routes/api/v3/forms/__tests__/public-forms.form.routes.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { errAsync } from 'neverthrow'
import supertest, { Session } from 'supertest-session'

import { DatabaseError } from 'src/app/modules/core/core.errors'
import { createSampleSubmissionData } from 'src/app/modules/form/form.service'
import {
MOCK_ACCESS_TOKEN,
MOCK_AUTH_CODE,
Expand Down Expand Up @@ -317,4 +318,122 @@ describe('public-form.form.routes', () => {
expect(actualResponse.body).toEqual(expectedResponseBody)
})
})

describe('GET /:formId/sample-submission', () => {
it('should return 200 with public form when form has a valid formId', async () => {
// Arrange
const { form } = await dbHandler.insertEmailForm({
formOptions: { status: FormStatus.Public },
})
// NOTE: This is needed to inject admin info into the form
const fullForm = await dbHandler.getFullFormById(form._id)
expect(fullForm).not.toBeNull()

const formFields = fullForm?.getPublicView().form_fields
if (!formFields) return
const expectedSampleData = {}
for (const field of formFields) {
createSampleSubmissionData(expectedSampleData, field)
}
const expectedResponseBody = JSON.parse(
JSON.stringify({
responses: expectedSampleData,
}),
)

// Act
const actualResponse = await request.get(
`/forms/${form._id}/sample-submission`,
)

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

it('should return 404 if the form does not exist', async () => {
const MOCK_FORM_ID = new ObjectId().toHexString()
const expectedResponseBody = JSON.parse(
JSON.stringify({
message: 'Form not found',
}),
)

// Act
const actualResponse = await request.get(
`/forms/${MOCK_FORM_ID}/sample-submission`,
)

// 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: FormStatus.Private },
})
const expectedResponseBody = JSON.parse(
JSON.stringify({
message: form.inactiveMessage,
formTitle: form.title,
isPageFound: true,
}),
)

// Act
const actualResponse = await request.get(
`/forms/${form._id}/sample-submission`,
)

// 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: FormStatus.Archived },
})
const expectedResponseBody = JSON.parse(
JSON.stringify({
message: 'This form is no longer active',
}),
)

// Act
const actualResponse = await request.get(
`/forms/${form._id}/sample-submission`,
)

// 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: FormStatus.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(
`/forms/${form._id}/sample-submission`,
)

// Assert
expect(actualResponse.status).toEqual(500)
expect(actualResponse.body).toEqual(expectedResponseBody)
})
})
})
14 changes: 14 additions & 0 deletions src/app/routes/api/v3/forms/public-forms.form.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,17 @@ export const PublicFormsFormRouter = Router()
PublicFormsFormRouter.route('/:formId([a-fA-F0-9]{24})').get(
PublicFormController.handleGetPublicForm,
)

/**
* Returns a sample submission response of the specified form to the user
*
* @route GET /:formId/sample-submission
*
* @returns 200 with form when form exists and is public
* @returns 404 when form is private or form with given ID does not exist
* @returns 410 when form is archived
* @returns 500 when database error occurs
*/
PublicFormsFormRouter.route('/:formId([a-fA-F0-9]{24})/sample-submission').get(
PublicFormController.handleGetPublicFormSampleSubmission,
)

0 comments on commit 9efd576

Please sign in to comment.