diff --git a/src/app/routes/api/v3/forms/__tests__/public-forms.feedback.routes.spec.ts b/src/app/routes/api/v3/forms/__tests__/public-forms.feedback.routes.spec.ts new file mode 100644 index 0000000000..8f77df7651 --- /dev/null +++ b/src/app/routes/api/v3/forms/__tests__/public-forms.feedback.routes.spec.ts @@ -0,0 +1,160 @@ +import { getReasonPhrase, StatusCodes } from 'http-status-codes' +import { errAsync } from 'neverthrow' +import supertest, { Session } from 'supertest-session' + +import { DatabaseError } from 'src/app/modules/core/core.errors' +import { Status } from 'src/types' + +import { setupApp } from 'tests/integration/helpers/express-setup' +import { buildCelebrateError } from 'tests/unit/backend/helpers/celebrate' +import dbHandler from 'tests/unit/backend/helpers/jest-db' + +import * as FormService from '../../../../../modules/form/form.service' +import { PublicFormsRouter } from '../public-forms.routes' + +const app = setupApp('/forms', PublicFormsRouter) + +describe('public-form.feedback.routes', () => { + let request: Session + + beforeAll(async () => await dbHandler.connect()) + beforeEach(async () => { + request = supertest(app) + }) + afterEach(async () => { + await dbHandler.clearDatabase() + jest.restoreAllMocks() + }) + afterAll(async () => await dbHandler.closeDatabase()) + describe('POST /forms/:formId/feedback', () => { + it('should return 200 when feedback was successfully saved', async () => { + // Arrange + const { form } = await dbHandler.insertEmailForm({ + formOptions: { + status: Status.Public, + }, + }) + const MOCK_FEEDBACK = { + rating: 5, + comment: 'great mock', + } + const expectedResponse = JSON.parse( + JSON.stringify({ message: 'Successfully submitted feedback' }), + ) + + // Act + const response = await request + .post(`/forms/${form._id}/feedback`) + .send(MOCK_FEEDBACK) + + // Assert + expect(response.status).toEqual(200) + expect(response.body).toEqual(expectedResponse) + }) + + it('should return 400 when form feedback submitted is malformed', async () => { + // Arrange + const { form } = await dbHandler.insertEmailForm() + const MOCK_FEEDBACK = { rating: 6 } + + // Act + const response = await request + .post(`/forms/${form._id}/feedback`) + .send(MOCK_FEEDBACK) + + // Assert + expect(response.status).toEqual(400) + expect(response.body).toEqual( + buildCelebrateError({ + body: { + key: 'rating', + message: '"rating" must be less than or equal to 5', + }, + }), + ) + }) + + it('should return 404 when form is private', async () => { + // Arrange + const { form } = await dbHandler.insertEmailForm() + const MOCK_FEEDBACK = { + rating: 5, + comment: 'great mock', + } + const expectedResponse = JSON.parse( + JSON.stringify({ + message: form.inactiveMessage, + formTitle: form.title, + isPageFound: true, + }), + ) + + // Act + const response = await request + .post(`/forms/${form._id}/feedback`) + .send(MOCK_FEEDBACK) + + // Assert + expect(response.status).toEqual(404) + expect(response.body).toEqual(expectedResponse) + }) + + it('should return 410 when form is archived', async () => { + // Arrange + const { form } = await dbHandler.insertEmailForm({ + formOptions: { + status: Status.Archived, + }, + }) + const MOCK_FEEDBACK = { + rating: 5, + comment: 'great mock', + } + const expectedResponse = JSON.parse( + JSON.stringify({ + message: getReasonPhrase(StatusCodes.GONE), + }), + ) + + // Act + const response = await request + .post(`/forms/${form._id}/feedback`) + .send(MOCK_FEEDBACK) + + // Assert + expect(response.status).toEqual(410) + expect(response.body).toEqual(expectedResponse) + }) + + it('should return 500 when form could not be retrieved due to a database error', async () => { + // Arrange + const { form } = await dbHandler.insertEmailForm({ + formOptions: { + status: Status.Public, + }, + }) + const MOCK_ERROR_MESSAGE = 'mock me' + const MOCK_FEEDBACK = { + rating: 5, + comment: 'great mock', + } + const expectedResponse = JSON.parse( + JSON.stringify({ + message: MOCK_ERROR_MESSAGE, + }), + ) + jest + .spyOn(FormService, 'retrieveFullFormById') + .mockReturnValueOnce(errAsync(new DatabaseError(MOCK_ERROR_MESSAGE))) + + // Act + const response = await request + .post(`/forms/${form._id}/feedback`) + .send(MOCK_FEEDBACK) + + // Assert + expect(response.status).toEqual(500) + expect(response.body).toEqual(expectedResponse) + }) + }) +}) 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 new file mode 100644 index 0000000000..65a0eca891 --- /dev/null +++ b/src/app/routes/api/v3/forms/__tests__/public-forms.form.routes.spec.ts @@ -0,0 +1,292 @@ +import MyInfoClient, { IMyInfoConfig } from '@opengovsg/myinfo-gov-client' +import SPCPAuthClient from '@opengovsg/spcp-auth-client' +import { ObjectId } from 'bson-ext' +import { errAsync } from 'neverthrow' +import supertest, { Session } from 'supertest-session' +import { mocked } from 'ts-jest/utils' + +import { DatabaseError } from 'src/app/modules/core/core.errors' +import { MYINFO_COOKIE_NAME } from 'src/app/modules/myinfo/myinfo.constants' +import { MyInfoCookieState } from 'src/app/modules/myinfo/myinfo.types' +import { AuthType, Status } from 'src/types' + +import { setupApp } from 'tests/integration/helpers/express-setup' +import dbHandler from 'tests/unit/backend/helpers/jest-db' + +import * as AuthService from '../../../../../modules/auth/auth.service' +import { PublicFormsRouter } from '../public-forms.routes' + +import { MOCK_UINFIN } from './public-forms.routes.spec.constants' + +jest.mock('@opengovsg/spcp-auth-client') +const MockAuthClient = mocked(SPCPAuthClient, true) + +jest.mock('@opengovsg/myinfo-gov-client', () => { + return { + MyInfoGovClient: jest.fn().mockReturnValue({ + extractUinFin: jest.fn(), + getPerson: jest.fn(), + }), + MyInfoMode: jest.requireActual('@opengovsg/myinfo-gov-client').MyInfoMode, + MyInfoSource: jest.requireActual('@opengovsg/myinfo-gov-client') + .MyInfoSource, + MyInfoAddressType: jest.requireActual('@opengovsg/myinfo-gov-client') + .MyInfoAddressType, + MyInfoAttribute: jest.requireActual('@opengovsg/myinfo-gov-client') + .MyInfoAttribute, + } +}) + +const MockMyInfoGovClient = mocked( + new MyInfoClient.MyInfoGovClient({} as IMyInfoConfig), + true, +) + +const app = setupApp('/forms', PublicFormsRouter) + +describe('public-form.form.routes', () => { + let request: Session + + const mockSpClient = mocked(MockAuthClient.mock.instances[0], true) + const mockCpClient = mocked(MockAuthClient.mock.instances[1], true) + + beforeAll(async () => await dbHandler.connect()) + beforeEach(async () => { + request = supertest(app) + }) + afterEach(async () => { + await dbHandler.clearDatabase() + jest.restoreAllMocks() + }) + afterAll(async () => await dbHandler.closeDatabase()) + describe('GET /:formId', () => { + const MOCK_COOKIE_PAYLOAD = { + userName: 'mock', + rememberMe: false, + } + + it('should return 200 with public form when form has AuthType.NIL and valid formId', async () => { + // Arrange + const { form } = await dbHandler.insertEmailForm({ + formOptions: { status: Status.Public }, + }) + // NOTE: This is needed to inject admin info into the form + const fullForm = await dbHandler.getFullFormById(form._id) + const expectedResponseBody = JSON.parse( + JSON.stringify({ + form: fullForm.getPublicView(), + isIntranetUser: false, + }), + ) + + // Act + const actualResponse = await request.get(`/forms/${form._id}`) + + // Assert + expect(actualResponse.status).toEqual(200) + expect(actualResponse.body).toEqual(expectedResponseBody) + }) + + it('should return 200 with public form when form has AuthType.SP and valid formId', async () => { + // Arrange + mockSpClient.verifyJWT.mockImplementationOnce((_jwt, cb) => + cb(null, { + userName: MOCK_COOKIE_PAYLOAD.userName, + }), + ) + const { form } = await dbHandler.insertEmailForm({ + formOptions: { + esrvcId: 'mockEsrvcId', + authType: AuthType.SP, + hasCaptcha: false, + status: Status.Public, + }, + }) + const formId = form._id + // NOTE: This is needed to inject admin info into the form + const fullForm = await dbHandler.getFullFormById(formId) + const expectedResponseBody = JSON.parse( + JSON.stringify({ + form: fullForm?.getPublicView(), + spcpSession: { userName: MOCK_COOKIE_PAYLOAD.userName }, + isIntranetUser: false, + }), + ) + + // Act + // Set cookie on request + const actualResponse = await request + .get(`/forms/${form._id}`) + .set('Cookie', ['jwtSp=mockJwt']) + + // Assert + expect(actualResponse.status).toEqual(200) + expect(actualResponse.body).toEqual(expectedResponseBody) + }) + it('should return 200 with public form when form has AuthType.CP and valid formId', async () => { + // Arrange + mockCpClient.verifyJWT.mockImplementationOnce((_jwt, cb) => + cb(null, { + userName: MOCK_COOKIE_PAYLOAD.userName, + userInfo: 'MyCorpPassUEN', + }), + ) + const { form } = await dbHandler.insertEmailForm({ + formOptions: { + esrvcId: 'mockEsrvcId', + authType: AuthType.CP, + hasCaptcha: false, + status: Status.Public, + }, + }) + const formId = form._id + // NOTE: This is needed to inject admin info into the form + const fullForm = await dbHandler.getFullFormById(formId) + const expectedResponseBody = JSON.parse( + JSON.stringify({ + form: fullForm?.getPublicView(), + spcpSession: { userName: MOCK_COOKIE_PAYLOAD.userName }, + isIntranetUser: false, + }), + ) + + // Act + // Set cookie on request + const actualResponse = await request + .get(`/forms/${form._id}`) + .set('Cookie', ['jwtCp=mockJwt']) + + // Assert + expect(actualResponse.status).toEqual(200) + expect(actualResponse.body).toEqual(expectedResponseBody) + }) + it('should return 200 with public form when form has AuthType.MyInfo and valid formId', async () => { + // Arrange + MockMyInfoGovClient.getPerson.mockResolvedValueOnce({ + uinFin: MOCK_UINFIN, + }) + const { form } = await dbHandler.insertEmailForm({ + formOptions: { + esrvcId: 'mockEsrvcId', + authType: AuthType.MyInfo, + hasCaptcha: false, + status: Status.Public, + }, + }) + // NOTE: This is needed to inject admin info into the form + const fullForm = await dbHandler.getFullFormById(form._id) + const expectedResponseBody = JSON.parse( + JSON.stringify({ + form: fullForm.getPublicView(), + spcpSession: { userName: 'S1234567A' }, + isIntranetUser: false, + }), + ) + const cookie = JSON.stringify({ + accessToken: 'mockAccessToken', + usedCount: 0, + state: MyInfoCookieState.Success, + }) + + // Act + const actualResponse = await request + .get(`/forms/${form._id}`) + .set('Cookie', [ + // The j: indicates that the cookie is in JSON + `${MYINFO_COOKIE_NAME}=j:${encodeURIComponent(cookie)}`, + ]) + + // Assert + expect(actualResponse.status).toEqual(200) + expect(actualResponse.body).toEqual(expectedResponseBody) + }) + + it('should return 404 if the form does not exist', async () => { + // Arrange + const cookie = JSON.stringify({ + accessToken: 'mockAccessToken', + usedCount: 0, + state: MyInfoCookieState.Success, + }) + 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}`) + .set('Cookie', [ + // The j: indicates that the cookie is in JSON + `${MYINFO_COOKIE_NAME}=j:${encodeURIComponent(cookie)}`, + ]) + + // 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: Status.Private }, + }) + const expectedResponseBody = JSON.parse( + JSON.stringify({ + message: form.inactiveMessage, + formTitle: form.title, + isPageFound: true, + }), + ) + + // Act + const actualResponse = await request.get(`/forms/${form._id}`) + + // 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: Status.Archived }, + }) + const expectedResponseBody = JSON.parse( + JSON.stringify({ + message: 'Gone', + }), + ) + + // Act + const actualResponse = await request.get(`/forms/${form._id}`) + + // 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: Status.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}`) + + // Assert + expect(actualResponse.status).toEqual(500) + expect(actualResponse.body).toEqual(expectedResponseBody) + }) + }) +}) diff --git a/src/app/routes/api/v3/public/__tests__/public-forms.routes.spec.constants.ts b/src/app/routes/api/v3/forms/__tests__/public-forms.routes.spec.constants.ts similarity index 100% rename from src/app/routes/api/v3/public/__tests__/public-forms.routes.spec.constants.ts rename to src/app/routes/api/v3/forms/__tests__/public-forms.routes.spec.constants.ts diff --git a/src/app/routes/api/v3/public/__tests__/public-forms.routes.spec.ts b/src/app/routes/api/v3/forms/__tests__/public-forms.submissions.routes.spec.ts similarity index 78% rename from src/app/routes/api/v3/public/__tests__/public-forms.routes.spec.ts rename to src/app/routes/api/v3/forms/__tests__/public-forms.submissions.routes.spec.ts index ff29d1744d..2908806e1e 100644 --- a/src/app/routes/api/v3/public/__tests__/public-forms.routes.spec.ts +++ b/src/app/routes/api/v3/forms/__tests__/public-forms.submissions.routes.spec.ts @@ -1,25 +1,18 @@ import MyInfoClient, { IMyInfoConfig } from '@opengovsg/myinfo-gov-client' import SPCPAuthClient from '@opengovsg/spcp-auth-client' -import { ObjectId } from 'bson-ext' -import { getReasonPhrase, StatusCodes } from 'http-status-codes' import { omit } from 'lodash' import mongoose from 'mongoose' -import { errAsync } from 'neverthrow' import supertest, { Session } from 'supertest-session' import { mocked } from 'ts-jest/utils' -import { DatabaseError } from 'src/app/modules/core/core.errors' import { MYINFO_COOKIE_NAME } from 'src/app/modules/myinfo/myinfo.constants' import { MyInfoCookieState } from 'src/app/modules/myinfo/myinfo.types' import getMyInfoHashModel from 'src/app/modules/myinfo/myinfo_hash.model' import { AuthType, IFieldSchema, Status } from 'src/types' import { setupApp } from 'tests/integration/helpers/express-setup' -import { buildCelebrateError } from 'tests/unit/backend/helpers/celebrate' import dbHandler from 'tests/unit/backend/helpers/jest-db' -import * as AuthService from '../../../../../modules/auth/auth.service' -import * as FormService from '../../../../../modules/form/form.service' import { PublicFormsRouter } from '../public-forms.routes' import { @@ -72,7 +65,7 @@ const MockMyInfoGovClient = mocked( const app = setupApp('/forms', PublicFormsRouter) -describe('public-form.routes', () => { +describe('public-form.submissions.routes', () => { let request: Session const mockSpClient = mocked(MockAuthClient.mock.instances[0], true) @@ -87,367 +80,6 @@ describe('public-form.routes', () => { jest.restoreAllMocks() }) afterAll(async () => await dbHandler.closeDatabase()) - describe('GET /:formId', () => { - const MOCK_COOKIE_PAYLOAD = { - userName: 'mock', - rememberMe: false, - } - - it('should return 200 with public form when form has AuthType.NIL and valid formId', async () => { - // Arrange - const { form } = await dbHandler.insertEmailForm({ - formOptions: { status: Status.Public }, - }) - // NOTE: This is needed to inject admin info into the form - const fullForm = await dbHandler.getFullFormById(form._id) - const expectedResponseBody = JSON.parse( - JSON.stringify({ - form: fullForm.getPublicView(), - isIntranetUser: false, - }), - ) - - // Act - const actualResponse = await request.get(`/forms/${form._id}`) - - // Assert - expect(actualResponse.status).toEqual(200) - expect(actualResponse.body).toEqual(expectedResponseBody) - }) - - it('should return 200 with public form when form has AuthType.SP and valid formId', async () => { - // Arrange - mockSpClient.verifyJWT.mockImplementationOnce((_jwt, cb) => - cb(null, { - userName: MOCK_COOKIE_PAYLOAD.userName, - }), - ) - const { form } = await dbHandler.insertEmailForm({ - formOptions: { - esrvcId: 'mockEsrvcId', - authType: AuthType.SP, - hasCaptcha: false, - status: Status.Public, - }, - }) - const formId = form._id - // NOTE: This is needed to inject admin info into the form - const fullForm = await dbHandler.getFullFormById(formId) - const expectedResponseBody = JSON.parse( - JSON.stringify({ - form: fullForm?.getPublicView(), - spcpSession: { userName: MOCK_COOKIE_PAYLOAD.userName }, - isIntranetUser: false, - }), - ) - - // Act - // Set cookie on request - const actualResponse = await request - .get(`/forms/${form._id}`) - .set('Cookie', ['jwtSp=mockJwt']) - - // Assert - expect(actualResponse.status).toEqual(200) - expect(actualResponse.body).toEqual(expectedResponseBody) - }) - it('should return 200 with public form when form has AuthType.CP and valid formId', async () => { - // Arrange - mockCpClient.verifyJWT.mockImplementationOnce((_jwt, cb) => - cb(null, { - userName: MOCK_COOKIE_PAYLOAD.userName, - userInfo: 'MyCorpPassUEN', - }), - ) - const { form } = await dbHandler.insertEmailForm({ - formOptions: { - esrvcId: 'mockEsrvcId', - authType: AuthType.CP, - hasCaptcha: false, - status: Status.Public, - }, - }) - const formId = form._id - // NOTE: This is needed to inject admin info into the form - const fullForm = await dbHandler.getFullFormById(formId) - const expectedResponseBody = JSON.parse( - JSON.stringify({ - form: fullForm?.getPublicView(), - spcpSession: { userName: MOCK_COOKIE_PAYLOAD.userName }, - isIntranetUser: false, - }), - ) - - // Act - // Set cookie on request - const actualResponse = await request - .get(`/forms/${form._id}`) - .set('Cookie', ['jwtCp=mockJwt']) - - // Assert - expect(actualResponse.status).toEqual(200) - expect(actualResponse.body).toEqual(expectedResponseBody) - }) - it('should return 200 with public form when form has AuthType.MyInfo and valid formId', async () => { - // Arrange - MockMyInfoGovClient.getPerson.mockResolvedValueOnce({ - uinFin: MOCK_UINFIN, - }) - const { form } = await dbHandler.insertEmailForm({ - formOptions: { - esrvcId: 'mockEsrvcId', - authType: AuthType.MyInfo, - hasCaptcha: false, - status: Status.Public, - }, - }) - // NOTE: This is needed to inject admin info into the form - const fullForm = await dbHandler.getFullFormById(form._id) - const expectedResponseBody = JSON.parse( - JSON.stringify({ - form: fullForm.getPublicView(), - spcpSession: { userName: 'S1234567A' }, - isIntranetUser: false, - }), - ) - const cookie = JSON.stringify({ - accessToken: 'mockAccessToken', - usedCount: 0, - state: MyInfoCookieState.Success, - }) - - // Act - const actualResponse = await request - .get(`/forms/${form._id}`) - .set('Cookie', [ - // The j: indicates that the cookie is in JSON - `${MYINFO_COOKIE_NAME}=j:${encodeURIComponent(cookie)}`, - ]) - - // Assert - expect(actualResponse.status).toEqual(200) - expect(actualResponse.body).toEqual(expectedResponseBody) - }) - - it('should return 404 if the form does not exist', async () => { - // Arrange - const cookie = JSON.stringify({ - accessToken: 'mockAccessToken', - usedCount: 0, - state: MyInfoCookieState.Success, - }) - 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}`) - .set('Cookie', [ - // The j: indicates that the cookie is in JSON - `${MYINFO_COOKIE_NAME}=j:${encodeURIComponent(cookie)}`, - ]) - - // 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: Status.Private }, - }) - const expectedResponseBody = JSON.parse( - JSON.stringify({ - message: form.inactiveMessage, - formTitle: form.title, - isPageFound: true, - }), - ) - - // Act - const actualResponse = await request.get(`/forms/${form._id}`) - - // 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: Status.Archived }, - }) - const expectedResponseBody = JSON.parse( - JSON.stringify({ - message: 'Gone', - }), - ) - - // Act - const actualResponse = await request.get(`/forms/${form._id}`) - - // 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: Status.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}`) - - // Assert - expect(actualResponse.status).toEqual(500) - expect(actualResponse.body).toEqual(expectedResponseBody) - }) - }) - describe('POST /forms/:formId/feedback', () => { - it('should return 200 when feedback was successfully saved', async () => { - // Arrange - const { form } = await dbHandler.insertEmailForm({ - formOptions: { - status: Status.Public, - }, - }) - const MOCK_FEEDBACK = { - rating: 5, - comment: 'great mock', - } - const expectedResponse = JSON.parse( - JSON.stringify({ message: 'Successfully submitted feedback' }), - ) - - // Act - const response = await request - .post(`/forms/${form._id}/feedback`) - .send(MOCK_FEEDBACK) - - // Assert - expect(response.status).toEqual(200) - expect(response.body).toEqual(expectedResponse) - }) - - it('should return 400 when form feedback submitted is malformed', async () => { - // Arrange - const { form } = await dbHandler.insertEmailForm() - const MOCK_FEEDBACK = { rating: 6 } - - // Act - const response = await request - .post(`/forms/${form._id}/feedback`) - .send(MOCK_FEEDBACK) - - // Assert - expect(response.status).toEqual(400) - expect(response.body).toEqual( - buildCelebrateError({ - body: { - key: 'rating', - message: '"rating" must be less than or equal to 5', - }, - }), - ) - }) - - it('should return 404 when form is private', async () => { - // Arrange - const { form } = await dbHandler.insertEmailForm() - const MOCK_FEEDBACK = { - rating: 5, - comment: 'great mock', - } - const expectedResponse = JSON.parse( - JSON.stringify({ - message: form.inactiveMessage, - formTitle: form.title, - isPageFound: true, - }), - ) - - // Act - const response = await request - .post(`/forms/${form._id}/feedback`) - .send(MOCK_FEEDBACK) - - // Assert - expect(response.status).toEqual(404) - expect(response.body).toEqual(expectedResponse) - }) - - it('should return 410 when form is archived', async () => { - // Arrange - const { form } = await dbHandler.insertEmailForm({ - formOptions: { - status: Status.Archived, - }, - }) - const MOCK_FEEDBACK = { - rating: 5, - comment: 'great mock', - } - const expectedResponse = JSON.parse( - JSON.stringify({ - message: getReasonPhrase(StatusCodes.GONE), - }), - ) - - // Act - const response = await request - .post(`/forms/${form._id}/feedback`) - .send(MOCK_FEEDBACK) - - // Assert - expect(response.status).toEqual(410) - expect(response.body).toEqual(expectedResponse) - }) - - it('should return 500 when form could not be retrieved due to a database error', async () => { - // Arrange - const { form } = await dbHandler.insertEmailForm({ - formOptions: { - status: Status.Public, - }, - }) - const MOCK_ERROR_MESSAGE = 'mock me' - const MOCK_FEEDBACK = { - rating: 5, - comment: 'great mock', - } - const expectedResponse = JSON.parse( - JSON.stringify({ - message: MOCK_ERROR_MESSAGE, - }), - ) - jest - .spyOn(FormService, 'retrieveFullFormById') - .mockReturnValueOnce(errAsync(new DatabaseError(MOCK_ERROR_MESSAGE))) - - // Act - const response = await request - .post(`/forms/${form._id}/feedback`) - .send(MOCK_FEEDBACK) - - // Assert - expect(response.status).toEqual(500) - expect(response.body).toEqual(expectedResponse) - }) - }) describe('POST /forms/:formId/submissions/email', () => { const mockSpClient = mocked(MockAuthClient.mock.instances[0], true) diff --git a/src/app/routes/api/v3/public/index.ts b/src/app/routes/api/v3/forms/index.ts similarity index 100% rename from src/app/routes/api/v3/public/index.ts rename to src/app/routes/api/v3/forms/index.ts diff --git a/src/app/routes/api/v3/forms/public-forms.feedback.routes.ts b/src/app/routes/api/v3/forms/public-forms.feedback.routes.ts new file mode 100644 index 0000000000..ee79212066 --- /dev/null +++ b/src/app/routes/api/v3/forms/public-forms.feedback.routes.ts @@ -0,0 +1,23 @@ +import { Router } from 'express' + +import * as PublicFormController from '../../../../modules/form/public-form/public-form.controller' + +export const PublicFormsFeedbackRouter = Router() + +/** + * Send feedback for a public form + * @route POST /:formId/feedback + * @group forms - endpoints to serve forms + * @param {string} formId.path.required - the form id + * @param {Feedback.model} feedback.body.required - the user's feedback + * @consumes application/json + * @produces application/json + * @returns 200 if feedback was successfully saved + * @returns 400 if form feedback was malformed and hence cannot be saved + * @returns 404 if form with formId does not exist or is private + * @returns 410 if form has been archived + * @returns 500 if database error occurs + */ +PublicFormsFeedbackRouter.route('/:formId([a-fA-F0-9]{24})/feedback').post( + PublicFormController.handleSubmitFeedback, +) 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 new file mode 100644 index 0000000000..515925ceec --- /dev/null +++ b/src/app/routes/api/v3/forms/public-forms.form.routes.ts @@ -0,0 +1,24 @@ +import { Router } from 'express' + +import * as PublicFormController from '../../../../modules/form/public-form/public-form.controller' + +export const PublicFormsFormRouter = Router() + +/** + * Returns the specified form to the user, along with any + * identify information obtained from Singpass/Corppass/MyInfo. + * + * WARNING: TemperatureSG batch jobs rely on this endpoint to + * retrieve the master list of personnel for daily reporting. + * Please strictly ensure backwards compatibility. + * @route GET /:formId + * + * @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.get( + '/:formId([a-fA-F0-9]{24})', + PublicFormController.handleGetPublicForm, +) diff --git a/src/app/routes/api/v3/forms/public-forms.routes.ts b/src/app/routes/api/v3/forms/public-forms.routes.ts new file mode 100644 index 0000000000..41129728c9 --- /dev/null +++ b/src/app/routes/api/v3/forms/public-forms.routes.ts @@ -0,0 +1,11 @@ +import { Router } from 'express' + +import { PublicFormsFeedbackRouter } from './public-forms.feedback.routes' +import { PublicFormsFormRouter } from './public-forms.form.routes' +import { PublicFormsSubmissionsRouter } from './public-forms.submissions.routes' + +export const PublicFormsRouter = Router() + +PublicFormsRouter.use(PublicFormsSubmissionsRouter) +PublicFormsRouter.use(PublicFormsFeedbackRouter) +PublicFormsRouter.use(PublicFormsFormRouter) diff --git a/src/app/routes/api/v3/public/public-forms.routes.ts b/src/app/routes/api/v3/forms/public-forms.submissions.routes.ts similarity index 56% rename from src/app/routes/api/v3/public/public-forms.routes.ts rename to src/app/routes/api/v3/forms/public-forms.submissions.routes.ts index c3a842b484..b226ea494a 100644 --- a/src/app/routes/api/v3/public/public-forms.routes.ts +++ b/src/app/routes/api/v3/forms/public-forms.submissions.routes.ts @@ -1,31 +1,12 @@ import { Router } from 'express' import { rateLimitConfig } from '../../../../config/config' -import * as PublicFormController from '../../../../modules/form/public-form/public-form.controller' import * as EmailSubmissionController from '../../../../modules/submission/email-submission/email-submission.controller' import * as EncryptSubmissionController from '../../../../modules/submission/encrypt-submission/encrypt-submission.controller' import { CaptchaFactory } from '../../../../services/captcha/captcha.factory' import { limitRate } from '../../../../utils/limit-rate' -export const PublicFormsRouter = Router() - -/** - * Send feedback for a public form - * @route POST /:formId/feedback - * @group forms - endpoints to serve forms - * @param {string} formId.path.required - the form id - * @param {Feedback.model} feedback.body.required - the user's feedback - * @consumes application/json - * @produces application/json - * @returns 200 if feedback was successfully saved - * @returns 400 if form feedback was malformed and hence cannot be saved - * @returns 404 if form with formId does not exist or is private - * @returns 410 if form has been archived - * @returns 500 if database error occurs - */ -PublicFormsRouter.route('/:formId([a-fA-F0-9]{24})/feedback').post( - PublicFormController.handleSubmitFeedback, -) +export const PublicFormsSubmissionsRouter = Router() /** * Submit a form response, processing it as an email to be sent to @@ -43,7 +24,9 @@ PublicFormsRouter.route('/:formId([a-fA-F0-9]{24})/feedback').post( * @returns 200 - submission made * @returns 400 - submission has bad data and could not be processed */ -PublicFormsRouter.route('/:formId([a-fA-F0-9]{24})/submissions/email').post( +PublicFormsSubmissionsRouter.route( + '/:formId([a-fA-F0-9]{24})/submissions/email', +).post( limitRate({ max: rateLimitConfig.submissions }), CaptchaFactory.validateCaptchaParams, EmailSubmissionController.handleEmailSubmission, @@ -61,27 +44,10 @@ PublicFormsRouter.route('/:formId([a-fA-F0-9]{24})/submissions/email').post( * @returns 200 - submission made * @returns 400 - submission has bad data and could not be processed */ -PublicFormsRouter.route('/:formId([a-fA-F0-9]{24})/submissions/encrypt').post( +PublicFormsSubmissionsRouter.route( + '/:formId([a-fA-F0-9]{24})/submissions/encrypt', +).post( limitRate({ max: rateLimitConfig.submissions }), CaptchaFactory.validateCaptchaParams, EncryptSubmissionController.handleEncryptedSubmission, ) - -/** - * Returns the specified form to the user, along with any - * identify information obtained from Singpass/Corppass/MyInfo. - * - * WARNING: TemperatureSG batch jobs rely on this endpoint to - * retrieve the master list of personnel for daily reporting. - * Please strictly ensure backwards compatibility. - * @route GET /:formId - * - * @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 - */ -PublicFormsRouter.get( - '/:formId([a-fA-F0-9]{24})', - PublicFormController.handleGetPublicForm, -) diff --git a/src/app/routes/api/v3/v3.routes.ts b/src/app/routes/api/v3/v3.routes.ts index f2e5c1b054..945c684441 100644 --- a/src/app/routes/api/v3/v3.routes.ts +++ b/src/app/routes/api/v3/v3.routes.ts @@ -5,8 +5,8 @@ import { AnalyticsRouter } from './analytics' import { AuthRouter } from './auth' import { BillingsRouter } from './billings' import { ClientRouter } from './client' +import { PublicFormsRouter } from './forms' import { NotificationsRouter } from './notifications' -import { PublicFormsRouter } from './public' import { UserRouter } from './user' export const V3Router = Router()