diff --git a/src/app/modules/form/admin-form/__tests__/admin-form.controller.spec.ts b/src/app/modules/form/admin-form/__tests__/admin-form.controller.spec.ts index d7f12d8910..721eb5e2dd 100644 --- a/src/app/modules/form/admin-form/__tests__/admin-form.controller.spec.ts +++ b/src/app/modules/form/admin-form/__tests__/admin-form.controller.spec.ts @@ -8052,7 +8052,7 @@ describe('admin-form.controller', () => { } as IPopulatedForm const MOCK_COLLABORATORS = [ { - email: `fakeuser@gov.sg`, + email: `fakeuser@test.gov.sg`, write: false, }, ] @@ -8214,4 +8214,162 @@ describe('admin-form.controller', () => { ).not.toHaveBeenCalled() }) }) + + describe('handleGetFormCollaborators', () => { + const MOCK_FORM_ID = new ObjectId().toHexString() + const MOCK_USER_ID = new ObjectId().toHexString() + const MOCK_USER = { + _id: MOCK_USER_ID, + email: 'somerandom@example.com', + } as IPopulatedUser + const MOCK_REQ = expressHandler.mockRequest({ + params: { + formId: MOCK_FORM_ID, + }, + session: { + user: MOCK_USER, + }, + }) + + const MOCK_COLLABORATORS = [ + { + email: `fakeuser@gov.sg`, + write: false, + }, + ] + const MOCK_FORM = { + admin: MOCK_USER, + _id: MOCK_FORM_ID, + permissionList: MOCK_COLLABORATORS, + } as IPopulatedForm + + beforeEach(() => { + // Mock various services to return expected results. + MockUserService.getPopulatedUserById.mockReturnValue(okAsync(MOCK_USER)) + MockAuthService.getFormAfterPermissionChecks.mockReturnValue( + okAsync(MOCK_FORM), + ) + }) + + it('should return 200 with the collaborators when the request is successful', async () => { + // Arrange + const mockRes = expressHandler.mockResponse() + + // Act + await AdminFormController.handleGetFormCollaborators( + MOCK_REQ, + mockRes, + jest.fn(), + ) + + // Assert + expect(mockRes.status).toBeCalledWith(StatusCodes.OK) + expect(mockRes.send).toBeCalledWith(MOCK_COLLABORATORS) + }) + + it('should return 403 when the user does not have sufficient permissions to retrieve collaborators', async () => { + // Arrange + const ERROR_MESSAGE = 'all your base are belong to us' + MockAuthService.getFormAfterPermissionChecks.mockReturnValueOnce( + errAsync(new ForbiddenFormError(ERROR_MESSAGE)), + ) + const mockRes = expressHandler.mockResponse() + const expectedResponse = { message: ERROR_MESSAGE } + + // Act + await AdminFormController.handleGetFormCollaborators( + MOCK_REQ, + mockRes, + jest.fn(), + ) + + // Assert + expect(mockRes.status).toBeCalledWith(StatusCodes.FORBIDDEN) + expect(mockRes.json).toBeCalledWith(expectedResponse) + }) + + it('should return 404 when the form could not be found', async () => { + // Arrange + const ERROR_MESSAGE = 'all your base are belong to us' + MockAuthService.getFormAfterPermissionChecks.mockReturnValueOnce( + errAsync(new FormNotFoundError(ERROR_MESSAGE)), + ) + const mockRes = expressHandler.mockResponse() + const expectedResponse = { message: ERROR_MESSAGE } + + // Act + await AdminFormController.handleGetFormCollaborators( + MOCK_REQ, + mockRes, + jest.fn(), + ) + + // Assert + expect(mockRes.status).toBeCalledWith(StatusCodes.NOT_FOUND) + expect(mockRes.json).toBeCalledWith(expectedResponse) + }) + + it('should return 410 when the form has been archived', async () => { + // Arrange + const ERROR_MESSAGE = 'all your base are belong to us' + MockAuthService.getFormAfterPermissionChecks.mockReturnValueOnce( + errAsync(new FormDeletedError(ERROR_MESSAGE)), + ) + const mockRes = expressHandler.mockResponse() + const expectedResponse = { message: ERROR_MESSAGE } + + // Act + await AdminFormController.handleGetFormCollaborators( + MOCK_REQ, + mockRes, + jest.fn(), + ) + + // Assert + expect(mockRes.status).toBeCalledWith(StatusCodes.GONE) + expect(mockRes.json).toBeCalledWith(expectedResponse) + }) + + it('should return 422 when the current user could not be retrieved from the database', async () => { + // Arrange + const ERROR_MESSAGE = 'all your base are belong to us' + MockUserService.getPopulatedUserById.mockReturnValueOnce( + errAsync(new MissingUserError(ERROR_MESSAGE)), + ) + const expectedResponse = { message: ERROR_MESSAGE } + const mockRes = expressHandler.mockResponse() + + // Act + await AdminFormController.handleGetFormCollaborators( + MOCK_REQ, + mockRes, + jest.fn(), + ) + + // Assert + expect(mockRes.status).toBeCalledWith(StatusCodes.UNPROCESSABLE_ENTITY) + expect(mockRes.json).toBeCalledWith(expectedResponse) + }) + + it('should return 500 when a database error occurs', async () => { + // Arrange + const ERROR_MESSAGE = 'all your base are belong to us' + MockUserService.getPopulatedUserById.mockReturnValueOnce( + errAsync(new DatabaseError(ERROR_MESSAGE)), + ) + const expectedResponse = { message: ERROR_MESSAGE } + const mockRes = expressHandler.mockResponse() + + // Act + await AdminFormController.handleGetFormCollaborators( + MOCK_REQ, + mockRes, + jest.fn(), + ) + + // Assert + expect(mockRes.status).toBeCalledWith(StatusCodes.INTERNAL_SERVER_ERROR) + expect(mockRes.json).toBeCalledWith(expectedResponse) + }) + }) }) diff --git a/src/app/modules/form/admin-form/admin-form.controller.ts b/src/app/modules/form/admin-form/admin-form.controller.ts index a8b5ec4087..096fe4458d 100644 --- a/src/app/modules/form/admin-form/admin-form.controller.ts +++ b/src/app/modules/form/admin-form/admin-form.controller.ts @@ -234,6 +234,54 @@ export const handleGetAdminForm: RequestHandler<{ formId: string }> = ( ) } +/** + * Handler for GET /api/v3/admin/forms/:formId/collaborators + * @security session + * + * @returns 200 with collaborators + * @returns 403 when current user does not have read permissions for the form + * @returns 404 when form cannot be found + * @returns 410 when retrieving collaborators for an archived form + * @returns 422 when user in session cannot be retrieved from the database + * @returns 500 when database error occurs + */ +export const handleGetFormCollaborators: RequestHandler< + { formId: string }, + PermissionsUpdateDto | ErrorDto +> = (req, res) => { + const { formId } = req.params + const sessionUserId = (req.session as Express.AuthedSession).user._id + + return ( + // Step 1: Retrieve currently logged in user. + UserService.getPopulatedUserById(sessionUserId) + .andThen((user) => + // Step 2: Check whether user has read permissions to form + AuthService.getFormAfterPermissionChecks({ + user, + formId, + level: PermissionLevel.Read, + }), + ) + .map(({ permissionList }) => + res.status(StatusCodes.OK).send(permissionList), + ) + .mapErr((error) => { + logger.error({ + message: 'Error retrieving form collaborators', + meta: { + action: 'handleGetFormCollaborators', + ...createReqMeta(req), + }, + error, + }) + + const { statusCode, errorMessage } = mapRouteError(error) + return res.status(statusCode).json({ message: errorMessage }) + }) + ) +} + /** * Handler for GET /:formId/adminform/preview. * @security session diff --git a/src/app/routes/api/v3/admin/forms/__tests__/admin-forms.settings.routes.spec.ts b/src/app/routes/api/v3/admin/forms/__tests__/admin-forms.settings.routes.spec.ts index fdd632fde5..dbdd3bcd67 100644 --- a/src/app/routes/api/v3/admin/forms/__tests__/admin-forms.settings.routes.spec.ts +++ b/src/app/routes/api/v3/admin/forms/__tests__/admin-forms.settings.routes.spec.ts @@ -12,6 +12,7 @@ import { createAuthedSession } from 'tests/integration/helpers/express-auth' 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 { jsonParseStringify } from 'tests/unit/backend/helpers/serialize-data' import { jsonParseStringify } from '../../../../../../../../tests/unit/backend/helpers/serialize-data' import * as UserService from '../../../../../../modules/user/user.service' @@ -396,4 +397,139 @@ describe('admin-form.settings.routes', () => { expect(response.body).toEqual(expectedResponse) }) }) + + describe('GET /admin/forms/:formId/collaborators', () => { + const MOCK_COLLABORATORS = [ + { + email: `fakeuser@test.gov.sg`, + write: false, + }, + ] + it('should return the list of collaborators on a valid request', async () => { + // Arrange + const { form, user } = await dbHandler.insertEmailForm({ + formOptions: { + permissionList: MOCK_COLLABORATORS, + }, + }) + const session = await createAuthedSession(user.email, request) + + // Act + const response = await session.get( + `/admin/forms/${form._id}/collaborators`, + ) + + // Assert + expect(response.status).toEqual(200) + expect(response.body).toMatchObject( + jsonParseStringify(MOCK_COLLABORATORS), + ) + }) + + it('should return 403 when the current user does not have read permissions for the specified form', async () => { + // Arrange + const { form } = await dbHandler.insertEmailForm({ + formOptions: { + permissionList: MOCK_COLLABORATORS, + }, + }) + const fakeUser = await dbHandler.insertUser({ + mailName: 'fakeUser', + agencyId: new ObjectId(), + }) + const session = await createAuthedSession(fakeUser.email, request) + const expectedResponse = jsonParseStringify({ + message: `User ${fakeUser.email} not authorized to perform read operation on Form ${form._id} with title: ${form.title}.`, + }) + + // Act + const response = await session.get( + `/admin/forms/${form._id}/collaborators`, + ) + + // Assert + expect(response.status).toEqual(403) + expect(response.body).toMatchObject(jsonParseStringify(expectedResponse)) + }) + + it('should return 404 when the form could not be found', async () => { + // Arrange + const { user } = await dbHandler.insertEmailForm() + const session = await createAuthedSession(user.email, request) + const expectedResponse = jsonParseStringify({ + message: 'Form not found', + }) + + // Act + const response = await session.get( + `/admin/forms/${new ObjectId().toHexString()}/collaborators`, + ) + + // Assert + expect(response.status).toEqual(404) + expect(response.body).toEqual(expectedResponse) + }) + + it('should return 410 when the form has been archived', async () => { + // Arrange + const { form, user } = await dbHandler.insertEmailForm({ + formOptions: { + status: Status.Archived, + }, + }) + const session = await createAuthedSession(user.email, request) + const expectedResponse = jsonParseStringify({ + message: 'Form has been archived', + }) + + // Act + const response = await session.get( + `/admin/forms/${form._id}/collaborators`, + ) + + // Assert + expect(response.status).toEqual(410) + expect(response.body).toEqual(expectedResponse) + }) + + it('should return 422 when the current session user cannot be retrieved', async () => { + // Arrange + const { form, user } = await dbHandler.insertEmailForm() + const session = await createAuthedSession(user.email, request) + const expectedResponse = jsonParseStringify({ + message: 'User not found', + }) + await dbHandler.clearCollection(UserModel.collection.name) + + // Act + const response = await session.get( + `/admin/forms/${form._id}/collaborators`, + ) + + // Assert + expect(response.status).toEqual(422) + expect(response.body).toEqual(expectedResponse) + }) + + it('should return 500 when a database error occurs', async () => { + // Arrange + const { form, user } = await dbHandler.insertEmailForm() + const session = await createAuthedSession(user.email, request) + const expectedResponse = jsonParseStringify({ + message: 'Something went wrong. Please try again.', + }) + jest + .spyOn(UserService, 'getPopulatedUserById') + .mockReturnValueOnce(errAsync(new DatabaseError())) + + // Act + const response = await session.get( + `/admin/forms/${form._id}/collaborators`, + ) + + // Assert + expect(response.status).toEqual(500) + expect(response.body).toEqual(expectedResponse) + }) + }) }) diff --git a/src/app/routes/api/v3/admin/forms/admin-forms.settings.routes.ts b/src/app/routes/api/v3/admin/forms/admin-forms.settings.routes.ts index d757d62114..5768e55e89 100644 --- a/src/app/routes/api/v3/admin/forms/admin-forms.settings.routes.ts +++ b/src/app/routes/api/v3/admin/forms/admin-forms.settings.routes.ts @@ -64,20 +64,35 @@ AdminFormsSettingsRouter.route('/:formId([a-fA-F0-9]{24})/settings') */ .get(AdminFormController.handleGetSettings) -/** - * Updates the collaborator list for a given formId - * @route GET /admin/forms/:formId/collaborators - * @group admin - * @precondition Must be preceded by request validation - * @security session - * - * @returns 200 with updated collaborators and permissions - * @returns 403 when current user does not have permissions to update the collaborators - * @returns 404 when form cannot be found - * @returns 410 when updating collaborators for an archived form - * @returns 422 when user in session cannot be retrieved from the database - * @returns 500 when database error occurs - */ -AdminFormsSettingsRouter.route('/:formId([a-fA-F0-9]{24})/collaborators').put( - AdminFormController.handleUpdateCollaborators, -) +AdminFormsSettingsRouter.route('/:formId([a-fA-F0-9]{24})/collaborators') + /** + * Updates the collaborator list for a given formId + * @route PUT /admin/forms/:formId/collaborators + * @group admin + * @precondition Must be preceded by request validation + * @security session + * + * @returns 200 with updated collaborators and permissions + * @returns 403 when current user does not have permissions to update the collaborators + * @returns 404 when form cannot be found + * @returns 410 when updating collaborators for an archived form + * @returns 422 when user in session cannot be retrieved from the database + * @returns 500 when database error occurs + */ + .put(AdminFormController.handleUpdateCollaborators) + /** + * Retrieves the collaborators for a given formId + * @route GET /admin/forms/:formId/collaborators + * @group admin + * @precondition Must be preceded by request validation + * @security session + + * + * @returns 200 with collaborators + * @returns 403 when current user does not have read permissions for the form + * @returns 404 when form cannot be found + * @returns 410 when retrieving collaborators for an archived form + * @returns 422 when user in session cannot be retrieved from the database + * @returns 500 when database error occurs + */ + .get(AdminFormController.handleGetFormCollaborators)