From ebebe2609f9a1cc63580373ef114876638cc5287 Mon Sep 17 00:00:00 2001
From: wanlingt <wanling@open.gov.sg>
Date: Wed, 10 May 2023 18:37:50 +0800
Subject: [PATCH 1/8] feat: add createSampleSubmissionData function

---
 src/app/modules/form/form.service.ts | 49 ++++++++++++++++++++++++++++
 1 file changed, 49 insertions(+)

diff --git a/src/app/modules/form/form.service.ts b/src/app/modules/form/form.service.ts
index 6d4d693038..f0a2f06f58 100644
--- a/src/app/modules/form/form.service.ts
+++ b/src/app/modules/form/form.service.ts
@@ -3,6 +3,7 @@ import { err, errAsync, ok, okAsync, Result, ResultAsync } from 'neverthrow'
 
 import {
   FormAuthType,
+  FormFieldDto,
   FormResponseMode,
   FormStatus,
 } from '../../../../shared/types'
@@ -343,3 +344,51 @@ export const retrievePublicFormsWithSmsVerification = (
     return okAsync(forms)
   })
 }
+
+export const createSampleSubmissionData = (
+  sampleData: Record<string, unknown>,
+  field: FormFieldDto,
+) => {
+  let sampleValue = null
+  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':
+      sampleValue = field.fieldOptions[0]
+      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
+    default:
+      break
+  }
+  if (sampleValue != null) {
+    sampleData[field._id] = {
+      question: field.title,
+      answer: sampleValue,
+    }
+  }
+  return sampleValue
+}

From 0d582b1e0b4363d52f5f762a46e81d28f49fb21b Mon Sep 17 00:00:00 2001
From: wanlingt <wanling@open.gov.sg>
Date: Wed, 10 May 2023 18:39:00 +0800
Subject: [PATCH 2/8] feat: add endpoint to get sample submission

---
 .../public-form/public-form.controller.ts     | 56 +++++++++++++++++++
 .../api/v3/forms/public-forms.form.routes.ts  |  4 ++
 2 files changed, 60 insertions(+)

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 3c69c27eef..04998ff26f 100644
--- a/src/app/modules/form/public-form/public-form.controller.ts
+++ b/src/app/modules/form/public-form/public-form.controller.ts
@@ -326,6 +326,62 @@ 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).andThen((form) =>
+    FormService.checkFormSubmissionLimitAndDeactivateForm(form),
+  )
+  // 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 sampleData: Record<string, any> = {}
+  const formFields = publicForm.form_fields
+  if (!formFields) {
+    throw new Error('unable to get form fields')
+  }
+  for (const field of formFields) {
+    FormService.createSampleSubmissionData(sampleData, field)
+  }
+  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/public-forms.form.routes.ts b/src/app/routes/api/v3/forms/public-forms.form.routes.ts
index b838984b35..e36312eabb 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
@@ -24,6 +24,10 @@ PublicFormsFormRouter.route('/:formId([a-fA-F0-9]{24})').get(
   PublicFormController.handleGetPublicForm,
 )
 
+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

From cb60d3c9228cdb47a65dba1031ff74c697541f70 Mon Sep 17 00:00:00 2001
From: wanlingt <wanling@open.gov.sg>
Date: Mon, 15 May 2023 16:20:02 +0800
Subject: [PATCH 3/8] docs: add comments for GET sample submission endpoint

---
 .../routes/api/v3/forms/public-forms.form.routes.ts    | 10 ++++++++++
 1 file changed, 10 insertions(+)

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 e36312eabb..dc728e78f7 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
@@ -24,6 +24,16 @@ 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,
 )

From 473614acfc2d7b372e035f738941b59fd9bda83b Mon Sep 17 00:00:00 2001
From: wanlingt <wanling@open.gov.sg>
Date: Mon, 15 May 2023 16:20:34 +0800
Subject: [PATCH 4/8] feat: randomise response, add attachment type

---
 src/app/modules/form/form.service.ts | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/src/app/modules/form/form.service.ts b/src/app/modules/form/form.service.ts
index f0a2f06f58..9d03408058 100644
--- a/src/app/modules/form/form.service.ts
+++ b/src/app/modules/form/form.service.ts
@@ -350,6 +350,8 @@ export const createSampleSubmissionData = (
   field: FormFieldDto,
 ) => {
   let sampleValue = null
+  let noOfOptions = 0
+  let randomSelectedOption = 0
   switch (field.fieldType) {
     case 'textarea':
     case 'textfield':
@@ -358,7 +360,9 @@ export const createSampleSubmissionData = (
       break
     case 'radiobutton':
     case 'dropdown':
-      sampleValue = field.fieldOptions[0]
+      noOfOptions = field.fieldOptions.length
+      randomSelectedOption = Math.floor(Math.random() * noOfOptions)
+      sampleValue = field.fieldOptions[randomSelectedOption]
       break
     case 'email':
       sampleValue = 'hello@example.com'
@@ -381,6 +385,9 @@ export const createSampleSubmissionData = (
     case 'rating':
       sampleValue = 1
       break
+    case 'attachment':
+      sampleValue = 'attachmentFileName'
+      break
     default:
       break
   }
@@ -388,6 +395,7 @@ export const createSampleSubmissionData = (
     sampleData[field._id] = {
       question: field.title,
       answer: sampleValue,
+      fieldType: field.fieldType,
     }
   }
   return sampleValue

From a4ac6a249e8d926d01ee7dc056ae06f1b3c137da Mon Sep 17 00:00:00 2001
From: wanlingt <wanling@open.gov.sg>
Date: Mon, 15 May 2023 17:42:01 +0800
Subject: [PATCH 5/8] fix: remove form deactivation check

---
 src/app/modules/form/public-form/public-form.controller.ts | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

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 04998ff26f..c490f81973 100644
--- a/src/app/modules/form/public-form/public-form.controller.ts
+++ b/src/app/modules/form/public-form/public-form.controller.ts
@@ -337,9 +337,7 @@ export const handleGetPublicFormSampleSubmission: ControllerHandler<
     formId,
   }
 
-  const formResult = await getFormIfPublic(formId).andThen((form) =>
-    FormService.checkFormSubmissionLimitAndDeactivateForm(form),
-  )
+  const formResult = await getFormIfPublic(formId)
   // Early return if form is not public or any error occurred.
   if (formResult.isErr()) {
     const { error } = formResult

From d039a65e152052e9c5af50be2771af79f979acf7 Mon Sep 17 00:00:00 2001
From: wanlingt <wanling@open.gov.sg>
Date: Mon, 15 May 2023 17:43:24 +0800
Subject: [PATCH 6/8] tests: add unit tests for sample submission endpoint

---
 .../public-forms.form.routes.spec.ts          | 119 ++++++++++++++++++
 1 file changed, 119 insertions(+)

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)
+    })
+  })
 })

From 3f2dcc19ac6a66253f05f172c83cb3dd2d70a458 Mon Sep 17 00:00:00 2001
From: wanlingt <wanling@open.gov.sg>
Date: Tue, 16 May 2023 12:00:34 +0800
Subject: [PATCH 7/8] ref: split sample submission creation into smaller
 functions

---
 src/app/modules/form/form.service.ts          | 24 ++++++++++++++-----
 .../public-form/public-form.controller.ts     |  7 +++---
 2 files changed, 21 insertions(+), 10 deletions(-)

diff --git a/src/app/modules/form/form.service.ts b/src/app/modules/form/form.service.ts
index 9d03408058..979d5ec089 100644
--- a/src/app/modules/form/form.service.ts
+++ b/src/app/modules/form/form.service.ts
@@ -3,6 +3,7 @@ import { err, errAsync, ok, okAsync, Result, ResultAsync } from 'neverthrow'
 
 import {
   FormAuthType,
+  FormField,
   FormFieldDto,
   FormResponseMode,
   FormStatus,
@@ -345,10 +346,7 @@ export const retrievePublicFormsWithSmsVerification = (
   })
 }
 
-export const createSampleSubmissionData = (
-  sampleData: Record<string, unknown>,
-  field: FormFieldDto,
-) => {
+export const createSingleSampleSubmissionAnswer = (field: FormFieldDto) => {
   let sampleValue = null
   let noOfOptions = 0
   let randomSelectedOption = 0
@@ -391,12 +389,26 @@ export const createSampleSubmissionData = (
     default:
       break
   }
+  let answer = {}
   if (sampleValue != null) {
-    sampleData[field._id] = {
+    answer = {
+      id: field._id,
       question: field.title,
       answer: sampleValue,
       fieldType: field.fieldType,
     }
   }
-  return sampleValue
+  return answer
+}
+
+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
 }
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 c490f81973..e368c9435a 100644
--- a/src/app/modules/form/public-form/public-form.controller.ts
+++ b/src/app/modules/form/public-form/public-form.controller.ts
@@ -370,14 +370,13 @@ export const handleGetPublicFormSampleSubmission: ControllerHandler<
 
   const publicForm = form.getPublicView() as PublicFormDto
 
-  const sampleData: Record<string, any> = {}
   const formFields = publicForm.form_fields
   if (!formFields) {
     throw new Error('unable to get form fields')
   }
-  for (const field of formFields) {
-    FormService.createSampleSubmissionData(sampleData, field)
-  }
+
+  const sampleData = FormService.createSampleSubmissionResponses(formFields)
+
   return res.json({ responses: sampleData })
 }
 /**

From d982f831f401f1651d252f02bd99e0e792e8d65d Mon Sep 17 00:00:00 2001
From: wanlingt <wanling@open.gov.sg>
Date: Tue, 16 May 2023 13:47:06 +0800
Subject: [PATCH 8/8] fix: return static fields in
 createSingleSampleSubmissionAnswer

---
 src/app/modules/form/form.service.ts | 14 +++++---------
 1 file changed, 5 insertions(+), 9 deletions(-)

diff --git a/src/app/modules/form/form.service.ts b/src/app/modules/form/form.service.ts
index 979d5ec089..b4218d75a3 100644
--- a/src/app/modules/form/form.service.ts
+++ b/src/app/modules/form/form.service.ts
@@ -389,16 +389,12 @@ export const createSingleSampleSubmissionAnswer = (field: FormFieldDto) => {
     default:
       break
   }
-  let answer = {}
-  if (sampleValue != null) {
-    answer = {
-      id: field._id,
-      question: field.title,
-      answer: sampleValue,
-      fieldType: field.fieldType,
-    }
+  return {
+    id: field._id,
+    question: field.title,
+    answer: sampleValue,
+    fieldType: field.fieldType,
   }
-  return answer
 }
 
 export const createSampleSubmissionResponses = (