diff --git a/src/app/modules/form/form.service.ts b/src/app/modules/form/form.service.ts index 6d4d693038..b4218d75a3 100644 --- a/src/app/modules/form/form.service.ts +++ b/src/app/modules/form/form.service.ts @@ -3,6 +3,8 @@ import { err, errAsync, ok, okAsync, Result, ResultAsync } from 'neverthrow' import { FormAuthType, + FormField, + FormFieldDto, FormResponseMode, FormStatus, } from '../../../../shared/types' @@ -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 = 'hello@example.com' + 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[], +) => { + const sampleData: Record = {} + formFields.forEach((field) => { + const answer = createSingleSampleSubmissionAnswer(field) + if (!answer) return + sampleData[field._id] = answer + }) + return sampleData +} diff --git a/src/app/modules/form/public-form/public-form.controller.ts b/src/app/modules/form/public-form/public-form.controller.ts index a75f5a78ae..2005f8509e 100644 --- a/src/app/modules/form/public-form/public-form.controller.ts +++ b/src/app/modules/form/public-form/public-form.controller.ts @@ -317,6 +317,59 @@ export const handleGetPublicForm: ControllerHandler< } } +export const handleGetPublicFormSampleSubmission: ControllerHandler< + { formId: string }, + Record | 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 diff --git a/src/app/routes/api/v3/forms/__tests__/public-forms.form.routes.spec.ts b/src/app/routes/api/v3/forms/__tests__/public-forms.form.routes.spec.ts index 22f1703f26..80101ac020 100644 --- a/src/app/routes/api/v3/forms/__tests__/public-forms.form.routes.spec.ts +++ b/src/app/routes/api/v3/forms/__tests__/public-forms.form.routes.spec.ts @@ -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, @@ -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) + }) + }) }) diff --git a/src/app/routes/api/v3/forms/public-forms.form.routes.ts b/src/app/routes/api/v3/forms/public-forms.form.routes.ts index e87cf9e443..b14e3746e0 100644 --- a/src/app/routes/api/v3/forms/public-forms.form.routes.ts +++ b/src/app/routes/api/v3/forms/public-forms.form.routes.ts @@ -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, +)