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: add sample-submission endpoint to retrieve sample submission data of a public form #6325

Merged
merged 9 commits into from
May 18, 2023
69 changes: 69 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,70 @@ 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':
foochifa marked this conversation as resolved.
Show resolved Hide resolved
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
wanlingt marked this conversation as resolved.
Show resolved Hide resolved
default:
break
}
let answer = {}
if (sampleValue != null) {
answer = {
id: field._id,
question: field.title,
wanlingt marked this conversation as resolved.
Show resolved Hide resolved
answer: sampleValue,
fieldType: field.fieldType,
}
}
return answer
timotheeg marked this conversation as resolved.
Show resolved Hide resolved
}

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 @@ -326,6 +326,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 })
}
timotheeg marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -24,6 +24,20 @@ 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,
)

// TODO #4279: Remove after React rollout is complete
/**
* Returns the React to Angular switch feedback form to the user
Expand Down