From d46970f539b48d99433733389fac3a477004d05b Mon Sep 17 00:00:00 2001 From: Walter Moar Date: Wed, 15 May 2024 14:48:59 -0700 Subject: [PATCH 01/14] refactor: FORMS-1241 clean up hasSubmissionPermissions (#1355) * refactor: FORMS-1241 clean up hasSubmissionPermissions * restored the use of detail for the Problems --- app/src/forms/auth/middleware/userAccess.js | 67 +- .../forms/auth/middleware/userAccess.spec.js | 587 ++++++++++-------- 2 files changed, 382 insertions(+), 272 deletions(-) diff --git a/app/src/forms/auth/middleware/userAccess.js b/app/src/forms/auth/middleware/userAccess.js index d3c22e0fd..ff46778e6 100644 --- a/app/src/forms/auth/middleware/userAccess.js +++ b/app/src/forms/auth/middleware/userAccess.js @@ -59,9 +59,8 @@ const _getForm = async (currentUser, formId, includeDeleted) => { /** * Express middleware that adds the user information as the res.currentUser * attribute so that all downstream middleware and business logic can use it. - * - * This will fall through if everything is OK. If the Bearer auth is not valid, - * this will produce a 401 error. + * This will fall through if everything is OK, otherwise it will call next() + * with a Problem describing the error. * * @param {*} req the Express object representing the HTTP request. * @param {*} _res the Express object representing the HTTP response - unused. @@ -92,7 +91,7 @@ const currentUser = async (req, _res, next) => { /** * Express middleware to check that a user has all the given permissions for a * form. This will fall through if everything is OK, otherwise it will call - * next() with a Problem that describes the error. + * next() with a Problem describing the error. * * @param {string[]} permissions the form permissions that the user must have. * @returns nothing @@ -133,29 +132,41 @@ const hasFormPermissions = (permissions) => { }; }; +/** + * Express middleware to check that the caller has the given permissions for the + * submission identified by params.formSubmissionId or query.formSubmissionId. + * This will fall through if everything is OK, otherwise it will call next() + * with a Problem describing the error. + * + * @param {string[]} permissions the access the user needs for the submission + * @returns nothing + */ const hasSubmissionPermissions = (permissions) => { return async (req, _res, next) => { try { - // Skip permission checks if requesting as API entity + // Skip permission checks if req is already authorized using an API key. if (req.apiUser) { - return next(); + next(); + + return; } if (!Array.isArray(permissions)) { permissions = [permissions]; } - // Get the provided submission ID whether in a param or query (precedence to param) + // The request must include a formSubmissionId, either in params or query, + // but give precedence to params. const submissionId = req.params.formSubmissionId || req.query.formSubmissionId; - if (!submissionId) { - // No submission provided to this route that secures based on form... that's a problem! - return next(new Problem(401, { detail: 'Submission Id not found on request.' })); + if (!uuid.validate(submissionId)) { + throw new Problem(400, { detail: 'Bad formSubmissionId' }); } - // Get the submission results so we know what form this submission is for + // Get the submission results so we know what form this submission is for. const submissionForm = await service.getSubmissionForm(submissionId); - // Does the user have permissions for this submission due to their FORM permissions + // If the current user has elevated permissions on the form, they may have + // access to all submissions for the form. if (req.currentUser) { const forms = await service.getUserForms(req.currentUser, { active: true, @@ -169,38 +180,34 @@ const hasSubmissionPermissions = (permissions) => { }); if (intersection.length === permissions.length) { req.formIdWithDeletePermission = submissionForm.form.id; - return next(); + next(); + + return; } } } - // Deleted submissions are inaccessible + // Deleted submissions are only accessible to users with the form + // permissions above. if (submissionForm.submission.deleted) { - return next( - new Problem(401, { - detail: 'You do not have access to this submission.', - }) - ); + throw new Problem(401, { detail: 'You do not have access to this submission.' }); } - // TODO: consider whether DRAFT submissions are restricted as deleted above - - // Public (annonymous) forms are publicly viewable + // Public (anonymous) forms are publicly viewable. const publicAllowed = submissionForm.form.identityProviders.find((p) => p.code === 'public') !== undefined; if (permissions.length === 1 && permissions.includes(Permissions.SUBMISSION_READ) && publicAllowed) { - return next(); + next(); + + return; } // check against the submission level permissions assigned to the user... const submissionPermission = await service.checkSubmissionPermission(req.currentUser, submissionId, permissions); - if (submissionPermission) return next(); + if (!submissionPermission) { + throw new Problem(401, { detail: 'You do not have access to this submission.' }); + } - // no access to this submission... - return next( - new Problem(401, { - detail: 'You do not have access to this submission.', - }) - ); + next(); } catch (error) { next(error); } diff --git a/app/tests/unit/forms/auth/middleware/userAccess.spec.js b/app/tests/unit/forms/auth/middleware/userAccess.spec.js index 3634d2af1..910dc4ade 100644 --- a/app/tests/unit/forms/auth/middleware/userAccess.spec.js +++ b/app/tests/unit/forms/auth/middleware/userAccess.spec.js @@ -351,355 +351,458 @@ describe('hasFormPermissions', () => { }); }); +// External dependencies used by the implementation are: +// - service.checkSubmissionPermission: gets whether the user has permission +// - service.getSubmissionForm: gets the submission that the user can access +// - service.getUserForms: gets the forms that the user can access +// describe('hasSubmissionPermissions', () => { + // Default mock value where the user has no access to submission + service.checkSubmissionPermission = jest.fn().mockReturnValue(false); + + // Default mock value where there is no submission + service.getSubmissionForm = jest.fn().mockReturnValue(); + + // Default mock value where the user has no access to forms + service.getUserForms = jest.fn().mockReturnValue([]); + it('returns a middleware function', () => { - const mw = hasSubmissionPermissions(['abc']); - expect(mw).toBeInstanceOf(Function); + const middleware = hasSubmissionPermissions(['submission_read']); + + expect(middleware).toBeInstanceOf(Function); }); - it('moves on if a valid API key user has already been set', async () => { - const mw = hasSubmissionPermissions(['abc']); - const nxt = jest.fn(); - const req = { - apiUser: 1, - }; + it('400s if the request has no formSubmissionId', async () => { + const req = getMockReq(); + const { res, next } = getMockRes(); - mw(req, testRes, nxt); - expect(nxt).toHaveBeenCalledTimes(1); - expect(nxt).toHaveBeenCalledWith(); + await hasSubmissionPermissions(['submission_read'])(req, res, next); + + expect(service.checkSubmissionPermission).toHaveBeenCalledTimes(0); + expect(service.getSubmissionForm).toHaveBeenCalledTimes(0); + expect(service.getUserForms).toHaveBeenCalledTimes(0); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(expect.objectContaining({ status: 400 })); }); - it('401s if the request has no formId', async () => { - const mw = hasSubmissionPermissions(['abc']); - const nxt = jest.fn(); - const req = { + it('400s if the formSubmissionId is not a uuid', async () => { + const req = getMockReq({ + currentUser: {}, params: { - formId: 123, - }, - query: { - otherQueryThing: 'abc', + formSubmissionId: 'not-a-uuid', }, - }; + }); + const { res, next } = getMockRes(); - await mw(req, testRes, nxt); - expect(nxt).toHaveBeenCalledTimes(1); - expect(nxt).toHaveBeenCalledWith(new Problem(401, { detail: 'Submission Id not found on request.' })); - }); + await hasSubmissionPermissions(['submission_read'])(req, res, next); - it('401s if the submission was deleted', async () => { - service.getSubmissionForm = jest.fn().mockReturnValue({ submission: { deleted: true } }); + expect(service.checkSubmissionPermission).toHaveBeenCalledTimes(0); + expect(service.getSubmissionForm).toHaveBeenCalledTimes(0); + expect(service.getUserForms).toHaveBeenCalledTimes(0); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(expect.objectContaining({ status: 400 })); + }); - const mw = hasSubmissionPermissions(['abc']); - const nxt = jest.fn(); - const req = { + it('401s for deleted submission when no current user', async () => { + service.getSubmissionForm.mockReturnValueOnce({ + form: { id: formId }, + submission: { deleted: true, id: formSubmissionId }, + }); + const req = getMockReq({ params: { - formSubmissionId: 123, + formSubmissionId: formSubmissionId, }, - }; + }); + const { res, next } = getMockRes(); - await mw(req, testRes, nxt); + await hasSubmissionPermissions(['submission_read'])(req, res, next); + + expect(service.checkSubmissionPermission).toHaveBeenCalledTimes(0); expect(service.getSubmissionForm).toHaveBeenCalledTimes(1); - expect(service.getSubmissionForm).toHaveBeenCalledWith(123); - expect(nxt).toHaveBeenCalledTimes(1); - expect(nxt).toHaveBeenCalledWith(new Problem(401, { detail: 'You do not have access to this submission.' })); + expect(service.getUserForms).toHaveBeenCalledTimes(0); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(expect.objectContaining({ status: 401 })); }); - it('moves on if the form is public and you are only requesting read permission', async () => { - service.getSubmissionForm = jest.fn().mockReturnValue({ - submission: { deleted: false }, - form: { identityProviders: [{ code: 'random' }, { code: 'public' }] }, + it('401s for deleted submission', async () => { + service.getSubmissionForm.mockReturnValueOnce({ + form: { id: formId }, + submission: { deleted: true, id: formSubmissionId }, }); - - const mw = hasSubmissionPermissions('submission_read'); - const nxt = jest.fn(); - const req = { + const req = getMockReq({ + currentUser: {}, params: { - formSubmissionId: 123, + formSubmissionId: formSubmissionId, }, - }; + }); + const { res, next } = getMockRes(); - await mw(req, testRes, nxt); - expect(nxt).toHaveBeenCalledTimes(1); - expect(nxt).toHaveBeenCalledWith(); + await hasSubmissionPermissions(['submission_read'])(req, res, next); + + expect(service.checkSubmissionPermission).toHaveBeenCalledTimes(0); + expect(service.getSubmissionForm).toHaveBeenCalledTimes(1); + expect(service.getUserForms).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(expect.objectContaining({ status: 401 })); }); - it('moves on if the form is public and you are only requesting read permission (only 1 idp)', async () => { - service.getSubmissionForm = jest.fn().mockReturnValue({ - submission: { deleted: false }, - form: { identityProviders: [{ code: 'public' }] }, + it('401s for deleted submission if user has no forms', async () => { + service.getSubmissionForm.mockReturnValueOnce({ + form: { id: formId }, + submission: { deleted: true, id: formSubmissionId }, }); - - const mw = hasSubmissionPermissions(['submission_read']); - const nxt = jest.fn(); - const req = { + const req = getMockReq({ + currentUser: {}, params: { - formSubmissionId: 123, + formSubmissionId: formSubmissionId, }, - }; + }); + const { res, next } = getMockRes(); - await mw(req, testRes, nxt); - expect(nxt).toHaveBeenCalledTimes(1); - expect(nxt).toHaveBeenCalledWith(); + await hasSubmissionPermissions(['submission_read'])(req, res, next); + + expect(service.checkSubmissionPermission).toHaveBeenCalledTimes(0); + expect(service.getSubmissionForm).toHaveBeenCalledTimes(1); + expect(service.getUserForms).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(expect.objectContaining({ status: 401 })); }); - it('does not allow public access if more than read permission is needed', async () => { - service.getSubmissionForm = jest.fn().mockReturnValue({ - submission: { deleted: false }, - form: { identityProviders: [{ code: 'public' }] }, + it('401s for deleted submission if user has no form access', async () => { + service.getSubmissionForm.mockReturnValueOnce({ + form: { id: formId }, + submission: { deleted: true, id: formSubmissionId }, }); - service.checkSubmissionPermission = jest.fn().mockReturnValue(undefined); - - const mw = hasSubmissionPermissions(['submission_read', 'submission_delete']); - const nxt = jest.fn(); - const req = { + service.getUserForms.mockReturnValueOnce([ + { + formId: formId, + permissions: [], + }, + ]); + const req = getMockReq({ + currentUser: {}, params: { - formSubmissionId: 123, + formSubmissionId: formSubmissionId, }, - }; + }); + const { res, next } = getMockRes(); - await mw(req, testRes, nxt); - // just run to the end and fall into the base case - expect(service.checkSubmissionPermission).toHaveBeenCalledTimes(1); - expect(service.checkSubmissionPermission).toHaveBeenCalledWith(undefined, 123, ['submission_read', 'submission_delete']); - expect(nxt).toHaveBeenCalledTimes(1); - expect(nxt).toHaveBeenCalledWith(new Problem(401, { detail: 'You do not have access to this submission.' })); + await hasSubmissionPermissions(['submission_read'])(req, res, next); + + expect(service.checkSubmissionPermission).toHaveBeenCalledTimes(0); + expect(service.getSubmissionForm).toHaveBeenCalledTimes(1); + expect(service.getUserForms).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(expect.objectContaining({ status: 401 })); }); - it('does not allow public access if the form does not have the public idp', async () => { - service.getSubmissionForm = jest.fn().mockReturnValue({ - submission: { deleted: false }, - form: { identityProviders: [{ code: 'idir' }, { code: 'bceid' }] }, + it('401s for deleted submission if user only has some form access', async () => { + service.getSubmissionForm.mockReturnValueOnce({ + form: { id: formId }, + submission: { deleted: true, id: formSubmissionId }, }); - service.checkSubmissionPermission = jest.fn().mockReturnValue(undefined); - - const mw = hasSubmissionPermissions('submission_read'); - const nxt = jest.fn(); - const req = { + service.getUserForms.mockReturnValueOnce([ + { + formId: formId, + permissions: ['submission_read'], + }, + ]); + const req = getMockReq({ + currentUser: {}, params: { - formSubmissionId: 123, + formSubmissionId: formSubmissionId, }, - }; + }); + const { res, next } = getMockRes(); - await mw(req, testRes, nxt); - // just run to the end and fall into the base case - expect(service.checkSubmissionPermission).toHaveBeenCalledTimes(1); - expect(service.checkSubmissionPermission).toHaveBeenCalledWith(undefined, 123, ['submission_read']); - expect(nxt).toHaveBeenCalledTimes(1); - expect(nxt).toHaveBeenCalledWith(new Problem(401, { detail: 'You do not have access to this submission.' })); + await hasSubmissionPermissions(['submission_read', 'submission_update'])(req, res, next); + + expect(service.checkSubmissionPermission).toHaveBeenCalledTimes(0); + expect(service.getSubmissionForm).toHaveBeenCalledTimes(1); + expect(service.getUserForms).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(expect.objectContaining({ status: 401 })); }); - it('moves on if the permission check query succeeds', async () => { - service.getSubmissionForm = jest.fn().mockReturnValue({ - submission: { deleted: false }, - form: { identityProviders: [{ code: 'idir' }, { code: 'bceid' }] }, + it('401s on submission permissions if public access and not read permission', async () => { + service.checkSubmissionPermission.mockReturnValueOnce(undefined); + service.getSubmissionForm.mockReturnValueOnce({ + form: { id: formId, identityProviders: [{ code: 'public' }] }, + submission: { deleted: false, id: formSubmissionId }, }); - service.checkSubmissionPermission = jest.fn().mockReturnValue(true); - - const mw = hasSubmissionPermissions('submission_read'); - const nxt = jest.fn(); - const req = { + const req = getMockReq({ + currentUser: {}, params: { - formSubmissionId: 123, + formSubmissionId: formSubmissionId, }, - }; + }); + const { res, next } = getMockRes(); - await mw(req, testRes, nxt); - expect(nxt).toHaveBeenCalledTimes(1); - expect(nxt).toHaveBeenCalledWith(); + await hasSubmissionPermissions(['submission_delete'])(req, res, next); + + expect(service.checkSubmissionPermission).toHaveBeenCalledTimes(1); + expect(service.getSubmissionForm).toHaveBeenCalledTimes(1); + expect(service.getUserForms).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(expect.objectContaining({ status: 401 })); }); - it('falls through to the query if the current user has no forms', async () => { - service.getSubmissionForm = jest.fn().mockReturnValue({ - submission: { deleted: false }, - form: { identityProviders: [{ code: 'idir' }, { code: 'bceid' }] }, + it('401s on submission permissions if public access and more than read permission', async () => { + service.getSubmissionForm.mockReturnValueOnce({ + form: { id: formId, identityProviders: [{ code: 'public' }] }, + submission: { deleted: false, id: formSubmissionId }, }); - service.checkSubmissionPermission = jest.fn().mockReturnValue(undefined); - - const mw = hasSubmissionPermissions('submission_read'); - const nxt = jest.fn(); - const cu = {}; - const req = { - currentUser: cu, + const req = getMockReq({ + currentUser: {}, params: { - formSubmissionId: 123, + formSubmissionId: formSubmissionId, }, - }; + }); + const { res, next } = getMockRes(); + + await hasSubmissionPermissions(['submission_read', 'submission_delete'])(req, res, next); - await mw(req, testRes, nxt); - // just run to the end and fall into the base case expect(service.checkSubmissionPermission).toHaveBeenCalledTimes(1); - expect(service.checkSubmissionPermission).toHaveBeenCalledWith(cu, 123, ['submission_read']); - expect(nxt).toHaveBeenCalledTimes(1); - expect(nxt).toHaveBeenCalledWith(new Problem(401, { detail: 'You do not have access to this submission.' })); + expect(service.getSubmissionForm).toHaveBeenCalledTimes(1); + expect(service.getUserForms).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(expect.objectContaining({ status: 401 })); }); - it('falls through to the query if the current user has no forms', async () => { - service.checkSubmissionPermission = jest.fn().mockReturnValue(undefined); - service.getSubmissionForm = jest.fn().mockReturnValue({ - submission: { id: 456, deleted: false }, - form: { identityProviders: [{ code: 'idir' }, { code: 'bceid' }] }, + it('401 on submission permissions if form does not have the public idp', async () => { + service.getSubmissionForm.mockReturnValueOnce({ + form: { + id: formId, + identityProviders: [{ code: 'idir' }, { code: 'bceid' }], + }, + submission: { deleted: false, id: formSubmissionId }, }); - service.getUserForms = jest.fn().mockReturnValue([]); - - const nxt = jest.fn(); - const req = { + const req = getMockReq({ currentUser: {}, params: { - formSubmissionId: 123, + formSubmissionId: formSubmissionId, }, - }; + }); + const { res, next } = getMockRes(); - const mw = hasSubmissionPermissions('submission_read'); - await mw(req, testRes, nxt); + await hasSubmissionPermissions(['submission_read'])(req, res, next); - // just run to the end and fall into the base case expect(service.checkSubmissionPermission).toHaveBeenCalledTimes(1); - expect(service.checkSubmissionPermission).toHaveBeenCalledWith(req.currentUser, 123, ['submission_read']); - expect(nxt).toHaveBeenCalledTimes(1); - expect(nxt).toHaveBeenCalledWith(new Problem(401, { detail: 'You do not have access to this submission.' })); + expect(service.getSubmissionForm).toHaveBeenCalledTimes(1); + expect(service.getUserForms).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(expect.objectContaining({ status: 401 })); }); - it('falls through to the query if the current user does not have any FORM access on the current form', async () => { - service.getSubmissionForm = jest.fn().mockReturnValue({ - submission: { deleted: false }, - form: { id: '999', identityProviders: [{ code: 'idir' }, { code: 'bceid' }] }, + it('goes to error handler when getSubmissionForm errors', async () => { + const error = new Error(); + service.getSubmissionForm.mockRejectedValueOnce(error); + const req = getMockReq({ + currentUser: {}, + params: { + formSubmissionId: formSubmissionId, + }, }); - service.checkSubmissionPermission = jest.fn().mockReturnValue(undefined); + const { res, next } = getMockRes(); - const mw = hasSubmissionPermissions('submission_read'); - const nxt = jest.fn(); - const cu = {}; - const req = { - currentUser: cu, + await hasSubmissionPermissions(['submission_read'])(req, res, next); + + expect(service.checkSubmissionPermission).toHaveBeenCalledTimes(0); + expect(service.getSubmissionForm).toHaveBeenCalledTimes(1); + expect(service.getUserForms).toHaveBeenCalledTimes(0); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(error); + }); + + it('goes to error handler when getUserForms errors', async () => { + service.getSubmissionForm.mockReturnValueOnce({ + form: { id: formId }, + submission: { submissionId: formSubmissionId }, + }); + const error = new Error(); + service.getUserForms.mockRejectedValueOnce(error); + const req = getMockReq({ + currentUser: {}, params: { - formSubmissionId: 123, + formSubmissionId: formSubmissionId, }, - }; + }); + const { res, next } = getMockRes(); - await mw(req, testRes, nxt); - // just run to the end and fall into the base case - expect(service.checkSubmissionPermission).toHaveBeenCalledTimes(1); - expect(service.checkSubmissionPermission).toHaveBeenCalledWith(cu, 123, ['submission_read']); - expect(nxt).toHaveBeenCalledTimes(1); - expect(nxt).toHaveBeenCalledWith(new Problem(401, { detail: 'You do not have access to this submission.' })); + await hasSubmissionPermissions(['submission_read'])(req, res, next); + + expect(service.checkSubmissionPermission).toHaveBeenCalledTimes(0); + expect(service.getSubmissionForm).toHaveBeenCalledTimes(1); + expect(service.getUserForms).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(error); }); - it('falls through to the query if the current user does not have the expected permission for FORM access on the current form', async () => { - service.getSubmissionForm = jest.fn().mockReturnValue({ - submission: { deleted: false }, - form: { - id: '999', - identityProviders: [{ code: 'idir' }, { code: 'bceid' }], + it('moves on if a valid API key user has already been set', async () => { + const req = getMockReq({ + apiUser: true, + params: { + formSubmissionId: formSubmissionId, }, }); - service.checkSubmissionPermission = jest.fn().mockReturnValue(undefined); + const { res, next } = getMockRes(); - const mw = hasSubmissionPermissions(['submission_delete', 'submission_create']); - const nxt = jest.fn(); - const cu = {}; - const req = { - currentUser: cu, + await hasSubmissionPermissions(['submission_read'])(req, res, next); + + expect(service.checkSubmissionPermission).toHaveBeenCalledTimes(0); + expect(service.getSubmissionForm).toHaveBeenCalledTimes(0); + expect(service.getUserForms).toHaveBeenCalledTimes(0); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(); + }); + + it('moves on if the user has the exact form permissions', async () => { + service.getSubmissionForm.mockReturnValueOnce({ + form: { id: formId }, + submission: { deleted: false, id: formSubmissionId }, + }); + service.getUserForms.mockReturnValueOnce([ + { + // Ignore this form but match formId on the next one. + formId: uuid.v4(), + }, + { + formId: formId, + permissions: ['submission_read', 'submission_update'], + }, + ]); + const req = getMockReq({ + currentUser: {}, params: { - formSubmissionId: 123, + formSubmissionId: formSubmissionId, }, - }; + }); + const { res, next } = getMockRes(); - await mw(req, testRes, nxt); - // just run to the end and fall into the base case - expect(service.checkSubmissionPermission).toHaveBeenCalledTimes(1); - expect(service.checkSubmissionPermission).toHaveBeenCalledWith(cu, 123, ['submission_delete', 'submission_create']); - expect(nxt).toHaveBeenCalledTimes(1); - expect(nxt).toHaveBeenCalledWith(new Problem(401, { detail: 'You do not have access to this submission.' })); + await hasSubmissionPermissions(['submission_read', 'submission_update'])(req, res, next); + + expect(service.checkSubmissionPermission).toHaveBeenCalledTimes(0); + expect(service.getSubmissionForm).toHaveBeenCalledTimes(1); + expect(service.getUserForms).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(); }); - it('falls through to the query if the current user does not have the expected permission for FORM access on the current form (single check)', async () => { - service.getSubmissionForm = jest.fn().mockReturnValue({ - submission: { deleted: false }, - form: { - id: '999', - identityProviders: [{ code: 'idir' }, { code: 'bceid' }], + it('moves on if the user has extra form permissions', async () => { + service.getSubmissionForm.mockReturnValueOnce({ + form: { id: formId }, + submission: { deleted: false, id: formSubmissionId }, + }); + service.getUserForms.mockReturnValueOnce([ + { + // Ignore this form but match formId on the next one. + formId: uuid.v4(), + }, + { + formId: formId, + permissions: ['submission_read', 'submission_update'], + }, + ]); + const req = getMockReq({ + currentUser: {}, + params: { + formSubmissionId: formSubmissionId, }, }); - service.checkSubmissionPermission = jest.fn().mockReturnValue(undefined); + const { res, next } = getMockRes(); - const mw = hasSubmissionPermissions('submission_delete'); - const nxt = jest.fn(); - const cu = {}; - const req = { - currentUser: cu, + await hasSubmissionPermissions(['submission_update'])(req, res, next); + + expect(service.checkSubmissionPermission).toHaveBeenCalledTimes(0); + expect(service.getSubmissionForm).toHaveBeenCalledTimes(1); + expect(service.getUserForms).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(); + }); + + it('moves on for public form and read permission', async () => { + service.getSubmissionForm.mockReturnValueOnce({ + form: { id: formId, identityProviders: [{ code: 'public' }] }, + submission: { deleted: false, id: formSubmissionId }, + }); + service.getUserForms.mockReturnValueOnce([ + { + formId: formId, + permissions: [], + }, + ]); + const req = getMockReq({ + currentUser: {}, params: { - formSubmissionId: 123, + formSubmissionId: formSubmissionId, }, - }; + }); + const { res, next } = getMockRes(); - await mw(req, testRes, nxt); - // just run to the end and fall into the base case - expect(service.checkSubmissionPermission).toHaveBeenCalledTimes(1); - expect(service.checkSubmissionPermission).toHaveBeenCalledWith(cu, 123, ['submission_delete']); - expect(nxt).toHaveBeenCalledTimes(1); - expect(nxt).toHaveBeenCalledWith(new Problem(401, { detail: 'You do not have access to this submission.' })); + await hasSubmissionPermissions(['submission_read'])(req, res, next); + + expect(service.checkSubmissionPermission).toHaveBeenCalledTimes(0); + expect(service.getSubmissionForm).toHaveBeenCalledTimes(1); + expect(service.getUserForms).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(); }); - it('moves on if the user has the appropriate requested permissions', async () => { - service.getSubmissionForm = jest.fn().mockReturnValue({ - submission: { deleted: false }, + it('moves on for public form and read permission with extra idp', async () => { + service.getSubmissionForm.mockReturnValueOnce({ form: { - id: '999', - identityProviders: [{ code: 'idir' }, { code: 'bceid' }], + id: formId, + identityProviders: [{ code: 'random' }, { code: 'public' }], }, + submission: { deleted: false, id: formSubmissionId }, }); - service.getUserForms = jest.fn().mockReturnValue([ - { - formId: '456', - }, + service.getUserForms.mockReturnValueOnce([ { - formId: '999', - permissions: ['submission_read', 'submission_update'], + formId: formId, + permissions: [], }, ]); - service.checkSubmissionPermission = jest.fn().mockReturnValue(undefined); - - const mw = hasSubmissionPermissions(['submission_read', 'submission_update']); - const nxt = jest.fn(); - const req = { + const req = getMockReq({ currentUser: {}, params: { - formSubmissionId: 123, + formSubmissionId: formSubmissionId, }, - }; + }); + const { res, next } = getMockRes(); + + await hasSubmissionPermissions(['submission_read'])(req, res, next); - await mw(req, testRes, nxt); - // just run to the end and fall into the base case expect(service.checkSubmissionPermission).toHaveBeenCalledTimes(0); - expect(nxt).toHaveBeenCalledTimes(1); - expect(nxt).toHaveBeenCalledWith(); + expect(service.getSubmissionForm).toHaveBeenCalledTimes(1); + expect(service.getUserForms).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(); }); - it('moves on if the user has the appropriate requested permissions (single included in array)', async () => { - service.getSubmissionForm = jest.fn().mockReturnValue({ - submission: { deleted: false }, + it('moves on if the permission check succeeds', async () => { + service.checkSubmissionPermission.mockReturnValueOnce(true); + service.getSubmissionForm.mockReturnValueOnce({ form: { - id: '999', + id: formId, identityProviders: [{ code: 'idir' }, { code: 'bceid' }], }, + submission: { deleted: false, id: formSubmissionId }, }); - service.checkSubmissionPermission = jest.fn().mockReturnValue(undefined); - - const mw = hasSubmissionPermissions('submission_read'); - const nxt = jest.fn(); - const cu = {}; - const req = { - currentUser: cu, + const req = getMockReq({ + currentUser: {}, params: { - formSubmissionId: 123, + formSubmissionId: formSubmissionId, }, - }; + }); + const { res, next } = getMockRes(); - await mw(req, testRes, nxt); - // just run to the end and fall into the base case - expect(service.checkSubmissionPermission).toHaveBeenCalledTimes(0); - expect(nxt).toHaveBeenCalledTimes(1); - expect(nxt).toHaveBeenCalledWith(); + await hasSubmissionPermissions(['submission_read'])(req, res, next); + + expect(service.checkSubmissionPermission).toHaveBeenCalledTimes(1); + expect(service.getSubmissionForm).toHaveBeenCalledTimes(1); + expect(service.getUserForms).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(); }); }); From bc28b95fd2d39deac745294103c78904aaeaf259 Mon Sep 17 00:00:00 2001 From: Vijaivir Dhaliwal <91633223+vijaivir@users.noreply.github.com> Date: Wed, 15 May 2024 16:17:23 -0700 Subject: [PATCH 02/14] update invalidFileMessage (#1354) --- app/frontend/src/internationalization/trans/chefs/ar/ar.json | 2 +- app/frontend/src/internationalization/trans/chefs/de/de.json | 2 +- app/frontend/src/internationalization/trans/chefs/en/en.json | 2 +- app/frontend/src/internationalization/trans/chefs/es/es.json | 2 +- app/frontend/src/internationalization/trans/chefs/fa/fa.json | 2 +- app/frontend/src/internationalization/trans/chefs/fr/fr.json | 2 +- app/frontend/src/internationalization/trans/chefs/hi/hi.json | 2 +- app/frontend/src/internationalization/trans/chefs/it/it.json | 2 +- app/frontend/src/internationalization/trans/chefs/ja/ja.json | 2 +- app/frontend/src/internationalization/trans/chefs/ko/ko.json | 2 +- app/frontend/src/internationalization/trans/chefs/pa/pa.json | 2 +- app/frontend/src/internationalization/trans/chefs/pt/pt.json | 2 +- app/frontend/src/internationalization/trans/chefs/ru/ru.json | 2 +- app/frontend/src/internationalization/trans/chefs/tl/tl.json | 2 +- app/frontend/src/internationalization/trans/chefs/uk/uk.json | 2 +- app/frontend/src/internationalization/trans/chefs/vi/vi.json | 2 +- app/frontend/src/internationalization/trans/chefs/zh/zh.json | 2 +- .../src/internationalization/trans/chefs/zhTW/zh-TW.json | 2 +- 18 files changed, 18 insertions(+), 18 deletions(-) diff --git a/app/frontend/src/internationalization/trans/chefs/ar/ar.json b/app/frontend/src/internationalization/trans/chefs/ar/ar.json index b822e2184..925151a5e 100644 --- a/app/frontend/src/internationalization/trans/chefs/ar/ar.json +++ b/app/frontend/src/internationalization/trans/chefs/ar/ar.json @@ -77,7 +77,7 @@ "upload": "تحميل", "delete": "حذف", "download": "تنزيل", - "invalidFileMessage": "يجب أن يستخدم القالب أحد الامتدادات التالية: .txt، .docx، .html، .odt، .pptx، .xlsx، .pdf", + "invalidFileMessage": "يجب أن يستخدم القالب أحد الامتدادات التالية: .txt، .docx، .html، .odt، .pptx، .xlsx", "uploadSuccess": "تم تحميل القالب بنجاح.", "uploadError": "حدث خطأ أثناء تحميل القالب.", "deleteSuccess": "تم حذف القالب بنجاح.", diff --git a/app/frontend/src/internationalization/trans/chefs/de/de.json b/app/frontend/src/internationalization/trans/chefs/de/de.json index f81aa4b52..e16590356 100644 --- a/app/frontend/src/internationalization/trans/chefs/de/de.json +++ b/app/frontend/src/internationalization/trans/chefs/de/de.json @@ -77,7 +77,7 @@ "upload": "Hochladen", "delete": "Löschen", "download": "Herunterladen", - "invalidFileMessage": "Die Vorlage muss eine der folgenden Erweiterungen verwenden: .txt, .docx, .html, .odt, .pptx, .xlsx, .pdf", + "invalidFileMessage": "Die Vorlage muss eine der folgenden Erweiterungen verwenden: .txt, .docx, .html, .odt, .pptx, .xlsx", "uploadSuccess": "Vorlage erfolgreich hochgeladen.", "uploadError": "Beim Hochladen der Vorlage ist ein Fehler aufgetreten.", "deleteSuccess": "Vorlage erfolgreich gelöscht.", diff --git a/app/frontend/src/internationalization/trans/chefs/en/en.json b/app/frontend/src/internationalization/trans/chefs/en/en.json index a7a14c110..5fb62686d 100644 --- a/app/frontend/src/internationalization/trans/chefs/en/en.json +++ b/app/frontend/src/internationalization/trans/chefs/en/en.json @@ -73,7 +73,7 @@ "upload": "Upload", "delete": "Delete", "download": "Download", - "invalidFileMessage": "The template must use one of the following extentions: .txt, .docx, .html, .odt, .pptx, .xlsx, .pdf", + "invalidFileMessage": "The template must use one of the following extentions: .txt, .docx, .html, .odt, .pptx, .xlsx", "uploadSuccess": "Template uploaded successfully.", "uploadError": "An error occurred while uploading the template.", "deleteSuccess": "Template deleted successfully.", diff --git a/app/frontend/src/internationalization/trans/chefs/es/es.json b/app/frontend/src/internationalization/trans/chefs/es/es.json index 94f3e3e54..a52f14881 100644 --- a/app/frontend/src/internationalization/trans/chefs/es/es.json +++ b/app/frontend/src/internationalization/trans/chefs/es/es.json @@ -77,7 +77,7 @@ "upload": "Subir", "delete": "Eliminar", "download": "Descargar", - "invalidFileMessage": "La plantilla debe usar una de las siguientes extensiones: .txt, .docx, .html, .odt, .pptx, .xlsx, .pdf", + "invalidFileMessage": "La plantilla debe usar una de las siguientes extensiones: .txt, .docx, .html, .odt, .pptx, .xlsx", "uploadSuccess": "Plantilla subida exitosamente.", "uploadError": "Ocurrió un error al subir la plantilla.", "deleteSuccess": "Plantilla eliminada exitosamente.", diff --git a/app/frontend/src/internationalization/trans/chefs/fa/fa.json b/app/frontend/src/internationalization/trans/chefs/fa/fa.json index 354a66513..7b7bd149f 100644 --- a/app/frontend/src/internationalization/trans/chefs/fa/fa.json +++ b/app/frontend/src/internationalization/trans/chefs/fa/fa.json @@ -77,7 +77,7 @@ "upload": "بارگذاری", "delete": "حذف", "download": "دانلود", - "invalidFileMessage": "قالب باید یکی از پسوندهای زیر را استفاده کند: .txt، .docx، .html، .odt، .pptx، .xlsx، .pdf", + "invalidFileMessage": "قالب باید یکی از پسوندهای زیر را استفاده کند: .txt، .docx، .html، .odt، .pptx، .xlsx", "uploadSuccess": "قالب با موفقیت بارگذاری شد.", "uploadError": "خطایی در هنگام بارگذاری قالب رخ داد.", "deleteSuccess": "قالب با موفقیت حذف شد.", diff --git a/app/frontend/src/internationalization/trans/chefs/fr/fr.json b/app/frontend/src/internationalization/trans/chefs/fr/fr.json index 787d8aabc..ae20b3f8a 100644 --- a/app/frontend/src/internationalization/trans/chefs/fr/fr.json +++ b/app/frontend/src/internationalization/trans/chefs/fr/fr.json @@ -77,7 +77,7 @@ "upload": "Télécharger", "delete": "Supprimer", "download": "Télécharger", - "invalidFileMessage": "Le modèle doit utiliser l'une des extensions suivantes : .txt, .docx, .html, .odt, .pptx, .xlsx, .pdf", + "invalidFileMessage": "Le modèle doit utiliser l'une des extensions suivantes : .txt, .docx, .html, .odt, .pptx, .xlsx", "uploadSuccess": "Modèle téléchargé avec succès.", "uploadError": "Une erreur s'est produite lors du téléchargement du modèle.", "deleteSuccess": "Modèle supprimé avec succès.", diff --git a/app/frontend/src/internationalization/trans/chefs/hi/hi.json b/app/frontend/src/internationalization/trans/chefs/hi/hi.json index 1948a28a9..6e6ac0f98 100644 --- a/app/frontend/src/internationalization/trans/chefs/hi/hi.json +++ b/app/frontend/src/internationalization/trans/chefs/hi/hi.json @@ -77,7 +77,7 @@ "upload": "अपलोड करें", "delete": "हटाएं", "download": "डाउनलोड करें", - "invalidFileMessage": "टेम्पलेट में निम्नलिखित में से एक एक्सटेंशन का उपयोग करना चाहिए: .txt, .docx, .html, .odt, .pptx, .xlsx, .pdf", + "invalidFileMessage": "टेम्पलेट में निम्नलिखित में से एक एक्सटेंशन का उपयोग करना चाहिए: .txt, .docx, .html, .odt, .pptx, .xlsx", "uploadSuccess": "टेम्पलेट सफलतापूर्वक अपलोड हुआ।", "uploadError": "टेम्पलेट अपलोड करते समय एक त्रुटि हुई।", "deleteSuccess": "टेम्पलेट सफलतापूर्वक हटाया गया।", diff --git a/app/frontend/src/internationalization/trans/chefs/it/it.json b/app/frontend/src/internationalization/trans/chefs/it/it.json index 96b1af787..0de4b1206 100644 --- a/app/frontend/src/internationalization/trans/chefs/it/it.json +++ b/app/frontend/src/internationalization/trans/chefs/it/it.json @@ -77,7 +77,7 @@ "upload": "Carica", "delete": "Elimina", "download": "Scarica", - "invalidFileMessage": "Il template deve usare una delle seguenti estensioni: .txt, .docx, .html, .odt, .pptx, .xlsx, .pdf", + "invalidFileMessage": "Il template deve usare una delle seguenti estensioni: .txt, .docx, .html, .odt, .pptx, .xlsx", "uploadSuccess": "Template caricato con successo.", "uploadError": "Si è verificato un errore durante il caricamento del template.", "deleteSuccess": "Template eliminato con successo.", diff --git a/app/frontend/src/internationalization/trans/chefs/ja/ja.json b/app/frontend/src/internationalization/trans/chefs/ja/ja.json index 279511739..565df047a 100644 --- a/app/frontend/src/internationalization/trans/chefs/ja/ja.json +++ b/app/frontend/src/internationalization/trans/chefs/ja/ja.json @@ -77,7 +77,7 @@ "upload": "アップロード", "delete": "削除", "download": "ダウンロード", - "invalidFileMessage": "テンプレートは次のいずれかの拡張子を使用する必要があります: .txt, .docx, .html, .odt, .pptx, .xlsx, .pdf", + "invalidFileMessage": "テンプレートは次のいずれかの拡張子を使用する必要があります: .txt, .docx, .html, .odt, .pptx, .xlsx", "uploadSuccess": "テンプレートが正常にアップロードされました。", "uploadError": "テンプレートのアップロード中にエラーが発生しました。", "deleteSuccess": "テンプレートが正常に削除されました。", diff --git a/app/frontend/src/internationalization/trans/chefs/ko/ko.json b/app/frontend/src/internationalization/trans/chefs/ko/ko.json index 700ddbcd6..a4532db38 100644 --- a/app/frontend/src/internationalization/trans/chefs/ko/ko.json +++ b/app/frontend/src/internationalization/trans/chefs/ko/ko.json @@ -77,7 +77,7 @@ "upload": "업로드", "delete": "삭제", "download": "다운로드", - "invalidFileMessage": "템플릿은 다음 확장자 중 하나를 사용해야 합니다: .txt, .docx, .html, .odt, .pptx, .xlsx, .pdf", + "invalidFileMessage": "템플릿은 다음 확장자 중 하나를 사용해야 합니다: .txt, .docx, .html, .odt, .pptx, .xlsx", "uploadSuccess": "템플릿이 성공적으로 업로드되었습니다.", "uploadError": "템플릿 업로드 중 오류가 발생했습니다.", "deleteSuccess": "템플릿이 성공적으로 삭제되었습니다.", diff --git a/app/frontend/src/internationalization/trans/chefs/pa/pa.json b/app/frontend/src/internationalization/trans/chefs/pa/pa.json index 080312882..eba62de18 100644 --- a/app/frontend/src/internationalization/trans/chefs/pa/pa.json +++ b/app/frontend/src/internationalization/trans/chefs/pa/pa.json @@ -77,7 +77,7 @@ "upload": "ਅੱਪਲੋਡ ਕਰੋ", "delete": "ਮਿਟਾਓ", "download": "ਡਾਊਨਲੋਡ ਕਰੋ", - "invalidFileMessage": "ਟੈਂਪਲੇਟ ਵਿੱਚ ਇਹਨਾਂ ਵਿੱਚੋਂ ਕੋਈ ਇੱਕ ਐਕਸਟੈਂਸ਼ਨ ਹੋਣੀ ਚਾਹੀਦੀ ਹੈ: .txt, .docx, .html, .odt, .pptx, .xlsx, .pdf", + "invalidFileMessage": "ਟੈਂਪਲੇਟ ਵਿੱਚ ਇਹਨਾਂ ਵਿੱਚੋਂ ਕੋਈ ਇੱਕ ਐਕਸਟੈਂਸ਼ਨ ਹੋਣੀ ਚਾਹੀਦੀ ਹੈ: .txt, .docx, .html, .odt, .pptx, .xlsx", "uploadSuccess": "ਟੈਂਪਲੇਟ ਸਫਲਤਾਪੂਰਵਕ ਅੱਪਲੋਡ ਹੋ ਗਈ।", "uploadError": "ਟੈਂਪਲੇਟ ਅੱਪਲੋਡ ਕਰਦੇ ਸਮੇਂ ਇੱਕ ਗਲਤੀ ਆਈ।", "deleteSuccess": "ਟੈਂਪਲੇਟ ਸਫਲਤਾਪੂਰਵਕ ਮਿਟਾਈ ਗਈ।", diff --git a/app/frontend/src/internationalization/trans/chefs/pt/pt.json b/app/frontend/src/internationalization/trans/chefs/pt/pt.json index fa4433dfa..a7e83f7ff 100644 --- a/app/frontend/src/internationalization/trans/chefs/pt/pt.json +++ b/app/frontend/src/internationalization/trans/chefs/pt/pt.json @@ -77,7 +77,7 @@ "upload": "Carregar", "delete": "Excluir", "download": "Baixar", - "invalidFileMessage": "O modelo deve usar uma das seguintes extensões: .txt, .docx, .html, .odt, .pptx, .xlsx, .pdf", + "invalidFileMessage": "O modelo deve usar uma das seguintes extensões: .txt, .docx, .html, .odt, .pptx, .xlsx", "uploadSuccess": "Modelo carregado com sucesso.", "uploadError": "Ocorreu um erro ao carregar o modelo.", "deleteSuccess": "Modelo excluído com sucesso.", diff --git a/app/frontend/src/internationalization/trans/chefs/ru/ru.json b/app/frontend/src/internationalization/trans/chefs/ru/ru.json index d80ca7cbc..98f76a57e 100644 --- a/app/frontend/src/internationalization/trans/chefs/ru/ru.json +++ b/app/frontend/src/internationalization/trans/chefs/ru/ru.json @@ -77,7 +77,7 @@ "upload": "Загрузить", "delete": "Удалить", "download": "Скачать", - "invalidFileMessage": "Шаблон должен использовать одно из следующих расширений: .txt, .docx, .html, .odt, .pptx, .xlsx, .pdf", + "invalidFileMessage": "Шаблон должен использовать одно из следующих расширений: .txt, .docx, .html, .odt, .pptx, .xlsx", "uploadSuccess": "Шаблон успешно загружен.", "uploadError": "Произошла ошибка при загрузке шаблона.", "deleteSuccess": "Шаблон успешно удален.", diff --git a/app/frontend/src/internationalization/trans/chefs/tl/tl.json b/app/frontend/src/internationalization/trans/chefs/tl/tl.json index d3d3f340d..afecd18e3 100644 --- a/app/frontend/src/internationalization/trans/chefs/tl/tl.json +++ b/app/frontend/src/internationalization/trans/chefs/tl/tl.json @@ -77,7 +77,7 @@ "upload": "I-upload", "delete": "Tanggalin", "download": "I-download", - "invalidFileMessage": "Ang template ay dapat gumamit ng isa sa mga sumusunod na extension: .txt, .docx, .html, .odt, .pptx, .xlsx, .pdf", + "invalidFileMessage": "Ang template ay dapat gumamit ng isa sa mga sumusunod na extension: .txt, .docx, .html, .odt, .pptx, .xlsx", "uploadSuccess": "Matagumpay na na-upload ang template.", "uploadError": "Nagkaroon ng error habang ina-upload ang template.", "deleteSuccess": "Matagumpay na natanggal ang template.", diff --git a/app/frontend/src/internationalization/trans/chefs/uk/uk.json b/app/frontend/src/internationalization/trans/chefs/uk/uk.json index 589635a11..0341e70d1 100644 --- a/app/frontend/src/internationalization/trans/chefs/uk/uk.json +++ b/app/frontend/src/internationalization/trans/chefs/uk/uk.json @@ -77,7 +77,7 @@ "upload": "Завантажити", "delete": "Видалити", "download": "Завантажити", - "invalidFileMessage": "Шаблон повинен використовувати одне з наступних розширень: .txt, .docx, .html, .odt, .pptx, .xlsx, .pdf", + "invalidFileMessage": "Шаблон повинен використовувати одне з наступних розширень: .txt, .docx, .html, .odt, .pptx, .xlsx", "uploadSuccess": "Шаблон успішно завантажено.", "uploadError": "Під час завантаження шаблону сталася помилка.", "deleteSuccess": "Шаблон успішно видалено.", diff --git a/app/frontend/src/internationalization/trans/chefs/vi/vi.json b/app/frontend/src/internationalization/trans/chefs/vi/vi.json index 517108fae..a8c64e5ea 100644 --- a/app/frontend/src/internationalization/trans/chefs/vi/vi.json +++ b/app/frontend/src/internationalization/trans/chefs/vi/vi.json @@ -77,7 +77,7 @@ "upload": "Tải lên", "delete": "Xóa", "download": "Tải xuống", - "invalidFileMessage": "Mẫu phải sử dụng một trong các phần mở rộng sau: .txt, .docx, .html, .odt, .pptx, .xlsx, .pdf", + "invalidFileMessage": "Mẫu phải sử dụng một trong các phần mở rộng sau: .txt, .docx, .html, .odt, .pptx, .xlsx", "uploadSuccess": "Tải lên mẫu thành công.", "uploadError": "Có lỗi xảy ra khi tải lên mẫu.", "deleteSuccess": "Xóa mẫu thành công.", diff --git a/app/frontend/src/internationalization/trans/chefs/zh/zh.json b/app/frontend/src/internationalization/trans/chefs/zh/zh.json index c1d0ebe21..5e917a3a7 100644 --- a/app/frontend/src/internationalization/trans/chefs/zh/zh.json +++ b/app/frontend/src/internationalization/trans/chefs/zh/zh.json @@ -77,7 +77,7 @@ "upload": "上传", "delete": "删除", "download": "下载", - "invalidFileMessage": "模板必须使用以下扩展名之一:.txt, .docx, .html, .odt, .pptx, .xlsx, .pdf", + "invalidFileMessage": "模板必须使用以下扩展名之一:.txt, .docx, .html, .odt, .pptx, .xlsx", "uploadSuccess": "模板上传成功。", "uploadError": "上传模板时发生错误。", "deleteSuccess": "模板删除成功。", diff --git a/app/frontend/src/internationalization/trans/chefs/zhTW/zh-TW.json b/app/frontend/src/internationalization/trans/chefs/zhTW/zh-TW.json index 003c72e76..1df310f5c 100644 --- a/app/frontend/src/internationalization/trans/chefs/zhTW/zh-TW.json +++ b/app/frontend/src/internationalization/trans/chefs/zhTW/zh-TW.json @@ -77,7 +77,7 @@ "upload": "上傳", "delete": "刪除", "download": "下載", - "invalidFileMessage": "範本必須使用以下其中一種擴展名:.txt, .docx, .html, .odt, .pptx, .xlsx, .pdf", + "invalidFileMessage": "範本必須使用以下其中一種擴展名:.txt, .docx, .html, .odt, .pptx, .xlsx", "uploadSuccess": "範本成功上傳。", "uploadError": "上傳範本時發生錯誤。", "deleteSuccess": "範本成功刪除。", From 54cf4dccea4281c060d44464d9c49b42d267fa59 Mon Sep 17 00:00:00 2001 From: Walter Moar Date: Thu, 16 May 2024 10:30:24 -0700 Subject: [PATCH 03/14] refactor: FORMS-1245 simplify hasSubmissionPermissions (#1356) --- app/src/forms/auth/middleware/userAccess.js | 67 ++++++++++----------- 1 file changed, 33 insertions(+), 34 deletions(-) diff --git a/app/src/forms/auth/middleware/userAccess.js b/app/src/forms/auth/middleware/userAccess.js index ff46778e6..a20267a96 100644 --- a/app/src/forms/auth/middleware/userAccess.js +++ b/app/src/forms/auth/middleware/userAccess.js @@ -10,18 +10,23 @@ const rbacService = require('../../rbac/service'); /** * Checks that every permission is in the user's form permissions. * - * @param {*} form the user's form metadata including permissions. - * @param {string[]} permissions the permissions needed for access. - * @returns true if every permissions value is in the user's form permissions. + * @param {string[]} formPermissions the user's form permissions. + * @param {string[]} requiredPermissions the permissions needed for access. + * @returns true if all required permissions values are in the form permissions. */ -const _formHasPermissions = (form, permissions) => { +const _formHasPermissions = (formPermissions, requiredPermissions) => { + // If there are no form permissions then the user can't have permission. + if (!formPermissions) { + return false; + } + // Get the intersection of the two sets of permissions. If it's the same // size as permissions then the user has all the needed permissions. - const intersection = permissions.filter((p) => { - return form.permissions.includes(p); + const intersection = requiredPermissions.filter((p) => { + return formPermissions.includes(p); }); - return intersection.length === permissions.length; + return intersection.length === requiredPermissions.length; }; /** @@ -32,27 +37,27 @@ const _formHasPermissions = (form, permissions) => { * @param {uuid} formId the ID of the form to retrieve for the current user. * @param {boolean} includeDeleted if active form not found, look for a deleted * form. - * @returns the form metadata. - * @throws Problem if the form metadata for the formId cannot be retrieved. + * @returns the form metadata if the currentUser has access, or undefined. */ const _getForm = async (currentUser, formId, includeDeleted) => { if (!uuid.validate(formId)) { throw new Problem(400, { detail: 'Bad formId' }); } - const forms = await service.getUserForms(currentUser, { active: true, formId: formId }); + const forms = await service.getUserForms(currentUser, { + active: true, + formId: formId, + }); let form = forms.find((f) => f.formId === formId); if (!form && includeDeleted) { - const deletedForms = await service.getUserForms(currentUser, { active: false, formId: formId }); + const deletedForms = await service.getUserForms(currentUser, { + active: false, + formId: formId, + }); form = deletedForms.find((f) => f.formId === formId); } - // Cannot find the form: either it doesn't exist or we don't have access. - if (!form) { - throw new Problem(401, { detail: 'Current user has no access to form' }); - } - return form; }; @@ -119,9 +124,11 @@ const hasFormPermissions = (permissions) => { // precedence to params. const form = await _getForm(req.currentUser, req.params.formId || req.query.formId, true); - if (!_formHasPermissions(form, permissions)) { + // If the form doesn't exist, or its permissions don't exist, then access + // will be denied - otherwise check to see if permissions is a subset. + if (!_formHasPermissions(form?.permissions, permissions)) { throw new Problem(401, { - detail: 'Current user does not have required permission(s) on form', + detail: 'You do not have access to this form.', }); } @@ -168,22 +175,14 @@ const hasSubmissionPermissions = (permissions) => { // If the current user has elevated permissions on the form, they may have // access to all submissions for the form. if (req.currentUser) { - const forms = await service.getUserForms(req.currentUser, { - active: true, - formId: submissionForm.form.id, - }); - let formFromCurrentUser = forms.find((f) => f.formId === submissionForm.form.id); - if (formFromCurrentUser) { - // Do they have the submission permissions being requested on this FORM - const intersection = permissions.filter((p) => { - return formFromCurrentUser.permissions.includes(p); - }); - if (intersection.length === permissions.length) { - req.formIdWithDeletePermission = submissionForm.form.id; - next(); - - return; - } + const formFromCurrentUser = await _getForm(req.currentUser, submissionForm.form.id, false); + + // Do they have the submission permissions requested on this form? + if (_formHasPermissions(formFromCurrentUser?.permissions, permissions)) { + req.formIdWithDeletePermission = submissionForm.form.id; + next(); + + return; } } From 06406e1bcd82e313ed414f4f6ac45a40be3c8ae7 Mon Sep 17 00:00:00 2001 From: Walter Moar Date: Thu, 16 May 2024 13:02:53 -0700 Subject: [PATCH 04/14] refactor: FORMS-1241 simplify hasSubmissionPermissions parameter (#1357) --- app/src/forms/auth/middleware/userAccess.js | 20 ++++++------- .../forms/file/middleware/filePermissions.js | 2 +- app/src/forms/file/routes.js | 4 +-- app/src/forms/rbac/routes.js | 4 +-- app/src/forms/submission/routes.js | 28 +++++++++---------- .../file/middleware/filePermissions.spec.js | 2 +- 6 files changed, 30 insertions(+), 30 deletions(-) diff --git a/app/src/forms/auth/middleware/userAccess.js b/app/src/forms/auth/middleware/userAccess.js index a20267a96..02bcdb849 100644 --- a/app/src/forms/auth/middleware/userAccess.js +++ b/app/src/forms/auth/middleware/userAccess.js @@ -145,7 +145,7 @@ const hasFormPermissions = (permissions) => { * This will fall through if everything is OK, otherwise it will call next() * with a Problem describing the error. * - * @param {string[]} permissions the access the user needs for the submission + * @param {string[]} permissions the access the user needs for the submission. * @returns nothing */ const hasSubmissionPermissions = (permissions) => { @@ -158,10 +158,6 @@ const hasSubmissionPermissions = (permissions) => { return; } - if (!Array.isArray(permissions)) { - permissions = [permissions]; - } - // The request must include a formSubmissionId, either in params or query, // but give precedence to params. const submissionId = req.params.formSubmissionId || req.query.formSubmissionId; @@ -189,7 +185,9 @@ const hasSubmissionPermissions = (permissions) => { // Deleted submissions are only accessible to users with the form // permissions above. if (submissionForm.submission.deleted) { - throw new Problem(401, { detail: 'You do not have access to this submission.' }); + throw new Problem(401, { + detail: 'You do not have access to this submission.', + }); } // Public (anonymous) forms are publicly viewable. @@ -203,7 +201,9 @@ const hasSubmissionPermissions = (permissions) => { // check against the submission level permissions assigned to the user... const submissionPermission = await service.checkSubmissionPermission(req.currentUser, submissionId, permissions); if (!submissionPermission) { - throw new Problem(401, { detail: 'You do not have access to this submission.' }); + throw new Problem(401, { + detail: 'You do not have access to this submission.', + }); } next(); @@ -444,10 +444,10 @@ const hasRolePermissions = (removingUsers = false) => { module.exports = { currentUser, + filterMultipleSubmissions, hasFormPermissions, - hasSubmissionPermissions, - hasFormRoles, hasFormRole, + hasFormRoles, hasRolePermissions, - filterMultipleSubmissions, + hasSubmissionPermissions, }; diff --git a/app/src/forms/file/middleware/filePermissions.js b/app/src/forms/file/middleware/filePermissions.js index 3e6e8f7c2..cb3d1f753 100644 --- a/app/src/forms/file/middleware/filePermissions.js +++ b/app/src/forms/file/middleware/filePermissions.js @@ -49,7 +49,7 @@ const hasFileCreate = (req, res, next) => { * Middleware to determine if the current user can do a specific permission on a file * This is generally based on the SUBMISSION permissions that the file is attached to * but has to handle management for files that are added before submit - * @param {string} permissions the permission to require on this route + * @param {string[]} permissions the submission permissions to require on this route * @returns {Function} a middleware function */ const hasFilePermissions = (permissions) => { diff --git a/app/src/forms/file/routes.js b/app/src/forms/file/routes.js index db7e9e1e9..00c648bd2 100644 --- a/app/src/forms/file/routes.js +++ b/app/src/forms/file/routes.js @@ -14,11 +14,11 @@ routes.post('/', hasFileCreate, fileUpload.upload, async (req, res, next) => { await controller.create(req, res, next); }); -routes.get('/:id', rateLimiter, apiAccess, currentFileRecord, hasFilePermissions(P.SUBMISSION_READ), async (req, res, next) => { +routes.get('/:id', rateLimiter, apiAccess, currentFileRecord, hasFilePermissions([P.SUBMISSION_READ]), async (req, res, next) => { await controller.read(req, res, next); }); -routes.delete('/:id', currentFileRecord, hasFilePermissions(P.SUBMISSION_UPDATE), async (req, res, next) => { +routes.delete('/:id', currentFileRecord, hasFilePermissions([P.SUBMISSION_UPDATE]), async (req, res, next) => { await controller.delete(req, res, next); }); diff --git a/app/src/forms/rbac/routes.js b/app/src/forms/rbac/routes.js index 57b5e8089..dfe2d29a1 100644 --- a/app/src/forms/rbac/routes.js +++ b/app/src/forms/rbac/routes.js @@ -28,11 +28,11 @@ routes.put('/forms', hasFormPermissions([P.TEAM_UPDATE]), async (req, res, next) await controller.setFormUsers(req, res, next); }); -routes.get('/submissions', hasSubmissionPermissions(P.SUBMISSION_READ), async (req, res, next) => { +routes.get('/submissions', hasSubmissionPermissions([P.SUBMISSION_READ]), async (req, res, next) => { await controller.getSubmissionUsers(req, res, next); }); -routes.put('/submissions', hasSubmissionPermissions(P.SUBMISSION_UPDATE), async (req, res, next) => { +routes.put('/submissions', hasSubmissionPermissions([P.SUBMISSION_UPDATE]), async (req, res, next) => { await controller.setSubmissionUserPermissions(req, res, next); }); diff --git a/app/src/forms/submission/routes.js b/app/src/forms/submission/routes.js index 10d79b727..b6d9e86a1 100644 --- a/app/src/forms/submission/routes.js +++ b/app/src/forms/submission/routes.js @@ -11,23 +11,23 @@ routes.use(currentUser); routes.param('documentTemplateId', validateParameter.validateDocumentTemplateId); -routes.get('/:formSubmissionId', rateLimiter, apiAccess, hasSubmissionPermissions(P.SUBMISSION_READ), async (req, res, next) => { +routes.get('/:formSubmissionId', rateLimiter, apiAccess, hasSubmissionPermissions([P.SUBMISSION_READ]), async (req, res, next) => { await controller.read(req, res, next); }); -routes.put('/:formSubmissionId', hasSubmissionPermissions(P.SUBMISSION_UPDATE), async (req, res, next) => { +routes.put('/:formSubmissionId', hasSubmissionPermissions([P.SUBMISSION_UPDATE]), async (req, res, next) => { await controller.update(req, res, next); }); -routes.delete('/:formSubmissionId', rateLimiter, apiAccess, hasSubmissionPermissions(P.SUBMISSION_DELETE), async (req, res, next) => { +routes.delete('/:formSubmissionId', rateLimiter, apiAccess, hasSubmissionPermissions([P.SUBMISSION_DELETE]), async (req, res, next) => { await controller.delete(req, res, next); }); -routes.put('/:formSubmissionId/:formId/submissions/restore', hasSubmissionPermissions(P.SUBMISSION_DELETE), filterMultipleSubmissions(), async (req, res, next) => { +routes.put('/:formSubmissionId/:formId/submissions/restore', hasSubmissionPermissions([P.SUBMISSION_DELETE]), filterMultipleSubmissions(), async (req, res, next) => { await controller.restoreMutipleSubmissions(req, res, next); }); -routes.put('/:formSubmissionId/restore', hasSubmissionPermissions(P.SUBMISSION_DELETE), async (req, res, next) => { +routes.put('/:formSubmissionId/restore', hasSubmissionPermissions([P.SUBMISSION_DELETE]), async (req, res, next) => { await controller.restore(req, res, next); }); @@ -35,39 +35,39 @@ routes.get('/:formSubmissionId/options', async (req, res, next) => { await controller.readOptions(req, res, next); }); -routes.get('/:formSubmissionId/notes', hasSubmissionPermissions(P.SUBMISSION_REVIEW), async (req, res, next) => { +routes.get('/:formSubmissionId/notes', hasSubmissionPermissions([P.SUBMISSION_REVIEW]), async (req, res, next) => { await controller.getNotes(req, res, next); }); -routes.post('/:formSubmissionId/notes', hasSubmissionPermissions(P.SUBMISSION_REVIEW), async (req, res, next) => { +routes.post('/:formSubmissionId/notes', hasSubmissionPermissions([P.SUBMISSION_REVIEW]), async (req, res, next) => { await controller.addNote(req, res, next); }); -routes.get('/:formSubmissionId/status', rateLimiter, apiAccess, hasSubmissionPermissions(P.SUBMISSION_REVIEW), async (req, res, next) => { +routes.get('/:formSubmissionId/status', rateLimiter, apiAccess, hasSubmissionPermissions([P.SUBMISSION_REVIEW]), async (req, res, next) => { await controller.getStatus(req, res, next); }); -routes.post('/:formSubmissionId/status', hasSubmissionPermissions(P.SUBMISSION_REVIEW), async (req, res, next) => { +routes.post('/:formSubmissionId/status', hasSubmissionPermissions([P.SUBMISSION_REVIEW]), async (req, res, next) => { await controller.addStatus(req, res, next); }); -routes.post('/:formSubmissionId/email', hasSubmissionPermissions(P.SUBMISSION_READ), async (req, res, next) => { +routes.post('/:formSubmissionId/email', hasSubmissionPermissions([P.SUBMISSION_READ]), async (req, res, next) => { await controller.email(req, res, next); }); -routes.get('/:formSubmissionId/edits', hasSubmissionPermissions(P.SUBMISSION_READ), async (req, res, next) => { +routes.get('/:formSubmissionId/edits', hasSubmissionPermissions([P.SUBMISSION_READ]), async (req, res, next) => { await controller.listEdits(req, res, next); }); -routes.get('/:formSubmissionId/template/:documentTemplateId/render', rateLimiter, apiAccess, hasSubmissionPermissions(P.SUBMISSION_READ), async (req, res, next) => { +routes.get('/:formSubmissionId/template/:documentTemplateId/render', rateLimiter, apiAccess, hasSubmissionPermissions([P.SUBMISSION_READ]), async (req, res, next) => { await controller.templateRender(req, res, next); }); -routes.post('/:formSubmissionId/template/render', rateLimiter, apiAccess, hasSubmissionPermissions(P.SUBMISSION_READ), async (req, res, next) => { +routes.post('/:formSubmissionId/template/render', rateLimiter, apiAccess, hasSubmissionPermissions([P.SUBMISSION_READ]), async (req, res, next) => { await controller.templateUploadAndRender(req, res, next); }); -routes.delete('/:formSubmissionId/:formId/submissions', hasSubmissionPermissions(P.SUBMISSION_DELETE), filterMultipleSubmissions(), async (req, res, next) => { +routes.delete('/:formSubmissionId/:formId/submissions', hasSubmissionPermissions([P.SUBMISSION_DELETE]), filterMultipleSubmissions(), async (req, res, next) => { await controller.deleteMutipleSubmissions(req, res, next); }); diff --git a/app/tests/unit/forms/file/middleware/filePermissions.spec.js b/app/tests/unit/forms/file/middleware/filePermissions.spec.js index b043e0a3b..e74000b5a 100644 --- a/app/tests/unit/forms/file/middleware/filePermissions.spec.js +++ b/app/tests/unit/forms/file/middleware/filePermissions.spec.js @@ -198,7 +198,7 @@ describe('hasFileCreate', () => { }); describe('hasFilePermissions', () => { - const perm = 'submission_read'; + const perm = ['submission_read']; const subPermSpy = jest.spyOn(userAccess, 'hasSubmissionPermissions'); beforeEach(() => { From 94956ec474e3518e3dd3576aefca90a675f1bd6b Mon Sep 17 00:00:00 2001 From: Walter Moar Date: Fri, 17 May 2024 08:13:06 -0700 Subject: [PATCH 05/14] refactor: FORMS-1245 simplify hasFormRoles (#1358) --- app/src/forms/auth/middleware/userAccess.js | 121 +++--- .../forms/auth/middleware/userAccess.spec.js | 363 ++++++++++++++---- 2 files changed, 344 insertions(+), 140 deletions(-) diff --git a/app/src/forms/auth/middleware/userAccess.js b/app/src/forms/auth/middleware/userAccess.js index 02bcdb849..d45a96d2c 100644 --- a/app/src/forms/auth/middleware/userAccess.js +++ b/app/src/forms/auth/middleware/userAccess.js @@ -8,27 +8,50 @@ const service = require('../service'); const rbacService = require('../../rbac/service'); /** - * Checks that every permission is in the user's form permissions. + * Checks that the user's permissions contains every required permission. * - * @param {string[]} formPermissions the user's form permissions. + * @param {string[]} userPermissions the permissions that the user has. * @param {string[]} requiredPermissions the permissions needed for access. - * @returns true if all required permissions values are in the form permissions. + * @returns true if all required permissions are in the user permissions. */ -const _formHasPermissions = (formPermissions, requiredPermissions) => { - // If there are no form permissions then the user can't have permission. - if (!formPermissions) { +const _hasAllPermissions = (userPermissions, requiredPermissions) => { + // If there are no user permissions then the user can't have permission. + if (!userPermissions) { return false; } - // Get the intersection of the two sets of permissions. If it's the same - // size as permissions then the user has all the needed permissions. + // Get the intersection of the two sets of permissions. const intersection = requiredPermissions.filter((p) => { - return formPermissions.includes(p); + return userPermissions.includes(p); }); + // If the intersection is the same size as the required permissions then the + // user has all the needed permissions. return intersection.length === requiredPermissions.length; }; +/** + * Checks that the user's permissions contains any of the required permissions. + * + * @param {string[]} userPermissions the permissions that the user has. + * @param {string[]} requiredPermissions the permissions needed for access. + * @returns true if any required permissions is in the user permissions. + */ +const _hasAnyPermission = (userPermissions, requiredPermissions) => { + // If there are no user permissions then the user can't have permission. + if (!userPermissions) { + return false; + } + + // Get the intersection of the two sets of permissions. + const intersection = requiredPermissions.filter((p) => { + return userPermissions.includes(p); + }); + + // If the intersection has any values then the user has permission. + return intersection.length > 0; +}; + /** * Gets the form metadata for the given formId from the forms available to the * current user. @@ -64,12 +87,13 @@ const _getForm = async (currentUser, formId, includeDeleted) => { /** * Express middleware that adds the user information as the res.currentUser * attribute so that all downstream middleware and business logic can use it. - * This will fall through if everything is OK, otherwise it will call next() - * with a Problem describing the error. + * This falls through if everything is OK, otherwise it calls next() with a + * Problem describing the error. * * @param {*} req the Express object representing the HTTP request. * @param {*} _res the Express object representing the HTTP response - unused. * @param {*} next the Express chaining function. + * @returns nothing */ const currentUser = async (req, _res, next) => { try { @@ -95,8 +119,8 @@ const currentUser = async (req, _res, next) => { /** * Express middleware to check that a user has all the given permissions for a - * form. This will fall through if everything is OK, otherwise it will call - * next() with a Problem describing the error. + * form. This falls through if everything is OK, otherwise it calls next() with + * a Problem describing the error. * * @param {string[]} permissions the form permissions that the user must have. * @returns nothing @@ -126,7 +150,7 @@ const hasFormPermissions = (permissions) => { // If the form doesn't exist, or its permissions don't exist, then access // will be denied - otherwise check to see if permissions is a subset. - if (!_formHasPermissions(form?.permissions, permissions)) { + if (!_hasAllPermissions(form?.permissions, permissions)) { throw new Problem(401, { detail: 'You do not have access to this form.', }); @@ -142,8 +166,8 @@ const hasFormPermissions = (permissions) => { /** * Express middleware to check that the caller has the given permissions for the * submission identified by params.formSubmissionId or query.formSubmissionId. - * This will fall through if everything is OK, otherwise it will call next() - * with a Problem describing the error. + * This falls through if everything is OK, otherwise it calls next() with a + * Problem describing the error. * * @param {string[]} permissions the access the user needs for the submission. * @returns nothing @@ -174,7 +198,7 @@ const hasSubmissionPermissions = (permissions) => { const formFromCurrentUser = await _getForm(req.currentUser, submissionForm.form.id, false); // Do they have the submission permissions requested on this form? - if (_formHasPermissions(formFromCurrentUser?.permissions, permissions)) { + if (_hasAllPermissions(formFromCurrentUser?.permissions, permissions)) { req.formIdWithDeletePermission = submissionForm.form.id; next(); @@ -293,51 +317,32 @@ const hasFormRole = async (formId, user, role) => { return hasRole; }; -const hasFormRoles = (formRoles, hasAll = false) => { - return async (req, res, next) => { - // If we invoke this middleware and the caller is acting on a specific formId, whether in a param or query (precedence to param) - const formId = req.params.formId || req.query.formId; - if (!formId) { - // No form provided to this route that secures based on form... that's a problem! - return new Problem(401, { detail: 'Form Id not found on request.' }).send(res); - } +/** + * Express middleware to check that the caller has one of the given roles for + * the form identified by params.formId or query.formId. This falls through if + * everything is OK, otherwise it calls next() with a Problem describing the + * error. + * + * @param {string[]} formRoles the roles the user needs one of for the form. + * @returns nothing + */ +const hasFormRoles = (formRoles) => { + return async (req, _res, next) => { + try { + // The request must include a formId, either in params or query, but give + // precedence to params. + const form = await _getForm(req.currentUser, req.params.formId || req.query.formId, false); - const forms = await service.getUserForms(req.currentUser, { - active: true, - formId: formId, - }); - const form = forms.find((f) => f.formId === formId); - if (form) { - for (let roleIndex = 0; roleIndex < form.roles.length; roleIndex++) { - let index = formRoles.indexOf(form.roles[roleIndex]); - // If the user has the indexed role requested by the route - if (index > -1) { - // If the route specifies all roles must exist for the form - if (hasAll) - // Remove that role from the search - formRoles.splice(index, 1); - // The user has at least one of the roles - else return next(); - } - // The user has all of the required roles - if (formRoles.length == 0) break; + if (!_hasAnyPermission(form?.roles, formRoles)) { + throw new Problem(401, { + detail: 'You do not have permission to update this role.', + }); } - } - if (hasAll) { - if (formRoles.length > 0) - return next( - new Problem(401, { - detail: 'You do not have permission to update this role.', - }) - ); - else return next(); + next(); + } catch (error) { + next(error); } - return next( - new Problem(401, { - detail: 'You do not have permission to update this role.', - }) - ); }; }; diff --git a/app/tests/unit/forms/auth/middleware/userAccess.spec.js b/app/tests/unit/forms/auth/middleware/userAccess.spec.js index 910dc4ade..900341b8e 100644 --- a/app/tests/unit/forms/auth/middleware/userAccess.spec.js +++ b/app/tests/unit/forms/auth/middleware/userAccess.spec.js @@ -806,107 +806,299 @@ describe('hasSubmissionPermissions', () => { }); }); +// External dependencies used by the implementation are: +// - service.getUserForms: gets the forms that the user can access +// describe('hasFormRoles', () => { - it('falls through if the current user does not have any forms', async () => { - const hfr = hasFormRoles([Roles.OWNER, Roles.TEAM_MANAGER]); - const nxt = jest.fn(); - const cu = {}; - const req = { - currentUser: cu, - params: {}, - query: { - formId: formId, - }, - }; + // Default mock value where the user has no access to forms + service.getUserForms = jest.fn().mockReturnValue([]); - await hfr(req, testRes, nxt); + it('returns a middleware function', async () => { + const middleware = hasFormRoles([Roles.OWNER]); - expect(nxt).toHaveBeenCalledTimes(1); - expect(nxt).toHaveBeenCalledWith(new Problem(401, { detail: 'You do not have any forms.' })); + expect(middleware).toBeInstanceOf(Function); }); - it('falls through if the current user does not have at least one of the required form roles', async () => { - const hfr = hasFormRoles([Roles.OWNER, Roles.TEAM_MANAGER]); - const nxt = jest.fn(); - const cu = {}; - const req = { - currentUser: cu, - params: {}, - query: { - formId: formId, - }, - }; + describe('400 response when', () => { + const expectedStatus = { status: 400 }; + + test('formId missing', async () => { + const req = getMockReq({ + params: { + submissionId: formSubmissionId, + }, + query: { + otherQueryThing: 'SOMETHING', + }, + }); + const { res, next } = getMockRes(); - await hfr(req, testRes, nxt); + await hasFormRoles([Roles.OWNER])(req, res, next); - expect(nxt).toHaveBeenCalledTimes(1); - expect(nxt).toHaveBeenCalledWith(new Problem(401, { detail: 'You do not have permission to update this role.' })); + expect(service.getUserForms).toHaveBeenCalledTimes(0); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(expect.objectContaining(expectedStatus)); + }); + + test('formId not a uuid', async () => { + const req = getMockReq({ + currentUser: {}, + params: { + formId: 'not-a-uuid', + }, + query: { + otherQueryThing: 'SOMETHING', + }, + }); + const { res, next } = getMockRes(); + + await hasFormRoles([Roles.OWNER])(req, res, next); + + expect(service.getUserForms).toHaveBeenCalledTimes(0); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(expect.objectContaining(expectedStatus)); + }); }); - it('falls through if the current user does not have all of the required form roles', async () => { - const hfr = hasFormRoles([Roles.OWNER, Roles.TEAM_MANAGER], true); - const nxt = jest.fn(); - const cu = {}; - const req = { - currentUser: cu, - params: {}, - query: { - formId: formId, - }, - }; + // TODO: These should be 403, but bundle all breaking changes in a small PR. + describe('401 response when', () => { + const expectedStatus = { status: 401 }; - await hfr(req, testRes, nxt); + test('no access to form', async () => { + const req = getMockReq({ + currentUser: {}, + params: { + formId: formId, + }, + }); + const { res, next } = getMockRes(); - expect(nxt).toHaveBeenCalledTimes(1); - expect(nxt).toHaveBeenCalledWith(new Problem(401, { detail: 'You do not have permission to update this role.' })); + await hasFormRoles([Roles.OWNER])(req, res, next); + + expect(service.getUserForms).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(expect.objectContaining(expectedStatus)); + }); + + test('role not on form', async () => { + service.getUserForms.mockReturnValueOnce([ + { + formId: formId, + roles: [Roles.FORM_DESIGNER, Roles.TEAM_MANAGER], + }, + ]); + const req = getMockReq({ + currentUser: {}, + params: { + formId: formId, + }, + }); + const { res, next } = getMockRes(); + + await hasFormRoles([Roles.OWNER])(req, res, next); + + expect(service.getUserForms).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(expect.objectContaining(expectedStatus)); + }); + + test('roles not on form', async () => { + service.getUserForms.mockReturnValueOnce([ + { + formId: formId, + roles: [Roles.FORM_DESIGNER, Roles.TEAM_MANAGER], + }, + ]); + const req = getMockReq({ + currentUser: {}, + params: { + formId: formId, + }, + }); + const { res, next } = getMockRes(); + + await hasFormRoles([Roles.FORM_SUBMITTER, Roles.OWNER])(req, res, next); + + expect(service.getUserForms).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(expect.objectContaining(expectedStatus)); + }); }); - it('moves on if the user has at least one of the required form roles', async () => { - service.getUserForms = jest.fn().mockReturnValue([ - { - formId: formId, - roles: [Roles.TEAM_MANAGER], - }, - ]); - const hfr = hasFormRoles([Roles.OWNER, Roles.TEAM_MANAGER]); - const nxt = jest.fn(); - const cu = {}; - const req = { - currentUser: cu, - params: {}, - query: { - formId: formId, - }, - }; + describe('handles error thrown by', () => { + test('getUserForms', async () => { + const error = new Error(); + service.getUserForms.mockRejectedValueOnce(error); + const req = getMockReq({ + currentUser: {}, + params: { + formId: formId, + }, + }); + const { res, next } = getMockRes(); - await hfr(req, testRes, nxt); + await hasFormRoles([Roles.OWNER])(req, res, next); - expect(nxt).toHaveBeenCalledTimes(1); - expect(nxt).toHaveBeenCalledWith(); + expect(service.getUserForms).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(error); + }); }); - it('moves on if the user has all of the required form roles', async () => { - service.getUserForms = jest.fn().mockReturnValue([ - { - formId: formId, - roles: [Roles.OWNER, Roles.TEAM_MANAGER], - }, - ]); - const hfr = hasFormRoles([Roles.OWNER, Roles.TEAM_MANAGER], true); - const nxt = jest.fn(); - const cu = {}; - const req = { - currentUser: cu, - params: {}, - query: { - formId: formId, - }, - }; + describe('allows', () => { + test('role is exact match', async () => { + service.getUserForms.mockReturnValueOnce([ + { + formId: formId, + roles: [Roles.OWNER], + }, + ]); + const req = getMockReq({ + currentUser: {}, + params: { + formId: formId, + }, + }); + const { res, next } = getMockRes(); - await hfr(req, testRes, nxt); + await hasFormRoles([Roles.OWNER])(req, res, next); - expect(nxt).toHaveBeenCalledTimes(1); - expect(nxt).toHaveBeenCalledWith(); + expect(service.getUserForms).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(); + }); + + test('single role is start of subset', async () => { + service.getUserForms.mockReturnValueOnce([ + { + formId: formId, + roles: [Roles.OWNER, Roles.TEAM_MANAGER], + }, + ]); + const req = getMockReq({ + currentUser: {}, + params: { + formId: formId, + }, + }); + const { res, next } = getMockRes(); + + await hasFormRoles([Roles.OWNER])(req, res, next); + + expect(service.getUserForms).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(); + }); + + test('single role is middle of subset', async () => { + service.getUserForms.mockReturnValueOnce([ + { + formId: formId, + roles: [Roles.FORM_DESIGNER, Roles.OWNER, Roles.TEAM_MANAGER], + }, + ]); + const req = getMockReq({ + currentUser: {}, + params: { + formId: formId, + }, + }); + const { res, next } = getMockRes(); + + await hasFormRoles([Roles.OWNER])(req, res, next); + + expect(service.getUserForms).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(); + }); + + test('single role is end of subset', async () => { + service.getUserForms.mockReturnValueOnce([ + { + formId: formId, + roles: [Roles.FORM_DESIGNER, Roles.FORM_SUBMITTER, Roles.OWNER], + }, + ]); + const req = getMockReq({ + currentUser: {}, + params: { + formId: formId, + }, + }); + const { res, next } = getMockRes(); + + await hasFormRoles([Roles.OWNER])(req, res, next); + + expect(service.getUserForms).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(); + }); + + test('second role is start of subset', async () => { + service.getUserForms.mockReturnValueOnce([ + { + formId: formId, + roles: [Roles.OWNER, Roles.TEAM_MANAGER], + }, + ]); + const req = getMockReq({ + currentUser: {}, + params: { + formId: formId, + }, + }); + const { res, next } = getMockRes(); + + await hasFormRoles([Roles.FORM_DESIGNER, Roles.OWNER])(req, res, next); + + expect(service.getUserForms).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(); + }); + + test('second role is middle of subset', async () => { + service.getUserForms.mockReturnValueOnce([ + { + formId: formId, + roles: [Roles.FORM_DESIGNER, Roles.OWNER, Roles.TEAM_MANAGER], + }, + ]); + const req = getMockReq({ + currentUser: {}, + params: { + formId: formId, + }, + }); + const { res, next } = getMockRes(); + + await hasFormRoles([Roles.FORM_DESIGNER, Roles.OWNER])(req, res, next); + + expect(service.getUserForms).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(); + }); + + test('second role is end of subset', async () => { + service.getUserForms.mockReturnValueOnce([ + { + formId: formId, + roles: [Roles.FORM_SUBMITTER, Roles.OWNER, Roles.SUBMISSION_REVIEWER], + }, + ]); + const req = getMockReq({ + currentUser: {}, + params: { + formId: formId, + }, + }); + const { res, next } = getMockRes(); + + await hasFormRoles([Roles.FORM_DESIGNER, Roles.SUBMISSION_REVIEWER])(req, res, next); + + expect(service.getUserForms).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(); + }); }); }); @@ -914,6 +1106,13 @@ describe('hasRolePermissions', () => { describe('when removing users from a team', () => { describe('as an owner', () => { it('should succeed with valid data', async () => { + service.getUserForms = jest.fn().mockReturnValue([ + { + userId: userId, + formId: formId, + roles: [Roles.OWNER], + }, + ]); rbacService.readUserRole = jest.fn().mockReturnValue([ { userId: userId2, From 89e17617dc3c16cefa1fa8ec6244d48e224161f4 Mon Sep 17 00:00:00 2001 From: Walter Moar Date: Fri, 17 May 2024 09:24:44 -0700 Subject: [PATCH 06/14] refactor: FORMS-1265 create new problems in a consistent way (#1359) --- app/src/forms/auth/middleware/userAccess.js | 126 ++++++++++---------- 1 file changed, 63 insertions(+), 63 deletions(-) diff --git a/app/src/forms/auth/middleware/userAccess.js b/app/src/forms/auth/middleware/userAccess.js index d45a96d2c..31f97ce90 100644 --- a/app/src/forms/auth/middleware/userAccess.js +++ b/app/src/forms/auth/middleware/userAccess.js @@ -64,7 +64,9 @@ const _hasAnyPermission = (userPermissions, requiredPermissions) => { */ const _getForm = async (currentUser, formId, includeDeleted) => { if (!uuid.validate(formId)) { - throw new Problem(400, { detail: 'Bad formId' }); + throw new Problem(400, { + detail: 'Bad formId', + }); } const forms = await service.getUserForms(currentUser, { @@ -102,7 +104,9 @@ const currentUser = async (req, _res, next) => { if (bearerToken) { const ok = await jwtService.validateAccessToken(bearerToken); if (!ok) { - throw new Problem(401, { detail: 'Authorization token is invalid.' }); + throw new Problem(401, { + detail: 'Authorization token is invalid.', + }); } } @@ -186,7 +190,9 @@ const hasSubmissionPermissions = (permissions) => { // but give precedence to params. const submissionId = req.params.formSubmissionId || req.query.formSubmissionId; if (!uuid.validate(submissionId)) { - throw new Problem(400, { detail: 'Bad formSubmissionId' }); + throw new Problem(400, { + detail: 'Bad formSubmissionId', + }); } // Get the submission results so we know what form this submission is for. @@ -244,7 +250,9 @@ const filterMultipleSubmissions = () => { const submissionIds = req.body && req.body.submissionIds; if (!Array.isArray(submissionIds)) { // No submission provided to this route that secures based on form... that's a problem! - return next(new Problem(401, { detail: 'SubmissionIds not found on request.' })); + throw new Problem(401, { + detail: 'SubmissionIds not found on request.', + }); } let formIdWithDeletePermission = req.formIdWithDeletePermission; @@ -253,22 +261,24 @@ const filterMultipleSubmissions = () => { const formId = req.params.formId || req.query.formId; if (!formId) { // No submission provided to this route that secures based on form... that's a problem! - return next(new Problem(401, { detail: 'Form Id not found on request.' })); + throw new Problem(401, { + detail: 'Form Id not found on request.', + }); } //validate form id if (!uuid.validate(formId)) { - return next(new Problem(401, { detail: 'Not a valid form id' })); + throw new Problem(401, { + detail: 'Not a valid form id', + }); } //validate all submission ids const isValidSubmissionId = submissionIds.every((submissionId) => uuid.validate(submissionId)); if (!isValidSubmissionId) { - return next( - new Problem(401, { - detail: 'Invalid submissionId(s) in the submissionIds list.', - }) - ); + throw new Problem(401, { + detail: 'Invalid submissionId(s) in the submissionIds list.', + }); } if (formIdWithDeletePermission === formId) { @@ -277,26 +287,23 @@ const filterMultipleSubmissions = () => { const isForeignSubmissionId = metaData.every((SubmissionMetadata) => SubmissionMetadata.formId === formId); if (!isForeignSubmissionId || metaData.length !== submissionIds.length) { - return next( - new Problem(401, { - detail: 'Current user does not have required permission(s) for some submissions in the submissionIds list.', - }) - ); + throw new Problem(401, { + detail: 'Current user does not have required permission(s) for some submissions in the submissionIds list.', + }); } return next(); } - return next( - new Problem(401, { - detail: 'Current user does not have required permission(s) for to delete submissions', - }) - ); + + throw new Problem(401, { + detail: 'Current user does not have required permission(s) for to delete submissions', + }); } catch (error) { next(error); } }; }; -const hasFormRole = async (formId, user, role) => { +const _hasFormRole = async (formId, user, role) => { let hasRole = false; const forms = await service.getUserForms(user, { @@ -323,17 +330,17 @@ const hasFormRole = async (formId, user, role) => { * everything is OK, otherwise it calls next() with a Problem describing the * error. * - * @param {string[]} formRoles the roles the user needs one of for the form. + * @param {string[]} roles the roles the user needs one of for the form. * @returns nothing */ -const hasFormRoles = (formRoles) => { +const hasFormRoles = (roles) => { return async (req, _res, next) => { try { // The request must include a formId, either in params or query, but give // precedence to params. const form = await _getForm(req.currentUser, req.params.formId || req.query.formId, false); - if (!_hasAnyPermission(form?.roles, formRoles)) { + if (!_hasAnyPermission(form?.roles, roles)) { throw new Problem(401, { detail: 'You do not have permission to update this role.', }); @@ -353,23 +360,21 @@ const hasRolePermissions = (removingUsers = false) => { const formId = req.params.formId || req.query.formId; if (!formId) { // No form provided to this route that secures based on form... that's a problem! - return new Problem(401, { + throw new Problem(401, { detail: 'Form Id not found on request.', - }).send(res); + }); } const currentUser = req.currentUser; const data = req.body; - const isOwner = await hasFormRole(formId, currentUser, Roles.OWNER); + const isOwner = await _hasFormRole(formId, currentUser, Roles.OWNER); if (removingUsers) { if (data.includes(currentUser.id)) - return next( - new Problem(401, { - detail: "You can't remove yourself from this form.", - }) - ); + throw new Problem(401, { + detail: "You can't remove yourself from this form.", + }); if (!isOwner) { for (let i = 0; i < data.length; i++) { @@ -379,27 +384,25 @@ const hasRolePermissions = (removingUsers = false) => { // Can't update another user's roles if they are an owner if (userRoles.some((fru) => fru.role === Roles.OWNER) && userId !== currentUser.id) { - return next( - new Problem(401, { - detail: "You can not update an owner's roles.", - }) - ); + throw new Problem(401, { + detail: "You can not update an owner's roles.", + }); } // If the user is trying to remove the designer role if (userRoles.some((fru) => fru.role === Roles.FORM_DESIGNER)) { - return next( - new Problem(401, { - detail: "You can't remove a form designer role.", - }) - ); + throw new Problem(401, { + detail: "You can't remove a form designer role.", + }); } } } } else { const userId = req.params.userId || req.query.userId; if (!userId || (userId && userId.length === 0)) { - return new Problem(401, { detail: 'User Id not found on request.' }); + throw new Problem(401, { + detail: 'User Id not found on request.', + }); } if (!isOwner) { @@ -407,40 +410,38 @@ const hasRolePermissions = (removingUsers = false) => { // If the user is trying to remove the team manager role for their own userid if (userRoles.some((fru) => fru.role === Roles.TEAM_MANAGER) && !data.some((role) => role.role === Roles.TEAM_MANAGER) && userId == currentUser.id) { - return next( - new Problem(401, { - detail: "You can't remove your own team manager role.", - }) - ); + throw new Problem(401, { + detail: "You can't remove your own team manager role.", + }); } // Can't update another user's roles if they are an owner if (userRoles.some((fru) => fru.role === Roles.OWNER) && userId !== currentUser.id) { - return next(new Problem(401, { detail: "You can't update an owner's roles." })); + throw new Problem(401, { + detail: "You can't update an owner's roles.", + }); } if (!userRoles.some((fru) => fru.role === Roles.OWNER) && data.some((role) => role.role === Roles.OWNER)) { - return next(new Problem(401, { detail: "You can't add an owner role." })); + throw new Problem(401, { + detail: "You can't add an owner role.", + }); } // If the user is trying to remove the designer role for another userid if (userRoles.some((fru) => fru.role === Roles.FORM_DESIGNER) && !data.some((role) => role.role === Roles.FORM_DESIGNER)) { - return next( - new Problem(401, { - detail: "You can't remove a form designer role.", - }) - ); + throw new Problem(401, { + detail: "You can't remove a form designer role.", + }); } if (!userRoles.some((fru) => fru.role === Roles.FORM_DESIGNER) && data.some((role) => role.role === Roles.FORM_DESIGNER)) { - return next( - new Problem(401, { - detail: "You can't add a form designer role.", - }) - ); + throw new Problem(401, { + detail: "You can't add a form designer role.", + }); } } } - return next(); + next(); } catch (error) { next(error); } @@ -451,7 +452,6 @@ module.exports = { currentUser, filterMultipleSubmissions, hasFormPermissions, - hasFormRole, hasFormRoles, hasRolePermissions, hasSubmissionPermissions, From 8d7ba54231724d4b1bb67a3d50afb7a03f990972 Mon Sep 17 00:00:00 2001 From: Walter Moar Date: Fri, 17 May 2024 13:55:22 -0700 Subject: [PATCH 07/14] test: FORMS-1265 rewrite tests for hasRolePermissions (#1360) --- app/src/forms/auth/middleware/userAccess.js | 15 +- .../forms/auth/middleware/userAccess.spec.js | 1585 ++++++----------- 2 files changed, 533 insertions(+), 1067 deletions(-) diff --git a/app/src/forms/auth/middleware/userAccess.js b/app/src/forms/auth/middleware/userAccess.js index 31f97ce90..8447fa534 100644 --- a/app/src/forms/auth/middleware/userAccess.js +++ b/app/src/forms/auth/middleware/userAccess.js @@ -10,7 +10,7 @@ const rbacService = require('../../rbac/service'); /** * Checks that the user's permissions contains every required permission. * - * @param {string[]} userPermissions the permissions that the user has. + * @param {string[]|undefined} userPermissions the permissions the user has. * @param {string[]} requiredPermissions the permissions needed for access. * @returns true if all required permissions are in the user permissions. */ @@ -33,7 +33,7 @@ const _hasAllPermissions = (userPermissions, requiredPermissions) => { /** * Checks that the user's permissions contains any of the required permissions. * - * @param {string[]} userPermissions the permissions that the user has. + * @param {string[]|undefined} userPermissions the permissions the user has. * @param {string[]} requiredPermissions the permissions needed for access. * @returns true if any required permissions is in the user permissions. */ @@ -353,8 +353,17 @@ const hasFormRoles = (roles) => { }; }; +/** + * Express middleware to check that the calling user has one of the given roles for + * the form identified by params.formId or query.formId. This falls through if + * everything is OK, otherwise it calls next() with a Problem describing the + * error. + * + * @param {string[]} roles the roles the user needs one of for the form. + * @returns nothing + */ const hasRolePermissions = (removingUsers = false) => { - return async (req, res, next) => { + return async (req, _res, next) => { try { // If we invoke this middleware and the caller is acting on a specific formId, whether in a param or query (precedence to param) const formId = req.params.formId || req.query.formId; diff --git a/app/tests/unit/forms/auth/middleware/userAccess.spec.js b/app/tests/unit/forms/auth/middleware/userAccess.spec.js index 900341b8e..a8d7cc031 100644 --- a/app/tests/unit/forms/auth/middleware/userAccess.spec.js +++ b/app/tests/unit/forms/auth/middleware/userAccess.spec.js @@ -5,13 +5,14 @@ const uuid = require('uuid'); const { currentUser, hasFormPermissions, hasSubmissionPermissions, hasFormRoles, hasRolePermissions } = require('../../../../../src/forms/auth/middleware/userAccess'); const jwtService = require('../../../../../src/components/jwtService'); -const service = require('../../../../../src/forms/auth/service'); const rbacService = require('../../../../../src/forms/rbac/service'); +const service = require('../../../../../src/forms/auth/service'); + const formId = uuid.v4(); const formSubmissionId = uuid.v4(); -const userId = 'c6455376-382c-439d-a811-0381a012d695'; -const userId2 = 'c6455376-382c-439d-a811-0381a012d696'; +const userId = uuid.v4(); +const userId2 = uuid.v4(); const bearerToken = Math.random().toString(36).substring(2); @@ -1102,1102 +1103,558 @@ describe('hasFormRoles', () => { }); }); +// External dependencies used by the implementation are: +// - service.getUserForms: gets the forms that the user can access +// - rbacService.readUserRole: gets the roles that user has on a form +// describe('hasRolePermissions', () => { - describe('when removing users from a team', () => { - describe('as an owner', () => { - it('should succeed with valid data', async () => { - service.getUserForms = jest.fn().mockReturnValue([ - { - userId: userId, - formId: formId, - roles: [Roles.OWNER], - }, - ]); - rbacService.readUserRole = jest.fn().mockReturnValue([ - { - userId: userId2, - formId: formId, - role: Roles.OWNER, - }, - ]); - const hrp = hasRolePermissions(true); - const nxt = jest.fn(); - - const cu = { + // Default mock value where the user has no access to forms + service.getUserForms = jest.fn().mockReturnValue([]); + + // Default mock value where the user has no roles + rbacService.readUserRole = jest.fn().mockReturnValue([]); + + it('returns a middleware function', async () => { + const middleware = hasRolePermissions(); + + expect(middleware).toBeInstanceOf(Function); + }); + + describe('401 response when', () => { + const expectedStatus = { status: 401 }; + + test('formId missing', async () => { + const req = getMockReq({ + params: { + submissionId: formSubmissionId, + }, + query: { + otherQueryThing: 'SOMETHING', + }, + }); + const { res, next } = getMockRes(); + + await hasRolePermissions()(req, res, next); + + expect(service.getUserForms).toHaveBeenCalledTimes(0); + expect(rbacService.readUserRole).toHaveBeenCalledTimes(0); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(expect.objectContaining(expectedStatus)); + expect(next).toHaveBeenCalledWith( + expect.objectContaining({ + detail: 'Form Id not found on request.', + }) + ); + }); + + test.skip('formId not a uuid', async () => { + const req = getMockReq({ + currentUser: { id: userId, - }; - const req = { - currentUser: cu, - params: {}, - query: { - formId: formId, - }, - body: [userId2], - }; - - await hrp(req, testRes, nxt); - - expect(nxt).toHaveBeenCalledTimes(1); - expect(nxt).toHaveBeenCalledWith(); + }, + params: { + formId: 'not-a-uuid', + }, + query: { + otherQueryThing: 'SOMETHING', + }, }); + const { res, next } = getMockRes(); + + await hasRolePermissions()(req, res, next); + + expect(service.getUserForms).toHaveBeenCalledTimes(1); + expect(rbacService.readUserRole).toHaveBeenCalledTimes(0); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(expect.objectContaining(expectedStatus)); + expect(next).toHaveBeenCalledWith( + expect.objectContaining({ + detail: 'Form Id invalid UUID.', // or whatever... missing functionality. + }) + ); }); - describe('as a team manager', () => { - it('should succeed with valid data', async () => { - rbacService.readUserRole = jest.fn().mockReturnValue([ - { - userId: userId2, - formId: formId, - role: Roles.FORM_SUBMITTER, - }, - ]); + test('removing and owner cannot remove self', async () => { + service.getUserForms.mockReturnValueOnce([ + { + formId: formId, + roles: [Roles.FORM_DESIGNER, Roles.OWNER, Roles.TEAM_MANAGER], + }, + ]); + const req = getMockReq({ + body: [userId], + currentUser: { + id: userId, + }, + params: { + formId: formId, + }, + }); + const { res, next } = getMockRes(); - const hrp = hasRolePermissions(true); + await hasRolePermissions(true)(req, res, next); - const nxt = jest.fn(); + expect(service.getUserForms).toHaveBeenCalledTimes(1); + expect(rbacService.readUserRole).toHaveBeenCalledTimes(0); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(expect.objectContaining(expectedStatus)); + expect(next).toHaveBeenCalledWith( + expect.objectContaining({ + detail: "You can't remove yourself from this form.", + }) + ); + }); - const cu = { + test('removing and non-owner cannot remove an owner', async () => { + service.getUserForms.mockReturnValueOnce([ + { + formId: formId, + roles: [Roles.TEAM_MANAGER], + }, + ]); + rbacService.readUserRole.mockReturnValueOnce([{ role: Roles.OWNER }]); + const req = getMockReq({ + body: [userId2], + currentUser: { id: userId, - }; - const req = { - currentUser: cu, - params: {}, - query: { - formId: formId, - }, - body: [userId2], - }; - - await hrp(req, testRes, nxt); - - expect(nxt).toHaveBeenCalledTimes(1); - expect(nxt).toHaveBeenCalledWith(); + }, + params: { + formId: formId, + }, }); + const { res, next } = getMockRes(); - it("falls through if you're trying to remove your own team manager role", async () => { - rbacService.readUserRole = jest.fn().mockReturnValue([ - { - userId: userId2, - formId: formId, - role: Roles.TEAM_MANAGER, - }, - ]); + await hasRolePermissions(true)(req, res, next); - const hrp = hasRolePermissions(true); + expect(service.getUserForms).toHaveBeenCalledTimes(1); + expect(rbacService.readUserRole).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(expect.objectContaining(expectedStatus)); + expect(next).toHaveBeenCalledWith( + expect.objectContaining({ + detail: "You can not update an owner's roles.", + }) + ); + }); - const nxt = jest.fn(); + test('removing and non-owner cannot remove a form designer', async () => { + service.getUserForms.mockReturnValueOnce([ + { + formId: formId, + roles: [Roles.TEAM_MANAGER], + }, + ]); + rbacService.readUserRole.mockReturnValueOnce([{ role: Roles.FORM_DESIGNER }]); + const req = getMockReq({ + body: [userId2], + currentUser: { + id: userId, + }, + params: { + formId: formId, + }, + }); + const { res, next } = getMockRes(); + + await hasRolePermissions(true)(req, res, next); + + expect(service.getUserForms).toHaveBeenCalledTimes(1); + expect(rbacService.readUserRole).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(expect.objectContaining(expectedStatus)); + expect(next).toHaveBeenCalledWith( + expect.objectContaining({ + detail: "You can't remove a form designer role.", + }) + ); + }); + + test('updating and user id missing', async () => { + service.getUserForms.mockReturnValueOnce([ + { + formId: formId, + roles: [Roles.FORM_DESIGNER, Roles.OWNER, Roles.TEAM_MANAGER], + }, + ]); + const req = getMockReq({ + currentUser: { + id: userId, + }, + params: { + formId: formId, + }, + }); + const { res, next } = getMockRes(); + + await hasRolePermissions()(req, res, next); + + expect(service.getUserForms).toHaveBeenCalledTimes(1); + expect(rbacService.readUserRole).toHaveBeenCalledTimes(0); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(expect.objectContaining(expectedStatus)); + expect(next).toHaveBeenCalledWith( + expect.objectContaining({ + detail: 'User Id not found on request.', + }) + ); + }); + + test.skip('updating and user id not a uuid', async () => { + service.getUserForms.mockReturnValueOnce([ + { + formId: formId, + roles: [Roles.FORM_DESIGNER, Roles.OWNER, Roles.TEAM_MANAGER], + }, + ]); + const req = getMockReq({ + currentUser: { + id: userId, + }, + params: { + formId: formId, + userId: 'not-a-uuid', + }, + }); + const { res, next } = getMockRes(); + + await hasRolePermissions()(req, res, next); + + expect(service.getUserForms).toHaveBeenCalledTimes(1); + expect(rbacService.readUserRole).toHaveBeenCalledTimes(0); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(expect.objectContaining(expectedStatus)); + expect(next).toHaveBeenCalledWith( + expect.objectContaining({ + detail: 'User Id not found on request.', + }) + ); + }); - const cu = { + test('updating and non-owner cannot remove own team manager', async () => { + service.getUserForms.mockReturnValueOnce([ + { + formId: formId, + roles: [Roles.TEAM_MANAGER], + }, + ]); + rbacService.readUserRole.mockReturnValueOnce([{ role: Roles.TEAM_MANAGER }]); + const req = getMockReq({ + body: [{ role: Roles.SUBMISSION_APPROVER }], + currentUser: { id: userId, - }; - const req = { - currentUser: cu, - params: {}, - query: { - formId: formId, - }, - body: [userId2], - }; - - await hrp(req, testRes, nxt); - - expect(nxt).toHaveBeenCalledTimes(1); - expect(nxt).toHaveBeenCalledWith(); + }, + params: { + formId: formId, + userId: userId, + }, + }); + const { res, next } = getMockRes(); + + await hasRolePermissions()(req, res, next); + + expect(service.getUserForms).toHaveBeenCalledTimes(1); + expect(rbacService.readUserRole).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(expect.objectContaining(expectedStatus)); + expect(next).toHaveBeenCalledWith( + expect.objectContaining({ + detail: "You can't remove your own team manager role.", + }) + ); + }); + + test('updating and non-owner cannot update an owner', async () => { + service.getUserForms.mockReturnValueOnce([ + { + formId: formId, + roles: [Roles.TEAM_MANAGER], + }, + ]); + rbacService.readUserRole.mockReturnValueOnce([{ role: Roles.OWNER }]); + const req = getMockReq({ + body: [{ role: Roles.SUBMISSION_APPROVER }], + currentUser: { + id: userId, + }, + params: { + formId: formId, + userId: userId2, + }, }); + const { res, next } = getMockRes(); + + await hasRolePermissions()(req, res, next); - it("falls through if you're trying to remove an owner role", async () => { - service.getUserForms = jest.fn().mockReturnValue([ - { - formId: formId, - roles: [Roles.TEAM_MANAGER], - }, - ]); - rbacService.readUserRole = jest.fn().mockReturnValue([ - { - userId: userId2, - formId: formId, - role: Roles.OWNER, - }, - ]); - - const hrp = hasRolePermissions(true); - - const nxt = jest.fn(); - - const cu = { + expect(service.getUserForms).toHaveBeenCalledTimes(1); + expect(rbacService.readUserRole).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(expect.objectContaining(expectedStatus)); + expect(next).toHaveBeenCalledWith( + expect.objectContaining({ + detail: "You can't update an owner's roles.", + }) + ); + }); + + test('updating and non-owner cannot add an owner', async () => { + service.getUserForms.mockReturnValueOnce([ + { + formId: formId, + roles: [Roles.TEAM_MANAGER], + }, + ]); + rbacService.readUserRole.mockReturnValueOnce([{ role: Roles.TEAM_MANAGER }]); + const req = getMockReq({ + body: [{ role: Roles.OWNER }], + currentUser: { id: userId, - }; - const req = { - currentUser: cu, - params: {}, - query: { - formId: formId, - }, - body: [userId2], - }; - - await hrp(req, testRes, nxt); - - expect(nxt).toHaveBeenCalledTimes(1); - expect(nxt).toHaveBeenCalledWith(new Problem(401, { detail: "You can't update an owner's roles." })); + }, + params: { + formId: formId, + userId: userId2, + }, }); + const { res, next } = getMockRes(); + + await hasRolePermissions()(req, res, next); + + expect(service.getUserForms).toHaveBeenCalledTimes(1); + expect(rbacService.readUserRole).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(expect.objectContaining(expectedStatus)); + expect(next).toHaveBeenCalledWith( + expect.objectContaining({ + detail: "You can't add an owner role.", + }) + ); + }); - it("falls through if you're trying to remove a form designer role", async () => { - rbacService.readUserRole = jest.fn().mockReturnValue([ - { - userId: userId2, - formId: formId, - role: Roles.FORM_DESIGNER, - }, - ]); + test('updating and non-owner cannot remove designer', async () => { + service.getUserForms.mockReturnValueOnce([ + { + formId: formId, + roles: [Roles.TEAM_MANAGER], + }, + ]); + rbacService.readUserRole.mockReturnValueOnce([{ role: Roles.FORM_DESIGNER }]); + const req = getMockReq({ + body: [{ role: Roles.SUBMISSION_APPROVER }], + currentUser: { + id: userId, + }, + params: { + formId: formId, + userId: userId2, + }, + }); + const { res, next } = getMockRes(); - const hrp = hasRolePermissions(true); + await hasRolePermissions()(req, res, next); - const nxt = jest.fn(); + expect(service.getUserForms).toHaveBeenCalledTimes(1); + expect(rbacService.readUserRole).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(expect.objectContaining(expectedStatus)); + expect(next).toHaveBeenCalledWith( + expect.objectContaining({ + detail: "You can't remove a form designer role.", + }) + ); + }); - const cu = { + test('updating and non-owner cannot add designer', async () => { + service.getUserForms.mockReturnValueOnce([ + { + formId: formId, + roles: [Roles.TEAM_MANAGER], + }, + ]); + rbacService.readUserRole.mockReturnValueOnce([{ role: Roles.TEAM_MANAGER }]); + const req = getMockReq({ + body: [{ role: Roles.FORM_DESIGNER }], + currentUser: { id: userId, - }; - const req = { - currentUser: cu, - params: {}, - query: { - formId: formId, - }, - body: [userId2], - }; - - await hrp(req, testRes, nxt); - - expect(nxt).toHaveBeenCalledTimes(1); - expect(nxt).toHaveBeenCalledWith(new Problem(401, { detail: "You can't remove a form designer role." })); + }, + params: { + formId: formId, + userId: userId2, + }, }); + const { res, next } = getMockRes(); + + await hasRolePermissions()(req, res, next); + + expect(service.getUserForms).toHaveBeenCalledTimes(1); + expect(rbacService.readUserRole).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(expect.objectContaining(expectedStatus)); + expect(next).toHaveBeenCalledWith( + expect.objectContaining({ + detail: "You can't add a form designer role.", + }) + ); }); }); - describe('when updating user roles on a team', () => { - describe('as an owner', () => { - it('should succeed when removing any roles', async () => { - service.getUserForms = jest.fn().mockReturnValue([ - { - formId: formId, - roles: [Roles.OWNER], - }, - ]); - rbacService.readUserRole = jest.fn().mockReturnValue([ - { - id: '1', - role: Roles.OWNER, - formId: formId, - userId: userId2, - createdBy: '', - createdAt: '', - updatedBy: '', - updatedAt: '', - }, - { - id: '2', - role: Roles.TEAM_MANAGER, - formId: formId, - userId: userId2, - createdBy: '', - createdAt: '', - updatedBy: '', - updatedAt: '', - }, - { - id: '3', - role: Roles.SUBMISSION_REVIEWER, - formId: formId, - userId: userId2, - createdBy: '', - createdAt: '', - updatedBy: '', - updatedAt: '', - }, - { - id: '4', - role: Roles.FORM_DESIGNER, - formId: formId, - userId: userId2, - createdBy: '', - createdAt: '', - updatedBy: '', - updatedAt: '', - }, - { - id: '5', - role: Roles.FORM_SUBMITTER, - formId: formId, - userId: userId2, - createdBy: '', - createdAt: '', - updatedBy: '', - updatedAt: '', - }, - { - id: '6', - role: Roles.SUBMISSION_APPROVER, - formId: formId, - userId: userId2, - createdBy: '', - createdAt: '', - updatedBy: '', - updatedAt: '', - }, - ]); - - const hrp = hasRolePermissions(false); - - const nxt = jest.fn(); - - const cu = { + describe('allows', () => { + test('deleting and non-owner can remove submission reviewer', async () => { + service.getUserForms.mockReturnValueOnce([ + { + formId: formId, + roles: [Roles.TEAM_MANAGER], + }, + ]); + rbacService.readUserRole.mockReturnValueOnce([{ role: Roles.SUBMISSION_REVIEWER }]); + const req = getMockReq({ + body: [userId2], + currentUser: { id: userId, - }; - const req = { - currentUser: cu, - params: { - userId: userId2, - formId: formId, - }, - query: { - formId: formId, - }, - body: [], - }; - - await hrp(req, testRes, nxt); - - expect(nxt).toHaveBeenCalledTimes(1); - expect(nxt).toHaveBeenCalledWith(); + }, + params: { + formId: formId, + }, }); + const { res, next } = getMockRes(); + + await hasRolePermissions(true)(req, res, next); + + expect(service.getUserForms).toHaveBeenCalledTimes(1); + expect(rbacService.readUserRole).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(); }); - describe('as a team manager', () => { - describe('the user being updated is your own', () => { - it("falls through if you're trying to remove your own team manager role", async () => { - service.getUserForms = jest.fn().mockReturnValue([ - { - formId: formId, - roles: [Roles.TEAM_MANAGER, Roles.FORM_DESIGNER], - }, - ]); - rbacService.readUserRole = jest.fn().mockReturnValue([ - { - id: '1', - role: Roles.TEAM_MANAGER, - formId: formId, - userId: userId, - createdBy: '', - createdAt: '', - updatedBy: '', - updatedAt: '', - }, - { - id: '2', - role: Roles.FORM_DESIGNER, - formId: formId, - userId: userId, - createdBy: '', - createdAt: '', - updatedBy: '', - updatedAt: '', - }, - ]); - - const hrp = hasRolePermissions(false); - - const nxt = jest.fn(); - - const cu = { - id: userId, - }; - const req = { - currentUser: cu, - params: { - userId: cu.id, - }, - query: { - formId: formId, - }, - body: [ - { - userId: cu.id, - formId: formId, - role: Roles.FORM_DESIGNER, - }, - ], - }; - - await hrp(req, testRes, nxt); - - expect(nxt).toHaveBeenCalledTimes(1); - expect(nxt).toHaveBeenCalledWith(new Problem(401, { detail: "You can't remove your own team manager role." })); - }); + test('deleting and owner can remove an owner', async () => { + service.getUserForms.mockReturnValueOnce([ + { + formId: formId, + roles: [Roles.OWNER], + }, + ]); + const req = getMockReq({ + body: [userId2], + currentUser: { + id: userId, + }, + params: { + formId: formId, + }, }); + const { res, next } = getMockRes(); - describe('the user being updated is not your own', () => { - describe('is an owner', () => { - it('falls through if trying to make any role changes', async () => { - rbacService.readUserRole = jest.fn().mockReturnValue([ - { - id: '1', - role: Roles.OWNER, - formId: formId, - userId: userId2, - createdBy: '', - createdAt: '', - updatedBy: '', - updatedAt: '', - }, - { - id: '2', - role: Roles.FORM_DESIGNER, - formId: formId, - userId: userId2, - createdBy: '', - createdAt: '', - updatedBy: '', - updatedAt: '', - }, - { - id: '3', - role: Roles.SUBMISSION_REVIEWER, - formId: formId, - userId: userId2, - createdBy: '', - createdAt: '', - updatedBy: '', - updatedAt: '', - }, - { - id: '6', - role: Roles.SUBMISSION_APPROVER, - formId: formId, - userId: userId2, - createdBy: '', - createdAt: '', - updatedBy: '', - updatedAt: '', - }, - ]); - - const hrp = hasRolePermissions(false); - - const nxt = jest.fn(); - - const cu = { - id: userId, - }; - const req = { - currentUser: cu, - params: { - userId: userId2, - }, - query: { - formId: formId, - }, - body: [ - { - userId: userId2, - formId: formId, - role: Roles.SUBMISSION_REVIEWER, - }, - { - userId: userId2, - formId: formId, - role: Roles.FORM_SUBMITTER, - }, - { - userId: userId2, - formId: formId, - role: Roles.SUBMISSION_APPROVER, - }, - ], - }; - - await hrp(req, testRes, nxt); - - expect(nxt).toHaveBeenCalledTimes(1); - expect(nxt).toHaveBeenCalledWith(new Problem(401, { detail: "You can't update an owner's roles." })); - }); - }); - - describe('is not an owner', () => { - it('falls through if trying to add an owner role', async () => { - rbacService.readUserRole = jest.fn().mockReturnValue([ - { - id: '1', - role: Roles.FORM_SUBMITTER, - formId: formId, - userId: userId2, - createdBy: '', - createdAt: '', - updatedBy: '', - updatedAt: '', - }, - { - id: '3', - role: Roles.SUBMISSION_REVIEWER, - formId: formId, - userId: userId2, - createdBy: '', - createdAt: '', - updatedBy: '', - updatedAt: '', - }, - { - id: '6', - role: Roles.SUBMISSION_APPROVER, - formId: formId, - userId: userId2, - createdBy: '', - createdAt: '', - updatedBy: '', - updatedAt: '', - }, - ]); - - const hrp = hasRolePermissions(false); - - const nxt = jest.fn(); - - const cu = { - id: userId, - }; - const req = { - currentUser: cu, - params: { - userId: userId2, - }, - query: { - formId: formId, - }, - body: [ - { - userId: userId2, - formId: formId, - role: Roles.OWNER, - }, - { - userId: userId2, - formId: formId, - role: Roles.FORM_SUBMITTER, - }, - ], - }; - - await hrp(req, testRes, nxt); - - expect(nxt).toHaveBeenCalledTimes(1); - expect(nxt).toHaveBeenCalledWith(new Problem(401, { detail: "You can't add an owner role." })); - }); - - it('falls through if trying to remove an owner role', async () => { - rbacService.readUserRole = jest.fn().mockReturnValue([ - { - id: '1', - role: Roles.OWNER, - formId: formId, - userId: userId2, - createdBy: '', - createdAt: '', - updatedBy: '', - updatedAt: '', - }, - { - id: '2', - role: Roles.FORM_DESIGNER, - formId: formId, - userId: userId2, - createdBy: '', - createdAt: '', - updatedBy: '', - updatedAt: '', - }, - { - id: '3', - role: Roles.SUBMISSION_REVIEWER, - formId: formId, - userId: userId2, - createdBy: '', - createdAt: '', - updatedBy: '', - updatedAt: '', - }, - { - id: '6', - role: Roles.SUBMISSION_APPROVER, - formId: formId, - userId: userId2, - createdBy: '', - createdAt: '', - updatedBy: '', - updatedAt: '', - }, - ]); - - const hrp = hasRolePermissions(false); - - const nxt = jest.fn(); - - const cu = { - id: userId, - }; - const req = { - currentUser: cu, - params: { - userId: userId2, - }, - query: { - formId: formId, - }, - body: [ - { - userId: userId2, - formId: formId, - role: Roles.SUBMISSION_REVIEWER, - }, - { - userId: userId2, - formId: formId, - role: Roles.FORM_SUBMITTER, - }, - { - userId: userId2, - formId: formId, - role: Roles.SUBMISSION_APPROVER, - }, - ], - }; - - await hrp(req, testRes, nxt); - - expect(nxt).toHaveBeenCalledTimes(1); - expect(nxt).toHaveBeenCalledWith(new Problem(401, { detail: "You can't update an owner's roles." })); - }); - - it('falls through if trying to add a designer role', async () => { - rbacService.readUserRole = jest.fn().mockReturnValue([ - { - id: '1', - role: Roles.FORM_SUBMITTER, - formId: formId, - userId: userId2, - createdBy: '', - createdAt: '', - updatedBy: '', - updatedAt: '', - }, - { - id: '3', - role: Roles.SUBMISSION_REVIEWER, - formId: formId, - userId: userId2, - createdBy: '', - createdAt: '', - updatedBy: '', - updatedAt: '', - }, - { - id: '6', - role: Roles.SUBMISSION_APPROVER, - formId: formId, - userId: userId2, - createdBy: '', - createdAt: '', - updatedBy: '', - updatedAt: '', - }, - ]); - - const hrp = hasRolePermissions(false); - - const nxt = jest.fn(); - - const cu = { - id: userId, - }; - const req = { - currentUser: cu, - params: { - userId: userId2, - }, - query: { - formId: formId, - }, - body: [ - { - userId: userId2, - formId: formId, - role: Roles.FORM_DESIGNER, - }, - { - userId: userId2, - formId: formId, - role: Roles.FORM_SUBMITTER, - }, - ], - }; - - await hrp(req, testRes, nxt); - - expect(nxt).toHaveBeenCalledTimes(1); - expect(nxt).toHaveBeenCalledWith(new Problem(401, { detail: "You can't add a form designer role." })); - }); - - it('falls through if trying to remove a designer role', async () => { - rbacService.readUserRole = jest.fn().mockReturnValue([ - { - id: '1', - role: Roles.FORM_SUBMITTER, - formId: formId, - userId: userId2, - createdBy: '', - createdAt: '', - updatedBy: '', - updatedAt: '', - }, - { - id: '2', - role: Roles.FORM_DESIGNER, - formId: formId, - userId: userId2, - createdBy: '', - createdAt: '', - updatedBy: '', - updatedAt: '', - }, - { - id: '3', - role: Roles.SUBMISSION_REVIEWER, - formId: formId, - userId: userId2, - createdBy: '', - createdAt: '', - updatedBy: '', - updatedAt: '', - }, - { - id: '6', - role: Roles.SUBMISSION_APPROVER, - formId: formId, - userId: userId2, - createdBy: '', - createdAt: '', - updatedBy: '', - updatedAt: '', - }, - ]); - - const hrp = hasRolePermissions(false); - - const nxt = jest.fn(); - - const cu = { - id: userId, - }; - const req = { - currentUser: cu, - params: { - userId: userId2, - }, - query: { - formId: formId, - }, - body: [ - { - userId: userId2, - formId: formId, - role: Roles.SUBMISSION_REVIEWER, - }, - { - userId: userId2, - formId: formId, - role: Roles.FORM_SUBMITTER, - }, - { - userId: userId2, - formId: formId, - role: Roles.SUBMISSION_APPROVER, - }, - ], - }; - - await hrp(req, testRes, nxt); - - expect(nxt).toHaveBeenCalledTimes(1); - expect(nxt).toHaveBeenCalledWith(new Problem(401, { detail: "You can't remove a form designer role." })); - }); - - it('should succeed when adding a manager/reviewer/submitter roles', async () => { - rbacService.readUserRole = jest.fn().mockReturnValue([]); - - const hrp = hasRolePermissions(false); - - const nxt = jest.fn(); - - const cu = { - id: userId, - }; - const req = { - currentUser: cu, - params: { - userId: userId2, - }, - query: { - formId: formId, - }, - body: [ - { - userId: userId2, - formId: formId, - role: Roles.TEAM_MANAGER, - }, - { - userId: userId2, - formId: formId, - role: Roles.SUBMISSION_REVIEWER, - }, - { - userId: userId2, - formId: formId, - role: Roles.FORM_SUBMITTER, - }, - { - userId: userId2, - formId: formId, - role: Roles.SUBMISSION_APPROVER, - }, - ], - }; - - await hrp(req, testRes, nxt); - - expect(nxt).toHaveBeenCalledTimes(1); - expect(nxt).toHaveBeenCalledWith(); - }); - - it('should succeed when removing a manager roles', async () => { - rbacService.readUserRole = jest.fn().mockReturnValue([ - { - id: '1', - role: Roles.FORM_SUBMITTER, - formId: formId, - userId: userId2, - createdBy: '', - createdAt: '', - updatedBy: '', - updatedAt: '', - }, - { - id: '2', - role: Roles.TEAM_MANAGER, - formId: formId, - userId: userId2, - createdBy: '', - createdAt: '', - updatedBy: '', - updatedAt: '', - }, - { - id: '3', - role: Roles.SUBMISSION_REVIEWER, - formId: formId, - userId: userId2, - createdBy: '', - createdAt: '', - updatedBy: '', - updatedAt: '', - }, - { - id: '6', - role: Roles.SUBMISSION_APPROVER, - formId: formId, - userId: userId2, - createdBy: '', - createdAt: '', - updatedBy: '', - updatedAt: '', - }, - ]); - - const hrp = hasRolePermissions(false); - - const nxt = jest.fn(); - - const cu = { - id: userId, - }; - const req = { - currentUser: cu, - params: { - userId: userId2, - formId: formId, - }, - query: { - formId: formId, - }, - body: [ - { - userId: userId2, - formId: formId, - role: Roles.SUBMISSION_REVIEWER, - }, - { - userId: userId2, - formId: formId, - role: Roles.FORM_SUBMITTER, - }, - { - userId: userId2, - formId: formId, - role: Roles.SUBMISSION_APPROVER, - }, - ], - }; - - await hrp(req, testRes, nxt); - - expect(nxt).toHaveBeenCalledTimes(1); - expect(nxt).toHaveBeenCalledWith(); - }); - - it('should succeed when removing a reviewer roles', async () => { - rbacService.readUserRole = jest.fn().mockReturnValue([ - { - id: '1', - role: Roles.FORM_SUBMITTER, - formId: formId, - userId: userId2, - createdBy: '', - createdAt: '', - updatedBy: '', - updatedAt: '', - }, - { - id: '2', - role: Roles.TEAM_MANAGER, - formId: formId, - userId: userId2, - createdBy: '', - createdAt: '', - updatedBy: '', - updatedAt: '', - }, - { - id: '3', - role: Roles.SUBMISSION_REVIEWER, - formId: formId, - userId: userId2, - createdBy: '', - createdAt: '', - updatedBy: '', - updatedAt: '', - }, - { - id: '6', - role: Roles.SUBMISSION_APPROVER, - formId: formId, - userId: userId2, - createdBy: '', - createdAt: '', - updatedBy: '', - updatedAt: '', - }, - ]); - - const hrp = hasRolePermissions(false); - - const nxt = jest.fn(); - - const cu = { - id: userId, - }; - const req = { - currentUser: cu, - params: { - userId: userId2, - formId: formId, - }, - query: { - formId: formId, - }, - body: [ - { - userId: userId2, - formId: formId, - role: Roles.TEAM_MANAGER, - }, - { - userId: userId2, - formId: formId, - role: Roles.FORM_SUBMITTER, - }, - ], - }; - - await hrp(req, testRes, nxt); - - expect(nxt).toHaveBeenCalledTimes(1); - expect(nxt).toHaveBeenCalledWith(); - }); - - it('should succeed when removing a submitter roles', async () => { - rbacService.readUserRole = jest.fn().mockReturnValue([ - { - id: '1', - role: Roles.FORM_SUBMITTER, - formId: formId, - userId: userId2, - createdBy: '', - createdAt: '', - updatedBy: '', - updatedAt: '', - }, - { - id: '2', - role: Roles.TEAM_MANAGER, - formId: formId, - userId: userId2, - createdBy: '', - createdAt: '', - updatedBy: '', - updatedAt: '', - }, - { - id: '3', - role: Roles.SUBMISSION_REVIEWER, - formId: formId, - userId: userId2, - createdBy: '', - createdAt: '', - updatedBy: '', - updatedAt: '', - }, - { - id: '6', - role: Roles.SUBMISSION_APPROVER, - formId: formId, - userId: userId2, - createdBy: '', - createdAt: '', - updatedBy: '', - updatedAt: '', - }, - ]); - - const hrp = hasRolePermissions(false); - - const nxt = jest.fn(); - - const cu = { - id: userId, - }; - const req = { - currentUser: cu, - params: { - userId: userId2, - formId: formId, - }, - query: { - formId: formId, - }, - body: [ - { - userId: userId2, - formId: formId, - role: Roles.TEAM_MANAGER, - }, - { - userId: userId2, - formId: formId, - role: Roles.SUBMISSION_REVIEWER, - }, - { - userId: userId2, - formId: formId, - role: Roles.SUBMISSION_APPROVER, - }, - ], - }; - - await hrp(req, testRes, nxt); - - expect(nxt).toHaveBeenCalledTimes(1); - expect(nxt).toHaveBeenCalledWith(); - }); - - it('should succeed when removing a manager/reviewer/submitter roles', async () => { - rbacService.readUserRole = jest.fn().mockReturnValue([ - { - id: '1', - role: Roles.FORM_SUBMITTER, - formId: formId, - userId: userId2, - createdBy: '', - createdAt: '', - updatedBy: '', - updatedAt: '', - }, - { - id: '2', - role: Roles.TEAM_MANAGER, - formId: formId, - userId: userId2, - createdBy: '', - createdAt: '', - updatedBy: '', - updatedAt: '', - }, - { - id: '3', - role: Roles.SUBMISSION_REVIEWER, - formId: formId, - userId: userId2, - createdBy: '', - createdAt: '', - updatedBy: '', - updatedAt: '', - }, - { - id: '6', - role: Roles.SUBMISSION_APPROVER, - formId: formId, - userId: userId2, - createdBy: '', - createdAt: '', - updatedBy: '', - updatedAt: '', - }, - ]); - - const hrp = hasRolePermissions(false); - - const nxt = jest.fn(); - - const cu = { - id: userId, - }; - const req = { - currentUser: cu, - params: { - userId: userId2, - formId: formId, - }, - query: { - formId: formId, - }, - body: [], - }; - - await hrp(req, testRes, nxt); - - expect(nxt).toHaveBeenCalledTimes(1); - expect(nxt).toHaveBeenCalledWith(); - }); - }); + await hasRolePermissions(true)(req, res, next); + + expect(service.getUserForms).toHaveBeenCalledTimes(1); + expect(rbacService.readUserRole).toHaveBeenCalledTimes(0); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(); + }); + + test('deleting and owner can remove a form designer', async () => { + service.getUserForms.mockReturnValueOnce([ + { + formId: formId, + roles: [Roles.OWNER], + }, + ]); + const req = getMockReq({ + body: [userId2], + currentUser: { + id: userId, + }, + params: { + formId: formId, + }, + }); + const { res, next } = getMockRes(); + + await hasRolePermissions(true)(req, res, next); + + expect(service.getUserForms).toHaveBeenCalledTimes(1); + expect(rbacService.readUserRole).toHaveBeenCalledTimes(0); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(); + }); + + test('deleting and owner can remove a form designer with form id in query', async () => { + service.getUserForms.mockReturnValueOnce([ + { + formId: formId, + roles: [Roles.OWNER], + }, + ]); + const req = getMockReq({ + body: [userId2], + currentUser: { + id: userId, + }, + query: { + formId: formId, + }, + }); + const { res, next } = getMockRes(); + + await hasRolePermissions(true)(req, res, next); + + expect(service.getUserForms).toHaveBeenCalledTimes(1); + expect(rbacService.readUserRole).toHaveBeenCalledTimes(0); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(); + }); + + test('updating and non-owner can add approver', async () => { + service.getUserForms.mockReturnValueOnce([ + { + formId: formId, + roles: [Roles.TEAM_MANAGER], + }, + ]); + rbacService.readUserRole.mockReturnValueOnce([{ role: Roles.FORM_SUBMITTER }]); + const req = getMockReq({ + body: [{ role: Roles.SUBMISSION_APPROVER }], + currentUser: { + id: userId, + }, + params: { + formId: formId, + userId: userId2, + }, }); + const { res, next } = getMockRes(); + + await hasRolePermissions()(req, res, next); + + expect(service.getUserForms).toHaveBeenCalledTimes(1); + expect(rbacService.readUserRole).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(); + }); + + test('updating and owner can add owner', async () => { + service.getUserForms.mockReturnValueOnce([ + { + formId: formId, + roles: [Roles.OWNER], + }, + ]); + const req = getMockReq({ + body: [{ role: Roles.OWNER }], + currentUser: { + id: userId, + }, + params: { + formId: formId, + userId: userId2, + }, + }); + const { res, next } = getMockRes(); + + await hasRolePermissions()(req, res, next); + + expect(service.getUserForms).toHaveBeenCalledTimes(1); + expect(rbacService.readUserRole).toHaveBeenCalledTimes(0); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(); }); }); }); From 25cef626ed6719aac393c09a550b6ae31078b0fe Mon Sep 17 00:00:00 2001 From: Walter Moar Date: Fri, 17 May 2024 16:07:03 -0700 Subject: [PATCH 08/14] test: FORMS-1265 rewrite current user tests (#1361) --- app/src/forms/auth/middleware/userAccess.js | 324 +++++++++--------- .../forms/auth/middleware/userAccess.spec.js | 166 ++++----- 2 files changed, 238 insertions(+), 252 deletions(-) diff --git a/app/src/forms/auth/middleware/userAccess.js b/app/src/forms/auth/middleware/userAccess.js index 8447fa534..cc43b4923 100644 --- a/app/src/forms/auth/middleware/userAccess.js +++ b/app/src/forms/auth/middleware/userAccess.js @@ -7,6 +7,40 @@ const Roles = require('../../common/constants').Roles; const service = require('../service'); const rbacService = require('../../rbac/service'); +/** + * Gets the form metadata for the given formId from the forms available to the + * current user. + * + * @param {*} currentUser the user that is currently logged in; may be public. + * @param {uuid} formId the ID of the form to retrieve for the current user. + * @param {boolean} includeDeleted if active form not found, look for a deleted + * form. + * @returns the form metadata if the currentUser has access, or undefined. + */ +const _getForm = async (currentUser, formId, includeDeleted) => { + if (!uuid.validate(formId)) { + throw new Problem(400, { + detail: 'Bad formId', + }); + } + + const forms = await service.getUserForms(currentUser, { + active: true, + formId: formId, + }); + let form = forms.find((f) => f.formId === formId); + + if (!form && includeDeleted) { + const deletedForms = await service.getUserForms(currentUser, { + active: false, + formId: formId, + }); + form = deletedForms.find((f) => f.formId === formId); + } + + return form; +}; + /** * Checks that the user's permissions contains every required permission. * @@ -52,38 +86,25 @@ const _hasAnyPermission = (userPermissions, requiredPermissions) => { return intersection.length > 0; }; -/** - * Gets the form metadata for the given formId from the forms available to the - * current user. - * - * @param {*} currentUser the user that is currently logged in; may be public. - * @param {uuid} formId the ID of the form to retrieve for the current user. - * @param {boolean} includeDeleted if active form not found, look for a deleted - * form. - * @returns the form metadata if the currentUser has access, or undefined. - */ -const _getForm = async (currentUser, formId, includeDeleted) => { - if (!uuid.validate(formId)) { - throw new Problem(400, { - detail: 'Bad formId', - }); - } +const _hasFormRole = async (formId, user, role) => { + let hasRole = false; - const forms = await service.getUserForms(currentUser, { + const forms = await service.getUserForms(user, { active: true, formId: formId, }); - let form = forms.find((f) => f.formId === formId); + const form = forms.find((f) => f.formId === formId); - if (!form && includeDeleted) { - const deletedForms = await service.getUserForms(currentUser, { - active: false, - formId: formId, - }); - form = deletedForms.find((f) => f.formId === formId); + if (form) { + for (let j = 0; j < form.roles.length; j++) { + if (form.roles[j] === role) { + hasRole = true; + break; + } + } } - return form; + return hasRole; }; /** @@ -121,128 +142,6 @@ const currentUser = async (req, _res, next) => { } }; -/** - * Express middleware to check that a user has all the given permissions for a - * form. This falls through if everything is OK, otherwise it calls next() with - * a Problem describing the error. - * - * @param {string[]} permissions the form permissions that the user must have. - * @returns nothing - */ -const hasFormPermissions = (permissions) => { - return async (req, _res, next) => { - try { - // Skip permission checks if req is already validated using an API key. - if (req.apiUser) { - next(); - - return; - } - - // If the currentUser does not exist it means that the route is not set up - // correctly - the currentUser middleware must be called before this - // middleware. - if (!req.currentUser) { - throw new Problem(500, { - detail: 'Current user not found on request', - }); - } - - // The request must include a formId, either in params or query, but give - // precedence to params. - const form = await _getForm(req.currentUser, req.params.formId || req.query.formId, true); - - // If the form doesn't exist, or its permissions don't exist, then access - // will be denied - otherwise check to see if permissions is a subset. - if (!_hasAllPermissions(form?.permissions, permissions)) { - throw new Problem(401, { - detail: 'You do not have access to this form.', - }); - } - - next(); - } catch (error) { - next(error); - } - }; -}; - -/** - * Express middleware to check that the caller has the given permissions for the - * submission identified by params.formSubmissionId or query.formSubmissionId. - * This falls through if everything is OK, otherwise it calls next() with a - * Problem describing the error. - * - * @param {string[]} permissions the access the user needs for the submission. - * @returns nothing - */ -const hasSubmissionPermissions = (permissions) => { - return async (req, _res, next) => { - try { - // Skip permission checks if req is already authorized using an API key. - if (req.apiUser) { - next(); - - return; - } - - // The request must include a formSubmissionId, either in params or query, - // but give precedence to params. - const submissionId = req.params.formSubmissionId || req.query.formSubmissionId; - if (!uuid.validate(submissionId)) { - throw new Problem(400, { - detail: 'Bad formSubmissionId', - }); - } - - // Get the submission results so we know what form this submission is for. - const submissionForm = await service.getSubmissionForm(submissionId); - - // If the current user has elevated permissions on the form, they may have - // access to all submissions for the form. - if (req.currentUser) { - const formFromCurrentUser = await _getForm(req.currentUser, submissionForm.form.id, false); - - // Do they have the submission permissions requested on this form? - if (_hasAllPermissions(formFromCurrentUser?.permissions, permissions)) { - req.formIdWithDeletePermission = submissionForm.form.id; - next(); - - return; - } - } - - // Deleted submissions are only accessible to users with the form - // permissions above. - if (submissionForm.submission.deleted) { - throw new Problem(401, { - detail: 'You do not have access to this submission.', - }); - } - - // Public (anonymous) forms are publicly viewable. - const publicAllowed = submissionForm.form.identityProviders.find((p) => p.code === 'public') !== undefined; - if (permissions.length === 1 && permissions.includes(Permissions.SUBMISSION_READ) && publicAllowed) { - next(); - - return; - } - - // check against the submission level permissions assigned to the user... - const submissionPermission = await service.checkSubmissionPermission(req.currentUser, submissionId, permissions); - if (!submissionPermission) { - throw new Problem(401, { - detail: 'You do not have access to this submission.', - }); - } - - next(); - } catch (error) { - next(error); - } - }; -}; - const filterMultipleSubmissions = () => { return async (req, _res, next) => { try { @@ -303,25 +202,50 @@ const filterMultipleSubmissions = () => { }; }; -const _hasFormRole = async (formId, user, role) => { - let hasRole = false; +/** + * Express middleware to check that a user has all the given permissions for a + * form. This falls through if everything is OK, otherwise it calls next() with + * a Problem describing the error. + * + * @param {string[]} permissions the form permissions that the user must have. + * @returns nothing + */ +const hasFormPermissions = (permissions) => { + return async (req, _res, next) => { + try { + // Skip permission checks if req is already validated using an API key. + if (req.apiUser) { + next(); - const forms = await service.getUserForms(user, { - active: true, - formId: formId, - }); - const form = forms.find((f) => f.formId === formId); + return; + } - if (form) { - for (let j = 0; j < form.roles.length; j++) { - if (form.roles[j] === role) { - hasRole = true; - break; + // If the currentUser does not exist it means that the route is not set up + // correctly - the currentUser middleware must be called before this + // middleware. + if (!req.currentUser) { + throw new Problem(500, { + detail: 'Current user not found on request', + }); } - } - } - return hasRole; + // The request must include a formId, either in params or query, but give + // precedence to params. + const form = await _getForm(req.currentUser, req.params.formId || req.query.formId, true); + + // If the form doesn't exist, or its permissions don't exist, then access + // will be denied - otherwise check to see if permissions is a subset. + if (!_hasAllPermissions(form?.permissions, permissions)) { + throw new Problem(401, { + detail: 'You do not have access to this form.', + }); + } + + next(); + } catch (error) { + next(error); + } + }; }; /** @@ -457,6 +381,82 @@ const hasRolePermissions = (removingUsers = false) => { }; }; +/** + * Express middleware to check that the caller has the given permissions for the + * submission identified by params.formSubmissionId or query.formSubmissionId. + * This falls through if everything is OK, otherwise it calls next() with a + * Problem describing the error. + * + * @param {string[]} permissions the access the user needs for the submission. + * @returns nothing + */ +const hasSubmissionPermissions = (permissions) => { + return async (req, _res, next) => { + try { + // Skip permission checks if req is already authorized using an API key. + if (req.apiUser) { + next(); + + return; + } + + // The request must include a formSubmissionId, either in params or query, + // but give precedence to params. + const submissionId = req.params.formSubmissionId || req.query.formSubmissionId; + if (!uuid.validate(submissionId)) { + throw new Problem(400, { + detail: 'Bad formSubmissionId', + }); + } + + // Get the submission results so we know what form this submission is for. + const submissionForm = await service.getSubmissionForm(submissionId); + + // If the current user has elevated permissions on the form, they may have + // access to all submissions for the form. + if (req.currentUser) { + const formFromCurrentUser = await _getForm(req.currentUser, submissionForm.form.id, false); + + // Do they have the submission permissions requested on this form? + if (_hasAllPermissions(formFromCurrentUser?.permissions, permissions)) { + req.formIdWithDeletePermission = submissionForm.form.id; + next(); + + return; + } + } + + // Deleted submissions are only accessible to users with the form + // permissions above. + if (submissionForm.submission.deleted) { + throw new Problem(401, { + detail: 'You do not have access to this submission.', + }); + } + + // Public (anonymous) forms are publicly viewable. + const publicAllowed = submissionForm.form.identityProviders.find((p) => p.code === 'public') !== undefined; + if (permissions.length === 1 && permissions.includes(Permissions.SUBMISSION_READ) && publicAllowed) { + next(); + + return; + } + + // check against the submission level permissions assigned to the user... + const submissionPermission = await service.checkSubmissionPermission(req.currentUser, submissionId, permissions); + if (!submissionPermission) { + throw new Problem(401, { + detail: 'You do not have access to this submission.', + }); + } + + next(); + } catch (error) { + next(error); + } + }; +}; + module.exports = { currentUser, filterMultipleSubmissions, diff --git a/app/tests/unit/forms/auth/middleware/userAccess.spec.js b/app/tests/unit/forms/auth/middleware/userAccess.spec.js index a8d7cc031..67a108396 100644 --- a/app/tests/unit/forms/auth/middleware/userAccess.spec.js +++ b/app/tests/unit/forms/auth/middleware/userAccess.spec.js @@ -1,5 +1,4 @@ const { getMockReq, getMockRes } = require('@jest-mock/express'); -const Problem = require('api-problem'); const uuid = require('uuid'); const { currentUser, hasFormPermissions, hasSubmissionPermissions, hasFormRoles, hasRolePermissions } = require('../../../../../src/forms/auth/middleware/userAccess'); @@ -14,8 +13,6 @@ const formSubmissionId = uuid.v4(); const userId = uuid.v4(); const userId2 = uuid.v4(); -const bearerToken = Math.random().toString(36).substring(2); - const Roles = { OWNER: 'owner', TEAM_MANAGER: 'team_manager', @@ -25,122 +22,111 @@ const Roles = { FORM_SUBMITTER: 'form_submitter', }; -jwtService.validateAccessToken = jest.fn().mockReturnValue(true); -jwtService.getBearerToken = jest.fn().mockReturnValue(bearerToken); -jwtService.getTokenPayload = jest.fn().mockReturnValue({ token: 'payload' }); - -// Mock the service login -const mockUser = { user: 'me' }; -service.login = jest.fn().mockReturnValue(mockUser); - -const testRes = { - writeHead: jest.fn(), - end: jest.fn(), -}; - afterEach(() => { jest.clearAllMocks(); }); // External dependencies used by the implementation are: -// - jwtService.validateAccessToken: to validate a Bearer token +// - jwtService.getBearerToken: to get the bearer token +// - jwtService.getTokenPayload to get the payload from the bearer token +// - jwtService.validateAccessToken: to validate a bearer token // - service.login: to create the object for req.currentUser // describe('currentUser', () => { - it('gets the current user with valid request', async () => { - const testReq = { - params: { - formId: 2, - }, - headers: { - authorization: 'Bearer ' + bearerToken, - }, - }; + // Bearer token and its authorization header. + const bearerToken = Math.random().toString(36).substring(2); - const nxt = jest.fn(); + // Default mock of the token validation. + jwtService.getBearerToken = jest.fn().mockReturnValue(bearerToken); + jwtService.getTokenPayload = jest.fn().mockReturnValue({ token: 'payload' }); + jwtService.validateAccessToken = jest.fn().mockReturnValue(true); + + // Default mock of the service login + const mockUser = { user: 'me' }; + service.login = jest.fn().mockReturnValue(mockUser); + + it('401s if the token is invalid', async () => { + jwtService.validateAccessToken.mockReturnValueOnce(false); + const req = getMockReq(); + const { res, next } = getMockRes(); + + await currentUser(req, res, next); - await currentUser(testReq, testRes, nxt); - expect(jwtService.validateAccessToken).toHaveBeenCalledTimes(1); expect(jwtService.getBearerToken).toHaveBeenCalledTimes(1); + expect(jwtService.validateAccessToken).toHaveBeenCalledTimes(1); expect(jwtService.validateAccessToken).toHaveBeenCalledWith(bearerToken); - expect(service.login).toHaveBeenCalledTimes(1); - expect(service.login).toHaveBeenCalledWith({ token: 'payload' }); - expect(testReq.currentUser).toEqual(mockUser); - expect(nxt).toHaveBeenCalledTimes(1); - expect(nxt).toHaveBeenCalledWith(); + expect(service.login).toHaveBeenCalledTimes(0); + expect(req.currentUser).toEqual(undefined); + expect(next).toHaveBeenCalledWith( + expect.objectContaining({ + detail: 'Authorization token is invalid.', + status: 401, + }) + ); }); - it('prioritizes the url param if both url and query are provided', async () => { - const testReq = { - params: { - formId: 2, - }, - query: { - formId: 99, - }, - headers: { - authorization: 'Bearer ' + bearerToken, - }, - }; + it('passes on the error if a service fails unexpectedly', async () => { + service.login.mockRejectedValueOnce(new Error()); + const req = getMockReq(); + const { res, next } = getMockRes(); + + await currentUser(req, res, next); - await currentUser(testReq, testRes, jest.fn()); expect(jwtService.getBearerToken).toHaveBeenCalledTimes(1); - expect(jwtService.getTokenPayload).toHaveBeenCalledTimes(1); - expect(service.login).toHaveBeenCalledWith({ token: 'payload' }); + expect(jwtService.validateAccessToken).toHaveBeenCalledTimes(1); + expect(jwtService.validateAccessToken).toHaveBeenCalledWith(bearerToken); + expect(service.login).toHaveBeenCalledTimes(1); + expect(req.currentUser).toEqual(undefined); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(expect.any(Error)); }); - it('uses the query param if both if that is whats provided', async () => { - const testReq = { - query: { - formId: 99, - }, - headers: { - authorization: 'Bearer ' + bearerToken, - }, - }; + it('gets the current user with no bearer token', async () => { + jwtService.getBearerToken.mockReturnValueOnce(null); + jwtService.getTokenPayload.mockReturnValueOnce(null); + const req = getMockReq(); + const { res, next } = getMockRes(); - await currentUser(testReq, testRes, jest.fn()); - expect(jwtService.getBearerToken).toHaveBeenCalledTimes(1); - expect(jwtService.getTokenPayload).toHaveBeenCalledTimes(1); - expect(service.login).toHaveBeenCalledWith({ token: 'payload' }); + await currentUser(req, res, next); + + expect(jwtService.validateAccessToken).toHaveBeenCalledTimes(0); + expect(service.login).toHaveBeenCalledTimes(1); + expect(service.login).toHaveBeenCalledWith(null); + expect(req.currentUser).toEqual(mockUser); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(); }); - it('401s if the token is invalid', async () => { - const testReq = { - headers: { - authorization: 'Bearer ' + bearerToken, - }, - }; + it('does not keycloak validate with no bearer token', async () => { + jwtService.getBearerToken.mockReturnValueOnce(null); + jwtService.getTokenPayload.mockReturnValueOnce(null); + const req = getMockReq(); + const { res, next } = getMockRes(); - const nxt = jest.fn(); - jwtService.validateAccessToken = jest.fn().mockReturnValue(false); + await currentUser(req, res, next); - await currentUser(testReq, testRes, nxt); - expect(jwtService.getBearerToken).toHaveBeenCalledTimes(1); - expect(jwtService.validateAccessToken).toHaveBeenCalledTimes(1); - expect(jwtService.validateAccessToken).toHaveBeenCalledWith(bearerToken); - expect(service.login).toHaveBeenCalledTimes(0); - expect(testReq.currentUser).toEqual(undefined); - expect(nxt).toHaveBeenCalledWith(new Problem(401, { detail: 'Authorization token is invalid.' })); + expect(jwtService.validateAccessToken).toHaveBeenCalledTimes(0); + expect(req.currentUser).toEqual(mockUser); + expect(service.login).toHaveBeenCalledTimes(1); + expect(service.login).toHaveBeenCalledWith(null); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(); }); -}); -describe('getToken', () => { - it('returns a null token if no auth bearer in the headers', async () => { - const testReq = { - params: { - formId: 2, - }, - }; + it('gets the current user with valid request', async () => { + const req = getMockReq(); + const { res, next } = getMockRes(); - jwtService.getBearerToken = jest.fn().mockReturnValue(null); - jwtService.getTokenPayload = jest.fn().mockReturnValue(null); + await currentUser(req, res, next); - await currentUser(testReq, testRes, jest.fn()); + expect(jwtService.validateAccessToken).toHaveBeenCalledTimes(1); expect(jwtService.getBearerToken).toHaveBeenCalledTimes(1); - expect(jwtService.getTokenPayload).toHaveBeenCalledTimes(1); + expect(jwtService.validateAccessToken).toHaveBeenCalledWith(bearerToken); expect(service.login).toHaveBeenCalledTimes(1); - expect(service.login).toHaveBeenCalledWith(null); + expect(service.login).toHaveBeenCalledWith({ token: 'payload' }); + expect(req.currentUser).toEqual(mockUser); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(); }); }); From cbc7ed9cc5b0c3bf916ef23239cba141f3f4e02f Mon Sep 17 00:00:00 2001 From: Walter Moar Date: Tue, 21 May 2024 13:10:30 -0700 Subject: [PATCH 09/14] refactor: FORMS-1265 reduce hasRolePermissions code repetition (#1362) --- app/src/forms/auth/middleware/userAccess.js | 81 ++++++-------- .../forms/auth/middleware/userAccess.spec.js | 105 ++++++++++++------ 2 files changed, 104 insertions(+), 82 deletions(-) diff --git a/app/src/forms/auth/middleware/userAccess.js b/app/src/forms/auth/middleware/userAccess.js index cc43b4923..a04059214 100644 --- a/app/src/forms/auth/middleware/userAccess.js +++ b/app/src/forms/auth/middleware/userAccess.js @@ -86,27 +86,6 @@ const _hasAnyPermission = (userPermissions, requiredPermissions) => { return intersection.length > 0; }; -const _hasFormRole = async (formId, user, role) => { - let hasRole = false; - - const forms = await service.getUserForms(user, { - active: true, - formId: formId, - }); - const form = forms.find((f) => f.formId === formId); - - if (form) { - for (let j = 0; j < form.roles.length; j++) { - if (form.roles[j] === role) { - hasRole = true; - break; - } - } - } - - return hasRole; -}; - /** * Express middleware that adds the user information as the res.currentUser * attribute so that all downstream middleware and business logic can use it. @@ -289,41 +268,41 @@ const hasFormRoles = (roles) => { const hasRolePermissions = (removingUsers = false) => { return async (req, _res, next) => { try { - // If we invoke this middleware and the caller is acting on a specific formId, whether in a param or query (precedence to param) - const formId = req.params.formId || req.query.formId; - if (!formId) { - // No form provided to this route that secures based on form... that's a problem! - throw new Problem(401, { - detail: 'Form Id not found on request.', - }); - } - const currentUser = req.currentUser; - const data = req.body; - const isOwner = await _hasFormRole(formId, currentUser, Roles.OWNER); + // The request must include a formId, either in params or query, but give + // precedence to params. + const form = await _getForm(currentUser, req.params.formId || req.query.formId, false); + const isOwner = form.roles.includes(Roles.OWNER); if (removingUsers) { - if (data.includes(currentUser.id)) + const userIds = req.body; + if (userIds.includes(currentUser.id)) { throw new Problem(401, { detail: "You can't remove yourself from this form.", }); + } if (!isOwner) { - for (let i = 0; i < data.length; i++) { - let userId = data[i]; + for (const userId of userIds) { + if (!uuid.validate(userId)) { + throw new Problem(400, { + detail: 'Bad userId', + }); + } - const userRoles = await rbacService.readUserRole(userId, formId); + // Convert to an array of role strings, rather than the objects. + const userRoles = (await rbacService.readUserRole(userId, form.formId)).map((userRole) => userRole.role); - // Can't update another user's roles if they are an owner - if (userRoles.some((fru) => fru.role === Roles.OWNER) && userId !== currentUser.id) { + // A non-owner can't delete an owner. + if (userRoles.includes(Roles.OWNER) && userId !== currentUser.id) { throw new Problem(401, { detail: "You can not update an owner's roles.", }); } - // If the user is trying to remove the designer role - if (userRoles.some((fru) => fru.role === Roles.FORM_DESIGNER)) { + // A non-owner can't delete a form designer. + if (userRoles.includes(Roles.FORM_DESIGNER)) { throw new Problem(401, { detail: "You can't remove a form designer role.", }); @@ -332,41 +311,45 @@ const hasRolePermissions = (removingUsers = false) => { } } else { const userId = req.params.userId || req.query.userId; - if (!userId || (userId && userId.length === 0)) { - throw new Problem(401, { - detail: 'User Id not found on request.', + if (!uuid.validate(userId)) { + throw new Problem(400, { + detail: 'Bad userId', }); } if (!isOwner) { - const userRoles = await rbacService.readUserRole(userId, formId); + // Convert to arrays of role strings, rather than the objects. + const userRoles = (await rbacService.readUserRole(userId, form.formId)).map((userRole) => userRole.role); + const futureRoles = req.body.map((userRole) => userRole.role); // If the user is trying to remove the team manager role for their own userid - if (userRoles.some((fru) => fru.role === Roles.TEAM_MANAGER) && !data.some((role) => role.role === Roles.TEAM_MANAGER) && userId == currentUser.id) { + if (userRoles.includes(Roles.TEAM_MANAGER) && !futureRoles.includes(Roles.TEAM_MANAGER) && userId == currentUser.id) { throw new Problem(401, { detail: "You can't remove your own team manager role.", }); } // Can't update another user's roles if they are an owner - if (userRoles.some((fru) => fru.role === Roles.OWNER) && userId !== currentUser.id) { + if (userRoles.includes(Roles.OWNER) && userId !== currentUser.id) { throw new Problem(401, { detail: "You can't update an owner's roles.", }); } - if (!userRoles.some((fru) => fru.role === Roles.OWNER) && data.some((role) => role.role === Roles.OWNER)) { + + if (!userRoles.includes(Roles.OWNER) && futureRoles.includes(Roles.OWNER)) { throw new Problem(401, { detail: "You can't add an owner role.", }); } // If the user is trying to remove the designer role for another userid - if (userRoles.some((fru) => fru.role === Roles.FORM_DESIGNER) && !data.some((role) => role.role === Roles.FORM_DESIGNER)) { + if (userRoles.includes(Roles.FORM_DESIGNER) && !futureRoles.includes(Roles.FORM_DESIGNER)) { throw new Problem(401, { detail: "You can't remove a form designer role.", }); } - if (!userRoles.some((fru) => fru.role === Roles.FORM_DESIGNER) && data.some((role) => role.role === Roles.FORM_DESIGNER)) { + + if (!userRoles.includes(Roles.FORM_DESIGNER) && futureRoles.includes(Roles.FORM_DESIGNER)) { throw new Problem(401, { detail: "You can't add a form designer role.", }); diff --git a/app/tests/unit/forms/auth/middleware/userAccess.spec.js b/app/tests/unit/forms/auth/middleware/userAccess.spec.js index 67a108396..dddefac90 100644 --- a/app/tests/unit/forms/auth/middleware/userAccess.spec.js +++ b/app/tests/unit/forms/auth/middleware/userAccess.spec.js @@ -1106,8 +1106,8 @@ describe('hasRolePermissions', () => { expect(middleware).toBeInstanceOf(Function); }); - describe('401 response when', () => { - const expectedStatus = { status: 401 }; + describe('400 response when', () => { + const expectedStatus = { status: 400 }; test('formId missing', async () => { const req = getMockReq({ @@ -1128,12 +1128,12 @@ describe('hasRolePermissions', () => { expect(next).toHaveBeenCalledWith(expect.objectContaining(expectedStatus)); expect(next).toHaveBeenCalledWith( expect.objectContaining({ - detail: 'Form Id not found on request.', + detail: 'Bad formId', }) ); }); - test.skip('formId not a uuid', async () => { + test('formId not a uuid', async () => { const req = getMockReq({ currentUser: { id: userId, @@ -1149,26 +1149,30 @@ describe('hasRolePermissions', () => { await hasRolePermissions()(req, res, next); - expect(service.getUserForms).toHaveBeenCalledTimes(1); + expect(service.getUserForms).toHaveBeenCalledTimes(0); expect(rbacService.readUserRole).toHaveBeenCalledTimes(0); expect(next).toHaveBeenCalledTimes(1); expect(next).toHaveBeenCalledWith(expect.objectContaining(expectedStatus)); expect(next).toHaveBeenCalledWith( expect.objectContaining({ - detail: 'Form Id invalid UUID.', // or whatever... missing functionality. + detail: 'Bad formId', }) ); }); + }); - test('removing and owner cannot remove self', async () => { + describe('400 response when', () => { + const expectedStatus = { status: 400 }; + + test('removing and user id not a uuid', async () => { service.getUserForms.mockReturnValueOnce([ { formId: formId, - roles: [Roles.FORM_DESIGNER, Roles.OWNER, Roles.TEAM_MANAGER], + roles: [Roles.TEAM_MANAGER], }, ]); const req = getMockReq({ - body: [userId], + body: ['not-a-uuid'], currentUser: { id: userId, }, @@ -1186,21 +1190,19 @@ describe('hasRolePermissions', () => { expect(next).toHaveBeenCalledWith(expect.objectContaining(expectedStatus)); expect(next).toHaveBeenCalledWith( expect.objectContaining({ - detail: "You can't remove yourself from this form.", + detail: 'Bad userId', }) ); }); - test('removing and non-owner cannot remove an owner', async () => { + test('updating and user id missing', async () => { service.getUserForms.mockReturnValueOnce([ { formId: formId, - roles: [Roles.TEAM_MANAGER], + roles: [Roles.FORM_DESIGNER, Roles.OWNER, Roles.TEAM_MANAGER], }, ]); - rbacService.readUserRole.mockReturnValueOnce([{ role: Roles.OWNER }]); const req = getMockReq({ - body: [userId2], currentUser: { id: userId, }, @@ -1210,52 +1212,55 @@ describe('hasRolePermissions', () => { }); const { res, next } = getMockRes(); - await hasRolePermissions(true)(req, res, next); + await hasRolePermissions()(req, res, next); expect(service.getUserForms).toHaveBeenCalledTimes(1); - expect(rbacService.readUserRole).toHaveBeenCalledTimes(1); + expect(rbacService.readUserRole).toHaveBeenCalledTimes(0); expect(next).toHaveBeenCalledTimes(1); expect(next).toHaveBeenCalledWith(expect.objectContaining(expectedStatus)); expect(next).toHaveBeenCalledWith( expect.objectContaining({ - detail: "You can not update an owner's roles.", + detail: 'Bad userId', }) ); }); - test('removing and non-owner cannot remove a form designer', async () => { + test('updating and user id not a uuid', async () => { service.getUserForms.mockReturnValueOnce([ { formId: formId, - roles: [Roles.TEAM_MANAGER], + roles: [Roles.FORM_DESIGNER, Roles.OWNER, Roles.TEAM_MANAGER], }, ]); - rbacService.readUserRole.mockReturnValueOnce([{ role: Roles.FORM_DESIGNER }]); const req = getMockReq({ - body: [userId2], currentUser: { id: userId, }, params: { formId: formId, + userId: 'not-a-uuid', }, }); const { res, next } = getMockRes(); - await hasRolePermissions(true)(req, res, next); + await hasRolePermissions()(req, res, next); expect(service.getUserForms).toHaveBeenCalledTimes(1); - expect(rbacService.readUserRole).toHaveBeenCalledTimes(1); + expect(rbacService.readUserRole).toHaveBeenCalledTimes(0); expect(next).toHaveBeenCalledTimes(1); expect(next).toHaveBeenCalledWith(expect.objectContaining(expectedStatus)); expect(next).toHaveBeenCalledWith( expect.objectContaining({ - detail: "You can't remove a form designer role.", + detail: 'Bad userId', }) ); }); + }); - test('updating and user id missing', async () => { + describe('401 response when', () => { + const expectedStatus = { status: 401 }; + + test('removing and owner cannot remove self', async () => { service.getUserForms.mockReturnValueOnce([ { formId: formId, @@ -1263,6 +1268,7 @@ describe('hasRolePermissions', () => { }, ]); const req = getMockReq({ + body: [userId], currentUser: { id: userId, }, @@ -1272,7 +1278,7 @@ describe('hasRolePermissions', () => { }); const { res, next } = getMockRes(); - await hasRolePermissions()(req, res, next); + await hasRolePermissions(true)(req, res, next); expect(service.getUserForms).toHaveBeenCalledTimes(1); expect(rbacService.readUserRole).toHaveBeenCalledTimes(0); @@ -1280,38 +1286,71 @@ describe('hasRolePermissions', () => { expect(next).toHaveBeenCalledWith(expect.objectContaining(expectedStatus)); expect(next).toHaveBeenCalledWith( expect.objectContaining({ - detail: 'User Id not found on request.', + detail: "You can't remove yourself from this form.", }) ); }); - test.skip('updating and user id not a uuid', async () => { + test('removing and non-owner cannot remove an owner', async () => { service.getUserForms.mockReturnValueOnce([ { formId: formId, - roles: [Roles.FORM_DESIGNER, Roles.OWNER, Roles.TEAM_MANAGER], + roles: [Roles.TEAM_MANAGER], }, ]); + rbacService.readUserRole.mockReturnValueOnce([{ role: Roles.OWNER }]); const req = getMockReq({ + body: [userId2], currentUser: { id: userId, }, params: { formId: formId, - userId: 'not-a-uuid', }, }); const { res, next } = getMockRes(); - await hasRolePermissions()(req, res, next); + await hasRolePermissions(true)(req, res, next); expect(service.getUserForms).toHaveBeenCalledTimes(1); - expect(rbacService.readUserRole).toHaveBeenCalledTimes(0); + expect(rbacService.readUserRole).toHaveBeenCalledTimes(1); expect(next).toHaveBeenCalledTimes(1); expect(next).toHaveBeenCalledWith(expect.objectContaining(expectedStatus)); expect(next).toHaveBeenCalledWith( expect.objectContaining({ - detail: 'User Id not found on request.', + detail: "You can not update an owner's roles.", + }) + ); + }); + + test('removing and non-owner cannot remove a form designer', async () => { + service.getUserForms.mockReturnValueOnce([ + { + formId: formId, + roles: [Roles.TEAM_MANAGER], + }, + ]); + rbacService.readUserRole.mockReturnValueOnce([{ role: Roles.FORM_DESIGNER }]); + const req = getMockReq({ + body: [userId2], + currentUser: { + id: userId, + }, + params: { + formId: formId, + }, + }); + const { res, next } = getMockRes(); + + await hasRolePermissions(true)(req, res, next); + + expect(service.getUserForms).toHaveBeenCalledTimes(1); + expect(rbacService.readUserRole).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledWith(expect.objectContaining(expectedStatus)); + expect(next).toHaveBeenCalledWith( + expect.objectContaining({ + detail: "You can't remove a form designer role.", }) ); }); From fab56f5e0570c90104840720ffe1ac87209f0234 Mon Sep 17 00:00:00 2001 From: Walter Moar Date: Tue, 21 May 2024 14:43:30 -0700 Subject: [PATCH 10/14] fix: FORMS-991 handle db unique violation errors (#1363) --- app/src/db/dataConnection.js | 11 +++----- app/src/db/stamps.js | 4 +-- app/src/forms/common/middleware/dataErrors.js | 20 +++++++++------ .../common/middleware/dataErrors.spec.js | 25 ++++++++++++++++--- 4 files changed, 40 insertions(+), 20 deletions(-) diff --git a/app/src/db/dataConnection.js b/app/src/db/dataConnection.js index ec6286bfa..fcb63f619 100755 --- a/app/src/db/dataConnection.js +++ b/app/src/db/dataConnection.js @@ -79,8 +79,7 @@ class DataConnection { const result = data && data.rows && data.rows[0].transaction_read_only === 'off'; if (result) { log.debug('Database connection ok', { function: 'checkConnection' }); - } - else { + } else { log.warn('Database connection is read-only', { function: 'checkConnection' }); } return result; @@ -98,10 +97,9 @@ class DataConnection { checkSchema() { const tables = tableNames(models); try { - return Promise - .all(tables.map(table => this._knex.schema.hasTable(table))) - .then(exists => exists.every(x => x)) - .then(result => { + return Promise.all(tables.map((table) => this._knex.schema.hasTable(table))) + .then((exists) => exists.every((x) => x)) + .then((result) => { if (result) log.debug('Database schema ok', { function: 'checkSchema' }); return result; }); @@ -129,7 +127,6 @@ class DataConnection { } } - /** * @function close * Will close the DataConnection diff --git a/app/src/db/stamps.js b/app/src/db/stamps.js index 3ccf93981..66b87da51 100755 --- a/app/src/db/stamps.js +++ b/app/src/db/stamps.js @@ -1,6 +1,6 @@ module.exports = (knex, table) => { table.string('createdBy').defaultTo('public'); - table.timestamp('createdAt', {useTz: true}).defaultTo(knex.fn.now()); + table.timestamp('createdAt', { useTz: true }).defaultTo(knex.fn.now()); table.string('updatedBy'); - table.timestamp('updatedAt', {useTz: true}).defaultTo(knex.fn.now()); + table.timestamp('updatedAt', { useTz: true }).defaultTo(knex.fn.now()); }; diff --git a/app/src/forms/common/middleware/dataErrors.js b/app/src/forms/common/middleware/dataErrors.js index 99001cef4..593906ec6 100644 --- a/app/src/forms/common/middleware/dataErrors.js +++ b/app/src/forms/common/middleware/dataErrors.js @@ -3,19 +3,23 @@ const Objection = require('objection'); module.exports.dataErrors = async (err, _req, res, next) => { let error = err; - if (err instanceof Objection.NotFoundError) { + if (err instanceof Objection.DataError) { + error = new Problem(422, { + detail: 'Sorry... the database does not like the data you provided :(', + }); + } else if (err instanceof Objection.NotFoundError) { error = new Problem(404, { detail: "Sorry... we still haven't found what you're looking for :(", }); + } else if (err instanceof Objection.UniqueViolationError) { + error = new Problem(422, { + detail: 'Unique Validation Error', + }); } else if (err instanceof Objection.ValidationError) { error = new Problem(422, { detail: 'Validation Error', errors: err.data, }); - } else if (err instanceof Objection.DataError) { - error = new Problem(422, { - detail: 'Sorry... the database does not like the data you provided :(', - }); } if (error instanceof Problem && error.status !== 500) { @@ -24,9 +28,9 @@ module.exports.dataErrors = async (err, _req, res, next) => { // ERROR level logs (below) for only the things that need investigation. error.send(res); } else { - // HTTP 500 Problems and all other exceptions should be handled by the - // Express error handler. It will log them at the ERROR level and include a - // full stack trace. + // HTTP 500 Problems and all other exceptions should be handled by the error + // handler in app.js. It will log them at the ERROR level and include a full + // stack trace. next(error); } }; diff --git a/app/tests/unit/forms/common/middleware/dataErrors.spec.js b/app/tests/unit/forms/common/middleware/dataErrors.spec.js index 77d2259f0..eb4b479ee 100644 --- a/app/tests/unit/forms/common/middleware/dataErrors.spec.js +++ b/app/tests/unit/forms/common/middleware/dataErrors.spec.js @@ -6,7 +6,9 @@ const middleware = require('../../../../../src/forms/common/middleware'); describe('test data errors middleware', () => { it('should handle an objection data error', () => { - const error = new Objection.DataError({ nativeError: { message: 'This is a DataError' } }); + const error = new Objection.DataError({ + nativeError: { message: 'This is a DataError' }, + }); const { res } = getMockRes(); const next = jest.fn(); @@ -17,7 +19,9 @@ describe('test data errors middleware', () => { }); it('should handle an objection not found error', () => { - const error = new Objection.NotFoundError({ nativeError: { message: 'This is a NotFoundError' } }); + const error = new Objection.NotFoundError({ + nativeError: { message: 'This is a NotFoundError' }, + }); const { res } = getMockRes(); const next = jest.fn(); @@ -27,8 +31,23 @@ describe('test data errors middleware', () => { expect(res.end).toBeCalledWith(expect.stringContaining('404')); }); + it('should handle an objection unique violation error', () => { + const error = new Objection.UniqueViolationError({ + nativeError: { message: 'This is a UniqueViolationError' }, + }); + const { res } = getMockRes(); + const next = jest.fn(); + + middleware.dataErrors(error, {}, res, next); + + expect(next).not.toBeCalled(); + expect(res.end).toBeCalledWith(expect.stringContaining('422')); + }); + it('should handle an objection validation error', () => { - const error = new Objection.ValidationError({ nativeError: { message: 'This is a ValidationError' } }); + const error = new Objection.ValidationError({ + nativeError: { message: 'This is a ValidationError' }, + }); const { res } = getMockRes(); const next = jest.fn(); From e97a256f053413601f9b6f58206bad7a5e267efe Mon Sep 17 00:00:00 2001 From: Walter Moar Date: Wed, 22 May 2024 09:40:25 -0700 Subject: [PATCH 11/14] refactor: FORMS-1265 filter multiple submissions (#1364) * refactor: FORMS-1265 remove unnecessary function creation * standardize the test structure and descriptions * use the already-tested _getForm so we don't need to test again * finish the 100% coverage; will wait for full refactor * forgot the documentation --- app/src/forms/auth/middleware/userAccess.js | 97 +- app/src/forms/submission/routes.js | 4 +- .../forms/auth/middleware/userAccess.spec.js | 1883 ++++++++++------- app/tests/unit/routes/v1/submission.spec.js | 6 +- 4 files changed, 1221 insertions(+), 769 deletions(-) diff --git a/app/src/forms/auth/middleware/userAccess.js b/app/src/forms/auth/middleware/userAccess.js index a04059214..444782859 100644 --- a/app/src/forms/auth/middleware/userAccess.js +++ b/app/src/forms/auth/middleware/userAccess.js @@ -121,64 +121,63 @@ const currentUser = async (req, _res, next) => { } }; -const filterMultipleSubmissions = () => { - return async (req, _res, next) => { - try { - // Get the provided list of submissions Id whether in a req body - const submissionIds = req.body && req.body.submissionIds; - if (!Array.isArray(submissionIds)) { - // No submission provided to this route that secures based on form... that's a problem! - throw new Problem(401, { - detail: 'SubmissionIds not found on request.', - }); - } +/** + * Express middleware that checks that a collection of submissions belong to a + * given form that the user has delete permissions for. This falls through if + * everything is OK, otherwise it calls next() with a Problem describing the + * error. + * + * This could use some refactoring to move the formIdWithDeletePermission + * creation code into this function. That way there is no external dependency, + * and the code will be easier to understand. + * + * @param {*} req the Express object representing the HTTP request. + * @param {*} _res the Express object representing the HTTP response - unused. + * @param {*} next the Express chaining function. + * @returns nothing + */ +const filterMultipleSubmissions = async (req, _res, next) => { + try { + // The request must include a formId, either in params or query, but give + // precedence to params. + const form = await _getForm(req.currentUser, req.params.formId || req.query.formId, true); - let formIdWithDeletePermission = req.formIdWithDeletePermission; + // Get the array of submission IDs from the request body. + const submissionIds = req.body && req.body.submissionIds; + if (!Array.isArray(submissionIds)) { + throw new Problem(401, { + detail: 'SubmissionIds not found on request.', + }); + } - // Get the provided form ID whether in a param or query (precedence to param) - const formId = req.params.formId || req.query.formId; - if (!formId) { - // No submission provided to this route that secures based on form... that's a problem! - throw new Problem(401, { - detail: 'Form Id not found on request.', - }); - } + // Check that all submission IDs are valid UUIDs. + const isValidSubmissionId = submissionIds.every((submissionId) => uuid.validate(submissionId)); + if (!isValidSubmissionId) { + throw new Problem(401, { + detail: 'Invalid submissionId(s) in the submissionIds list.', + }); + } - //validate form id - if (!uuid.validate(formId)) { - throw new Problem(401, { - detail: 'Not a valid form id', - }); - } + if (req.formIdWithDeletePermission === form.formId) { + // check if users has not injected submission id that does not belong to this form + const metaData = await service.getMultipleSubmission(submissionIds); - //validate all submission ids - const isValidSubmissionId = submissionIds.every((submissionId) => uuid.validate(submissionId)); - if (!isValidSubmissionId) { + const isForeignSubmissionId = metaData.every((SubmissionMetadata) => SubmissionMetadata.formId === form.formId); + if (!isForeignSubmissionId || metaData.length !== submissionIds.length) { throw new Problem(401, { - detail: 'Invalid submissionId(s) in the submissionIds list.', + detail: 'Current user does not have required permission(s) for some submissions in the submissionIds list.', }); } - if (formIdWithDeletePermission === formId) { - // check if users has not injected submission id that does not belong to this form - const metaData = await service.getMultipleSubmission(submissionIds); - - const isForeignSubmissionId = metaData.every((SubmissionMetadata) => SubmissionMetadata.formId === formId); - if (!isForeignSubmissionId || metaData.length !== submissionIds.length) { - throw new Problem(401, { - detail: 'Current user does not have required permission(s) for some submissions in the submissionIds list.', - }); - } - return next(); - } - - throw new Problem(401, { - detail: 'Current user does not have required permission(s) for to delete submissions', - }); - } catch (error) { - next(error); + return next(); } - }; + + throw new Problem(401, { + detail: 'Current user does not have required permission(s) for to delete submissions', + }); + } catch (error) { + next(error); + } }; /** diff --git a/app/src/forms/submission/routes.js b/app/src/forms/submission/routes.js index b6d9e86a1..2425ef4bf 100644 --- a/app/src/forms/submission/routes.js +++ b/app/src/forms/submission/routes.js @@ -23,7 +23,7 @@ routes.delete('/:formSubmissionId', rateLimiter, apiAccess, hasSubmissionPermiss await controller.delete(req, res, next); }); -routes.put('/:formSubmissionId/:formId/submissions/restore', hasSubmissionPermissions([P.SUBMISSION_DELETE]), filterMultipleSubmissions(), async (req, res, next) => { +routes.put('/:formSubmissionId/:formId/submissions/restore', hasSubmissionPermissions([P.SUBMISSION_DELETE]), filterMultipleSubmissions, async (req, res, next) => { await controller.restoreMutipleSubmissions(req, res, next); }); @@ -67,7 +67,7 @@ routes.post('/:formSubmissionId/template/render', rateLimiter, apiAccess, hasSub await controller.templateUploadAndRender(req, res, next); }); -routes.delete('/:formSubmissionId/:formId/submissions', hasSubmissionPermissions([P.SUBMISSION_DELETE]), filterMultipleSubmissions(), async (req, res, next) => { +routes.delete('/:formSubmissionId/:formId/submissions', hasSubmissionPermissions([P.SUBMISSION_DELETE]), filterMultipleSubmissions, async (req, res, next) => { await controller.deleteMutipleSubmissions(req, res, next); }); diff --git a/app/tests/unit/forms/auth/middleware/userAccess.spec.js b/app/tests/unit/forms/auth/middleware/userAccess.spec.js index dddefac90..07475f67e 100644 --- a/app/tests/unit/forms/auth/middleware/userAccess.spec.js +++ b/app/tests/unit/forms/auth/middleware/userAccess.spec.js @@ -1,7 +1,14 @@ const { getMockReq, getMockRes } = require('@jest-mock/express'); const uuid = require('uuid'); -const { currentUser, hasFormPermissions, hasSubmissionPermissions, hasFormRoles, hasRolePermissions } = require('../../../../../src/forms/auth/middleware/userAccess'); +const { + currentUser, + filterMultipleSubmissions, + hasFormPermissions, + hasSubmissionPermissions, + hasFormRoles, + hasRolePermissions, +} = require('../../../../../src/forms/auth/middleware/userAccess'); const jwtService = require('../../../../../src/components/jwtService'); const rbacService = require('../../../../../src/forms/rbac/service'); @@ -27,10 +34,10 @@ afterEach(() => { }); // External dependencies used by the implementation are: -// - jwtService.getBearerToken: to get the bearer token -// - jwtService.getTokenPayload to get the payload from the bearer token -// - jwtService.validateAccessToken: to validate a bearer token -// - service.login: to create the object for req.currentUser +// - jwtService.getBearerToken: to get the bearer token. +// - jwtService.getTokenPayload to get the payload from the bearer token. +// - jwtService.validateAccessToken: to validate a bearer token. +// - service.login: to create the object for req.currentUser. // describe('currentUser', () => { // Bearer token and its authorization header. @@ -41,28 +48,32 @@ describe('currentUser', () => { jwtService.getTokenPayload = jest.fn().mockReturnValue({ token: 'payload' }); jwtService.validateAccessToken = jest.fn().mockReturnValue(true); - // Default mock of the service login + // Default mock of the service login. const mockUser = { user: 'me' }; service.login = jest.fn().mockReturnValue(mockUser); - it('401s if the token is invalid', async () => { - jwtService.validateAccessToken.mockReturnValueOnce(false); - const req = getMockReq(); - const { res, next } = getMockRes(); + describe('401 response when', () => { + const expectedStatus = { status: 401 }; - await currentUser(req, res, next); + test('the token is not valid', async () => { + jwtService.validateAccessToken.mockReturnValueOnce(false); + const req = getMockReq(); + const { res, next } = getMockRes(); - expect(jwtService.getBearerToken).toHaveBeenCalledTimes(1); - expect(jwtService.validateAccessToken).toHaveBeenCalledTimes(1); - expect(jwtService.validateAccessToken).toHaveBeenCalledWith(bearerToken); - expect(service.login).toHaveBeenCalledTimes(0); - expect(req.currentUser).toEqual(undefined); - expect(next).toHaveBeenCalledWith( - expect.objectContaining({ - detail: 'Authorization token is invalid.', - status: 401, - }) - ); + await currentUser(req, res, next); + + expect(jwtService.getBearerToken).toBeCalledTimes(1); + expect(jwtService.validateAccessToken).toBeCalledTimes(1); + expect(jwtService.validateAccessToken).toBeCalledWith(bearerToken); + expect(service.login).toBeCalledTimes(0); + expect(req.currentUser).toEqual(undefined); + expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); + expect(next).toBeCalledWith( + expect.objectContaining({ + detail: 'Authorization token is invalid.', + }) + ); + }); }); it('passes on the error if a service fails unexpectedly', async () => { @@ -72,13 +83,13 @@ describe('currentUser', () => { await currentUser(req, res, next); - expect(jwtService.getBearerToken).toHaveBeenCalledTimes(1); - expect(jwtService.validateAccessToken).toHaveBeenCalledTimes(1); - expect(jwtService.validateAccessToken).toHaveBeenCalledWith(bearerToken); - expect(service.login).toHaveBeenCalledTimes(1); + expect(jwtService.getBearerToken).toBeCalledTimes(1); + expect(jwtService.validateAccessToken).toBeCalledTimes(1); + expect(jwtService.validateAccessToken).toBeCalledWith(bearerToken); + expect(service.login).toBeCalledTimes(1); expect(req.currentUser).toEqual(undefined); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(expect.any(Error)); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.any(Error)); }); it('gets the current user with no bearer token', async () => { @@ -89,12 +100,12 @@ describe('currentUser', () => { await currentUser(req, res, next); - expect(jwtService.validateAccessToken).toHaveBeenCalledTimes(0); - expect(service.login).toHaveBeenCalledTimes(1); - expect(service.login).toHaveBeenCalledWith(null); + expect(jwtService.validateAccessToken).toBeCalledTimes(0); + expect(service.login).toBeCalledTimes(1); + expect(service.login).toBeCalledWith(null); expect(req.currentUser).toEqual(mockUser); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(); }); it('does not keycloak validate with no bearer token', async () => { @@ -105,12 +116,12 @@ describe('currentUser', () => { await currentUser(req, res, next); - expect(jwtService.validateAccessToken).toHaveBeenCalledTimes(0); + expect(jwtService.validateAccessToken).toBeCalledTimes(0); expect(req.currentUser).toEqual(mockUser); - expect(service.login).toHaveBeenCalledTimes(1); - expect(service.login).toHaveBeenCalledWith(null); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(); + expect(service.login).toBeCalledTimes(1); + expect(service.login).toBeCalledWith(null); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(); }); it('gets the current user with valid request', async () => { @@ -119,685 +130,1129 @@ describe('currentUser', () => { await currentUser(req, res, next); - expect(jwtService.validateAccessToken).toHaveBeenCalledTimes(1); - expect(jwtService.getBearerToken).toHaveBeenCalledTimes(1); - expect(jwtService.validateAccessToken).toHaveBeenCalledWith(bearerToken); - expect(service.login).toHaveBeenCalledTimes(1); - expect(service.login).toHaveBeenCalledWith({ token: 'payload' }); + expect(jwtService.validateAccessToken).toBeCalledTimes(1); + expect(jwtService.getBearerToken).toBeCalledTimes(1); + expect(jwtService.validateAccessToken).toBeCalledWith(bearerToken); + expect(service.login).toBeCalledTimes(1); + expect(service.login).toBeCalledWith({ token: 'payload' }); expect(req.currentUser).toEqual(mockUser); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(); }); }); // External dependencies used by the implementation are: -// - service.getUserForms: gets the forms that the user can access +// - service.getMultipleSubmission: gets multiple submissions. +// - service.getUserForms: gets the forms that the user can access. // -describe('hasFormPermissions', () => { - // Default mock value where the user has no access to forms - service.getUserForms = jest.fn().mockReturnValue([]); - - it('returns a middleware function', async () => { - const middleware = hasFormPermissions(['FORM_READ']); +describe('filterMultipleSubmissions', () => { + // Default mock value where there are no submissions to get. + service.getMultipleSubmission = jest.fn().mockReturnValue([]); - expect(middleware).toBeInstanceOf(Function); - }); + // Default mock value where the user has no access to forms. + service.getUserForms = jest.fn().mockReturnValue([]); - it('400s if the request has no formId', async () => { - const req = getMockReq({ - currentUser: {}, - params: { - submissionId: formSubmissionId, - }, - query: { - otherQueryThing: 'SOMETHING', - }, - }); - const { res, next } = getMockRes(); + describe('400 response when', () => { + const expectedStatus = { status: 400 }; - await hasFormPermissions(['FORM_READ'])(req, res, next); + test('the request has no formId', async () => { + const req = getMockReq({ + currentUser: {}, + params: { + submissionId: formSubmissionId, + }, + query: { + otherQueryThing: 'SOMETHING', + }, + }); + const { res, next } = getMockRes(); - expect(service.getUserForms).toHaveBeenCalledTimes(0); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(expect.objectContaining({ status: 400 })); - }); + await filterMultipleSubmissions(req, res, next); - it('400s if the formId is not a uuid', async () => { - const req = getMockReq({ - currentUser: {}, - params: { - formId: 'undefined', - }, - query: { - otherQueryThing: 'SOMETHING', - }, + expect(service.getUserForms).toBeCalledTimes(0); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); }); - const { res, next } = getMockRes(); - await hasFormPermissions(['FORM_READ'])(req, res, next); + test('the formId is not a uuid', async () => { + const req = getMockReq({ + currentUser: {}, + params: { + formId: 'undefined', + }, + query: { + otherQueryThing: 'SOMETHING', + }, + }); + const { res, next } = getMockRes(); - expect(service.getUserForms).toHaveBeenCalledTimes(0); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(expect.objectContaining({ status: 400 })); - }); + await filterMultipleSubmissions(req, res, next); - // TODO: This should be a 403, but bundle all breaking changes in a small PR. - it('401s if the user does not have access to the form', async () => { - const req = getMockReq({ - currentUser: {}, - params: { - formId: formId, - }, + expect(service.getUserForms).toBeCalledTimes(0); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); }); - const { res, next } = getMockRes(); - - await hasFormPermissions(['FORM_READ'])(req, res, next); - - expect(service.getUserForms).toHaveBeenCalled(); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(expect.objectContaining({ status: 401 })); }); - // TODO: This should be a 403, but bundle all breaking changes in a small PR. - it('401s if the expected permissions are not included', async () => { - service.getUserForms.mockReturnValueOnce([ - { - formId: formId, - permissions: ['DESIGN_CREATE', 'FORM_READ', 'SUBMISSION_READ'], - }, - ]); - const req = getMockReq({ - currentUser: {}, - params: { - formId: formId, - }, - }); - const { res, next } = getMockRes(); + describe('401 response when', () => { + const expectedStatus = { status: 401 }; - await hasFormPermissions(['DESIGN_CREATE', 'FORM_READ', 'SUBMISSION_DELETE'])(req, res, next); + // TODO: This should be a 400, but bundle all breaking changes in a small PR. + test('the request does not have a body', async () => { + const req = getMockReq({ + currentUser: {}, + params: { + formId: formId, + }, + }); + const { res, next } = getMockRes(); - expect(service.getUserForms).toHaveBeenCalled(); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(expect.objectContaining({ status: 401 })); - }); + await filterMultipleSubmissions(req, res, next); - // TODO: This should be a 403, but bundle all breaking changes in a small PR. - it('401s if the permissions are a subset but not including everything', async () => { - service.getUserForms.mockReturnValueOnce([ - { - formId: formId, - permissions: ['DESIGN_CREATE', 'FORM_READ'], - }, - ]); - const req = getMockReq({ - currentUser: {}, - params: { - formId: formId, - }, + expect(service.getUserForms).toBeCalled(); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); + expect(next).toBeCalledWith( + expect.objectContaining({ + detail: 'SubmissionIds not found on request.', + }) + ); }); - const { res, next } = getMockRes(); - await hasFormPermissions(['DESIGN_CREATE', 'FORM_READ', 'SUBMISSION_DELETE'])(req, res, next); + // TODO: This should be a 400, but bundle all breaking changes in a small PR. + test('there is no req.body.submissionIds', async () => { + const req = getMockReq({ + body: {}, + currentUser: {}, + params: { + formId: formId, + }, + }); + const { res, next } = getMockRes(); - expect(service.getUserForms).toHaveBeenCalled(); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(expect.objectContaining({ status: 401 })); - }); + await filterMultipleSubmissions(req, res, next); - it('500s if the request has no current user', async () => { - const req = getMockReq({ - params: { formId: formId }, + expect(service.getUserForms).toBeCalled(); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); + expect(next).toBeCalledWith( + expect.objectContaining({ + detail: 'SubmissionIds not found on request.', + }) + ); }); - const { res, next } = getMockRes(); - await hasFormPermissions(['FORM_READ'])(req, res, next); + // TODO: This should be a 400, but bundle all breaking changes in a small PR. + test('the req.body.submissionIds is not an array', async () => { + const req = getMockReq({ + body: { + submissionIds: uuid.v4(), + }, + currentUser: {}, + params: { + formId: formId, + }, + }); + const { res, next } = getMockRes(); - expect(service.getUserForms).toHaveBeenCalledTimes(0); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(expect.objectContaining({ status: 500 })); - }); + await filterMultipleSubmissions(req, res, next); - it('moves on if a valid API key user has already been set', async () => { - const req = getMockReq({ - apiUser: true, - params: { formId: formId }, + expect(service.getUserForms).toBeCalled(); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); + expect(next).toBeCalledWith( + expect.objectContaining({ + detail: 'SubmissionIds not found on request.', + }) + ); }); - const { res, next } = getMockRes(); - await hasFormPermissions(['FORM_READ'])(req, res, next); + // TODO: This should be a 400, but bundle all breaking changes in a small PR. + test('the req.body.submissionIds does not have a valid uuid', async () => { + const req = getMockReq({ + body: { + submissionIds: ['not-a-uuid'], + }, + currentUser: {}, + params: { + formId: formId, + }, + }); + const { res, next } = getMockRes(); - expect(service.getUserForms).toHaveBeenCalledTimes(0); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(); - }); + await filterMultipleSubmissions(req, res, next); - it('moves on if the expected permissions are included', async () => { - service.getUserForms.mockReturnValueOnce([ - { - formId: formId, - permissions: ['DESIGN_CREATE', 'FORM_READ', 'SUBMISSION_DELETE'], - }, - ]); - const req = getMockReq({ - currentUser: {}, - params: { - formId: formId, - }, + expect(service.getUserForms).toBeCalled(); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); + expect(next).toBeCalledWith( + expect.objectContaining({ + detail: 'Invalid submissionId(s) in the submissionIds list.', + }) + ); }); - const { res, next } = getMockRes(); - await hasFormPermissions(['DESIGN_CREATE', 'FORM_READ', 'SUBMISSION_DELETE'])(req, res, next); + // TODO: This should be a 400, but bundle all breaking changes in a small PR. + test('the req.body.submissionIds does not have all valid uuids', async () => { + const req = getMockReq({ + body: { + submissionIds: [uuid.v4(), 'not-a-uuid', uuid.v4()], + }, + currentUser: {}, + params: { + formId: formId, + }, + }); + const { res, next } = getMockRes(); - expect(service.getUserForms).toHaveBeenCalled(); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(); - }); + await filterMultipleSubmissions(req, res, next); - it('moves on if the expected permissions are included with query formId', async () => { - service.getUserForms.mockReturnValueOnce([ - { - formId: formId, - permissions: ['DESIGN_CREATE', 'FORM_READ', 'SUBMISSION_DELETE'], - }, - ]); - const req = getMockReq({ - currentUser: {}, - query: { - formId: formId, - }, + expect(service.getUserForms).toBeCalled(); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); + expect(next).toBeCalledWith( + expect.objectContaining({ + detail: 'Invalid submissionId(s) in the submissionIds list.', + }) + ); }); - const { res, next } = getMockRes(); - await hasFormPermissions(['DESIGN_CREATE', 'FORM_READ', 'SUBMISSION_DELETE'])(req, res, next); + test('the formIdWithDeletePermission does not match', async () => { + service.getUserForms.mockReturnValueOnce([ + { + formId: formId, + }, + ]); + const req = getMockReq({ + body: { + submissionIds: [uuid.v4()], + }, + currentUser: {}, + formIdWithDeletePermission: uuid.v4(), + params: { + formId: formId, + }, + }); + const { res, next } = getMockRes(); - expect(service.getUserForms).toHaveBeenCalled(); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(); - }); + await filterMultipleSubmissions(req, res, next); - it('moves on if the user has deleted form access', async () => { - service.getUserForms.mockReturnValueOnce([]).mockReturnValueOnce([ - { - formId: formId, - permissions: ['FORM_READ'], - }, - ]); - const req = getMockReq({ - currentUser: {}, - params: { - formId: formId, - }, + expect(service.getUserForms).toBeCalledTimes(1); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); + expect(next).toBeCalledWith( + expect.objectContaining({ + detail: 'Current user does not have required permission(s) for to delete submissions', + }) + ); }); - const { res, next } = getMockRes(); - await hasFormPermissions(['FORM_READ'])(req, res, next); - - expect(service.getUserForms).toHaveBeenCalled(); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(); - }); -}); + test('the submission form id does not match', async () => { + service.getMultipleSubmission.mockReturnValueOnce([ + { + formId: uuid.v4(), + }, + ]); + service.getUserForms.mockReturnValueOnce([ + { + formId: formId, + }, + ]); + const req = getMockReq({ + body: { + submissionIds: [uuid.v4()], + }, + currentUser: {}, + formIdWithDeletePermission: formId, + params: { + formId: formId, + }, + }); + const { res, next } = getMockRes(); -// External dependencies used by the implementation are: -// - service.checkSubmissionPermission: gets whether the user has permission -// - service.getSubmissionForm: gets the submission that the user can access -// - service.getUserForms: gets the forms that the user can access -// -describe('hasSubmissionPermissions', () => { - // Default mock value where the user has no access to submission - service.checkSubmissionPermission = jest.fn().mockReturnValue(false); + await filterMultipleSubmissions(req, res, next); - // Default mock value where there is no submission - service.getSubmissionForm = jest.fn().mockReturnValue(); + expect(service.getMultipleSubmission).toBeCalledTimes(1); + expect(service.getUserForms).toBeCalledTimes(1); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); + expect(next).toBeCalledWith( + expect.objectContaining({ + detail: 'Current user does not have required permission(s) for some submissions in the submissionIds list.', + }) + ); + }); - // Default mock value where the user has no access to forms - service.getUserForms = jest.fn().mockReturnValue([]); + test('the submission lengths do not match', async () => { + service.getMultipleSubmission.mockReturnValueOnce([ + { + formId: formId, + }, + ]); + service.getUserForms.mockReturnValueOnce([ + { + formId: formId, + }, + ]); + const req = getMockReq({ + body: { + submissionIds: [uuid.v4(), uuid.v4()], + }, + currentUser: {}, + formIdWithDeletePermission: formId, + params: { + formId: formId, + }, + }); + const { res, next } = getMockRes(); - it('returns a middleware function', () => { - const middleware = hasSubmissionPermissions(['submission_read']); + await filterMultipleSubmissions(req, res, next); - expect(middleware).toBeInstanceOf(Function); + expect(service.getMultipleSubmission).toBeCalledTimes(1); + expect(service.getUserForms).toBeCalledTimes(1); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); + expect(next).toBeCalledWith( + expect.objectContaining({ + detail: 'Current user does not have required permission(s) for some submissions in the submissionIds list.', + }) + ); + }); }); - it('400s if the request has no formSubmissionId', async () => { - const req = getMockReq(); - const { res, next } = getMockRes(); - - await hasSubmissionPermissions(['submission_read'])(req, res, next); + describe('handles error thrown by', () => { + test('getMultipleSubmission', async () => { + const error = new Error(); + service.getMultipleSubmission.mockRejectedValueOnce(error); + service.getUserForms.mockReturnValueOnce([ + { + formId: formId, + }, + ]); + const req = getMockReq({ + body: { submissionIds: [] }, + currentUser: {}, + formIdWithDeletePermission: formId, + params: { + formId: formId, + }, + }); + const { res, next } = getMockRes(); - expect(service.checkSubmissionPermission).toHaveBeenCalledTimes(0); - expect(service.getSubmissionForm).toHaveBeenCalledTimes(0); - expect(service.getUserForms).toHaveBeenCalledTimes(0); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(expect.objectContaining({ status: 400 })); - }); + await filterMultipleSubmissions(req, res, next); - it('400s if the formSubmissionId is not a uuid', async () => { - const req = getMockReq({ - currentUser: {}, - params: { - formSubmissionId: 'not-a-uuid', - }, + expect(service.getMultipleSubmission).toBeCalledTimes(1); + expect(service.getUserForms).toBeCalledTimes(1); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(error); }); - const { res, next } = getMockRes(); - await hasSubmissionPermissions(['submission_read'])(req, res, next); + test('getUserForms', async () => { + const error = new Error(); + service.getUserForms.mockRejectedValueOnce(error); + const req = getMockReq({ + body: { submissionIds: [] }, + currentUser: {}, + formIdWithDeletePermission: formId, + params: { + formId: formId, + }, + }); + const { res, next } = getMockRes(); - expect(service.checkSubmissionPermission).toHaveBeenCalledTimes(0); - expect(service.getSubmissionForm).toHaveBeenCalledTimes(0); - expect(service.getUserForms).toHaveBeenCalledTimes(0); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(expect.objectContaining({ status: 400 })); - }); + await filterMultipleSubmissions(req, res, next); - it('401s for deleted submission when no current user', async () => { - service.getSubmissionForm.mockReturnValueOnce({ - form: { id: formId }, - submission: { deleted: true, id: formSubmissionId }, - }); - const req = getMockReq({ - params: { - formSubmissionId: formSubmissionId, - }, + expect(service.getMultipleSubmission).toBeCalledTimes(0); + expect(service.getUserForms).toBeCalledTimes(1); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(error); }); - const { res, next } = getMockRes(); + }); - await hasSubmissionPermissions(['submission_read'])(req, res, next); + describe('allows', () => { + test('an empty array of submission ids', async () => { + service.getUserForms.mockReturnValueOnce([ + { + formId: formId, + }, + ]); + const req = getMockReq({ + body: { submissionIds: [] }, + currentUser: {}, + formIdWithDeletePermission: formId, + params: { + formId: formId, + }, + }); + const { res, next } = getMockRes(); - expect(service.checkSubmissionPermission).toHaveBeenCalledTimes(0); - expect(service.getSubmissionForm).toHaveBeenCalledTimes(1); - expect(service.getUserForms).toHaveBeenCalledTimes(0); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(expect.objectContaining({ status: 401 })); - }); + await filterMultipleSubmissions(req, res, next); - it('401s for deleted submission', async () => { - service.getSubmissionForm.mockReturnValueOnce({ - form: { id: formId }, - submission: { deleted: true, id: formSubmissionId }, + expect(service.getUserForms).toBeCalled(); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(); }); - const req = getMockReq({ - currentUser: {}, - params: { - formSubmissionId: formSubmissionId, - }, - }); - const { res, next } = getMockRes(); - await hasSubmissionPermissions(['submission_read'])(req, res, next); + test('one submission id', async () => { + service.getMultipleSubmission.mockReturnValueOnce([ + { + formId: formId, + }, + ]); + service.getUserForms.mockReturnValueOnce([ + { + formId: formId, + }, + ]); + const req = getMockReq({ + body: { submissionIds: [uuid.v4()] }, + currentUser: {}, + formIdWithDeletePermission: formId, + params: { + formId: formId, + }, + }); + const { res, next } = getMockRes(); - expect(service.checkSubmissionPermission).toHaveBeenCalledTimes(0); - expect(service.getSubmissionForm).toHaveBeenCalledTimes(1); - expect(service.getUserForms).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(expect.objectContaining({ status: 401 })); - }); + await filterMultipleSubmissions(req, res, next); - it('401s for deleted submission if user has no forms', async () => { - service.getSubmissionForm.mockReturnValueOnce({ - form: { id: formId }, - submission: { deleted: true, id: formSubmissionId }, - }); - const req = getMockReq({ - currentUser: {}, - params: { - formSubmissionId: formSubmissionId, - }, + expect(service.getMultipleSubmission).toBeCalledTimes(1); + expect(service.getUserForms).toBeCalled(); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(); }); - const { res, next } = getMockRes(); - - await hasSubmissionPermissions(['submission_read'])(req, res, next); - expect(service.checkSubmissionPermission).toHaveBeenCalledTimes(0); - expect(service.getSubmissionForm).toHaveBeenCalledTimes(1); - expect(service.getUserForms).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(expect.objectContaining({ status: 401 })); - }); + test('three submission ids', async () => { + service.getMultipleSubmission.mockReturnValueOnce([ + { + formId: formId, + }, + { + formId: formId, + }, + { + formId: formId, + }, + ]); + service.getUserForms.mockReturnValueOnce([ + { + formId: formId, + }, + ]); + const req = getMockReq({ + body: { submissionIds: [uuid.v4(), uuid.v4(), uuid.v4()] }, + currentUser: {}, + formIdWithDeletePermission: formId, + params: { + formId: formId, + }, + }); + const { res, next } = getMockRes(); + + await filterMultipleSubmissions(req, res, next); - it('401s for deleted submission if user has no form access', async () => { - service.getSubmissionForm.mockReturnValueOnce({ - form: { id: formId }, - submission: { deleted: true, id: formSubmissionId }, - }); - service.getUserForms.mockReturnValueOnce([ - { - formId: formId, - permissions: [], - }, - ]); - const req = getMockReq({ - currentUser: {}, - params: { - formSubmissionId: formSubmissionId, - }, + expect(service.getMultipleSubmission).toBeCalledTimes(1); + expect(service.getUserForms).toBeCalled(); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(); }); - const { res, next } = getMockRes(); + }); +}); + +// External dependencies used by the implementation are: +// - service.getUserForms: gets the forms that the user can access. +// +describe('hasFormPermissions', () => { + // Default mock value where the user has no access to forms. + service.getUserForms = jest.fn().mockReturnValue([]); - await hasSubmissionPermissions(['submission_read'])(req, res, next); + it('returns a middleware function', async () => { + const middleware = hasFormPermissions(['FORM_READ']); - expect(service.checkSubmissionPermission).toHaveBeenCalledTimes(0); - expect(service.getSubmissionForm).toHaveBeenCalledTimes(1); - expect(service.getUserForms).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(expect.objectContaining({ status: 401 })); + expect(middleware).toBeInstanceOf(Function); }); - it('401s for deleted submission if user only has some form access', async () => { - service.getSubmissionForm.mockReturnValueOnce({ - form: { id: formId }, - submission: { deleted: true, id: formSubmissionId }, - }); - service.getUserForms.mockReturnValueOnce([ - { - formId: formId, - permissions: ['submission_read'], - }, - ]); - const req = getMockReq({ - currentUser: {}, - params: { - formSubmissionId: formSubmissionId, - }, + describe('400 response when', () => { + const expectedStatus = { status: 400 }; + + test('the request has no formId', async () => { + const req = getMockReq({ + currentUser: {}, + params: { + submissionId: formSubmissionId, + }, + query: { + otherQueryThing: 'SOMETHING', + }, + }); + const { res, next } = getMockRes(); + + await hasFormPermissions(['FORM_READ'])(req, res, next); + + expect(service.getUserForms).toBeCalledTimes(0); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); }); - const { res, next } = getMockRes(); - await hasSubmissionPermissions(['submission_read', 'submission_update'])(req, res, next); + test('the formId is not a uuid', async () => { + const req = getMockReq({ + currentUser: {}, + params: { + formId: 'undefined', + }, + query: { + otherQueryThing: 'SOMETHING', + }, + }); + const { res, next } = getMockRes(); + + await hasFormPermissions(['FORM_READ'])(req, res, next); - expect(service.checkSubmissionPermission).toHaveBeenCalledTimes(0); - expect(service.getSubmissionForm).toHaveBeenCalledTimes(1); - expect(service.getUserForms).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(expect.objectContaining({ status: 401 })); + expect(service.getUserForms).toBeCalledTimes(0); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); + }); }); - it('401s on submission permissions if public access and not read permission', async () => { - service.checkSubmissionPermission.mockReturnValueOnce(undefined); - service.getSubmissionForm.mockReturnValueOnce({ - form: { id: formId, identityProviders: [{ code: 'public' }] }, - submission: { deleted: false, id: formSubmissionId }, + describe('401 response when', () => { + const expectedStatus = { status: 401 }; + + // TODO: This should be a 403, but bundle all breaking changes in a small PR. + test('the user does not have access to the form', async () => { + const req = getMockReq({ + currentUser: {}, + params: { + formId: formId, + }, + }); + const { res, next } = getMockRes(); + + await hasFormPermissions(['FORM_READ'])(req, res, next); + + expect(service.getUserForms).toBeCalled(); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); }); - const req = getMockReq({ - currentUser: {}, - params: { - formSubmissionId: formSubmissionId, - }, + + // TODO: This should be a 403, but bundle all breaking changes in a small PR. + test('the expected permissions are not included', async () => { + service.getUserForms.mockReturnValueOnce([ + { + formId: formId, + permissions: ['DESIGN_CREATE', 'FORM_READ', 'SUBMISSION_READ'], + }, + ]); + const req = getMockReq({ + currentUser: {}, + params: { + formId: formId, + }, + }); + const { res, next } = getMockRes(); + + await hasFormPermissions(['DESIGN_CREATE', 'FORM_READ', 'SUBMISSION_DELETE'])(req, res, next); + + expect(service.getUserForms).toBeCalled(); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); }); - const { res, next } = getMockRes(); - await hasSubmissionPermissions(['submission_delete'])(req, res, next); + // TODO: This should be a 403, but bundle all breaking changes in a small PR. + test('the permissions are a subset but not everything', async () => { + service.getUserForms.mockReturnValueOnce([ + { + formId: formId, + permissions: ['DESIGN_CREATE', 'FORM_READ'], + }, + ]); + const req = getMockReq({ + currentUser: {}, + params: { + formId: formId, + }, + }); + const { res, next } = getMockRes(); + + await hasFormPermissions(['DESIGN_CREATE', 'FORM_READ', 'SUBMISSION_DELETE'])(req, res, next); - expect(service.checkSubmissionPermission).toHaveBeenCalledTimes(1); - expect(service.getSubmissionForm).toHaveBeenCalledTimes(1); - expect(service.getUserForms).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(expect.objectContaining({ status: 401 })); + expect(service.getUserForms).toBeCalled(); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); + }); }); - it('401s on submission permissions if public access and more than read permission', async () => { - service.getSubmissionForm.mockReturnValueOnce({ - form: { id: formId, identityProviders: [{ code: 'public' }] }, - submission: { deleted: false, id: formSubmissionId }, + describe('500 response when', () => { + const expectedStatus = { status: 500 }; + + test('the request has no current user', async () => { + const req = getMockReq({ + params: { formId: formId }, + }); + const { res, next } = getMockRes(); + + await hasFormPermissions(['FORM_READ'])(req, res, next); + + expect(service.getUserForms).toBeCalledTimes(0); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); }); - const req = getMockReq({ - currentUser: {}, - params: { - formSubmissionId: formSubmissionId, - }, + }); + + describe('allows', () => { + test('a valid API key user', async () => { + const req = getMockReq({ + apiUser: true, + params: { formId: formId }, + }); + const { res, next } = getMockRes(); + + await hasFormPermissions(['FORM_READ'])(req, res, next); + + expect(service.getUserForms).toBeCalledTimes(0); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(); }); - const { res, next } = getMockRes(); - await hasSubmissionPermissions(['submission_read', 'submission_delete'])(req, res, next); + test('the expected permissions are included', async () => { + service.getUserForms.mockReturnValueOnce([ + { + formId: formId, + permissions: ['DESIGN_CREATE', 'FORM_READ', 'SUBMISSION_DELETE'], + }, + ]); + const req = getMockReq({ + currentUser: {}, + params: { + formId: formId, + }, + }); + const { res, next } = getMockRes(); - expect(service.checkSubmissionPermission).toHaveBeenCalledTimes(1); - expect(service.getSubmissionForm).toHaveBeenCalledTimes(1); - expect(service.getUserForms).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(expect.objectContaining({ status: 401 })); - }); + await hasFormPermissions(['DESIGN_CREATE', 'FORM_READ', 'SUBMISSION_DELETE'])(req, res, next); + + expect(service.getUserForms).toBeCalled(); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(); + }); + + test('the expected permissions are included with query formId', async () => { + service.getUserForms.mockReturnValueOnce([ + { + formId: formId, + permissions: ['DESIGN_CREATE', 'FORM_READ', 'SUBMISSION_DELETE'], + }, + ]); + const req = getMockReq({ + currentUser: {}, + query: { + formId: formId, + }, + }); + const { res, next } = getMockRes(); - it('401 on submission permissions if form does not have the public idp', async () => { - service.getSubmissionForm.mockReturnValueOnce({ - form: { - id: formId, - identityProviders: [{ code: 'idir' }, { code: 'bceid' }], - }, - submission: { deleted: false, id: formSubmissionId }, + await hasFormPermissions(['DESIGN_CREATE', 'FORM_READ', 'SUBMISSION_DELETE'])(req, res, next); + + expect(service.getUserForms).toBeCalled(); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(); }); - const req = getMockReq({ - currentUser: {}, - params: { - formSubmissionId: formSubmissionId, - }, + + test('the user has deleted form access', async () => { + service.getUserForms.mockReturnValueOnce([]).mockReturnValueOnce([ + { + formId: formId, + permissions: ['FORM_READ'], + }, + ]); + const req = getMockReq({ + currentUser: {}, + params: { + formId: formId, + }, + }); + const { res, next } = getMockRes(); + + await hasFormPermissions(['FORM_READ'])(req, res, next); + + expect(service.getUserForms).toBeCalled(); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(); }); - const { res, next } = getMockRes(); + }); +}); + +// External dependencies used by the implementation are: +// - service.checkSubmissionPermission: gets whether the user has permission. +// - service.getSubmissionForm: gets the submission that the user can access. +// - service.getUserForms: gets the forms that the user can access. +// +describe('hasSubmissionPermissions', () => { + // Default mock value where the user has no access to submission. + service.checkSubmissionPermission = jest.fn().mockReturnValue(false); - await hasSubmissionPermissions(['submission_read'])(req, res, next); + // Default mock value where there is no submission. + service.getSubmissionForm = jest.fn().mockReturnValue(); - expect(service.checkSubmissionPermission).toHaveBeenCalledTimes(1); - expect(service.getSubmissionForm).toHaveBeenCalledTimes(1); - expect(service.getUserForms).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(expect.objectContaining({ status: 401 })); + // Default mock value where the user has no access to forms. + service.getUserForms = jest.fn().mockReturnValue([]); + + it('returns a middleware function', () => { + const middleware = hasSubmissionPermissions(['submission_read']); + + expect(middleware).toBeInstanceOf(Function); }); - it('goes to error handler when getSubmissionForm errors', async () => { - const error = new Error(); - service.getSubmissionForm.mockRejectedValueOnce(error); - const req = getMockReq({ - currentUser: {}, - params: { - formSubmissionId: formSubmissionId, - }, + describe('400 response when', () => { + const expectedStatus = { status: 400 }; + + test('the request has no formSubmissionId', async () => { + const req = getMockReq(); + const { res, next } = getMockRes(); + + await hasSubmissionPermissions(['submission_read'])(req, res, next); + + expect(service.checkSubmissionPermission).toBeCalledTimes(0); + expect(service.getSubmissionForm).toBeCalledTimes(0); + expect(service.getUserForms).toBeCalledTimes(0); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); }); - const { res, next } = getMockRes(); - await hasSubmissionPermissions(['submission_read'])(req, res, next); + test('the formSubmissionId is not a uuid', async () => { + const req = getMockReq({ + currentUser: {}, + params: { + formSubmissionId: 'not-a-uuid', + }, + }); + const { res, next } = getMockRes(); - expect(service.checkSubmissionPermission).toHaveBeenCalledTimes(0); - expect(service.getSubmissionForm).toHaveBeenCalledTimes(1); - expect(service.getUserForms).toHaveBeenCalledTimes(0); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(error); + await hasSubmissionPermissions(['submission_read'])(req, res, next); + + expect(service.checkSubmissionPermission).toBeCalledTimes(0); + expect(service.getSubmissionForm).toBeCalledTimes(0); + expect(service.getUserForms).toBeCalledTimes(0); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); + }); }); - it('goes to error handler when getUserForms errors', async () => { - service.getSubmissionForm.mockReturnValueOnce({ - form: { id: formId }, - submission: { submissionId: formSubmissionId }, + describe('401 response when', () => { + const expectedStatus = { status: 401 }; + + test('deleted submission when no current user', async () => { + service.getSubmissionForm.mockReturnValueOnce({ + form: { id: formId }, + submission: { deleted: true, id: formSubmissionId }, + }); + const req = getMockReq({ + params: { + formSubmissionId: formSubmissionId, + }, + }); + const { res, next } = getMockRes(); + + await hasSubmissionPermissions(['submission_read'])(req, res, next); + + expect(service.checkSubmissionPermission).toBeCalledTimes(0); + expect(service.getSubmissionForm).toBeCalledTimes(1); + expect(service.getUserForms).toBeCalledTimes(0); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); }); - const error = new Error(); - service.getUserForms.mockRejectedValueOnce(error); - const req = getMockReq({ - currentUser: {}, - params: { - formSubmissionId: formSubmissionId, - }, + + test('deleted submission', async () => { + service.getSubmissionForm.mockReturnValueOnce({ + form: { id: formId }, + submission: { deleted: true, id: formSubmissionId }, + }); + const req = getMockReq({ + currentUser: {}, + params: { + formSubmissionId: formSubmissionId, + }, + }); + const { res, next } = getMockRes(); + + await hasSubmissionPermissions(['submission_read'])(req, res, next); + + expect(service.checkSubmissionPermission).toBeCalledTimes(0); + expect(service.getSubmissionForm).toBeCalledTimes(1); + expect(service.getUserForms).toBeCalledTimes(1); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); }); - const { res, next } = getMockRes(); - await hasSubmissionPermissions(['submission_read'])(req, res, next); + test('user has no forms for deleted submission', async () => { + service.getSubmissionForm.mockReturnValueOnce({ + form: { id: formId }, + submission: { deleted: true, id: formSubmissionId }, + }); + const req = getMockReq({ + currentUser: {}, + params: { + formSubmissionId: formSubmissionId, + }, + }); + const { res, next } = getMockRes(); - expect(service.checkSubmissionPermission).toHaveBeenCalledTimes(0); - expect(service.getSubmissionForm).toHaveBeenCalledTimes(1); - expect(service.getUserForms).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(error); - }); + await hasSubmissionPermissions(['submission_read'])(req, res, next); - it('moves on if a valid API key user has already been set', async () => { - const req = getMockReq({ - apiUser: true, - params: { - formSubmissionId: formSubmissionId, - }, + expect(service.checkSubmissionPermission).toBeCalledTimes(0); + expect(service.getSubmissionForm).toBeCalledTimes(1); + expect(service.getUserForms).toBeCalledTimes(1); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); }); - const { res, next } = getMockRes(); - await hasSubmissionPermissions(['submission_read'])(req, res, next); + test('user has no form access for deleted submission', async () => { + service.getSubmissionForm.mockReturnValueOnce({ + form: { id: formId }, + submission: { deleted: true, id: formSubmissionId }, + }); + service.getUserForms.mockReturnValueOnce([ + { + formId: formId, + permissions: [], + }, + ]); + const req = getMockReq({ + currentUser: {}, + params: { + formSubmissionId: formSubmissionId, + }, + }); + const { res, next } = getMockRes(); - expect(service.checkSubmissionPermission).toHaveBeenCalledTimes(0); - expect(service.getSubmissionForm).toHaveBeenCalledTimes(0); - expect(service.getUserForms).toHaveBeenCalledTimes(0); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(); - }); + await hasSubmissionPermissions(['submission_read'])(req, res, next); - it('moves on if the user has the exact form permissions', async () => { - service.getSubmissionForm.mockReturnValueOnce({ - form: { id: formId }, - submission: { deleted: false, id: formSubmissionId }, - }); - service.getUserForms.mockReturnValueOnce([ - { - // Ignore this form but match formId on the next one. - formId: uuid.v4(), - }, - { - formId: formId, - permissions: ['submission_read', 'submission_update'], - }, - ]); - const req = getMockReq({ - currentUser: {}, - params: { - formSubmissionId: formSubmissionId, - }, + expect(service.checkSubmissionPermission).toBeCalledTimes(0); + expect(service.getSubmissionForm).toBeCalledTimes(1); + expect(service.getUserForms).toBeCalledTimes(1); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); }); - const { res, next } = getMockRes(); - await hasSubmissionPermissions(['submission_read', 'submission_update'])(req, res, next); + test('user only has some form access for deleted submission', async () => { + service.getSubmissionForm.mockReturnValueOnce({ + form: { id: formId }, + submission: { deleted: true, id: formSubmissionId }, + }); + service.getUserForms.mockReturnValueOnce([ + { + formId: formId, + permissions: ['submission_read'], + }, + ]); + const req = getMockReq({ + currentUser: {}, + params: { + formSubmissionId: formSubmissionId, + }, + }); + const { res, next } = getMockRes(); + + await hasSubmissionPermissions(['submission_read', 'submission_update'])(req, res, next); - expect(service.checkSubmissionPermission).toHaveBeenCalledTimes(0); - expect(service.getSubmissionForm).toHaveBeenCalledTimes(1); - expect(service.getUserForms).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(); - }); + expect(service.checkSubmissionPermission).toBeCalledTimes(0); + expect(service.getSubmissionForm).toBeCalledTimes(1); + expect(service.getUserForms).toBeCalledTimes(1); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); + }); - it('moves on if the user has extra form permissions', async () => { - service.getSubmissionForm.mockReturnValueOnce({ - form: { id: formId }, - submission: { deleted: false, id: formSubmissionId }, - }); - service.getUserForms.mockReturnValueOnce([ - { - // Ignore this form but match formId on the next one. - formId: uuid.v4(), - }, - { - formId: formId, - permissions: ['submission_read', 'submission_update'], - }, - ]); - const req = getMockReq({ - currentUser: {}, - params: { - formSubmissionId: formSubmissionId, - }, + test('public access and no read permission', async () => { + service.checkSubmissionPermission.mockReturnValueOnce(undefined); + service.getSubmissionForm.mockReturnValueOnce({ + form: { id: formId, identityProviders: [{ code: 'public' }] }, + submission: { deleted: false, id: formSubmissionId }, + }); + const req = getMockReq({ + currentUser: {}, + params: { + formSubmissionId: formSubmissionId, + }, + }); + const { res, next } = getMockRes(); + + await hasSubmissionPermissions(['submission_delete'])(req, res, next); + + expect(service.checkSubmissionPermission).toBeCalledTimes(1); + expect(service.getSubmissionForm).toBeCalledTimes(1); + expect(service.getUserForms).toBeCalledTimes(1); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); }); - const { res, next } = getMockRes(); - await hasSubmissionPermissions(['submission_update'])(req, res, next); + test('public access and more than read permission', async () => { + service.getSubmissionForm.mockReturnValueOnce({ + form: { id: formId, identityProviders: [{ code: 'public' }] }, + submission: { deleted: false, id: formSubmissionId }, + }); + const req = getMockReq({ + currentUser: {}, + params: { + formSubmissionId: formSubmissionId, + }, + }); + const { res, next } = getMockRes(); - expect(service.checkSubmissionPermission).toHaveBeenCalledTimes(0); - expect(service.getSubmissionForm).toHaveBeenCalledTimes(1); - expect(service.getUserForms).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(); - }); + await hasSubmissionPermissions(['submission_read', 'submission_delete'])(req, res, next); - it('moves on for public form and read permission', async () => { - service.getSubmissionForm.mockReturnValueOnce({ - form: { id: formId, identityProviders: [{ code: 'public' }] }, - submission: { deleted: false, id: formSubmissionId }, - }); - service.getUserForms.mockReturnValueOnce([ - { - formId: formId, - permissions: [], - }, - ]); - const req = getMockReq({ - currentUser: {}, - params: { - formSubmissionId: formSubmissionId, - }, + expect(service.checkSubmissionPermission).toBeCalledTimes(1); + expect(service.getSubmissionForm).toBeCalledTimes(1); + expect(service.getUserForms).toBeCalledTimes(1); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); }); - const { res, next } = getMockRes(); - await hasSubmissionPermissions(['submission_read'])(req, res, next); + test('form does not have the public idp', async () => { + service.getSubmissionForm.mockReturnValueOnce({ + form: { + id: formId, + identityProviders: [{ code: 'idir' }, { code: 'bceid' }], + }, + submission: { deleted: false, id: formSubmissionId }, + }); + const req = getMockReq({ + currentUser: {}, + params: { + formSubmissionId: formSubmissionId, + }, + }); + const { res, next } = getMockRes(); + + await hasSubmissionPermissions(['submission_read'])(req, res, next); - expect(service.checkSubmissionPermission).toHaveBeenCalledTimes(0); - expect(service.getSubmissionForm).toHaveBeenCalledTimes(1); - expect(service.getUserForms).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(); + expect(service.checkSubmissionPermission).toBeCalledTimes(1); + expect(service.getSubmissionForm).toBeCalledTimes(1); + expect(service.getUserForms).toBeCalledTimes(1); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); + }); }); - it('moves on for public form and read permission with extra idp', async () => { - service.getSubmissionForm.mockReturnValueOnce({ - form: { - id: formId, - identityProviders: [{ code: 'random' }, { code: 'public' }], - }, - submission: { deleted: false, id: formSubmissionId }, - }); - service.getUserForms.mockReturnValueOnce([ - { - formId: formId, - permissions: [], - }, - ]); - const req = getMockReq({ - currentUser: {}, - params: { - formSubmissionId: formSubmissionId, - }, + describe('handles error thrown by', () => { + test('getSubmissionForm', async () => { + const error = new Error(); + service.getSubmissionForm.mockRejectedValueOnce(error); + const req = getMockReq({ + currentUser: {}, + params: { + formSubmissionId: formSubmissionId, + }, + }); + const { res, next } = getMockRes(); + + await hasSubmissionPermissions(['submission_read'])(req, res, next); + + expect(service.checkSubmissionPermission).toBeCalledTimes(0); + expect(service.getSubmissionForm).toBeCalledTimes(1); + expect(service.getUserForms).toBeCalledTimes(0); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(error); }); - const { res, next } = getMockRes(); - await hasSubmissionPermissions(['submission_read'])(req, res, next); + test('getUserForms', async () => { + service.getSubmissionForm.mockReturnValueOnce({ + form: { id: formId }, + submission: { submissionId: formSubmissionId }, + }); + const error = new Error(); + service.getUserForms.mockRejectedValueOnce(error); + const req = getMockReq({ + currentUser: {}, + params: { + formSubmissionId: formSubmissionId, + }, + }); + const { res, next } = getMockRes(); + + await hasSubmissionPermissions(['submission_read'])(req, res, next); - expect(service.checkSubmissionPermission).toHaveBeenCalledTimes(0); - expect(service.getSubmissionForm).toHaveBeenCalledTimes(1); - expect(service.getUserForms).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(); + expect(service.checkSubmissionPermission).toBeCalledTimes(0); + expect(service.getSubmissionForm).toBeCalledTimes(1); + expect(service.getUserForms).toBeCalledTimes(1); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(error); + }); }); - it('moves on if the permission check succeeds', async () => { - service.checkSubmissionPermission.mockReturnValueOnce(true); - service.getSubmissionForm.mockReturnValueOnce({ - form: { - id: formId, - identityProviders: [{ code: 'idir' }, { code: 'bceid' }], - }, - submission: { deleted: false, id: formSubmissionId }, - }); - const req = getMockReq({ - currentUser: {}, - params: { - formSubmissionId: formSubmissionId, - }, + describe('allows', () => { + test('a valid API key user', async () => { + const req = getMockReq({ + apiUser: true, + params: { + formSubmissionId: formSubmissionId, + }, + }); + const { res, next } = getMockRes(); + + await hasSubmissionPermissions(['submission_read'])(req, res, next); + + expect(service.checkSubmissionPermission).toBeCalledTimes(0); + expect(service.getSubmissionForm).toBeCalledTimes(0); + expect(service.getUserForms).toBeCalledTimes(0); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(); }); - const { res, next } = getMockRes(); - await hasSubmissionPermissions(['submission_read'])(req, res, next); + test('the user has the exact form permissions', async () => { + service.getSubmissionForm.mockReturnValueOnce({ + form: { id: formId }, + submission: { deleted: false, id: formSubmissionId }, + }); + service.getUserForms.mockReturnValueOnce([ + { + // Ignore this form but match formId on the next one. + formId: uuid.v4(), + }, + { + formId: formId, + permissions: ['submission_read', 'submission_update'], + }, + ]); + const req = getMockReq({ + currentUser: {}, + params: { + formSubmissionId: formSubmissionId, + }, + }); + const { res, next } = getMockRes(); + + await hasSubmissionPermissions(['submission_read', 'submission_update'])(req, res, next); + + expect(service.checkSubmissionPermission).toBeCalledTimes(0); + expect(service.getSubmissionForm).toBeCalledTimes(1); + expect(service.getUserForms).toBeCalledTimes(1); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(); + }); + + test('the user has extra form permissions', async () => { + service.getSubmissionForm.mockReturnValueOnce({ + form: { id: formId }, + submission: { deleted: false, id: formSubmissionId }, + }); + service.getUserForms.mockReturnValueOnce([ + { + // Ignore this form but match formId on the next one. + formId: uuid.v4(), + }, + { + formId: formId, + permissions: ['submission_read', 'submission_update'], + }, + ]); + const req = getMockReq({ + currentUser: {}, + params: { + formSubmissionId: formSubmissionId, + }, + }); + const { res, next } = getMockRes(); + + await hasSubmissionPermissions(['submission_update'])(req, res, next); - expect(service.checkSubmissionPermission).toHaveBeenCalledTimes(1); - expect(service.getSubmissionForm).toHaveBeenCalledTimes(1); - expect(service.getUserForms).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(); + expect(service.checkSubmissionPermission).toBeCalledTimes(0); + expect(service.getSubmissionForm).toBeCalledTimes(1); + expect(service.getUserForms).toBeCalledTimes(1); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(); + }); + + test('public form and read permission', async () => { + service.getSubmissionForm.mockReturnValueOnce({ + form: { id: formId, identityProviders: [{ code: 'public' }] }, + submission: { deleted: false, id: formSubmissionId }, + }); + service.getUserForms.mockReturnValueOnce([ + { + formId: formId, + permissions: [], + }, + ]); + const req = getMockReq({ + currentUser: {}, + params: { + formSubmissionId: formSubmissionId, + }, + }); + const { res, next } = getMockRes(); + + await hasSubmissionPermissions(['submission_read'])(req, res, next); + + expect(service.checkSubmissionPermission).toBeCalledTimes(0); + expect(service.getSubmissionForm).toBeCalledTimes(1); + expect(service.getUserForms).toBeCalledTimes(1); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(); + }); + + test('public form and read permission with extra idp', async () => { + service.getSubmissionForm.mockReturnValueOnce({ + form: { + id: formId, + identityProviders: [{ code: 'random' }, { code: 'public' }], + }, + submission: { deleted: false, id: formSubmissionId }, + }); + service.getUserForms.mockReturnValueOnce([ + { + formId: formId, + permissions: [], + }, + ]); + const req = getMockReq({ + currentUser: {}, + params: { + formSubmissionId: formSubmissionId, + }, + }); + const { res, next } = getMockRes(); + + await hasSubmissionPermissions(['submission_read'])(req, res, next); + + expect(service.checkSubmissionPermission).toBeCalledTimes(0); + expect(service.getSubmissionForm).toBeCalledTimes(1); + expect(service.getUserForms).toBeCalledTimes(1); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(); + }); + + test('the permission check succeeds', async () => { + service.checkSubmissionPermission.mockReturnValueOnce(true); + service.getSubmissionForm.mockReturnValueOnce({ + form: { + id: formId, + identityProviders: [{ code: 'idir' }, { code: 'bceid' }], + }, + submission: { deleted: false, id: formSubmissionId }, + }); + const req = getMockReq({ + currentUser: {}, + params: { + formSubmissionId: formSubmissionId, + }, + }); + const { res, next } = getMockRes(); + + await hasSubmissionPermissions(['submission_read'])(req, res, next); + + expect(service.checkSubmissionPermission).toBeCalledTimes(1); + expect(service.getSubmissionForm).toBeCalledTimes(1); + expect(service.getUserForms).toBeCalledTimes(1); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(); + }); }); }); // External dependencies used by the implementation are: -// - service.getUserForms: gets the forms that the user can access +// - service.getUserForms: gets the forms that the user can access. // describe('hasFormRoles', () => { - // Default mock value where the user has no access to forms + // Default mock value where the user has no access to forms. service.getUserForms = jest.fn().mockReturnValue([]); it('returns a middleware function', async () => { @@ -822,9 +1277,9 @@ describe('hasFormRoles', () => { await hasFormRoles([Roles.OWNER])(req, res, next); - expect(service.getUserForms).toHaveBeenCalledTimes(0); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(expect.objectContaining(expectedStatus)); + expect(service.getUserForms).toBeCalledTimes(0); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); }); test('formId not a uuid', async () => { @@ -841,9 +1296,9 @@ describe('hasFormRoles', () => { await hasFormRoles([Roles.OWNER])(req, res, next); - expect(service.getUserForms).toHaveBeenCalledTimes(0); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(expect.objectContaining(expectedStatus)); + expect(service.getUserForms).toBeCalledTimes(0); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); }); }); @@ -862,9 +1317,9 @@ describe('hasFormRoles', () => { await hasFormRoles([Roles.OWNER])(req, res, next); - expect(service.getUserForms).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(expect.objectContaining(expectedStatus)); + expect(service.getUserForms).toBeCalledTimes(1); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); }); test('role not on form', async () => { @@ -884,9 +1339,9 @@ describe('hasFormRoles', () => { await hasFormRoles([Roles.OWNER])(req, res, next); - expect(service.getUserForms).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(expect.objectContaining(expectedStatus)); + expect(service.getUserForms).toBeCalledTimes(1); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); }); test('roles not on form', async () => { @@ -906,9 +1361,9 @@ describe('hasFormRoles', () => { await hasFormRoles([Roles.FORM_SUBMITTER, Roles.OWNER])(req, res, next); - expect(service.getUserForms).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(expect.objectContaining(expectedStatus)); + expect(service.getUserForms).toBeCalledTimes(1); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); }); }); @@ -926,9 +1381,9 @@ describe('hasFormRoles', () => { await hasFormRoles([Roles.OWNER])(req, res, next); - expect(service.getUserForms).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(error); + expect(service.getUserForms).toBeCalledTimes(1); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(error); }); }); @@ -950,9 +1405,9 @@ describe('hasFormRoles', () => { await hasFormRoles([Roles.OWNER])(req, res, next); - expect(service.getUserForms).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(); + expect(service.getUserForms).toBeCalledTimes(1); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(); }); test('single role is start of subset', async () => { @@ -972,9 +1427,9 @@ describe('hasFormRoles', () => { await hasFormRoles([Roles.OWNER])(req, res, next); - expect(service.getUserForms).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(); + expect(service.getUserForms).toBeCalledTimes(1); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(); }); test('single role is middle of subset', async () => { @@ -994,9 +1449,9 @@ describe('hasFormRoles', () => { await hasFormRoles([Roles.OWNER])(req, res, next); - expect(service.getUserForms).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(); + expect(service.getUserForms).toBeCalledTimes(1); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(); }); test('single role is end of subset', async () => { @@ -1016,9 +1471,9 @@ describe('hasFormRoles', () => { await hasFormRoles([Roles.OWNER])(req, res, next); - expect(service.getUserForms).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(); + expect(service.getUserForms).toBeCalledTimes(1); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(); }); test('second role is start of subset', async () => { @@ -1038,9 +1493,9 @@ describe('hasFormRoles', () => { await hasFormRoles([Roles.FORM_DESIGNER, Roles.OWNER])(req, res, next); - expect(service.getUserForms).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(); + expect(service.getUserForms).toBeCalledTimes(1); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(); }); test('second role is middle of subset', async () => { @@ -1060,9 +1515,9 @@ describe('hasFormRoles', () => { await hasFormRoles([Roles.FORM_DESIGNER, Roles.OWNER])(req, res, next); - expect(service.getUserForms).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(); + expect(service.getUserForms).toBeCalledTimes(1); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(); }); test('second role is end of subset', async () => { @@ -1082,22 +1537,22 @@ describe('hasFormRoles', () => { await hasFormRoles([Roles.FORM_DESIGNER, Roles.SUBMISSION_REVIEWER])(req, res, next); - expect(service.getUserForms).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(); + expect(service.getUserForms).toBeCalledTimes(1); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(); }); }); }); // External dependencies used by the implementation are: -// - service.getUserForms: gets the forms that the user can access -// - rbacService.readUserRole: gets the roles that user has on a form +// - service.getUserForms: gets the forms that the user can access. +// - rbacService.readUserRole: gets the roles that user has on a form. // describe('hasRolePermissions', () => { - // Default mock value where the user has no access to forms + // Default mock value where the user has no access to forms. service.getUserForms = jest.fn().mockReturnValue([]); - // Default mock value where the user has no roles + // Default mock value where the user has no roles. rbacService.readUserRole = jest.fn().mockReturnValue([]); it('returns a middleware function', async () => { @@ -1122,11 +1577,11 @@ describe('hasRolePermissions', () => { await hasRolePermissions()(req, res, next); - expect(service.getUserForms).toHaveBeenCalledTimes(0); - expect(rbacService.readUserRole).toHaveBeenCalledTimes(0); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(expect.objectContaining(expectedStatus)); - expect(next).toHaveBeenCalledWith( + expect(service.getUserForms).toBeCalledTimes(0); + expect(rbacService.readUserRole).toBeCalledTimes(0); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); + expect(next).toBeCalledWith( expect.objectContaining({ detail: 'Bad formId', }) @@ -1149,11 +1604,11 @@ describe('hasRolePermissions', () => { await hasRolePermissions()(req, res, next); - expect(service.getUserForms).toHaveBeenCalledTimes(0); - expect(rbacService.readUserRole).toHaveBeenCalledTimes(0); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(expect.objectContaining(expectedStatus)); - expect(next).toHaveBeenCalledWith( + expect(service.getUserForms).toBeCalledTimes(0); + expect(rbacService.readUserRole).toBeCalledTimes(0); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); + expect(next).toBeCalledWith( expect.objectContaining({ detail: 'Bad formId', }) @@ -1184,11 +1639,11 @@ describe('hasRolePermissions', () => { await hasRolePermissions(true)(req, res, next); - expect(service.getUserForms).toHaveBeenCalledTimes(1); - expect(rbacService.readUserRole).toHaveBeenCalledTimes(0); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(expect.objectContaining(expectedStatus)); - expect(next).toHaveBeenCalledWith( + expect(service.getUserForms).toBeCalledTimes(1); + expect(rbacService.readUserRole).toBeCalledTimes(0); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); + expect(next).toBeCalledWith( expect.objectContaining({ detail: 'Bad userId', }) @@ -1214,11 +1669,11 @@ describe('hasRolePermissions', () => { await hasRolePermissions()(req, res, next); - expect(service.getUserForms).toHaveBeenCalledTimes(1); - expect(rbacService.readUserRole).toHaveBeenCalledTimes(0); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(expect.objectContaining(expectedStatus)); - expect(next).toHaveBeenCalledWith( + expect(service.getUserForms).toBeCalledTimes(1); + expect(rbacService.readUserRole).toBeCalledTimes(0); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); + expect(next).toBeCalledWith( expect.objectContaining({ detail: 'Bad userId', }) @@ -1245,11 +1700,11 @@ describe('hasRolePermissions', () => { await hasRolePermissions()(req, res, next); - expect(service.getUserForms).toHaveBeenCalledTimes(1); - expect(rbacService.readUserRole).toHaveBeenCalledTimes(0); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(expect.objectContaining(expectedStatus)); - expect(next).toHaveBeenCalledWith( + expect(service.getUserForms).toBeCalledTimes(1); + expect(rbacService.readUserRole).toBeCalledTimes(0); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); + expect(next).toBeCalledWith( expect.objectContaining({ detail: 'Bad userId', }) @@ -1280,11 +1735,11 @@ describe('hasRolePermissions', () => { await hasRolePermissions(true)(req, res, next); - expect(service.getUserForms).toHaveBeenCalledTimes(1); - expect(rbacService.readUserRole).toHaveBeenCalledTimes(0); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(expect.objectContaining(expectedStatus)); - expect(next).toHaveBeenCalledWith( + expect(service.getUserForms).toBeCalledTimes(1); + expect(rbacService.readUserRole).toBeCalledTimes(0); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); + expect(next).toBeCalledWith( expect.objectContaining({ detail: "You can't remove yourself from this form.", }) @@ -1312,11 +1767,11 @@ describe('hasRolePermissions', () => { await hasRolePermissions(true)(req, res, next); - expect(service.getUserForms).toHaveBeenCalledTimes(1); - expect(rbacService.readUserRole).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(expect.objectContaining(expectedStatus)); - expect(next).toHaveBeenCalledWith( + expect(service.getUserForms).toBeCalledTimes(1); + expect(rbacService.readUserRole).toBeCalledTimes(1); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); + expect(next).toBeCalledWith( expect.objectContaining({ detail: "You can not update an owner's roles.", }) @@ -1344,11 +1799,11 @@ describe('hasRolePermissions', () => { await hasRolePermissions(true)(req, res, next); - expect(service.getUserForms).toHaveBeenCalledTimes(1); - expect(rbacService.readUserRole).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(expect.objectContaining(expectedStatus)); - expect(next).toHaveBeenCalledWith( + expect(service.getUserForms).toBeCalledTimes(1); + expect(rbacService.readUserRole).toBeCalledTimes(1); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); + expect(next).toBeCalledWith( expect.objectContaining({ detail: "You can't remove a form designer role.", }) @@ -1377,11 +1832,11 @@ describe('hasRolePermissions', () => { await hasRolePermissions()(req, res, next); - expect(service.getUserForms).toHaveBeenCalledTimes(1); - expect(rbacService.readUserRole).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(expect.objectContaining(expectedStatus)); - expect(next).toHaveBeenCalledWith( + expect(service.getUserForms).toBeCalledTimes(1); + expect(rbacService.readUserRole).toBeCalledTimes(1); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); + expect(next).toBeCalledWith( expect.objectContaining({ detail: "You can't remove your own team manager role.", }) @@ -1410,11 +1865,11 @@ describe('hasRolePermissions', () => { await hasRolePermissions()(req, res, next); - expect(service.getUserForms).toHaveBeenCalledTimes(1); - expect(rbacService.readUserRole).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(expect.objectContaining(expectedStatus)); - expect(next).toHaveBeenCalledWith( + expect(service.getUserForms).toBeCalledTimes(1); + expect(rbacService.readUserRole).toBeCalledTimes(1); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); + expect(next).toBeCalledWith( expect.objectContaining({ detail: "You can't update an owner's roles.", }) @@ -1443,11 +1898,11 @@ describe('hasRolePermissions', () => { await hasRolePermissions()(req, res, next); - expect(service.getUserForms).toHaveBeenCalledTimes(1); - expect(rbacService.readUserRole).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(expect.objectContaining(expectedStatus)); - expect(next).toHaveBeenCalledWith( + expect(service.getUserForms).toBeCalledTimes(1); + expect(rbacService.readUserRole).toBeCalledTimes(1); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); + expect(next).toBeCalledWith( expect.objectContaining({ detail: "You can't add an owner role.", }) @@ -1476,11 +1931,11 @@ describe('hasRolePermissions', () => { await hasRolePermissions()(req, res, next); - expect(service.getUserForms).toHaveBeenCalledTimes(1); - expect(rbacService.readUserRole).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(expect.objectContaining(expectedStatus)); - expect(next).toHaveBeenCalledWith( + expect(service.getUserForms).toBeCalledTimes(1); + expect(rbacService.readUserRole).toBeCalledTimes(1); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); + expect(next).toBeCalledWith( expect.objectContaining({ detail: "You can't remove a form designer role.", }) @@ -1509,11 +1964,11 @@ describe('hasRolePermissions', () => { await hasRolePermissions()(req, res, next); - expect(service.getUserForms).toHaveBeenCalledTimes(1); - expect(rbacService.readUserRole).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(expect.objectContaining(expectedStatus)); - expect(next).toHaveBeenCalledWith( + expect(service.getUserForms).toBeCalledTimes(1); + expect(rbacService.readUserRole).toBeCalledTimes(1); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); + expect(next).toBeCalledWith( expect.objectContaining({ detail: "You can't add a form designer role.", }) @@ -1543,10 +1998,10 @@ describe('hasRolePermissions', () => { await hasRolePermissions(true)(req, res, next); - expect(service.getUserForms).toHaveBeenCalledTimes(1); - expect(rbacService.readUserRole).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(); + expect(service.getUserForms).toBeCalledTimes(1); + expect(rbacService.readUserRole).toBeCalledTimes(1); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(); }); test('deleting and owner can remove an owner', async () => { @@ -1569,10 +2024,10 @@ describe('hasRolePermissions', () => { await hasRolePermissions(true)(req, res, next); - expect(service.getUserForms).toHaveBeenCalledTimes(1); - expect(rbacService.readUserRole).toHaveBeenCalledTimes(0); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(); + expect(service.getUserForms).toBeCalledTimes(1); + expect(rbacService.readUserRole).toBeCalledTimes(0); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(); }); test('deleting and owner can remove a form designer', async () => { @@ -1595,10 +2050,10 @@ describe('hasRolePermissions', () => { await hasRolePermissions(true)(req, res, next); - expect(service.getUserForms).toHaveBeenCalledTimes(1); - expect(rbacService.readUserRole).toHaveBeenCalledTimes(0); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(); + expect(service.getUserForms).toBeCalledTimes(1); + expect(rbacService.readUserRole).toBeCalledTimes(0); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(); }); test('deleting and owner can remove a form designer with form id in query', async () => { @@ -1621,10 +2076,10 @@ describe('hasRolePermissions', () => { await hasRolePermissions(true)(req, res, next); - expect(service.getUserForms).toHaveBeenCalledTimes(1); - expect(rbacService.readUserRole).toHaveBeenCalledTimes(0); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(); + expect(service.getUserForms).toBeCalledTimes(1); + expect(rbacService.readUserRole).toBeCalledTimes(0); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(); }); test('updating and non-owner can add approver', async () => { @@ -1649,10 +2104,10 @@ describe('hasRolePermissions', () => { await hasRolePermissions()(req, res, next); - expect(service.getUserForms).toHaveBeenCalledTimes(1); - expect(rbacService.readUserRole).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(); + expect(service.getUserForms).toBeCalledTimes(1); + expect(rbacService.readUserRole).toBeCalledTimes(1); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(); }); test('updating and owner can add owner', async () => { @@ -1676,10 +2131,10 @@ describe('hasRolePermissions', () => { await hasRolePermissions()(req, res, next); - expect(service.getUserForms).toHaveBeenCalledTimes(1); - expect(rbacService.readUserRole).toHaveBeenCalledTimes(0); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(); + expect(service.getUserForms).toBeCalledTimes(1); + expect(rbacService.readUserRole).toBeCalledTimes(0); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(); }); }); }); diff --git a/app/tests/unit/routes/v1/submission.spec.js b/app/tests/unit/routes/v1/submission.spec.js index de2b1e0ad..4df8e4b15 100644 --- a/app/tests/unit/routes/v1/submission.spec.js +++ b/app/tests/unit/routes/v1/submission.spec.js @@ -10,10 +10,8 @@ const userAccess = require('../../../../src/forms/auth/middleware/userAccess'); userAccess.currentUser = jest.fn((_req, _res, next) => { next(); }); -userAccess.filterMultipleSubmissions = jest.fn(() => { - return jest.fn((_req, _res, next) => { - next(); - }); +userAccess.filterMultipleSubmissions = jest.fn((_req, _res, next) => { + next(); }); userAccess.hasSubmissionPermissions = jest.fn(() => { return jest.fn((_req, _res, next) => { From 6a734b3f194154ecdabe3241a301a8a352a6e4b0 Mon Sep 17 00:00:00 2001 From: Walter Moar Date: Wed, 22 May 2024 11:52:14 -0700 Subject: [PATCH 12/14] refactor: FORMS-1265 split hasRolePermissions into two methods (#1367) * refactor: FORMS-1265 split hasRolePermissions into two methods * tightened up some logic --- app/src/forms/auth/middleware/userAccess.js | 205 +++++++----- app/src/forms/rbac/routes.js | 6 +- .../forms/auth/middleware/userAccess.spec.js | 316 +++++++++++------- app/tests/unit/routes/v1/rbac.spec.js | 9 +- 4 files changed, 318 insertions(+), 218 deletions(-) diff --git a/app/src/forms/auth/middleware/userAccess.js b/app/src/forms/auth/middleware/userAccess.js index 444782859..2661e3abc 100644 --- a/app/src/forms/auth/middleware/userAccess.js +++ b/app/src/forms/auth/middleware/userAccess.js @@ -256,111 +256,137 @@ const hasFormRoles = (roles) => { }; /** - * Express middleware to check that the calling user has one of the given roles for - * the form identified by params.formId or query.formId. This falls through if - * everything is OK, otherwise it calls next() with a Problem describing the + * Express middleware to check that the calling user is allowed to delete roles + * for the form identified by params.formId or query.formId. This falls through + * if everything is OK, otherwise it calls next() with a Problem describing the * error. * - * @param {string[]} roles the roles the user needs one of for the form. + * @param {*} req the Express object representing the HTTP request. + * @param {*} _res the Express object representing the HTTP response - unused. + * @param {*} next the Express chaining function. * @returns nothing */ -const hasRolePermissions = (removingUsers = false) => { - return async (req, _res, next) => { - try { - const currentUser = req.currentUser; +const hasRoleDeletePermissions = async (req, _res, next) => { + try { + const currentUser = req.currentUser; - // The request must include a formId, either in params or query, but give - // precedence to params. - const form = await _getForm(currentUser, req.params.formId || req.query.formId, false); - const isOwner = form.roles.includes(Roles.OWNER); + // The request must include a formId, either in params or query, but give + // precedence to params. + const form = await _getForm(currentUser, req.params.formId || req.query.formId, false); - if (removingUsers) { - const userIds = req.body; - if (userIds.includes(currentUser.id)) { - throw new Problem(401, { - detail: "You can't remove yourself from this form.", - }); - } + const userIds = req.body; + if (userIds.includes(currentUser.id)) { + throw new Problem(401, { + detail: "You can't remove yourself from this form.", + }); + } - if (!isOwner) { - for (const userId of userIds) { - if (!uuid.validate(userId)) { - throw new Problem(400, { - detail: 'Bad userId', - }); - } - - // Convert to an array of role strings, rather than the objects. - const userRoles = (await rbacService.readUserRole(userId, form.formId)).map((userRole) => userRole.role); - - // A non-owner can't delete an owner. - if (userRoles.includes(Roles.OWNER) && userId !== currentUser.id) { - throw new Problem(401, { - detail: "You can not update an owner's roles.", - }); - } - - // A non-owner can't delete a form designer. - if (userRoles.includes(Roles.FORM_DESIGNER)) { - throw new Problem(401, { - detail: "You can't remove a form designer role.", - }); - } - } - } - } else { - const userId = req.params.userId || req.query.userId; + const isOwner = form.roles.includes(Roles.OWNER); + if (!isOwner) { + for (const userId of userIds) { if (!uuid.validate(userId)) { throw new Problem(400, { detail: 'Bad userId', }); } - if (!isOwner) { - // Convert to arrays of role strings, rather than the objects. - const userRoles = (await rbacService.readUserRole(userId, form.formId)).map((userRole) => userRole.role); - const futureRoles = req.body.map((userRole) => userRole.role); - - // If the user is trying to remove the team manager role for their own userid - if (userRoles.includes(Roles.TEAM_MANAGER) && !futureRoles.includes(Roles.TEAM_MANAGER) && userId == currentUser.id) { - throw new Problem(401, { - detail: "You can't remove your own team manager role.", - }); - } - - // Can't update another user's roles if they are an owner - if (userRoles.includes(Roles.OWNER) && userId !== currentUser.id) { - throw new Problem(401, { - detail: "You can't update an owner's roles.", - }); - } - - if (!userRoles.includes(Roles.OWNER) && futureRoles.includes(Roles.OWNER)) { - throw new Problem(401, { - detail: "You can't add an owner role.", - }); - } - - // If the user is trying to remove the designer role for another userid - if (userRoles.includes(Roles.FORM_DESIGNER) && !futureRoles.includes(Roles.FORM_DESIGNER)) { - throw new Problem(401, { - detail: "You can't remove a form designer role.", - }); - } - - if (!userRoles.includes(Roles.FORM_DESIGNER) && futureRoles.includes(Roles.FORM_DESIGNER)) { - throw new Problem(401, { - detail: "You can't add a form designer role.", - }); - } + // Convert to an array of role strings, rather than the objects. + const userRoles = (await rbacService.readUserRole(userId, form.formId)).map((userRole) => userRole.role); + + // A non-owner can't delete an owner. + if (userRoles.includes(Roles.OWNER) && userId !== currentUser.id) { + throw new Problem(401, { + detail: "You can not update an owner's roles.", + }); + } + + // A non-owner can't delete a form designer. + if (userRoles.includes(Roles.FORM_DESIGNER)) { + throw new Problem(401, { + detail: "You can't remove a form designer role.", + }); } } + } - next(); - } catch (error) { - next(error); + next(); + } catch (error) { + next(error); + } +}; + +/** + * Express middleware to check that the calling user is allowed to modify roles + * for the form identified by params.formId or query.formId. This falls through + * if everything is OK, otherwise it calls next() with a Problem describing the + * error. + * + * @param {*} req the Express object representing the HTTP request. + * @param {*} _res the Express object representing the HTTP response - unused. + * @param {*} next the Express chaining function. + * @returns nothing + */ +const hasRoleModifyPermissions = async (req, _res, next) => { + try { + const currentUser = req.currentUser; + + // The request must include a formId, either in params or query, but give + // precedence to params. + const form = await _getForm(currentUser, req.params.formId || req.query.formId, false); + + const userId = req.params.userId || req.query.userId; + if (!uuid.validate(userId)) { + throw new Problem(400, { + detail: 'Bad userId', + }); } - }; + + const isOwner = form.roles.includes(Roles.OWNER); + if (!isOwner) { + // Convert to arrays of role strings, rather than the objects. + const userRoles = (await rbacService.readUserRole(userId, form.formId)).map((userRole) => userRole.role); + const futureRoles = req.body.map((userRole) => userRole.role); + + // If the user is trying to remove the team manager role for their own userid + if (userRoles.includes(Roles.TEAM_MANAGER) && !futureRoles.includes(Roles.TEAM_MANAGER) && userId === currentUser.id) { + throw new Problem(401, { + detail: "You can't remove your own team manager role.", + }); + } + + if (userRoles.includes(Roles.OWNER)) { + // Can't remove a different user's owner role unless you are an owner. + if (userId !== currentUser.id) { + throw new Problem(401, { + detail: "You can't update an owner's roles.", + }); + } + } else if (futureRoles.includes(Roles.OWNER)) { + // Can't add an owner role unless you are an owner. + throw new Problem(401, { + detail: "You can't add an owner role.", + }); + } + + if (userRoles.includes(Roles.FORM_DESIGNER)) { + // Can't remove form designer if you are not an owner. + if (!futureRoles.includes(Roles.FORM_DESIGNER)) { + throw new Problem(401, { + detail: "You can't remove a form designer role.", + }); + } + } else if (futureRoles.includes(Roles.FORM_DESIGNER)) { + // Can't add form designer if you are not an owner. + throw new Problem(401, { + detail: "You can't add a form designer role.", + }); + } + } + + next(); + } catch (error) { + next(error); + } }; /** @@ -444,6 +470,7 @@ module.exports = { filterMultipleSubmissions, hasFormPermissions, hasFormRoles, - hasRolePermissions, + hasRoleDeletePermissions, + hasRoleModifyPermissions, hasSubmissionPermissions, }; diff --git a/app/src/forms/rbac/routes.js b/app/src/forms/rbac/routes.js index dfe2d29a1..aacb85618 100644 --- a/app/src/forms/rbac/routes.js +++ b/app/src/forms/rbac/routes.js @@ -4,7 +4,7 @@ const controller = require('./controller'); const jwtService = require('../../components/jwtService'); const P = require('../common/constants').Permissions; const R = require('../common/constants').Roles; -const { currentUser, hasFormPermissions, hasSubmissionPermissions, hasFormRoles, hasRolePermissions } = require('../auth/middleware/userAccess'); +const { currentUser, hasFormPermissions, hasFormRoles, hasRoleDeletePermissions, hasRoleModifyPermissions, hasSubmissionPermissions } = require('../auth/middleware/userAccess'); routes.use(currentUser); @@ -40,11 +40,11 @@ routes.get('/users', jwtService.protect('admin'), async (req, res, next) => { await controller.getUserForms(req, res, next); }); -routes.put('/users', hasFormPermissions([P.TEAM_UPDATE]), hasFormRoles([R.OWNER, R.TEAM_MANAGER]), hasRolePermissions(false), async (req, res, next) => { +routes.put('/users', hasFormPermissions([P.TEAM_UPDATE]), hasFormRoles([R.OWNER, R.TEAM_MANAGER]), hasRoleModifyPermissions, async (req, res, next) => { await controller.setUserForms(req, res, next); }); -routes.delete('/users', hasFormPermissions([P.TEAM_UPDATE]), hasFormRoles([R.OWNER, R.TEAM_MANAGER]), hasRolePermissions(true), async (req, res, next) => { +routes.delete('/users', hasFormPermissions([P.TEAM_UPDATE]), hasFormRoles([R.OWNER, R.TEAM_MANAGER]), hasRoleDeletePermissions, async (req, res, next) => { await controller.removeMultiUsers(req, res, next); }); diff --git a/app/tests/unit/forms/auth/middleware/userAccess.spec.js b/app/tests/unit/forms/auth/middleware/userAccess.spec.js index 07475f67e..ce0db68ec 100644 --- a/app/tests/unit/forms/auth/middleware/userAccess.spec.js +++ b/app/tests/unit/forms/auth/middleware/userAccess.spec.js @@ -5,9 +5,10 @@ const { currentUser, filterMultipleSubmissions, hasFormPermissions, - hasSubmissionPermissions, hasFormRoles, - hasRolePermissions, + hasRoleDeletePermissions, + hasRoleModifyPermissions, + hasSubmissionPermissions, } = require('../../../../../src/forms/auth/middleware/userAccess'); const jwtService = require('../../../../../src/components/jwtService'); @@ -1548,19 +1549,13 @@ describe('hasFormRoles', () => { // - service.getUserForms: gets the forms that the user can access. // - rbacService.readUserRole: gets the roles that user has on a form. // -describe('hasRolePermissions', () => { +describe('hasRoleDeletePermissions', () => { // Default mock value where the user has no access to forms. service.getUserForms = jest.fn().mockReturnValue([]); // Default mock value where the user has no roles. rbacService.readUserRole = jest.fn().mockReturnValue([]); - it('returns a middleware function', async () => { - const middleware = hasRolePermissions(); - - expect(middleware).toBeInstanceOf(Function); - }); - describe('400 response when', () => { const expectedStatus = { status: 400 }; @@ -1575,7 +1570,7 @@ describe('hasRolePermissions', () => { }); const { res, next } = getMockRes(); - await hasRolePermissions()(req, res, next); + await hasRoleDeletePermissions(req, res, next); expect(service.getUserForms).toBeCalledTimes(0); expect(rbacService.readUserRole).toBeCalledTimes(0); @@ -1602,7 +1597,7 @@ describe('hasRolePermissions', () => { }); const { res, next } = getMockRes(); - await hasRolePermissions()(req, res, next); + await hasRoleDeletePermissions(req, res, next); expect(service.getUserForms).toBeCalledTimes(0); expect(rbacService.readUserRole).toBeCalledTimes(0); @@ -1637,7 +1632,7 @@ describe('hasRolePermissions', () => { }); const { res, next } = getMockRes(); - await hasRolePermissions(true)(req, res, next); + await hasRoleDeletePermissions(req, res, next); expect(service.getUserForms).toBeCalledTimes(1); expect(rbacService.readUserRole).toBeCalledTimes(0); @@ -1649,8 +1644,12 @@ describe('hasRolePermissions', () => { }) ); }); + }); - test('updating and user id missing', async () => { + describe('401 response when', () => { + const expectedStatus = { status: 401 }; + + test('removing and owner cannot remove self', async () => { service.getUserForms.mockReturnValueOnce([ { formId: formId, @@ -1658,6 +1657,7 @@ describe('hasRolePermissions', () => { }, ]); const req = getMockReq({ + body: [userId], currentUser: { id: userId, }, @@ -1667,7 +1667,7 @@ describe('hasRolePermissions', () => { }); const { res, next } = getMockRes(); - await hasRolePermissions()(req, res, next); + await hasRoleDeletePermissions(req, res, next); expect(service.getUserForms).toBeCalledTimes(1); expect(rbacService.readUserRole).toBeCalledTimes(0); @@ -1675,55 +1675,53 @@ describe('hasRolePermissions', () => { expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); expect(next).toBeCalledWith( expect.objectContaining({ - detail: 'Bad userId', + detail: "You can't remove yourself from this form.", }) ); }); - test('updating and user id not a uuid', async () => { + test('removing and non-owner cannot remove an owner', async () => { service.getUserForms.mockReturnValueOnce([ { formId: formId, - roles: [Roles.FORM_DESIGNER, Roles.OWNER, Roles.TEAM_MANAGER], + roles: [Roles.TEAM_MANAGER], }, ]); + rbacService.readUserRole.mockReturnValueOnce([{ role: Roles.OWNER }]); const req = getMockReq({ + body: [userId2], currentUser: { id: userId, }, params: { formId: formId, - userId: 'not-a-uuid', }, }); const { res, next } = getMockRes(); - await hasRolePermissions()(req, res, next); + await hasRoleDeletePermissions(req, res, next); expect(service.getUserForms).toBeCalledTimes(1); - expect(rbacService.readUserRole).toBeCalledTimes(0); + expect(rbacService.readUserRole).toBeCalledTimes(1); expect(next).toBeCalledTimes(1); expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); expect(next).toBeCalledWith( expect.objectContaining({ - detail: 'Bad userId', + detail: "You can not update an owner's roles.", }) ); }); - }); - - describe('401 response when', () => { - const expectedStatus = { status: 401 }; - test('removing and owner cannot remove self', async () => { + test('removing and non-owner cannot remove a form designer', async () => { service.getUserForms.mockReturnValueOnce([ { formId: formId, - roles: [Roles.FORM_DESIGNER, Roles.OWNER, Roles.TEAM_MANAGER], + roles: [Roles.TEAM_MANAGER], }, ]); + rbacService.readUserRole.mockReturnValueOnce([{ role: Roles.FORM_DESIGNER }]); const req = getMockReq({ - body: [userId], + body: [userId2], currentUser: { id: userId, }, @@ -1733,27 +1731,29 @@ describe('hasRolePermissions', () => { }); const { res, next } = getMockRes(); - await hasRolePermissions(true)(req, res, next); + await hasRoleDeletePermissions(req, res, next); expect(service.getUserForms).toBeCalledTimes(1); - expect(rbacService.readUserRole).toBeCalledTimes(0); + expect(rbacService.readUserRole).toBeCalledTimes(1); expect(next).toBeCalledTimes(1); expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); expect(next).toBeCalledWith( expect.objectContaining({ - detail: "You can't remove yourself from this form.", + detail: "You can't remove a form designer role.", }) ); }); + }); - test('removing and non-owner cannot remove an owner', async () => { + describe('allows', () => { + test('deleting and non-owner can remove submission reviewer', async () => { service.getUserForms.mockReturnValueOnce([ { formId: formId, roles: [Roles.TEAM_MANAGER], }, ]); - rbacService.readUserRole.mockReturnValueOnce([{ role: Roles.OWNER }]); + rbacService.readUserRole.mockReturnValueOnce([{ role: Roles.SUBMISSION_REVIEWER }]); const req = getMockReq({ body: [userId2], currentUser: { @@ -1765,27 +1765,21 @@ describe('hasRolePermissions', () => { }); const { res, next } = getMockRes(); - await hasRolePermissions(true)(req, res, next); + await hasRoleDeletePermissions(req, res, next); expect(service.getUserForms).toBeCalledTimes(1); expect(rbacService.readUserRole).toBeCalledTimes(1); expect(next).toBeCalledTimes(1); - expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); - expect(next).toBeCalledWith( - expect.objectContaining({ - detail: "You can not update an owner's roles.", - }) - ); + expect(next).toBeCalledWith(); }); - test('removing and non-owner cannot remove a form designer', async () => { + test('deleting and owner can remove an owner', async () => { service.getUserForms.mockReturnValueOnce([ { formId: formId, - roles: [Roles.TEAM_MANAGER], + roles: [Roles.OWNER], }, ]); - rbacService.readUserRole.mockReturnValueOnce([{ role: Roles.FORM_DESIGNER }]); const req = getMockReq({ body: [userId2], currentUser: { @@ -1797,152 +1791,203 @@ describe('hasRolePermissions', () => { }); const { res, next } = getMockRes(); - await hasRolePermissions(true)(req, res, next); + await hasRoleDeletePermissions(req, res, next); expect(service.getUserForms).toBeCalledTimes(1); - expect(rbacService.readUserRole).toBeCalledTimes(1); + expect(rbacService.readUserRole).toBeCalledTimes(0); expect(next).toBeCalledTimes(1); - expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); - expect(next).toBeCalledWith( - expect.objectContaining({ - detail: "You can't remove a form designer role.", - }) - ); + expect(next).toBeCalledWith(); }); - test('updating and non-owner cannot remove own team manager', async () => { + test('deleting and owner can remove a form designer', async () => { service.getUserForms.mockReturnValueOnce([ { formId: formId, - roles: [Roles.TEAM_MANAGER], + roles: [Roles.OWNER], }, ]); - rbacService.readUserRole.mockReturnValueOnce([{ role: Roles.TEAM_MANAGER }]); const req = getMockReq({ - body: [{ role: Roles.SUBMISSION_APPROVER }], + body: [userId2], currentUser: { id: userId, }, params: { formId: formId, - userId: userId, }, }); const { res, next } = getMockRes(); - await hasRolePermissions()(req, res, next); + await hasRoleDeletePermissions(req, res, next); expect(service.getUserForms).toBeCalledTimes(1); - expect(rbacService.readUserRole).toBeCalledTimes(1); + expect(rbacService.readUserRole).toBeCalledTimes(0); expect(next).toBeCalledTimes(1); - expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); - expect(next).toBeCalledWith( - expect.objectContaining({ - detail: "You can't remove your own team manager role.", - }) - ); + expect(next).toBeCalledWith(); }); - test('updating and non-owner cannot update an owner', async () => { + test('deleting and owner can remove a form designer with form id in query', async () => { service.getUserForms.mockReturnValueOnce([ { formId: formId, - roles: [Roles.TEAM_MANAGER], + roles: [Roles.OWNER], }, ]); - rbacService.readUserRole.mockReturnValueOnce([{ role: Roles.OWNER }]); const req = getMockReq({ - body: [{ role: Roles.SUBMISSION_APPROVER }], + body: [userId2], currentUser: { id: userId, }, - params: { + query: { formId: formId, - userId: userId2, }, }); const { res, next } = getMockRes(); - await hasRolePermissions()(req, res, next); + await hasRoleDeletePermissions(req, res, next); expect(service.getUserForms).toBeCalledTimes(1); - expect(rbacService.readUserRole).toBeCalledTimes(1); + expect(rbacService.readUserRole).toBeCalledTimes(0); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(); + }); + }); +}); + +// External dependencies used by the implementation are: +// - service.getUserForms: gets the forms that the user can access. +// - rbacService.readUserRole: gets the roles that user has on a form. +// +describe('hasRoleModifyPermissions', () => { + // Default mock value where the user has no access to forms. + service.getUserForms = jest.fn().mockReturnValue([]); + + // Default mock value where the user has no roles. + rbacService.readUserRole = jest.fn().mockReturnValue([]); + + describe('400 response when', () => { + const expectedStatus = { status: 400 }; + + test('formId missing', async () => { + const req = getMockReq({ + params: { + submissionId: formSubmissionId, + }, + query: { + otherQueryThing: 'SOMETHING', + }, + }); + const { res, next } = getMockRes(); + + await hasRoleModifyPermissions(req, res, next); + + expect(service.getUserForms).toBeCalledTimes(0); + expect(rbacService.readUserRole).toBeCalledTimes(0); expect(next).toBeCalledTimes(1); expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); expect(next).toBeCalledWith( expect.objectContaining({ - detail: "You can't update an owner's roles.", + detail: 'Bad formId', }) ); }); - test('updating and non-owner cannot add an owner', async () => { + test('formId not a uuid', async () => { + const req = getMockReq({ + currentUser: { + id: userId, + }, + params: { + formId: 'not-a-uuid', + }, + query: { + otherQueryThing: 'SOMETHING', + }, + }); + const { res, next } = getMockRes(); + + await hasRoleModifyPermissions(req, res, next); + + expect(service.getUserForms).toBeCalledTimes(0); + expect(rbacService.readUserRole).toBeCalledTimes(0); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); + expect(next).toBeCalledWith( + expect.objectContaining({ + detail: 'Bad formId', + }) + ); + }); + }); + + describe('400 response when', () => { + const expectedStatus = { status: 400 }; + + test('updating and user id missing', async () => { service.getUserForms.mockReturnValueOnce([ { formId: formId, - roles: [Roles.TEAM_MANAGER], + roles: [Roles.FORM_DESIGNER, Roles.OWNER, Roles.TEAM_MANAGER], }, ]); - rbacService.readUserRole.mockReturnValueOnce([{ role: Roles.TEAM_MANAGER }]); const req = getMockReq({ - body: [{ role: Roles.OWNER }], currentUser: { id: userId, }, params: { formId: formId, - userId: userId2, }, }); const { res, next } = getMockRes(); - await hasRolePermissions()(req, res, next); + await hasRoleModifyPermissions(req, res, next); expect(service.getUserForms).toBeCalledTimes(1); - expect(rbacService.readUserRole).toBeCalledTimes(1); + expect(rbacService.readUserRole).toBeCalledTimes(0); expect(next).toBeCalledTimes(1); expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); expect(next).toBeCalledWith( expect.objectContaining({ - detail: "You can't add an owner role.", + detail: 'Bad userId', }) ); }); - test('updating and non-owner cannot remove designer', async () => { + test('updating and user id not a uuid', async () => { service.getUserForms.mockReturnValueOnce([ { formId: formId, - roles: [Roles.TEAM_MANAGER], + roles: [Roles.FORM_DESIGNER, Roles.OWNER, Roles.TEAM_MANAGER], }, ]); - rbacService.readUserRole.mockReturnValueOnce([{ role: Roles.FORM_DESIGNER }]); const req = getMockReq({ - body: [{ role: Roles.SUBMISSION_APPROVER }], currentUser: { id: userId, }, params: { formId: formId, - userId: userId2, + userId: 'not-a-uuid', }, }); const { res, next } = getMockRes(); - await hasRolePermissions()(req, res, next); + await hasRoleModifyPermissions(req, res, next); expect(service.getUserForms).toBeCalledTimes(1); - expect(rbacService.readUserRole).toBeCalledTimes(1); + expect(rbacService.readUserRole).toBeCalledTimes(0); expect(next).toBeCalledTimes(1); expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); expect(next).toBeCalledWith( expect.objectContaining({ - detail: "You can't remove a form designer role.", + detail: 'Bad userId', }) ); }); + }); - test('updating and non-owner cannot add designer', async () => { + describe('401 response when', () => { + const expectedStatus = { status: 401 }; + + test('updating and non-owner cannot remove own team manager', async () => { service.getUserForms.mockReturnValueOnce([ { formId: formId, @@ -1951,18 +1996,18 @@ describe('hasRolePermissions', () => { ]); rbacService.readUserRole.mockReturnValueOnce([{ role: Roles.TEAM_MANAGER }]); const req = getMockReq({ - body: [{ role: Roles.FORM_DESIGNER }], + body: [{ role: Roles.SUBMISSION_APPROVER }], currentUser: { id: userId, }, params: { formId: formId, - userId: userId2, + userId: userId, }, }); const { res, next } = getMockRes(); - await hasRolePermissions()(req, res, next); + await hasRoleModifyPermissions(req, res, next); expect(service.getUserForms).toBeCalledTimes(1); expect(rbacService.readUserRole).toBeCalledTimes(1); @@ -1970,118 +2015,145 @@ describe('hasRolePermissions', () => { expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); expect(next).toBeCalledWith( expect.objectContaining({ - detail: "You can't add a form designer role.", + detail: "You can't remove your own team manager role.", }) ); }); - }); - describe('allows', () => { - test('deleting and non-owner can remove submission reviewer', async () => { + test('updating and non-owner cannot update an owner', async () => { service.getUserForms.mockReturnValueOnce([ { formId: formId, roles: [Roles.TEAM_MANAGER], }, ]); - rbacService.readUserRole.mockReturnValueOnce([{ role: Roles.SUBMISSION_REVIEWER }]); + rbacService.readUserRole.mockReturnValueOnce([{ role: Roles.OWNER }]); const req = getMockReq({ - body: [userId2], + body: [{ role: Roles.SUBMISSION_APPROVER }], currentUser: { id: userId, }, params: { formId: formId, + userId: userId2, }, }); const { res, next } = getMockRes(); - await hasRolePermissions(true)(req, res, next); + await hasRoleModifyPermissions(req, res, next); expect(service.getUserForms).toBeCalledTimes(1); expect(rbacService.readUserRole).toBeCalledTimes(1); expect(next).toBeCalledTimes(1); - expect(next).toBeCalledWith(); + expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); + expect(next).toBeCalledWith( + expect.objectContaining({ + detail: "You can't update an owner's roles.", + }) + ); }); - test('deleting and owner can remove an owner', async () => { + test('updating and non-owner cannot add an owner', async () => { service.getUserForms.mockReturnValueOnce([ { formId: formId, - roles: [Roles.OWNER], + roles: [Roles.TEAM_MANAGER], }, ]); + rbacService.readUserRole.mockReturnValueOnce([{ role: Roles.TEAM_MANAGER }]); const req = getMockReq({ - body: [userId2], + body: [{ role: Roles.OWNER }], currentUser: { id: userId, }, params: { formId: formId, + userId: userId2, }, }); const { res, next } = getMockRes(); - await hasRolePermissions(true)(req, res, next); + await hasRoleModifyPermissions(req, res, next); expect(service.getUserForms).toBeCalledTimes(1); - expect(rbacService.readUserRole).toBeCalledTimes(0); + expect(rbacService.readUserRole).toBeCalledTimes(1); expect(next).toBeCalledTimes(1); - expect(next).toBeCalledWith(); + expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); + expect(next).toBeCalledWith( + expect.objectContaining({ + detail: "You can't add an owner role.", + }) + ); }); - test('deleting and owner can remove a form designer', async () => { + test('updating and non-owner cannot remove designer', async () => { service.getUserForms.mockReturnValueOnce([ { formId: formId, - roles: [Roles.OWNER], + roles: [Roles.TEAM_MANAGER], }, ]); + rbacService.readUserRole.mockReturnValueOnce([{ role: Roles.FORM_DESIGNER }]); const req = getMockReq({ - body: [userId2], + body: [{ role: Roles.SUBMISSION_APPROVER }], currentUser: { id: userId, }, params: { formId: formId, + userId: userId2, }, }); const { res, next } = getMockRes(); - await hasRolePermissions(true)(req, res, next); + await hasRoleModifyPermissions(req, res, next); expect(service.getUserForms).toBeCalledTimes(1); - expect(rbacService.readUserRole).toBeCalledTimes(0); + expect(rbacService.readUserRole).toBeCalledTimes(1); expect(next).toBeCalledTimes(1); - expect(next).toBeCalledWith(); + expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); + expect(next).toBeCalledWith( + expect.objectContaining({ + detail: "You can't remove a form designer role.", + }) + ); }); - test('deleting and owner can remove a form designer with form id in query', async () => { + test('updating and non-owner cannot add designer', async () => { service.getUserForms.mockReturnValueOnce([ { formId: formId, - roles: [Roles.OWNER], + roles: [Roles.TEAM_MANAGER], }, ]); + rbacService.readUserRole.mockReturnValueOnce([{ role: Roles.TEAM_MANAGER }]); const req = getMockReq({ - body: [userId2], + body: [{ role: Roles.FORM_DESIGNER }], currentUser: { id: userId, }, - query: { + params: { formId: formId, + userId: userId2, }, }); const { res, next } = getMockRes(); - await hasRolePermissions(true)(req, res, next); + await hasRoleModifyPermissions(req, res, next); expect(service.getUserForms).toBeCalledTimes(1); - expect(rbacService.readUserRole).toBeCalledTimes(0); + expect(rbacService.readUserRole).toBeCalledTimes(1); expect(next).toBeCalledTimes(1); - expect(next).toBeCalledWith(); + expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); + expect(next).toBeCalledWith( + expect.objectContaining({ + detail: "You can't add a form designer role.", + }) + ); }); + }); + describe('allows', () => { test('updating and non-owner can add approver', async () => { service.getUserForms.mockReturnValueOnce([ { @@ -2102,7 +2174,7 @@ describe('hasRolePermissions', () => { }); const { res, next } = getMockRes(); - await hasRolePermissions()(req, res, next); + await hasRoleModifyPermissions(req, res, next); expect(service.getUserForms).toBeCalledTimes(1); expect(rbacService.readUserRole).toBeCalledTimes(1); @@ -2129,7 +2201,7 @@ describe('hasRolePermissions', () => { }); const { res, next } = getMockRes(); - await hasRolePermissions()(req, res, next); + await hasRoleModifyPermissions(req, res, next); expect(service.getUserForms).toBeCalledTimes(1); expect(rbacService.readUserRole).toBeCalledTimes(0); diff --git a/app/tests/unit/routes/v1/rbac.spec.js b/app/tests/unit/routes/v1/rbac.spec.js index 4744ff848..7046e21ea 100644 --- a/app/tests/unit/routes/v1/rbac.spec.js +++ b/app/tests/unit/routes/v1/rbac.spec.js @@ -31,10 +31,11 @@ userAccess.hasFormRoles = jest.fn(() => { next(); }); }); -userAccess.hasRolePermissions = jest.fn(() => { - return jest.fn((_req, _res, next) => { - next(); - }); +userAccess.hasRoleDeletePermissions = jest.fn((_req, _res, next) => { + next(); +}); +userAccess.hasRoleModifyPermissions = jest.fn((_req, _res, next) => { + next(); }); userAccess.hasSubmissionPermissions = jest.fn(() => { return jest.fn((_req, _res, next) => { From 82180e12c98b4c747f4c0df863c0db716fc83d97 Mon Sep 17 00:00:00 2001 From: Walter Moar Date: Thu, 23 May 2024 08:18:34 -0700 Subject: [PATCH 13/14] fix: FORMS-993 handle trailing comma in get submissions (#1368) --- app/src/forms/form/service.js | 13 ++++++++++--- app/tests/unit/forms/form/service.spec.js | 8 ++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/app/src/forms/form/service.js b/app/src/forms/form/service.js index 74dccbac7..5033711ab 100644 --- a/app/src/forms/form/service.js +++ b/app/src/forms/form/service.js @@ -358,9 +358,11 @@ const service = { .modify('filterVersion', params.version) .modify('filterformSubmissionStatusCode', params.filterformSubmissionStatusCode) .modify('orderDefault', params.sortBy && params.page ? true : false, params); - if (params.createdAt && Array.isArray(params.createdAt) && params.createdAt.length == 2) { + + if (params.createdAt && Array.isArray(params.createdAt) && params.createdAt.length === 2) { query.modify('filterCreatedAt', params.createdAt[0], params.createdAt[1]); } + const selection = ['confirmationId', 'createdAt', 'formId', 'formSubmissionStatusCode', 'submissionId', 'deleted', 'createdBy', 'formVersionId']; if (params.fields && params.fields.length) { @@ -376,8 +378,11 @@ const service = { } else { fields = params.fields.split(',').map((s) => s.trim()); } - // remove updatedAt and updatedBy from custom selected field so they won't be pulled from submission columns - fields = fields.filter((f) => f !== 'updatedAt' && f !== 'updatedBy'); + + // Remove updatedAt and updatedBy so they won't be pulled from submission + // columns. Also remove empty values to handle the case of trailing commas + // and other malformed data too. + fields = fields.filter((f) => f !== 'updatedAt' && f !== 'updatedBy' && f.trim() !== ''); fields.push('lateEntry'); query.select( @@ -390,9 +395,11 @@ const service = { ['lateEntry'].map((f) => ref(`submission:data.${f}`).as(f.split('.').slice(-1))) ); } + if (params.paginationEnabled) { return await service.processPaginationData(query, parseInt(params.page), parseInt(params.itemsPerPage), params.totalSubmissions, params.search, params.searchEnabled); } + return query; }, diff --git a/app/tests/unit/forms/form/service.spec.js b/app/tests/unit/forms/form/service.spec.js index 6c172a0ad..587fa2e7a 100644 --- a/app/tests/unit/forms/form/service.spec.js +++ b/app/tests/unit/forms/form/service.spec.js @@ -446,6 +446,14 @@ describe('_findFileIds', () => { }); }); +describe('listFormSubmissions', () => { + it('should not error if fields has a trailing commma', async () => { + await service.listFormSubmissions(formId, { fields: 'x,' }); + + expect(MockModel.select).toBeCalledTimes(1); + }); +}); + describe('readVersionFields', () => { it('should not return hidden fields', async () => { const schema = { From cc49b3e21c478f019e544c5e39dc8b7b364f6797 Mon Sep 17 00:00:00 2001 From: Walter Moar Date: Thu, 23 May 2024 14:50:21 -0700 Subject: [PATCH 14/14] test: FORMS-1265 consistency and coverage (#1369) --- app/src/forms/auth/middleware/userAccess.js | 4 + app/tests/unit/components/idpService.spec.js | 16 +- app/tests/unit/components/jwtService.spec.js | 30 +- app/tests/unit/forms/admin/controller.spec.js | 6 +- app/tests/unit/forms/auth/authService.spec.js | 8 +- .../forms/auth/middleware/apiAccess.spec.js | 190 +++---- .../forms/auth/middleware/userAccess.spec.js | 77 ++- .../forms/bcgeoaddress/controller.spec.js | 4 +- .../unit/forms/bcgeoaddress/service.spec.js | 4 +- .../common/middleware/rateLimiter.spec.js | 36 +- .../unit/forms/email/emailService.spec.js | 110 ++-- .../file/middleware/filePermissions.spec.js | 509 ++++++++++-------- app/tests/unit/forms/form/controller.spec.js | 54 +- .../unit/forms/form/exportService.spec.js | 46 +- app/tests/unit/forms/form/routes.spec.js | 44 +- app/tests/unit/forms/form/service.spec.js | 46 +- app/tests/unit/forms/rbac/controller.spec.js | 8 +- .../unit/forms/submission/routes.spec.js | 10 +- .../unit/forms/submission/service.spec.js | 50 +- app/tests/unit/forms/user/service.spec.js | 174 +++--- app/tests/unit/routes/v1/admin.spec.js | 2 +- app/tests/unit/routes/v1/form.spec.js | 12 +- app/tests/unit/routes/v1/submission.spec.js | 2 +- 23 files changed, 763 insertions(+), 679 deletions(-) diff --git a/app/src/forms/auth/middleware/userAccess.js b/app/src/forms/auth/middleware/userAccess.js index 2661e3abc..24099068c 100644 --- a/app/src/forms/auth/middleware/userAccess.js +++ b/app/src/forms/auth/middleware/userAccess.js @@ -356,6 +356,10 @@ const hasRoleModifyPermissions = async (req, _res, next) => { if (userRoles.includes(Roles.OWNER)) { // Can't remove a different user's owner role unless you are an owner. + // + // TODO: Remove this if statement and just throw the exception. It's not + // possible for userId === currentUser.id since we're in an if that we + // are !isOwner but also that userRoles.includes(Roles.OWNER). if (userId !== currentUser.id) { throw new Problem(401, { detail: "You can't update an owner's roles.", diff --git a/app/tests/unit/components/idpService.spec.js b/app/tests/unit/components/idpService.spec.js index 724f4fd6a..300d6274b 100644 --- a/app/tests/unit/components/idpService.spec.js +++ b/app/tests/unit/components/idpService.spec.js @@ -136,19 +136,19 @@ describe('idpService', () => { it('should return a user search', async () => { const s = await idpService.userSearch({ idpCode: 'idir', email: 'em@il.com' }); expect(s).toBeFalsy(); - expect(MockModel.query).toHaveBeenCalledTimes(1); - expect(MockModel.modify).toHaveBeenCalledTimes(9); - expect(MockModel.modify).toHaveBeenCalledWith('filterIdpCode', 'idir'); - expect(MockModel.modify).toHaveBeenCalledWith('filterEmail', 'em@il.com', false); + expect(MockModel.query).toBeCalledTimes(1); + expect(MockModel.modify).toBeCalledTimes(9); + expect(MockModel.modify).toBeCalledWith('filterIdpCode', 'idir'); + expect(MockModel.modify).toBeCalledWith('filterEmail', 'em@il.com', false); }); it('should return a customized user search', async () => { const s = await idpService.userSearch({ idpCode: 'bceid-business', email: 'em@il.com' }); expect(s).toBeFalsy(); - expect(MockModel.query).toHaveBeenCalledTimes(1); - expect(MockModel.modify).toHaveBeenCalledWith('filterIdpCode', 'bceid-business'); - expect(MockModel.modify).toHaveBeenCalledWith('filterEmail', 'em@il.com', true); - expect(MockModel.modify).toHaveBeenCalledTimes(9); + expect(MockModel.query).toBeCalledTimes(1); + expect(MockModel.modify).toBeCalledWith('filterIdpCode', 'bceid-business'); + expect(MockModel.modify).toBeCalledWith('filterEmail', 'em@il.com', true); + expect(MockModel.modify).toBeCalledTimes(9); }); it('should throw error when customized user search fails validation', async () => { diff --git a/app/tests/unit/components/jwtService.spec.js b/app/tests/unit/components/jwtService.spec.js index 33b3d9d57..92869fc12 100644 --- a/app/tests/unit/components/jwtService.spec.js +++ b/app/tests/unit/components/jwtService.spec.js @@ -39,8 +39,8 @@ describe('jwtService', () => { const req = getMockReq({ headers: { authorization: 'Bearer JWT' } }); const r = await jwtService.getTokenPayload(req); expect(r).toBe(payload); - expect(jwtService.getBearerToken).toHaveBeenCalledTimes(1); - expect(jwtService._verify).toHaveBeenCalledTimes(1); + expect(jwtService.getBearerToken).toBeCalledTimes(1); + expect(jwtService._verify).toBeCalledTimes(1); }); it('should error if token not valid', async () => { @@ -59,8 +59,8 @@ describe('jwtService', () => { expect(e).toBeInstanceOf(jose.errors.JWTClaimValidationFailed); expect(payload).toBe(undefined); } - expect(jwtService.getBearerToken).toHaveBeenCalledTimes(1); - expect(jwtService._verify).toHaveBeenCalledTimes(1); + expect(jwtService.getBearerToken).toBeCalledTimes(1); + expect(jwtService._verify).toBeCalledTimes(1); }); it('should validate access token on good jwt', async () => { @@ -71,7 +71,7 @@ describe('jwtService', () => { const req = getMockReq({ headers: { authorization: 'Bearer JWT' } }); const r = await jwtService.validateAccessToken(req); expect(r).toBeTruthy(); - expect(jwtService._verify).toHaveBeenCalledTimes(1); + expect(jwtService._verify).toBeCalledTimes(1); }); it('should not validate access token on jwt error', async () => { @@ -84,7 +84,7 @@ describe('jwtService', () => { const r = await jwtService.validateAccessToken(req); expect(r).toBeFalsy(); - expect(jwtService._verify).toHaveBeenCalledTimes(1); + expect(jwtService._verify).toBeCalledTimes(1); }); it('should throw problem when validate access token catches (non-jwt) error)', async () => { @@ -104,7 +104,7 @@ describe('jwtService', () => { expect(e).toBeInstanceOf(Problem); expect(r).toBe(undefined); - expect(jwtService._verify).toHaveBeenCalledTimes(1); + expect(jwtService._verify).toBeCalledTimes(1); }); it('should pass middleware protect with valid jwt)', async () => { @@ -122,8 +122,8 @@ describe('jwtService', () => { const middleware = jwtService.protect(); await middleware(req, res, next); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(); }); it('should fail middleware protect with invalid jwt', async () => { @@ -142,8 +142,8 @@ describe('jwtService', () => { const middleware = jwtService.protect(); await middleware(req, res, next); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(expect.objectContaining({ status: 401 })); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining({ status: 401 })); }); it('should pass middleware protect with valid jwt and role', async () => { @@ -161,8 +161,8 @@ describe('jwtService', () => { const middleware = jwtService.protect('admin'); await middleware(req, res, next); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(); }); it('should fail middleware protect with valid jwt and but no role', async () => { @@ -180,7 +180,7 @@ describe('jwtService', () => { const middleware = jwtService.protect('admin'); await middleware(req, res, next); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(expect.objectContaining({ status: 401 })); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining({ status: 401 })); }); }); diff --git a/app/tests/unit/forms/admin/controller.spec.js b/app/tests/unit/forms/admin/controller.spec.js index d31bbd93c..7174f5b5b 100644 --- a/app/tests/unit/forms/admin/controller.spec.js +++ b/app/tests/unit/forms/admin/controller.spec.js @@ -23,7 +23,7 @@ describe('form controller', () => { service.createFormComponentsProactiveHelp = jest.fn().mockReturnValue(formComponentsProactiveHelp); await controller.createFormComponentsProactiveHelp(req, {}, jest.fn()); - expect(service.createFormComponentsProactiveHelp).toHaveBeenCalledTimes(1); + expect(service.createFormComponentsProactiveHelp).toBeCalledTimes(1); }); it('should update proactive help component publish status', async () => { @@ -43,13 +43,13 @@ describe('form controller', () => { service.updateFormComponentsProactiveHelp = jest.fn().mockReturnValue(formComponentsProactiveHelp); await controller.updateFormComponentsProactiveHelp(req, {}, jest.fn()); - expect(service.updateFormComponentsProactiveHelp).toHaveBeenCalledTimes(1); + expect(service.updateFormComponentsProactiveHelp).toBeCalledTimes(1); }); it('should get list of all proactive help components', async () => { service.listFormComponentsProactiveHelp = jest.fn().mockReturnValue({}); await controller.listFormComponentsProactiveHelp(req, {}, jest.fn()); - expect(service.listFormComponentsProactiveHelp).toHaveBeenCalledTimes(1); + expect(service.listFormComponentsProactiveHelp).toBeCalledTimes(1); }); }); diff --git a/app/tests/unit/forms/auth/authService.spec.js b/app/tests/unit/forms/auth/authService.spec.js index e9d297f79..38a7384dd 100644 --- a/app/tests/unit/forms/auth/authService.spec.js +++ b/app/tests/unit/forms/auth/authService.spec.js @@ -54,10 +54,10 @@ describe('login', () => { service.getUserId = jest.fn().mockReturnValue({ user: 'me' }); const token = 'token'; const result = await service.login(token); - expect(idpService.parseToken).toHaveBeenCalledTimes(1); - expect(idpService.parseToken).toHaveBeenCalledWith(token); - expect(service.getUserId).toHaveBeenCalledTimes(1); - expect(service.getUserId).toHaveBeenCalledWith({ idp: 'fake' }); + expect(idpService.parseToken).toBeCalledTimes(1); + expect(idpService.parseToken).toBeCalledWith(token); + expect(service.getUserId).toBeCalledTimes(1); + expect(service.getUserId).toBeCalledWith({ idp: 'fake' }); expect(result).toBeTruthy(); expect(result).toEqual(resultSample); }); diff --git a/app/tests/unit/forms/auth/middleware/apiAccess.spec.js b/app/tests/unit/forms/auth/middleware/apiAccess.spec.js index dd41280e5..b82ca8b75 100644 --- a/app/tests/unit/forms/auth/middleware/apiAccess.spec.js +++ b/app/tests/unit/forms/auth/middleware/apiAccess.spec.js @@ -37,10 +37,10 @@ describe('apiAccess', () => { await apiAccess(req, res, next); expect(req.apiUser).toBeFalsy(); - expect(mockReadApiKey).toHaveBeenCalledTimes(0); - expect(res.status).not.toHaveBeenCalled(); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(); + expect(mockReadApiKey).toBeCalledTimes(0); + expect(res.status).not.toBeCalled(); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(); }); it('should pass through with bearer authorization', async () => { @@ -50,10 +50,10 @@ describe('apiAccess', () => { await apiAccess(req, res, next); expect(req.apiUser).toBeFalsy(); - expect(mockReadApiKey).toHaveBeenCalledTimes(0); - expect(res.status).not.toHaveBeenCalled(); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(); + expect(mockReadApiKey).toBeCalledTimes(0); + expect(res.status).not.toBeCalled(); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(); }); it('should be unauthorized with no uuid in the params', async () => { @@ -63,10 +63,10 @@ describe('apiAccess', () => { await apiAccess(req, res, next); expect(req.apiUser).toBeFalsy(); - expect(mockReadApiKey).toHaveBeenCalledTimes(0); - expect(res.status).not.toHaveBeenCalled(); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(expect.objectContaining({ status: 401 })); + expect(mockReadApiKey).toBeCalledTimes(0); + expect(res.status).not.toBeCalled(); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining({ status: 401 })); }); }); @@ -81,10 +81,10 @@ describe('apiAccess', () => { await apiAccess(req, res, next); expect(req.apiUser).toBeFalsy(); - expect(mockReadApiKey).toHaveBeenCalledTimes(0); - expect(res.status).not.toHaveBeenCalled(); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(expect.objectContaining({ status: 400 })); + expect(mockReadApiKey).toBeCalledTimes(0); + expect(res.status).not.toBeCalled(); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining({ status: 400 })); }); it('should be unauthorized when db api key result is missing', async () => { @@ -98,10 +98,10 @@ describe('apiAccess', () => { await apiAccess(req, res, next); expect(req.apiUser).toBeFalsy(); - expect(mockReadApiKey).toHaveBeenCalledTimes(1); - expect(res.status).not.toHaveBeenCalled(); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(expect.objectContaining({ status: 401 })); + expect(mockReadApiKey).toBeCalledTimes(1); + expect(res.status).not.toBeCalled(); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining({ status: 401 })); }); it('should be unauthorized when db api key result is empty', async () => { @@ -115,10 +115,10 @@ describe('apiAccess', () => { await apiAccess(req, res, next); expect(req.apiUser).toBeFalsy(); - expect(mockReadApiKey).toHaveBeenCalledTimes(1); - expect(res.status).not.toHaveBeenCalled(); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(expect.objectContaining({ status: 401 })); + expect(mockReadApiKey).toBeCalledTimes(1); + expect(res.status).not.toBeCalled(); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining({ status: 401 })); }); it('should be unauthorized when db api key does not match', async () => { @@ -132,9 +132,9 @@ describe('apiAccess', () => { await apiAccess(req, res, next); expect(req.apiUser).toBeFalsy(); - expect(mockReadApiKey).toHaveBeenCalledTimes(1); - expect(res.status).toHaveBeenCalledWith(401); - expect(next).toHaveBeenCalledTimes(0); + expect(mockReadApiKey).toBeCalledTimes(1); + expect(res.status).toBeCalledWith(401); + expect(next).toBeCalledTimes(0); }); it('should flag apiUser as true with valid form id and credentials', async () => { @@ -148,10 +148,10 @@ describe('apiAccess', () => { await apiAccess(req, res, next); expect(req.apiUser).toBeTruthy(); - expect(mockReadApiKey).toHaveBeenCalledTimes(1); - expect(res.status).not.toHaveBeenCalled(); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(); + expect(mockReadApiKey).toBeCalledTimes(1); + expect(res.status).not.toBeCalled(); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(); }); }); @@ -166,10 +166,10 @@ describe('apiAccess', () => { await apiAccess(req, res, next); expect(req.apiUser).toBeFalsy(); - expect(mockReadApiKey).toHaveBeenCalledTimes(0); - expect(res.status).not.toHaveBeenCalled(); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(expect.objectContaining({ status: 400 })); + expect(mockReadApiKey).toBeCalledTimes(0); + expect(res.status).not.toBeCalled(); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining({ status: 400 })); }); it('should pass exceptions through when form submission does not exist', async () => { @@ -183,10 +183,10 @@ describe('apiAccess', () => { await apiAccess(req, res, next); expect(req.apiUser).toBeFalsy(); - expect(mockReadApiKey).toHaveBeenCalledTimes(0); - expect(res.status).not.toHaveBeenCalled(); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(expect.any(Error)); + expect(mockReadApiKey).toBeCalledTimes(0); + expect(res.status).not.toBeCalled(); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.any(Error)); }); it('should be unauthorized when form submission is empty', async () => { @@ -200,10 +200,10 @@ describe('apiAccess', () => { await apiAccess(req, res, next); expect(req.apiUser).toBeFalsy(); - expect(mockReadApiKey).toHaveBeenCalledTimes(0); - expect(res.status).not.toHaveBeenCalled(); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(expect.objectContaining({ status: 401 })); + expect(mockReadApiKey).toBeCalledTimes(0); + expect(res.status).not.toBeCalled(); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining({ status: 401 })); }); it('should be unauthorized when form submission has no form id', async () => { @@ -217,10 +217,10 @@ describe('apiAccess', () => { await apiAccess(req, res, next); expect(req.apiUser).toBeFalsy(); - expect(mockReadApiKey).toHaveBeenCalledTimes(0); - expect(res.status).not.toHaveBeenCalled(); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(expect.objectContaining({ status: 401 })); + expect(mockReadApiKey).toBeCalledTimes(0); + expect(res.status).not.toBeCalled(); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining({ status: 401 })); }); it('should be unauthorized when db api key does not match', async () => { @@ -235,9 +235,9 @@ describe('apiAccess', () => { await apiAccess(req, res, next); expect(req.apiUser).toBeFalsy(); - expect(mockReadApiKey).toHaveBeenCalledTimes(1); - expect(res.status).toHaveBeenCalledWith(401); - expect(next).toHaveBeenCalledTimes(0); + expect(mockReadApiKey).toBeCalledTimes(1); + expect(res.status).toBeCalledWith(401); + expect(next).toBeCalledTimes(0); }); it('should flag apiUser as true with valid form submission id and credentials', async () => { @@ -252,10 +252,10 @@ describe('apiAccess', () => { await apiAccess(req, res, next); expect(req.apiUser).toBeTruthy(); - expect(mockReadApiKey).toHaveBeenCalledTimes(1); - expect(res.status).not.toHaveBeenCalled(); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(); + expect(mockReadApiKey).toBeCalledTimes(1); + expect(res.status).not.toBeCalled(); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(); }); }); @@ -270,10 +270,10 @@ describe('apiAccess', () => { await apiAccess(req, res, next); expect(req.apiUser).toBeFalsy(); - expect(mockReadApiKey).toHaveBeenCalledTimes(0); - expect(res.status).not.toHaveBeenCalled(); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(expect.objectContaining({ status: 400 })); + expect(mockReadApiKey).toBeCalledTimes(0); + expect(res.status).not.toBeCalled(); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining({ status: 400 })); }); it('should pass exceptions through when file does not exist', async () => { @@ -287,10 +287,10 @@ describe('apiAccess', () => { await apiAccess(req, res, next); expect(req.apiUser).toBeFalsy(); - expect(mockReadApiKey).toHaveBeenCalledTimes(0); - expect(res.status).not.toHaveBeenCalled(); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(expect.any(Error)); + expect(mockReadApiKey).toBeCalledTimes(0); + expect(res.status).not.toBeCalled(); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.any(Error)); }); it('should be unauthorized when file is empty', async () => { @@ -304,10 +304,10 @@ describe('apiAccess', () => { await apiAccess(req, res, next); expect(req.apiUser).toBeFalsy(); - expect(mockReadApiKey).toHaveBeenCalledTimes(0); - expect(res.status).not.toHaveBeenCalled(); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(expect.objectContaining({ status: 500 })); + expect(mockReadApiKey).toBeCalledTimes(0); + expect(res.status).not.toBeCalled(); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining({ status: 500 })); }); it('should be unauthorized when file has no form submission id', async () => { @@ -321,10 +321,10 @@ describe('apiAccess', () => { await apiAccess(req, res, next); expect(req.apiUser).toBeFalsy(); - expect(mockReadApiKey).toHaveBeenCalledTimes(0); - expect(res.status).not.toHaveBeenCalled(); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(expect.objectContaining({ status: 500 })); + expect(mockReadApiKey).toBeCalledTimes(0); + expect(res.status).not.toBeCalled(); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining({ status: 500 })); }); it('should be unauthorized when form submission does not exist', async () => { @@ -339,10 +339,10 @@ describe('apiAccess', () => { await apiAccess(req, res, next); expect(req.apiUser).toBeFalsy(); - expect(mockReadApiKey).toHaveBeenCalledTimes(0); - expect(res.status).not.toHaveBeenCalled(); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(expect.objectContaining({ status: 401 })); + expect(mockReadApiKey).toBeCalledTimes(0); + expect(res.status).not.toBeCalled(); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining({ status: 401 })); }); it('should be unauthorized when form submission is empty', async () => { @@ -357,10 +357,10 @@ describe('apiAccess', () => { await apiAccess(req, res, next); expect(req.apiUser).toBeFalsy(); - expect(mockReadApiKey).toHaveBeenCalledTimes(0); - expect(res.status).not.toHaveBeenCalled(); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(expect.objectContaining({ status: 401 })); + expect(mockReadApiKey).toBeCalledTimes(0); + expect(res.status).not.toBeCalled(); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining({ status: 401 })); }); it('should be unauthorized when form submission has no form id', async () => { @@ -375,10 +375,10 @@ describe('apiAccess', () => { await apiAccess(req, res, next); expect(req.apiUser).toBeFalsy(); - expect(mockReadApiKey).toHaveBeenCalledTimes(0); - expect(res.status).not.toHaveBeenCalled(); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(expect.objectContaining({ status: 401 })); + expect(mockReadApiKey).toBeCalledTimes(0); + expect(res.status).not.toBeCalled(); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining({ status: 401 })); }); it('should be unauthorized when db api key does not match', async () => { @@ -394,9 +394,9 @@ describe('apiAccess', () => { await apiAccess(req, res, next); expect(req.apiUser).toBeFalsy(); - expect(mockReadApiKey).toHaveBeenCalledTimes(1); - expect(res.status).toHaveBeenCalledWith(401); - expect(next).toHaveBeenCalledTimes(0); + expect(mockReadApiKey).toBeCalledTimes(1); + expect(res.status).toBeCalledWith(401); + expect(next).toBeCalledTimes(0); }); it('should flag apiUser as true with valid file id and credentials', async () => { @@ -412,10 +412,10 @@ describe('apiAccess', () => { await apiAccess(req, res, next); expect(req.apiUser).toBeTruthy(); - expect(mockReadApiKey).toHaveBeenCalledTimes(1); - expect(res.status).not.toHaveBeenCalled(); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(); + expect(mockReadApiKey).toBeCalledTimes(1); + expect(res.status).not.toBeCalled(); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(); }); it('should be forbidden if filesApiAccess is false', async () => { @@ -430,11 +430,11 @@ describe('apiAccess', () => { await apiAccess(req, res, next); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(expect.objectContaining({ status: 403 })); - expect(res.status).not.toHaveBeenCalled(); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining({ status: 403 })); + expect(res.status).not.toBeCalled(); expect(req.apiUser).toBeUndefined(); - expect(mockReadApiKey).toHaveBeenCalledTimes(1); + expect(mockReadApiKey).toBeCalledTimes(1); }); it('should allow access to files if filesAPIAccess is true', async () => { @@ -447,8 +447,8 @@ describe('apiAccess', () => { await apiAccess(req, res, next); - expect(next).toHaveBeenCalledTimes(1); - expect(mockReadApiKey).toHaveBeenCalledTimes(1); + expect(next).toBeCalledTimes(1); + expect(mockReadApiKey).toBeCalledTimes(1); expect(req.apiUser).toBeTruthy(); }); }); diff --git a/app/tests/unit/forms/auth/middleware/userAccess.spec.js b/app/tests/unit/forms/auth/middleware/userAccess.spec.js index ce0db68ec..efa1c5189 100644 --- a/app/tests/unit/forms/auth/middleware/userAccess.spec.js +++ b/app/tests/unit/forms/auth/middleware/userAccess.spec.js @@ -1614,7 +1614,7 @@ describe('hasRoleDeletePermissions', () => { describe('400 response when', () => { const expectedStatus = { status: 400 }; - test('removing and user id not a uuid', async () => { + test('user id not a uuid', async () => { service.getUserForms.mockReturnValueOnce([ { formId: formId, @@ -1649,7 +1649,7 @@ describe('hasRoleDeletePermissions', () => { describe('401 response when', () => { const expectedStatus = { status: 401 }; - test('removing and owner cannot remove self', async () => { + test('owner cannot remove self', async () => { service.getUserForms.mockReturnValueOnce([ { formId: formId, @@ -1680,7 +1680,7 @@ describe('hasRoleDeletePermissions', () => { ); }); - test('removing and non-owner cannot remove an owner', async () => { + test('non-owner cannot remove an owner', async () => { service.getUserForms.mockReturnValueOnce([ { formId: formId, @@ -1712,7 +1712,7 @@ describe('hasRoleDeletePermissions', () => { ); }); - test('removing and non-owner cannot remove a form designer', async () => { + test('non-owner cannot remove a form designer', async () => { service.getUserForms.mockReturnValueOnce([ { formId: formId, @@ -1746,7 +1746,7 @@ describe('hasRoleDeletePermissions', () => { }); describe('allows', () => { - test('deleting and non-owner can remove submission reviewer', async () => { + test('non-owner can remove submission reviewer', async () => { service.getUserForms.mockReturnValueOnce([ { formId: formId, @@ -1773,7 +1773,7 @@ describe('hasRoleDeletePermissions', () => { expect(next).toBeCalledWith(); }); - test('deleting and owner can remove an owner', async () => { + test('owner can remove an owner', async () => { service.getUserForms.mockReturnValueOnce([ { formId: formId, @@ -1799,7 +1799,7 @@ describe('hasRoleDeletePermissions', () => { expect(next).toBeCalledWith(); }); - test('deleting and owner can remove a form designer', async () => { + test('owner can remove a form designer', async () => { service.getUserForms.mockReturnValueOnce([ { formId: formId, @@ -1825,7 +1825,7 @@ describe('hasRoleDeletePermissions', () => { expect(next).toBeCalledWith(); }); - test('deleting and owner can remove a form designer with form id in query', async () => { + test('owner can remove a form designer with form id in query', async () => { service.getUserForms.mockReturnValueOnce([ { formId: formId, @@ -1922,7 +1922,7 @@ describe('hasRoleModifyPermissions', () => { describe('400 response when', () => { const expectedStatus = { status: 400 }; - test('updating and user id missing', async () => { + test('user id missing', async () => { service.getUserForms.mockReturnValueOnce([ { formId: formId, @@ -1952,7 +1952,7 @@ describe('hasRoleModifyPermissions', () => { ); }); - test('updating and user id not a uuid', async () => { + test('user id not a uuid', async () => { service.getUserForms.mockReturnValueOnce([ { formId: formId, @@ -1987,7 +1987,7 @@ describe('hasRoleModifyPermissions', () => { describe('401 response when', () => { const expectedStatus = { status: 401 }; - test('updating and non-owner cannot remove own team manager', async () => { + test('non-owner cannot remove own team manager', async () => { service.getUserForms.mockReturnValueOnce([ { formId: formId, @@ -2020,7 +2020,7 @@ describe('hasRoleModifyPermissions', () => { ); }); - test('updating and non-owner cannot update an owner', async () => { + test('non-owner cannot update an owner', async () => { service.getUserForms.mockReturnValueOnce([ { formId: formId, @@ -2053,7 +2053,7 @@ describe('hasRoleModifyPermissions', () => { ); }); - test('updating and non-owner cannot add an owner', async () => { + test('non-owner cannot add an owner', async () => { service.getUserForms.mockReturnValueOnce([ { formId: formId, @@ -2086,7 +2086,7 @@ describe('hasRoleModifyPermissions', () => { ); }); - test('updating and non-owner cannot remove designer', async () => { + test('non-owner cannot remove designer', async () => { service.getUserForms.mockReturnValueOnce([ { formId: formId, @@ -2119,7 +2119,7 @@ describe('hasRoleModifyPermissions', () => { ); }); - test('updating and non-owner cannot add designer', async () => { + test('non-owner cannot add designer', async () => { service.getUserForms.mockReturnValueOnce([ { formId: formId, @@ -2154,16 +2154,55 @@ describe('hasRoleModifyPermissions', () => { }); describe('allows', () => { - test('updating and non-owner can add approver', async () => { + test('non-owner can add approver', async () => { service.getUserForms.mockReturnValueOnce([ { formId: formId, roles: [Roles.TEAM_MANAGER], }, ]); - rbacService.readUserRole.mockReturnValueOnce([{ role: Roles.FORM_SUBMITTER }]); + rbacService.readUserRole.mockReturnValueOnce([]); const req = getMockReq({ - body: [{ role: Roles.SUBMISSION_APPROVER }], + body: [ + { + role: Roles.SUBMISSION_APPROVER, + }, + ], + currentUser: { + id: userId, + }, + params: { + formId: formId, + userId: userId2, + }, + }); + const { res, next } = getMockRes(); + + await hasRoleModifyPermissions(req, res, next); + + expect(service.getUserForms).toBeCalledTimes(1); + expect(rbacService.readUserRole).toBeCalledTimes(1); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(); + }); + + test('non-owner can add approver to designer', async () => { + service.getUserForms.mockReturnValueOnce([ + { + formId: formId, + roles: [Roles.TEAM_MANAGER], + }, + ]); + rbacService.readUserRole.mockReturnValueOnce([{ role: Roles.FORM_DESIGNER }]); + const req = getMockReq({ + body: [ + { + role: Roles.FORM_DESIGNER, + }, + { + role: Roles.SUBMISSION_APPROVER, + }, + ], currentUser: { id: userId, }, @@ -2182,7 +2221,7 @@ describe('hasRoleModifyPermissions', () => { expect(next).toBeCalledWith(); }); - test('updating and owner can add owner', async () => { + test('owner can add owner', async () => { service.getUserForms.mockReturnValueOnce([ { formId: formId, diff --git a/app/tests/unit/forms/bcgeoaddress/controller.spec.js b/app/tests/unit/forms/bcgeoaddress/controller.spec.js index 3ec7add1c..e511041fd 100644 --- a/app/tests/unit/forms/bcgeoaddress/controller.spec.js +++ b/app/tests/unit/forms/bcgeoaddress/controller.spec.js @@ -18,7 +18,7 @@ describe('search BCGEO Address', () => { ], }); await controller.searchBCGeoAddress(req, res, next); - expect(service.searchBCGeoAddress).toHaveBeenCalledTimes(1); + expect(service.searchBCGeoAddress).toBeCalledTimes(1); }); }); describe('search BCGEO Address', () => { @@ -37,6 +37,6 @@ describe('search BCGEO Address', () => { ], }); await controller.searchBCGeoAddress(req, res, next); - expect(service.searchBCGeoAddress).toHaveBeenCalledTimes(1); + expect(service.searchBCGeoAddress).toBeCalledTimes(1); }); }); diff --git a/app/tests/unit/forms/bcgeoaddress/service.spec.js b/app/tests/unit/forms/bcgeoaddress/service.spec.js index 32e368b95..33566f396 100644 --- a/app/tests/unit/forms/bcgeoaddress/service.spec.js +++ b/app/tests/unit/forms/bcgeoaddress/service.spec.js @@ -18,7 +18,7 @@ describe('searchBCGeoAddress', () => { const query = { brief: true, autocomplete: true, matchAccuracy: 100, addressString: 25, url: 'test Url' }; await service.searchBCGeoAddress(query).then(() => { - expect(geoAddressService.addressQuerySearch).toHaveBeenCalledTimes(1); + expect(geoAddressService.addressQuerySearch).toBeCalledTimes(1); }); }); @@ -41,7 +41,7 @@ describe('searchBCGeoAddress', () => { const result = await service.advanceSearchBCGeoAddress(query); - expect(geoAddressService.addressQuerySearch).toHaveBeenCalledTimes(1); + expect(geoAddressService.addressQuerySearch).toBeCalledTimes(1); expect(result).toEqual(response); }); diff --git a/app/tests/unit/forms/common/middleware/rateLimiter.spec.js b/app/tests/unit/forms/common/middleware/rateLimiter.spec.js index 001daebfc..8268c5fd8 100644 --- a/app/tests/unit/forms/common/middleware/rateLimiter.spec.js +++ b/app/tests/unit/forms/common/middleware/rateLimiter.spec.js @@ -52,12 +52,12 @@ describe('apiKeyRateLimiter', () => { await apiKeyRateLimiter(req, res, next); - expect(res.setHeader).toHaveBeenCalledTimes(2); + expect(res.setHeader).toBeCalledTimes(2); // These also test that the rate limiter uses our custom config values. expect(res.setHeader).toHaveBeenNthCalledWith(1, rateLimitPolicyName, rateLimitPolicyValue); expect(res.setHeader).toHaveBeenNthCalledWith(2, rateLimitName, rateLimitValue); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(); }); describe('skips rate limiting for', () => { @@ -70,9 +70,9 @@ describe('apiKeyRateLimiter', () => { await apiKeyRateLimiter(req, res, next); - expect(res.setHeader).toHaveBeenCalledTimes(0); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(); + expect(res.setHeader).toBeCalledTimes(0); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(); }); test('no authorization header', async () => { @@ -85,9 +85,9 @@ describe('apiKeyRateLimiter', () => { await apiKeyRateLimiter(req, res, next); - expect(res.setHeader).toHaveBeenCalledTimes(0); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(); + expect(res.setHeader).toBeCalledTimes(0); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(); }); test('empty authorization header', async () => { @@ -102,9 +102,9 @@ describe('apiKeyRateLimiter', () => { await apiKeyRateLimiter(req, res, next); - expect(res.setHeader).toHaveBeenCalledTimes(0); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(); + expect(res.setHeader).toBeCalledTimes(0); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(); }); test('unexpected authorization type', async () => { @@ -119,9 +119,9 @@ describe('apiKeyRateLimiter', () => { await apiKeyRateLimiter(req, res, next); - expect(res.setHeader).toHaveBeenCalledTimes(0); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(); + expect(res.setHeader).toBeCalledTimes(0); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(); }); test('bearer auth', async () => { @@ -136,9 +136,9 @@ describe('apiKeyRateLimiter', () => { await apiKeyRateLimiter(req, res, next); - expect(res.setHeader).toHaveBeenCalledTimes(0); - expect(next).toHaveBeenCalledTimes(1); - expect(next).toHaveBeenCalledWith(); + expect(res.setHeader).toBeCalledTimes(0); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(); }); }); }); diff --git a/app/tests/unit/forms/email/emailService.spec.js b/app/tests/unit/forms/email/emailService.spec.js index 8ca79e218..26674adbc 100644 --- a/app/tests/unit/forms/email/emailService.spec.js +++ b/app/tests/unit/forms/email/emailService.spec.js @@ -48,7 +48,7 @@ describe('_sendEmailTemplate', () => { const result = emailService._sendEmailTemplate('sendStatusAssigned', configData, submission, referer); expect(result).toBeTruthy(); - expect(chesService.merge).toHaveBeenCalledTimes(1); + expect(chesService.merge).toBeCalledTimes(1); }); it('should call chesService to send an email with type sendSubmissionConfirmation', () => { @@ -58,7 +58,7 @@ describe('_sendEmailTemplate', () => { const result = emailService._sendEmailTemplate('sendSubmissionConfirmation', configData, submission, referer); expect(result).toBeTruthy(); - expect(chesService.merge).toHaveBeenCalledTimes(1); + expect(chesService.merge).toBeCalledTimes(1); }); it('should call chesService to send an email with type sendSubmissionReceived', () => { @@ -68,7 +68,7 @@ describe('_sendEmailTemplate', () => { const result = emailService._sendEmailTemplate('sendSubmissionReceived', configData, submission, referer); expect(result).toBeTruthy(); - expect(chesService.merge).toHaveBeenCalledTimes(1); + expect(chesService.merge).toBeCalledTimes(1); }); }); @@ -149,8 +149,8 @@ describe('public methods', () => { ]; expect(result).toEqual('ret'); - expect(emailService._sendEmailTemplate).toHaveBeenCalledTimes(1); - expect(emailService._sendEmailTemplate).toHaveBeenCalledWith(configData, contexts); + expect(emailService._sendEmailTemplate).toBeCalledTimes(1); + expect(emailService._sendEmailTemplate).toBeCalledWith(configData, contexts); }); it('statusRevising should send a status email', async () => { @@ -183,8 +183,8 @@ describe('public methods', () => { ]; expect(result).toEqual('ret'); - expect(emailService._sendEmailTemplate).toHaveBeenCalledTimes(1); - expect(emailService._sendEmailTemplate).toHaveBeenCalledWith(configData, contexts); + expect(emailService._sendEmailTemplate).toBeCalledTimes(1); + expect(emailService._sendEmailTemplate).toBeCalledWith(configData, contexts); }); it('statusCompleted should send a status email', async () => { @@ -217,8 +217,8 @@ describe('public methods', () => { ]; expect(result).toEqual('ret'); - expect(emailService._sendEmailTemplate).toHaveBeenCalledTimes(1); - expect(emailService._sendEmailTemplate).toHaveBeenCalledWith(configData, contexts); + expect(emailService._sendEmailTemplate).toBeCalledTimes(1); + expect(emailService._sendEmailTemplate).toBeCalledWith(configData, contexts); }); it('submissionConfirmation should send login email for idir', async () => { @@ -258,12 +258,12 @@ describe('public methods', () => { ]; expect(result).toEqual('ret'); - expect(formService.readForm).toHaveBeenCalledTimes(1); - expect(formService.readForm).toHaveBeenCalledWith(form_idir.id); - expect(formService.readSubmission).toHaveBeenCalledTimes(1); - expect(formService.readSubmission).toHaveBeenCalledWith(submission.id); - expect(emailService._sendEmailTemplate).toHaveBeenCalledTimes(1); - expect(emailService._sendEmailTemplate).toHaveBeenCalledWith(configData, contexts); + expect(formService.readForm).toBeCalledTimes(1); + expect(formService.readForm).toBeCalledWith(form_idir.id); + expect(formService.readSubmission).toBeCalledTimes(1); + expect(formService.readSubmission).toBeCalledWith(submission.id); + expect(emailService._sendEmailTemplate).toBeCalledTimes(1); + expect(emailService._sendEmailTemplate).toBeCalledWith(configData, contexts); }); it('submissionConfirmation should send a low priority email', async () => { @@ -303,12 +303,12 @@ describe('public methods', () => { ]; expect(result).toEqual('ret'); - expect(formService.readForm).toHaveBeenCalledTimes(1); - expect(formService.readForm).toHaveBeenCalledWith(form.id); - expect(formService.readSubmission).toHaveBeenCalledTimes(1); - expect(formService.readSubmission).toHaveBeenCalledWith(submission.id); - expect(emailService._sendEmailTemplate).toHaveBeenCalledTimes(1); - expect(emailService._sendEmailTemplate).toHaveBeenCalledWith(configData, contexts); + expect(formService.readForm).toBeCalledTimes(1); + expect(formService.readForm).toBeCalledWith(form.id); + expect(formService.readSubmission).toBeCalledTimes(1); + expect(formService.readSubmission).toBeCalledWith(submission.id); + expect(emailService._sendEmailTemplate).toBeCalledTimes(1); + expect(emailService._sendEmailTemplate).toBeCalledWith(configData, contexts); }); it('submissionConfirmation should send a normal priority email', async () => { @@ -348,12 +348,12 @@ describe('public methods', () => { ]; expect(result).toEqual('ret'); - expect(formService.readForm).toHaveBeenCalledTimes(1); - expect(formService.readForm).toHaveBeenCalledWith(form.id); - expect(formService.readSubmission).toHaveBeenCalledTimes(1); - expect(formService.readSubmission).toHaveBeenCalledWith(submission.id); - expect(emailService._sendEmailTemplate).toHaveBeenCalledTimes(1); - expect(emailService._sendEmailTemplate).toHaveBeenCalledWith(configData, contexts); + expect(formService.readForm).toBeCalledTimes(1); + expect(formService.readForm).toBeCalledWith(form.id); + expect(formService.readSubmission).toBeCalledTimes(1); + expect(formService.readSubmission).toBeCalledWith(submission.id); + expect(emailService._sendEmailTemplate).toBeCalledTimes(1); + expect(emailService._sendEmailTemplate).toBeCalledWith(configData, contexts); }); it('submissionConfirmation should send a high priority email', async () => { @@ -393,12 +393,12 @@ describe('public methods', () => { ]; expect(result).toEqual('ret'); - expect(formService.readForm).toHaveBeenCalledTimes(1); - expect(formService.readForm).toHaveBeenCalledWith(form.id); - expect(formService.readSubmission).toHaveBeenCalledTimes(1); - expect(formService.readSubmission).toHaveBeenCalledWith(submission.id); - expect(emailService._sendEmailTemplate).toHaveBeenCalledTimes(1); - expect(emailService._sendEmailTemplate).toHaveBeenCalledWith(configData, contexts); + expect(formService.readForm).toBeCalledTimes(1); + expect(formService.readForm).toBeCalledWith(form.id); + expect(formService.readSubmission).toBeCalledTimes(1); + expect(formService.readSubmission).toBeCalledWith(submission.id); + expect(emailService._sendEmailTemplate).toBeCalledTimes(1); + expect(emailService._sendEmailTemplate).toBeCalledWith(configData, contexts); }); it('submissionConfirmation should send an email without submission fields', async () => { @@ -438,12 +438,12 @@ describe('public methods', () => { ]; expect(result).toEqual('ret'); - expect(formService.readForm).toHaveBeenCalledTimes(1); - expect(formService.readForm).toHaveBeenCalledWith(form.id); - expect(formService.readSubmission).toHaveBeenCalledTimes(1); - expect(formService.readSubmission).toHaveBeenCalledWith(submission.id); - expect(emailService._sendEmailTemplate).toHaveBeenCalledTimes(1); - expect(emailService._sendEmailTemplate).toHaveBeenCalledWith(configData, contexts); + expect(formService.readForm).toBeCalledTimes(1); + expect(formService.readForm).toBeCalledWith(form.id); + expect(formService.readSubmission).toBeCalledTimes(1); + expect(formService.readSubmission).toBeCalledWith(submission.id); + expect(emailService._sendEmailTemplate).toBeCalledTimes(1); + expect(emailService._sendEmailTemplate).toBeCalledWith(configData, contexts); }); it('submissionConfirmation should produce errors on failure', async () => { @@ -488,13 +488,13 @@ describe('public methods', () => { ]; expect(result).toEqual('ret'); - expect(formService.readForm).toHaveBeenCalledTimes(1); - expect(formService.readForm).toHaveBeenCalledWith(form.id); - expect(formService.readSubmission).toHaveBeenCalledTimes(1); - expect(formService.readSubmission).toHaveBeenCalledWith(submission.id); - expect(emailService._sendEmailTemplate).toHaveBeenCalledTimes(1); + expect(formService.readForm).toBeCalledTimes(1); + expect(formService.readForm).toBeCalledWith(form.id); + expect(formService.readSubmission).toBeCalledTimes(1); + expect(formService.readSubmission).toBeCalledWith(submission.id); + expect(emailService._sendEmailTemplate).toBeCalledTimes(1); expect(form.submissionReceivedEmails).toBeInstanceOf(Array); - expect(emailService._sendEmailTemplate).toHaveBeenCalledWith(configData, contexts); + expect(emailService._sendEmailTemplate).toBeCalledWith(configData, contexts); }); it('submissionUnassigned should send a uninvited email', async () => { @@ -529,10 +529,10 @@ describe('public methods', () => { ]; expect(result).toEqual('ret'); - expect(formService.readForm).toHaveBeenCalledTimes(1); - expect(formService.readForm).toHaveBeenCalledWith(form.id); - expect(emailService._sendEmailTemplate).toHaveBeenCalledTimes(1); - expect(emailService._sendEmailTemplate).toHaveBeenCalledWith(configData, contexts); + expect(formService.readForm).toBeCalledTimes(1); + expect(formService.readForm).toBeCalledWith(form.id); + expect(emailService._sendEmailTemplate).toBeCalledTimes(1); + expect(emailService._sendEmailTemplate).toBeCalledWith(configData, contexts); }); it('submissionAssigned should send a uninvited email', async () => { @@ -567,11 +567,11 @@ describe('public methods', () => { ]; expect(result).toEqual('ret'); - expect(formService.readForm).toHaveBeenCalledTimes(1); - expect(formService.readForm).toHaveBeenCalledWith(form.id); - expect(formService.readSubmission).toHaveBeenCalledTimes(1); - expect(formService.readSubmission).toHaveBeenCalledWith(currentStatus.formSubmissionId); - expect(emailService._sendEmailTemplate).toHaveBeenCalledTimes(1); - expect(emailService._sendEmailTemplate).toHaveBeenCalledWith(configData, contexts); + expect(formService.readForm).toBeCalledTimes(1); + expect(formService.readForm).toBeCalledWith(form.id); + expect(formService.readSubmission).toBeCalledTimes(1); + expect(formService.readSubmission).toBeCalledWith(currentStatus.formSubmissionId); + expect(emailService._sendEmailTemplate).toBeCalledTimes(1); + expect(emailService._sendEmailTemplate).toBeCalledWith(configData, contexts); }); }); diff --git a/app/tests/unit/forms/file/middleware/filePermissions.spec.js b/app/tests/unit/forms/file/middleware/filePermissions.spec.js index e74000b5a..f8b003a4c 100644 --- a/app/tests/unit/forms/file/middleware/filePermissions.spec.js +++ b/app/tests/unit/forms/file/middleware/filePermissions.spec.js @@ -1,18 +1,20 @@ -const Problem = require('api-problem'); +const { getMockReq, getMockRes } = require('@jest-mock/express'); +const uuid = require('uuid'); const { currentFileRecord, hasFileCreate, hasFilePermissions } = require('../../../../../src/forms/file/middleware/filePermissions'); const service = require('../../../../../src/forms/file/service'); const userAccess = require('../../../../../src/forms/auth/middleware/userAccess'); -const testRes = { - writeHead: jest.fn(), - end: jest.fn(), -}; -const zeroUuid = '00000000-0000-0000-0000-000000000000'; -const oneUuid = '11111111-1111-1111-1111-111111111111'; +const fileId = uuid.v4(); +const formSubmissionId = uuid.v4(); +const idpUserId = uuid.v4(); const bearerToken = Math.random().toString(36).substring(2); +const currentUserIdp = { + idpUserId: idpUserId, +}; + describe('currentFileRecord', () => { const readFileSpy = jest.spyOn(service, 'read'); @@ -20,276 +22,315 @@ describe('currentFileRecord', () => { readFileSpy.mockReset(); }); - it('403s if there is no current user on the request scope', async () => { - const testReq = { - params: { - id: zeroUuid, - }, - }; - - const nxt = jest.fn(); - - await currentFileRecord(testReq, testRes, nxt); - expect(testReq.currentFileRecord).toEqual(undefined); - expect(nxt).toHaveBeenCalledTimes(1); - expect(nxt).toHaveBeenCalledWith(new Problem(403, { detail: 'File access to this ID is unauthorized.' })); - expect(readFileSpy).toHaveBeenCalledTimes(0); - }); - - it('403s if there is no file ID on the request scope', async () => { - const testReq = { - params: {}, - currentUser: { - idpUserId: zeroUuid, - username: 'jsmith@idir', - }, - }; - - const nxt = jest.fn(); - - await currentFileRecord(testReq, testRes, nxt); - expect(testReq.currentFileRecord).toEqual(undefined); - expect(nxt).toHaveBeenCalledTimes(1); - expect(nxt).toHaveBeenCalledWith(new Problem(403, { detail: 'File access to this ID is unauthorized.' })); - expect(readFileSpy).toHaveBeenCalledTimes(0); - }); - - it('403s if there is no file record to be found', async () => { - const testReq = { - params: { id: zeroUuid }, - currentUser: { - idpUserId: oneUuid, - username: 'jsmith@idir', - }, - }; - - const nxt = jest.fn(); - readFileSpy.mockImplementation(() => { - return undefined; + describe('403 response when', () => { + const expectedStatus = { status: 403 }; + + test('there is no current user on the request', async () => { + const req = getMockReq({ + params: { + id: fileId, + }, + }); + const { res, next } = getMockRes(); + + await currentFileRecord(req, res, next); + + expect(readFileSpy).toBeCalledTimes(0); + expect(req.currentFileRecord).toEqual(undefined); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); + expect(next).toBeCalledWith( + expect.objectContaining({ + detail: 'File access to this ID is unauthorized.', + }) + ); }); - await currentFileRecord(testReq, testRes, nxt); - expect(testReq.currentFileRecord).toEqual(undefined); - expect(readFileSpy).toHaveBeenCalledTimes(1); - expect(readFileSpy).toHaveBeenCalledWith(zeroUuid); - expect(nxt).toHaveBeenCalledTimes(1); - expect(nxt).toHaveBeenCalledWith(new Problem(403, { detail: 'File access to this ID is unauthorized.' })); - }); - - it('403s if an exception occurs from the file lookup', async () => { - const testReq = { - params: { id: zeroUuid }, - currentUser: { - idpUserId: oneUuid, - username: 'jsmith@idir', - }, - }; - - const nxt = jest.fn(); - readFileSpy.mockImplementation(() => { - throw new Error(); + test('there is no file id on the request', async () => { + const req = getMockReq({ + currentUser: currentUserIdp, + params: {}, + }); + const { res, next } = getMockRes(); + + await currentFileRecord(req, res, next); + + expect(readFileSpy).toBeCalledTimes(0); + expect(req.currentFileRecord).toEqual(undefined); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); + expect(next).toBeCalledWith( + expect.objectContaining({ + detail: 'File access to this ID is unauthorized.', + }) + ); }); - await currentFileRecord(testReq, testRes, nxt); - expect(testReq.currentFileRecord).toEqual(undefined); - expect(readFileSpy).toHaveBeenCalledTimes(1); - expect(readFileSpy).toHaveBeenCalledWith(zeroUuid); - expect(nxt).toHaveBeenCalledTimes(1); - expect(nxt).toHaveBeenCalledWith(new Problem(403, { detail: 'File access to this ID is unauthorized.' })); - }); - - it('403s if an exception occurs from the file lookup', async () => { - const testReq = { - params: { id: zeroUuid }, - currentUser: { - idpUserId: oneUuid, - username: 'jsmith@idir', - }, - }; - const testRecord = { - name: 'test', - }; - - const nxt = jest.fn(); - readFileSpy.mockImplementation(() => { - return testRecord; + test('there is no file record to be found', async () => { + readFileSpy.mockImplementation(() => { + return undefined; + }); + const req = getMockReq({ + currentUser: currentUserIdp, + params: { + id: fileId, + }, + }); + const { res, next } = getMockRes(); + + await currentFileRecord(req, res, next); + + expect(readFileSpy).toBeCalledTimes(1); + expect(readFileSpy).toBeCalledWith(fileId); + expect(req.currentFileRecord).toEqual(undefined); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); + expect(next).toBeCalledWith( + expect.objectContaining({ + detail: 'File access to this ID is unauthorized.', + }) + ); }); - await currentFileRecord(testReq, testRes, nxt); - expect(readFileSpy).toHaveBeenCalledTimes(1); - expect(readFileSpy).toHaveBeenCalledWith(zeroUuid); - expect(nxt).toHaveBeenCalledTimes(1); - expect(nxt).toHaveBeenCalledWith(); - expect(testReq.currentFileRecord).toEqual(testRecord); + test('service.read throws an error', async () => { + readFileSpy.mockImplementation(() => { + throw new Error(); + }); + const req = getMockReq({ + currentUser: currentUserIdp, + params: { + id: fileId, + }, + }); + const { res, next } = getMockRes(); + + await currentFileRecord(req, res, next); + + expect(readFileSpy).toBeCalledTimes(1); + expect(readFileSpy).toBeCalledWith(fileId); + expect(req.currentFileRecord).toEqual(undefined); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); + expect(next).toBeCalledWith( + expect.objectContaining({ + detail: 'File access to this ID is unauthorized.', + }) + ); + }); }); - it('retrieves file record successfully for an API user', async () => { - const testReq = { - params: { id: zeroUuid }, - apiUser: true, - }; - + describe('success when', () => { const testRecord = { name: 'test', }; - const nxt = jest.fn(); - readFileSpy.mockImplementation(() => { - return testRecord; + test('an idp user on the request', async () => { + readFileSpy.mockImplementation(() => { + return testRecord; + }); + const req = getMockReq({ + currentUser: currentUserIdp, + params: { id: fileId }, + }); + const { res, next } = getMockRes(); + + await currentFileRecord(req, res, next); + + expect(readFileSpy).toBeCalledTimes(1); + expect(readFileSpy).toBeCalledWith(fileId); + expect(req.currentFileRecord).toEqual(testRecord); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(); }); - await currentFileRecord(testReq, testRes, nxt); - expect(readFileSpy).toHaveBeenCalledWith(zeroUuid); - expect(testReq.currentFileRecord).toEqual(testRecord); - expect(nxt).toHaveBeenCalledWith(); + test('an api key user on the request', async () => { + readFileSpy.mockImplementation(() => { + return testRecord; + }); + const req = getMockReq({ + apiUser: true, + params: { id: fileId }, + }); + const { res, next } = getMockRes(); + + await currentFileRecord(req, res, next); + + expect(readFileSpy).toBeCalledWith(fileId); + expect(req.currentFileRecord).toEqual(testRecord); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(); + }); }); }); +// External dependencies used by the implementation are: none +// describe('hasFileCreate', () => { - it('403s if there is no current user on the request scope', async () => { - const testReq = { - headers: { - authorization: 'Bearer ' + bearerToken, - }, - }; - - const nxt = jest.fn(); - - await hasFileCreate(testReq, testRes, nxt); - expect(testReq.currentFileRecord).toEqual(undefined); - expect(testReq.currentUser).toEqual(undefined); - expect(nxt).toHaveBeenCalledTimes(1); - expect(nxt).toHaveBeenCalledWith(new Problem(403, { detail: 'Invalid authorization credentials.' })); - }); - - it('403s if the current user is not an actual user (IE, public)', async () => { - const testReq = { - currentUser: { - idpUserId: undefined, - username: 'public', - }, - }; - - const nxt = jest.fn(); + describe('403 response when', () => { + const expectedStatus = { status: 403 }; + + test('there is no current user on the request scope', () => { + const req = getMockReq({ + headers: { + authorization: 'Bearer ' + bearerToken, + }, + }); + const { res, next } = getMockRes(); + + hasFileCreate(req, res, next); + + expect(req.currentFileRecord).toEqual(undefined); + expect(req.currentUser).toEqual(undefined); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); + expect(next).toBeCalledWith( + expect.objectContaining({ + detail: 'Invalid authorization credentials.', + }) + ); + }); - await hasFileCreate(testReq, testRes, nxt); - expect(testReq.currentFileRecord).toEqual(undefined); - expect(testReq.currentUser).toEqual(testReq.currentUser); - expect(nxt).toHaveBeenCalledTimes(1); - expect(nxt).toHaveBeenCalledWith(new Problem(403, { detail: 'Invalid authorization credentials.' })); + test('the current user is a public user', () => { + const req = getMockReq({ + currentUser: { + username: 'public', + }, + }); + const { res, next } = getMockRes(); + + hasFileCreate(req, res, next); + + expect(req.currentFileRecord).toEqual(undefined); + expect(req.currentUser).toEqual(req.currentUser); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); + expect(next).toBeCalledWith( + expect.objectContaining({ + detail: 'Invalid authorization credentials.', + }) + ); + }); }); - it('passes if a authed user is on the request', async () => { - const testReq = { - currentUser: { - idpUserId: zeroUuid, - username: 'jsmith@idir', - }, - }; + describe('allows', () => { + test('an idp user on the request', async () => { + const req = getMockReq({ + currentUser: currentUserIdp, + }); + const { res, next } = getMockRes(); - const nxt = jest.fn(); + hasFileCreate(req, res, next); - await hasFileCreate(testReq, testRes, nxt); - expect(testReq.currentFileRecord).toEqual(undefined); - expect(testReq.currentUser).toEqual(testReq.currentUser); - expect(nxt).toHaveBeenCalledTimes(1); - expect(nxt).toHaveBeenCalledWith(); + expect(req.currentFileRecord).toEqual(undefined); + expect(req.currentUser).toEqual(currentUserIdp); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(); + }); }); }); describe('hasFilePermissions', () => { - const perm = ['submission_read']; + const permissions = ['submission_read']; - const subPermSpy = jest.spyOn(userAccess, 'hasSubmissionPermissions'); + const submissionPermissionsSpy = jest.spyOn(userAccess, 'hasSubmissionPermissions'); beforeEach(() => { - subPermSpy.mockReset(); + submissionPermissionsSpy.mockReset(); }); it('returns a middleware function', async () => { - const mw = hasFilePermissions(perm); - expect(mw).toBeInstanceOf(Function); - }); + const middleware = hasFilePermissions(permissions); - it('403s if the request has no current user', async () => { - const mw = hasFilePermissions(perm); - const nxt = jest.fn(); - const req = { a: '1' }; - - mw(req, testRes, nxt); - expect(nxt).toHaveBeenCalledTimes(1); - expect(nxt).toHaveBeenCalledWith(new Problem(403, { detail: 'Unauthorized to read file' })); + expect(middleware).toBeInstanceOf(Function); }); - it('403s if the request is a unauthed user', async () => { - const mw = hasFilePermissions(perm); - const nxt = jest.fn(); - const req = { - currentUser: { - idpUserId: undefined, - username: 'public', - }, - }; + describe('403 response when', () => { + const expectedStatus = { status: 403 }; - mw(req, testRes, nxt); - expect(nxt).toHaveBeenCalledTimes(1); - expect(nxt).toHaveBeenCalledWith(new Problem(403, { detail: 'Unauthorized to read file' })); - }); + test('the request has no current user', async () => { + const req = getMockReq(); + const { res, next } = getMockRes(); - it('passes through if the user is authed and the file record has no submission', async () => { - const mw = hasFilePermissions(perm); - const nxt = jest.fn(); - const req = { - currentUser: { - idpUserId: zeroUuid, - username: 'jsmith@idir', - }, - currentFileRecord: { - name: 'unsubmitted file', - }, - }; + hasFilePermissions(permissions)(req, res, next); - mw(req, testRes, nxt); - expect(nxt).toHaveBeenCalledTimes(1); - expect(nxt).toHaveBeenCalledWith(); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); + expect(next).toBeCalledWith( + expect.objectContaining({ + detail: 'Unauthorized to read file.', + }) + ); + }); + + test('the current user is a public user', async () => { + const req = getMockReq({ + currentUser: { + username: 'public', + }, + }); + const { res, next } = getMockRes(); + + hasFilePermissions(permissions)(req, res, next); + + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(expect.objectContaining(expectedStatus)); + expect(next).toBeCalledWith( + expect.objectContaining({ + detail: 'Unauthorized to read file.', + }) + ); + }); }); - it('returns the result of the submission checking middleware', async () => { - // Submission checking middleware is fully tested out in useraccess.spec.js - // treat as black box for this testing - subPermSpy.mockReturnValue(jest.fn()); - - const mw = hasFilePermissions(perm); - const nxt = jest.fn(); - const req = { - query: {}, - currentUser: { - idpUserId: zeroUuid, - username: 'jsmith@idir', - }, - currentFileRecord: { - formSubmissionId: oneUuid, - name: 'unsubmitted file', - }, - }; + describe('allows', () => { + test('an api key user on the request', async () => { + const req = getMockReq({ + apiUser: true, + currentFileRecord: { + formSubmissionId: formSubmissionId, + }, + }); + const { res, next } = getMockRes(); + + hasFilePermissions(permissions)(req, res, next); + + expect(submissionPermissionsSpy).toBeCalledTimes(0); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(); + }); - mw(req, testRes, nxt); - expect(subPermSpy).toHaveBeenCalledTimes(1); - expect(subPermSpy).toHaveBeenCalledWith(perm); - expect(nxt).toHaveBeenCalledTimes(0); - }); + test('authed user when file record has submission', async () => { + submissionPermissionsSpy.mockReturnValue(jest.fn()); + const req = getMockReq({ + query: {}, + currentFileRecord: { + formSubmissionId: formSubmissionId, + name: 'unsubmitted file', + }, + currentUser: currentUserIdp, + }); + const { res, next } = getMockRes(); + + hasFilePermissions(permissions)(req, res, next); + + expect(submissionPermissionsSpy).toBeCalledTimes(1); + expect(submissionPermissionsSpy).toBeCalledWith(permissions); + expect(next).toBeCalledTimes(0); + }); - it('bypasses permission check for an API user', async () => { - const testReq = { - apiUser: true, - currentFileRecord: { formSubmissionId: oneUuid }, - }; + test('authed user when file record has no submission', async () => { + const req = getMockReq({ + currentFileRecord: { + name: 'unsubmitted file', + }, + currentUser: currentUserIdp, + }); + const { res, next } = getMockRes(); - const nxt = jest.fn(); - const mw = hasFilePermissions(perm); + hasFilePermissions(permissions)(req, res, next); - mw(testReq, testRes, nxt); - expect(nxt).toHaveBeenCalledTimes(1); - expect(nxt).toHaveBeenCalledWith(); - expect(subPermSpy).not.toHaveBeenCalled(); + expect(submissionPermissionsSpy).toBeCalledTimes(0); + expect(next).toBeCalledTimes(1); + expect(next).toBeCalledWith(); + }); }); }); diff --git a/app/tests/unit/forms/form/controller.spec.js b/app/tests/unit/forms/form/controller.spec.js index 606b80311..78e70f65c 100644 --- a/app/tests/unit/forms/form/controller.spec.js +++ b/app/tests/unit/forms/form/controller.spec.js @@ -255,7 +255,7 @@ describe('form controller', () => { exportService.fieldsForCSVExport = jest.fn().mockReturnValue(formFields); await controller.readFieldsForCSVExport(req, {}, jest.fn()); - expect(exportService.fieldsForCSVExport).toHaveBeenCalledTimes(1); + expect(exportService.fieldsForCSVExport).toBeCalledTimes(1); }); it('should not continue with export if there are no submissions', async () => { @@ -267,10 +267,10 @@ describe('form controller', () => { exportService._getSubmissions = jest.fn().mockReturnValue([]); await controller.export(req, {}, jest.fn()); - expect(exportServiceSpy).toHaveBeenCalledTimes(1); - expect(exportService._getForm).toHaveBeenCalledTimes(1); - expect(exportService._getSubmissions).toHaveBeenCalledTimes(1); - expect(formatDataSpy).toHaveBeenCalledTimes(1); + expect(exportServiceSpy).toBeCalledTimes(1); + expect(exportService._getForm).toBeCalledTimes(1); + expect(exportService._getSubmissions).toBeCalledTimes(1); + expect(formatDataSpy).toBeCalledTimes(1); }); }); @@ -294,9 +294,9 @@ describe('listFormSubmissions', () => { await controller.listFormSubmissions(req, res, next); // Assert - expect(service.listFormSubmissions).toHaveBeenCalled(); - expect(res.status).toHaveBeenCalledWith(200); - expect(res.json).toHaveBeenCalledWith(mockResponse); + expect(service.listFormSubmissions).toBeCalled(); + expect(res.status).toBeCalledWith(200); + expect(res.json).toBeCalledWith(mockResponse); }); it('should 400 if the formId is missing', async () => { @@ -308,9 +308,9 @@ describe('listFormSubmissions', () => { await controller.listFormSubmissions(req, res, next); // Assert - expect(service.listFormSubmissions).toHaveBeenCalledTimes(0); - expect(res.status).toHaveBeenCalledWith(400); - expect(res.json).toHaveBeenCalledWith({ detail: 'Bad formId "undefined".' }); + expect(service.listFormSubmissions).toBeCalledTimes(0); + expect(res.status).toBeCalledWith(400); + expect(res.json).toBeCalledWith({ detail: 'Bad formId "undefined".' }); }); test.each(testCases400)('should 400 if the formId is "%s"', async (formId) => { @@ -322,9 +322,9 @@ describe('listFormSubmissions', () => { await controller.listFormSubmissions(req, res, next); // Assert - expect(service.listFormSubmissions).toHaveBeenCalledTimes(0); - expect(res.status).toHaveBeenCalledWith(400); - expect(res.json).toHaveBeenCalledWith({ detail: `Bad formId "${formId}".` }); + expect(service.listFormSubmissions).toBeCalledTimes(0); + expect(res.status).toBeCalledWith(400); + expect(res.json).toBeCalledWith({ detail: `Bad formId "${formId}".` }); }); it('should forward service errors for handling elsewhere', async () => { @@ -340,8 +340,8 @@ describe('listFormSubmissions', () => { await controller.listFormSubmissions(req, res, next); // Assert - expect(service.listFormSubmissions).toHaveBeenCalled(); - expect(next).toHaveBeenCalledWith(error); + expect(service.listFormSubmissions).toBeCalled(); + expect(next).toBeCalledWith(error); }); }); @@ -363,9 +363,9 @@ describe('readFormOptions', () => { await controller.readFormOptions(req, res, next); // Assert - expect(service.readFormOptions).toHaveBeenCalled(); - expect(res.status).toHaveBeenCalledWith(200); - expect(res.json).toHaveBeenCalledWith(mockReadResponse); + expect(service.readFormOptions).toBeCalled(); + expect(res.status).toBeCalledWith(200); + expect(res.json).toBeCalledWith(mockReadResponse); }); it('should 400 if the formId is missing', async () => { @@ -377,9 +377,9 @@ describe('readFormOptions', () => { await controller.readFormOptions(req, res, next); // Assert - expect(service.readFormOptions).toHaveBeenCalledTimes(0); - expect(res.status).toHaveBeenCalledWith(400); - expect(res.json).toHaveBeenCalledWith({ detail: 'Bad formId "undefined".' }); + expect(service.readFormOptions).toBeCalledTimes(0); + expect(res.status).toBeCalledWith(400); + expect(res.json).toBeCalledWith({ detail: 'Bad formId "undefined".' }); }); test.each(testCases400)('should 400 if the formId is "%s"', async (formId) => { @@ -391,9 +391,9 @@ describe('readFormOptions', () => { await controller.readFormOptions(req, res, next); // Assert - expect(service.readFormOptions).toHaveBeenCalledTimes(0); - expect(res.status).toHaveBeenCalledWith(400); - expect(res.json).toHaveBeenCalledWith({ detail: `Bad formId "${formId}".` }); + expect(service.readFormOptions).toBeCalledTimes(0); + expect(res.status).toBeCalledWith(400); + expect(res.json).toBeCalledWith({ detail: `Bad formId "${formId}".` }); }); it('should forward service errors for handling elsewhere', async () => { @@ -409,7 +409,7 @@ describe('readFormOptions', () => { await controller.readFormOptions(req, res, next); // Assert - expect(service.readFormOptions).toHaveBeenCalled(); - expect(next).toHaveBeenCalledWith(error); + expect(service.readFormOptions).toBeCalled(); + expect(next).toBeCalledWith(error); }); }); diff --git a/app/tests/unit/forms/form/exportService.spec.js b/app/tests/unit/forms/form/exportService.spec.js index 84e99ddb0..5b9842993 100644 --- a/app/tests/unit/forms/form/exportService.spec.js +++ b/app/tests/unit/forms/form/exportService.spec.js @@ -428,8 +428,8 @@ describe('_buildCsvHeaders', () => { expect(result).toHaveLength(44); expect(result).toEqual(expect.arrayContaining(['form.confirmationId', 'textFieldNested1', 'textFieldNested2'])); - expect(exportService._readLatestFormSchema).toHaveBeenCalledTimes(1); - // expect(exportService._readLatestFormSchema).toHaveBeenCalledWith(123); + expect(exportService._readLatestFormSchema).toBeCalledTimes(1); + // expect(exportService._readLatestFormSchema).toBeCalledWith(123); // restore mocked function to it's original implementation exportService._readLatestFormSchema.mockRestore(); @@ -462,8 +462,8 @@ describe('_buildCsvHeaders', () => { expect(result).toEqual( expect.arrayContaining(['form.confirmationId', 'oneRowPerLake.0.closestTown', 'oneRowPerLake.0.dataGrid.0.fishType', 'oneRowPerLake.0.dataGrid.1.fishType']) ); - expect(exportService._readLatestFormSchema).toHaveBeenCalledTimes(1); - expect(exportService._readLatestFormSchema).toHaveBeenCalledWith(123, 1); + expect(exportService._readLatestFormSchema).toBeCalledTimes(1); + expect(exportService._readLatestFormSchema).toBeCalledWith(123, 1); // restore mocked function to it's original implementation exportService._readLatestFormSchema.mockRestore(); @@ -511,8 +511,8 @@ describe('_buildCsvHeaders', () => { 'lateEntry', ]) ); - expect(exportService._readLatestFormSchema).toHaveBeenCalledTimes(1); - expect(exportService._readLatestFormSchema).toHaveBeenCalledWith(123, 1); + expect(exportService._readLatestFormSchema).toBeCalledTimes(1); + expect(exportService._readLatestFormSchema).toBeCalledWith(123, 1); // restore mocked function to it's original implementation exportService._readLatestFormSchema.mockRestore(); @@ -549,8 +549,8 @@ describe('_buildCsvHeaders', () => { expect(result[13]).toEqual('oneRowPerLake.0.dataGrid.0.numberCaught'); expect(result[18]).toEqual('oneRowPerLake.0.closestTown'); expect(result[28]).toEqual('oneRowPerLake.1.numberOfDays'); - expect(exportService._readLatestFormSchema).toHaveBeenCalledTimes(1); - expect(exportService._readLatestFormSchema).toHaveBeenCalledWith(123, 1); + expect(exportService._readLatestFormSchema).toBeCalledTimes(1); + expect(exportService._readLatestFormSchema).toBeCalledWith(123, 1); // restore mocked function to it's original implementation exportService._readLatestFormSchema.mockRestore(); @@ -581,8 +581,8 @@ describe('_buildCsvHeaders', () => { expect(result).toHaveLength(41); expect(result).toEqual(expect.arrayContaining(['number1', 'selectBoxes1.a', 'number'])); - expect(exportService._readLatestFormSchema).toHaveBeenCalledTimes(1); - expect(exportService._readLatestFormSchema).toHaveBeenCalledWith(123, 1); + expect(exportService._readLatestFormSchema).toBeCalledTimes(1); + expect(exportService._readLatestFormSchema).toBeCalledWith(123, 1); // restore mocked function to it's original implementation exportService._readLatestFormSchema.mockRestore(); @@ -652,11 +652,11 @@ describe('', () => { // get fields const fields = await exportService.fieldsForCSVExport('bd4dcf26-65bd-429b-967f-125500bfd8a4', params); - expect(exportService._getForm).toHaveBeenCalledWith('bd4dcf26-65bd-429b-967f-125500bfd8a4'); - expect(exportService._getData).toHaveBeenCalledWith(params.type, params.version, form, params); - expect(exportService._getForm).toHaveBeenCalledTimes(1); - expect(exportService._getData).toHaveBeenCalledTimes(1); - expect(exportService._buildCsvHeaders).toHaveBeenCalledTimes(1); + expect(exportService._getForm).toBeCalledWith('bd4dcf26-65bd-429b-967f-125500bfd8a4'); + expect(exportService._getData).toBeCalledWith(params.type, params.version, form, params); + expect(exportService._getForm).toBeCalledTimes(1); + expect(exportService._getData).toBeCalledTimes(1); + expect(exportService._buildCsvHeaders).toBeCalledTimes(1); // test cases expect(fields.length).toEqual(19); }); @@ -797,9 +797,9 @@ describe('_getSubmissions', () => { preference = params.preference; } exportService._getSubmissions(form, params, params.version); - expect(MockModel.query).toHaveBeenCalledTimes(1); - expect(MockModel.modify).toHaveBeenCalledTimes(7); - expect(MockModel.modify).toHaveBeenCalledWith('filterUpdatedAt', preference && preference.updatedMinDate, preference && preference.updatedMaxDate); + expect(MockModel.query).toBeCalledTimes(1); + expect(MockModel.modify).toBeCalledTimes(7); + expect(MockModel.modify).toBeCalledWith('filterUpdatedAt', preference && preference.updatedMinDate, preference && preference.updatedMaxDate); }); it('Should pass this test without preference passed to _getSubmissions and without calling updatedAt modifier', async () => { @@ -813,8 +813,8 @@ describe('_getSubmissions', () => { MockModel.query.mockImplementation(() => MockModel); exportService._submissionsColumns = jest.fn().mockReturnThis(); exportService._getSubmissions(form, params, params.version); - expect(MockModel.query).toHaveBeenCalledTimes(1); - expect(MockModel.modify).toHaveBeenCalledTimes(7); + expect(MockModel.query).toBeCalledTimes(1); + expect(MockModel.modify).toBeCalledTimes(7); }); it('Should pass this test with preference passed to _getSubmissions', async () => { @@ -839,8 +839,8 @@ describe('_getSubmissions', () => { preference = params.preference; } exportService._getSubmissions(form, params, params.version); - expect(MockModel.query).toHaveBeenCalledTimes(1); - expect(MockModel.modify).toHaveBeenCalledTimes(7); - expect(MockModel.modify).toHaveBeenCalledWith('filterUpdatedAt', preference && preference.updatedMinDate, preference && preference.updatedMaxDate); + expect(MockModel.query).toBeCalledTimes(1); + expect(MockModel.modify).toBeCalledTimes(7); + expect(MockModel.modify).toBeCalledWith('filterUpdatedAt', preference && preference.updatedMinDate, preference && preference.updatedMaxDate); }); }); diff --git a/app/tests/unit/forms/form/routes.spec.js b/app/tests/unit/forms/form/routes.spec.js index 784a72e99..242958cd8 100644 --- a/app/tests/unit/forms/form/routes.spec.js +++ b/app/tests/unit/forms/form/routes.spec.js @@ -92,21 +92,21 @@ describe(`${basePath}/:formId/documentTemplates`, () => { it('should have correct middleware for GET', async () => { await appRequest.get(path); - expect(validateParameter.validateFormId).toHaveBeenCalledTimes(1); - expect(apiAccess).toHaveBeenCalledTimes(1); - expect(rateLimiter.apiKeyRateLimiter).toHaveBeenCalledTimes(1); - expect(hasFormPermissionsMock).toHaveBeenCalledTimes(1); - expect(controller.documentTemplateList).toHaveBeenCalledTimes(1); + expect(validateParameter.validateFormId).toBeCalledTimes(1); + expect(apiAccess).toBeCalledTimes(1); + expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1); + expect(hasFormPermissionsMock).toBeCalledTimes(1); + expect(controller.documentTemplateList).toBeCalledTimes(1); }); it('should have correct middleware for POST', async () => { await appRequest.post(path); - expect(validateParameter.validateFormId).toHaveBeenCalledTimes(1); - expect(apiAccess).toHaveBeenCalledTimes(1); - expect(rateLimiter.apiKeyRateLimiter).toHaveBeenCalledTimes(1); - expect(hasFormPermissionsMock).toHaveBeenCalledTimes(1); - expect(controller.documentTemplateCreate).toHaveBeenCalledTimes(1); + expect(validateParameter.validateFormId).toBeCalledTimes(1); + expect(apiAccess).toBeCalledTimes(1); + expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1); + expect(hasFormPermissionsMock).toBeCalledTimes(1); + expect(controller.documentTemplateCreate).toBeCalledTimes(1); }); }); @@ -116,22 +116,22 @@ describe(`${basePath}/:formId/documentTemplates/:documentTemplateId`, () => { it('should have correct middleware for DELETE', async () => { await appRequest.delete(path); - expect(validateParameter.validateDocumentTemplateId).toHaveBeenCalledTimes(1); - expect(validateParameter.validateFormId).toHaveBeenCalledTimes(1); - expect(apiAccess).toHaveBeenCalledTimes(1); - expect(rateLimiter.apiKeyRateLimiter).toHaveBeenCalledTimes(1); - expect(hasFormPermissionsMock).toHaveBeenCalledTimes(1); - expect(controller.documentTemplateDelete).toHaveBeenCalledTimes(1); + expect(validateParameter.validateDocumentTemplateId).toBeCalledTimes(1); + expect(validateParameter.validateFormId).toBeCalledTimes(1); + expect(apiAccess).toBeCalledTimes(1); + expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1); + expect(hasFormPermissionsMock).toBeCalledTimes(1); + expect(controller.documentTemplateDelete).toBeCalledTimes(1); }); it('should have correct middleware for GET', async () => { await appRequest.get(path); - expect(validateParameter.validateDocumentTemplateId).toHaveBeenCalledTimes(1); - expect(validateParameter.validateFormId).toHaveBeenCalledTimes(1); - expect(apiAccess).toHaveBeenCalledTimes(1); - expect(rateLimiter.apiKeyRateLimiter).toHaveBeenCalledTimes(1); - expect(hasFormPermissionsMock).toHaveBeenCalledTimes(1); - expect(controller.documentTemplateRead).toHaveBeenCalledTimes(1); + expect(validateParameter.validateDocumentTemplateId).toBeCalledTimes(1); + expect(validateParameter.validateFormId).toBeCalledTimes(1); + expect(apiAccess).toBeCalledTimes(1); + expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1); + expect(hasFormPermissionsMock).toBeCalledTimes(1); + expect(controller.documentTemplateRead).toBeCalledTimes(1); }); }); diff --git a/app/tests/unit/forms/form/service.spec.js b/app/tests/unit/forms/form/service.spec.js index 587fa2e7a..eccf00d24 100644 --- a/app/tests/unit/forms/form/service.spec.js +++ b/app/tests/unit/forms/form/service.spec.js @@ -695,10 +695,10 @@ describe('readEmailTemplate', () => { const template = await service.readEmailTemplate(emailTemplate.formId, emailTemplate.type); - expect(MockModel.query).toHaveBeenCalledTimes(1); - expect(MockModel.modify).toHaveBeenCalledTimes(2); - expect(MockModel.modify).toHaveBeenCalledWith('filterFormId', emailTemplate.formId); - expect(MockModel.modify).toHaveBeenCalledWith('filterType', emailTemplate.type); + expect(MockModel.query).toBeCalledTimes(1); + expect(MockModel.modify).toBeCalledTimes(2); + expect(MockModel.modify).toBeCalledWith('filterFormId', emailTemplate.formId); + expect(MockModel.modify).toBeCalledWith('filterType', emailTemplate.type); expect(template).toEqual(emailTemplate); }); @@ -708,10 +708,10 @@ describe('readEmailTemplate', () => { const template = await service.readEmailTemplate(emailTemplate.formId, emailTemplate.type); - expect(MockModel.query).toHaveBeenCalledTimes(1); - expect(MockModel.modify).toHaveBeenCalledTimes(2); - expect(MockModel.modify).toHaveBeenCalledWith('filterFormId', emailTemplate.formId); - expect(MockModel.modify).toHaveBeenCalledWith('filterType', emailTemplate.type); + expect(MockModel.query).toBeCalledTimes(1); + expect(MockModel.modify).toBeCalledTimes(2); + expect(MockModel.modify).toBeCalledWith('filterFormId', emailTemplate.formId); + expect(MockModel.modify).toBeCalledWith('filterType', emailTemplate.type); expect(template).toEqual(emailTemplateSubmissionConfirmation); }); }); @@ -723,9 +723,9 @@ describe('readEmailTemplates', () => { const template = await service.readEmailTemplates(emailTemplate.formId); - expect(MockModel.query).toHaveBeenCalledTimes(1); - expect(MockModel.modify).toHaveBeenCalledTimes(1); - expect(MockModel.modify).toHaveBeenCalledWith('filterFormId', emailTemplate.formId); + expect(MockModel.query).toBeCalledTimes(1); + expect(MockModel.modify).toBeCalledTimes(1); + expect(MockModel.modify).toBeCalledWith('filterFormId', emailTemplate.formId); expect(template).toEqual([emailTemplate]); }); @@ -735,9 +735,9 @@ describe('readEmailTemplates', () => { const template = await service.readEmailTemplates(emailTemplate.formId); - expect(MockModel.query).toHaveBeenCalledTimes(1); - expect(MockModel.modify).toHaveBeenCalledTimes(1); - expect(MockModel.modify).toHaveBeenCalledWith('filterFormId', emailTemplate.formId); + expect(MockModel.query).toBeCalledTimes(1); + expect(MockModel.modify).toBeCalledTimes(1); + expect(MockModel.modify).toBeCalledWith('filterFormId', emailTemplate.formId); expect(template).toEqual([emailTemplateSubmissionConfirmation]); }); }); @@ -751,13 +751,13 @@ describe('createOrUpdateEmailTemplates', () => { await service.createOrUpdateEmailTemplate(emailTemplate.formId, emailTemplate, user); - expect(MockModel.insert).toHaveBeenCalledTimes(1); - expect(MockModel.insert).toHaveBeenCalledWith({ + expect(MockModel.insert).toBeCalledTimes(1); + expect(MockModel.insert).toBeCalledWith({ createdBy: user.usernameIdp, id: expect.any(String), ...emailTemplate, }); - expect(MockTransaction.commit).toHaveBeenCalledTimes(1); + expect(MockTransaction.commit).toBeCalledTimes(1); }); it('should update template when it already exists', async () => { @@ -767,12 +767,12 @@ describe('createOrUpdateEmailTemplates', () => { await service.createOrUpdateEmailTemplate(emailTemplate.formId, emailTemplate, user); - expect(MockModel.update).toHaveBeenCalledTimes(1); - expect(MockModel.update).toHaveBeenCalledWith({ + expect(MockModel.update).toBeCalledTimes(1); + expect(MockModel.update).toBeCalledWith({ updatedBy: user.usernameIdp, ...emailTemplate, }); - expect(MockTransaction.commit).toHaveBeenCalledTimes(1); + expect(MockTransaction.commit).toBeCalledTimes(1); }); it('should not rollback when an error occurs outside transaction', async () => { @@ -780,7 +780,7 @@ describe('createOrUpdateEmailTemplates', () => { await expect(service.createOrUpdateEmailTemplate(emailTemplate.formId, emailTemplate, user)).rejects.toThrow(); - expect(MockTransaction.rollback).toHaveBeenCalledTimes(0); + expect(MockTransaction.rollback).toBeCalledTimes(0); }); it('should rollback when an insert error occurs inside transaction', async () => { @@ -790,7 +790,7 @@ describe('createOrUpdateEmailTemplates', () => { await expect(service.createOrUpdateEmailTemplate(emailTemplate.formId, emailTemplate, user)).rejects.toThrow(); - expect(MockTransaction.rollback).toHaveBeenCalledTimes(1); + expect(MockTransaction.rollback).toBeCalledTimes(1); }); it('should rollback when an update error occurs inside transaction', async () => { @@ -801,6 +801,6 @@ describe('createOrUpdateEmailTemplates', () => { await expect(service.createOrUpdateEmailTemplate(emailTemplate.formId, emailTemplate, user)).rejects.toThrow(); - expect(MockTransaction.rollback).toHaveBeenCalledTimes(1); + expect(MockTransaction.rollback).toBeCalledTimes(1); }); }); diff --git a/app/tests/unit/forms/rbac/controller.spec.js b/app/tests/unit/forms/rbac/controller.spec.js index 0a2e482b6..897dd9763 100644 --- a/app/tests/unit/forms/rbac/controller.spec.js +++ b/app/tests/unit/forms/rbac/controller.spec.js @@ -14,8 +14,8 @@ describe('getSubmissionUsers', () => { service.getSubmissionUsers = jest.fn().mockReturnValue({ form: { id: '123' } }); await controller.getSubmissionUsers(req, {}, jest.fn()); - expect(service.getSubmissionUsers).toHaveBeenCalledTimes(1); - expect(service.getSubmissionUsers).toHaveBeenCalledWith(req.query); + expect(service.getSubmissionUsers).toBeCalledTimes(1); + expect(service.getSubmissionUsers).toBeCalledWith(req.query); }); }); @@ -32,7 +32,7 @@ describe('setSubmissionUserPermissions', () => { emailService.submissionAssigned = jest.fn().mockReturnValue({}); await controller.setSubmissionUserPermissions(req, {}, jest.fn()); - expect(service.modifySubmissionUser).toHaveBeenCalledTimes(1); - expect(service.modifySubmissionUser).toHaveBeenCalledWith(req.query.formSubmissionId, req.query.userId, req.body, req.currentUser); + expect(service.modifySubmissionUser).toBeCalledTimes(1); + expect(service.modifySubmissionUser).toBeCalledWith(req.query.formSubmissionId, req.query.userId, req.body, req.currentUser); }); }); diff --git a/app/tests/unit/forms/submission/routes.spec.js b/app/tests/unit/forms/submission/routes.spec.js index a5a0d2555..d47f04df3 100644 --- a/app/tests/unit/forms/submission/routes.spec.js +++ b/app/tests/unit/forms/submission/routes.spec.js @@ -77,10 +77,10 @@ describe(`${basePath}/:formSubmissionId/template/:documentTemplateId/render`, () it('should have correct middleware for GET', async () => { await appRequest.get(path); - expect(validateParameter.validateDocumentTemplateId).toHaveBeenCalledTimes(1); - expect(apiAccess).toHaveBeenCalledTimes(1); - expect(rateLimiter.apiKeyRateLimiter).toHaveBeenCalledTimes(1); - expect(hasSubmissionPermissionsMock).toHaveBeenCalledTimes(1); - expect(controller.templateRender).toHaveBeenCalledTimes(1); + expect(validateParameter.validateDocumentTemplateId).toBeCalledTimes(1); + expect(apiAccess).toBeCalledTimes(1); + expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(1); + expect(hasSubmissionPermissionsMock).toBeCalledTimes(1); + expect(controller.templateRender).toBeCalledTimes(1); }); }); diff --git a/app/tests/unit/forms/submission/service.spec.js b/app/tests/unit/forms/submission/service.spec.js index 3b1e020de..2070af1b1 100644 --- a/app/tests/unit/forms/submission/service.spec.js +++ b/app/tests/unit/forms/submission/service.spec.js @@ -21,8 +21,8 @@ describe('read', () => { const res = await service.read('abc'); expect(res).toEqual({ a: 'b' }); - expect(service._fetchSubmissionData).toHaveBeenCalledTimes(1); - expect(service._fetchSubmissionData).toHaveBeenCalledWith('abc'); + expect(service._fetchSubmissionData).toBeCalledTimes(1); + expect(service._fetchSubmissionData).toBeCalledWith('abc'); }); }); @@ -32,8 +32,8 @@ describe('addNote', () => { const res = await service.addNote('abc', { data: true }, { user: 'me' }); expect(res).toEqual({ a: 'b' }); - expect(service._createNote).toHaveBeenCalledTimes(1); - expect(service._createNote).toHaveBeenCalledWith('abc', { data: true }, { user: 'me' }); + expect(service._createNote).toBeCalledTimes(1); + expect(service._createNote).toBeCalledWith('abc', { data: true }, { user: 'me' }); }); }); @@ -44,11 +44,11 @@ describe('createStatus', () => { const res = await service.createStatus('abc', { data: true }, { user: 'me' }, trx); expect(res).toEqual({ a: 'b' }); - expect(MockModel.startTransaction).toHaveBeenCalledTimes(0); - expect(MockModel.query).toHaveBeenCalledTimes(1); - expect(MockModel.query).toHaveBeenCalledWith(expect.anything()); - expect(MockModel.insert).toHaveBeenCalledTimes(1); - expect(MockModel.insert).toHaveBeenCalledWith(expect.anything()); + expect(MockModel.startTransaction).toBeCalledTimes(0); + expect(MockModel.query).toBeCalledTimes(1); + expect(MockModel.query).toBeCalledWith(expect.anything()); + expect(MockModel.insert).toBeCalledTimes(1); + expect(MockModel.insert).toBeCalledWith(expect.anything()); }); }); @@ -71,14 +71,14 @@ describe('deleteMutipleSubmissions', () => { service.readSubmissionData = jest.fn().mockReturnValue(returnValue); const spy = jest.spyOn(service, 'readSubmissionData'); const res = await service.deleteMutipleSubmissions(submissionIds, currentUser); - expect(MockModel.startTransaction).toHaveBeenCalledTimes(1); - expect(MockModel.query).toHaveBeenCalledTimes(1); - expect(MockModel.query).toHaveBeenCalledWith(expect.anything()); - expect(MockModel.patch).toHaveBeenCalledTimes(1); - expect(MockModel.patch).toHaveBeenCalledWith({ deleted: true, updatedBy: currentUser.usernameIdp }); - expect(MockModel.whereIn).toHaveBeenCalledTimes(1); - expect(MockModel.whereIn).toHaveBeenCalledWith('id', submissionIds); - expect(spy).toHaveBeenCalledWith(submissionIds); + expect(MockModel.startTransaction).toBeCalledTimes(1); + expect(MockModel.query).toBeCalledTimes(1); + expect(MockModel.query).toBeCalledWith(expect.anything()); + expect(MockModel.patch).toBeCalledTimes(1); + expect(MockModel.patch).toBeCalledWith({ deleted: true, updatedBy: currentUser.usernameIdp }); + expect(MockModel.whereIn).toBeCalledTimes(1); + expect(MockModel.whereIn).toBeCalledWith('id', submissionIds); + expect(spy).toBeCalledWith(submissionIds); expect(res).toEqual(returnValue); }); }); @@ -102,14 +102,14 @@ describe('restoreMutipleSubmissions', () => { service.readSubmissionData = jest.fn().mockReturnValue(returnValue); const spy = jest.spyOn(service, 'readSubmissionData'); const res = await service.restoreMutipleSubmissions(submissionIds, currentUser); - expect(MockModel.startTransaction).toHaveBeenCalledTimes(1); - expect(MockModel.query).toHaveBeenCalledTimes(1); - expect(MockModel.query).toHaveBeenCalledWith(expect.anything()); - expect(MockModel.patch).toHaveBeenCalledTimes(1); - expect(MockModel.patch).toHaveBeenCalledWith({ deleted: false, updatedBy: currentUser.usernameIdp }); - expect(MockModel.whereIn).toHaveBeenCalledTimes(1); - expect(MockModel.whereIn).toHaveBeenCalledWith('id', submissionIds); - expect(spy).toHaveBeenCalledWith(submissionIds); + expect(MockModel.startTransaction).toBeCalledTimes(1); + expect(MockModel.query).toBeCalledTimes(1); + expect(MockModel.query).toBeCalledWith(expect.anything()); + expect(MockModel.patch).toBeCalledTimes(1); + expect(MockModel.patch).toBeCalledWith({ deleted: false, updatedBy: currentUser.usernameIdp }); + expect(MockModel.whereIn).toBeCalledTimes(1); + expect(MockModel.whereIn).toBeCalledWith('id', submissionIds); + expect(spy).toBeCalledWith(submissionIds); expect(res).toEqual(returnValue); }); }); diff --git a/app/tests/unit/forms/user/service.spec.js b/app/tests/unit/forms/user/service.spec.js index 6036ad793..ba77baf76 100644 --- a/app/tests/unit/forms/user/service.spec.js +++ b/app/tests/unit/forms/user/service.spec.js @@ -33,18 +33,18 @@ describe('list', () => { await service.list(params); - expect(MockModel.query).toHaveBeenCalledTimes(1); - expect(MockModel.query).toHaveBeenCalledWith(); - expect(MockModel.modify).toHaveBeenCalledTimes(9); - expect(MockModel.modify).toHaveBeenCalledWith('filterIdpUserId', params.idpUserId); - expect(MockModel.modify).toHaveBeenCalledWith('filterIdpCode', params.idpCode); - expect(MockModel.modify).toHaveBeenCalledWith('filterUsername', params.username, false); - expect(MockModel.modify).toHaveBeenCalledWith('filterFullName', params.fullName); - expect(MockModel.modify).toHaveBeenCalledWith('filterFirstName', params.firstName); - expect(MockModel.modify).toHaveBeenCalledWith('filterLastName', params.lastName); - expect(MockModel.modify).toHaveBeenCalledWith('filterEmail', params.email, false); - expect(MockModel.modify).toHaveBeenCalledWith('filterSearch', params.search); - expect(MockModel.modify).toHaveBeenCalledWith('orderLastFirstAscending'); + expect(MockModel.query).toBeCalledTimes(1); + expect(MockModel.query).toBeCalledWith(); + expect(MockModel.modify).toBeCalledTimes(9); + expect(MockModel.modify).toBeCalledWith('filterIdpUserId', params.idpUserId); + expect(MockModel.modify).toBeCalledWith('filterIdpCode', params.idpCode); + expect(MockModel.modify).toBeCalledWith('filterUsername', params.username, false); + expect(MockModel.modify).toBeCalledWith('filterFullName', params.fullName); + expect(MockModel.modify).toBeCalledWith('filterFirstName', params.firstName); + expect(MockModel.modify).toBeCalledWith('filterLastName', params.lastName); + expect(MockModel.modify).toBeCalledWith('filterEmail', params.email, false); + expect(MockModel.modify).toBeCalledWith('filterSearch', params.search); + expect(MockModel.modify).toBeCalledWith('orderLastFirstAscending'); }); }); @@ -52,12 +52,12 @@ describe('read', () => { it('should query user table by id', async () => { await service.read(userId); - expect(MockModel.query).toHaveBeenCalledTimes(1); - expect(MockModel.query).toHaveBeenCalledWith(); - expect(MockModel.findById).toHaveBeenCalledTimes(1); - expect(MockModel.findById).toHaveBeenCalledWith(userId); - expect(MockModel.throwIfNotFound).toHaveBeenCalledTimes(1); - expect(MockModel.throwIfNotFound).toHaveBeenCalledWith(); + expect(MockModel.query).toBeCalledTimes(1); + expect(MockModel.query).toBeCalledWith(); + expect(MockModel.findById).toBeCalledTimes(1); + expect(MockModel.findById).toBeCalledWith(userId); + expect(MockModel.throwIfNotFound).toBeCalledTimes(1); + expect(MockModel.throwIfNotFound).toBeCalledWith(); }); }); @@ -65,14 +65,14 @@ describe('deleteUserPreferences', () => { it('should delete user form preference table by user id', async () => { await service.deleteUserPreferences({ id: userId }); - expect(MockModel.query).toHaveBeenCalledTimes(1); - expect(MockModel.query).toHaveBeenCalledWith(); - expect(MockModel.delete).toHaveBeenCalledTimes(1); - expect(MockModel.delete).toHaveBeenCalledWith(); - expect(MockModel.where).toHaveBeenCalledTimes(1); - expect(MockModel.where).toHaveBeenCalledWith('userId', userId); - expect(MockModel.throwIfNotFound).toHaveBeenCalledTimes(1); - expect(MockModel.throwIfNotFound).toHaveBeenCalledWith(); + expect(MockModel.query).toBeCalledTimes(1); + expect(MockModel.query).toBeCalledWith(); + expect(MockModel.delete).toBeCalledTimes(1); + expect(MockModel.delete).toBeCalledWith(); + expect(MockModel.where).toBeCalledTimes(1); + expect(MockModel.where).toBeCalledWith('userId', userId); + expect(MockModel.throwIfNotFound).toBeCalledTimes(1); + expect(MockModel.throwIfNotFound).toBeCalledWith(); }); }); @@ -80,10 +80,10 @@ describe('readUserPreferences', () => { it('should query user form preference table by user id', async () => { await service.readUserPreferences({ id: userId }); - expect(MockModel.query).toHaveBeenCalledTimes(1); - expect(MockModel.query).toHaveBeenCalledWith(); - expect(MockModel.where).toHaveBeenCalledTimes(1); - expect(MockModel.where).toHaveBeenCalledWith('userId', userId); + expect(MockModel.query).toBeCalledTimes(1); + expect(MockModel.query).toBeCalledWith(); + expect(MockModel.where).toBeCalledTimes(1); + expect(MockModel.where).toBeCalledWith('userId', userId); }); }); @@ -91,12 +91,12 @@ describe('deleteUserFormPreferences', () => { it('should delete user form preference table by user id', async () => { await service.deleteUserFormPreferences({ id: userId }, formId); - expect(MockModel.query).toHaveBeenCalledTimes(1); - expect(MockModel.query).toHaveBeenCalledWith(); - expect(MockModel.deleteById).toHaveBeenCalledTimes(1); - expect(MockModel.deleteById).toHaveBeenCalledWith([userId, formId]); - expect(MockModel.throwIfNotFound).toHaveBeenCalledTimes(1); - expect(MockModel.throwIfNotFound).toHaveBeenCalledWith(); + expect(MockModel.query).toBeCalledTimes(1); + expect(MockModel.query).toBeCalledWith(); + expect(MockModel.deleteById).toBeCalledTimes(1); + expect(MockModel.deleteById).toBeCalledWith([userId, formId]); + expect(MockModel.throwIfNotFound).toBeCalledTimes(1); + expect(MockModel.throwIfNotFound).toBeCalledWith(); }); }); @@ -104,12 +104,12 @@ describe('readUserFormPreferences', () => { it('should query user form preference table by user id', async () => { await service.readUserFormPreferences({ id: userId }, formId); - expect(MockModel.query).toHaveBeenCalledTimes(1); - expect(MockModel.query).toHaveBeenCalledWith(); - expect(MockModel.findById).toHaveBeenCalledTimes(1); - expect(MockModel.findById).toHaveBeenCalledWith([userId, formId]); - expect(MockModel.first).toHaveBeenCalledTimes(1); - expect(MockModel.first).toHaveBeenCalledWith(); + expect(MockModel.query).toBeCalledTimes(1); + expect(MockModel.query).toBeCalledWith(); + expect(MockModel.findById).toBeCalledTimes(1); + expect(MockModel.findById).toBeCalledWith([userId, formId]); + expect(MockModel.first).toBeCalledTimes(1); + expect(MockModel.first).toBeCalledWith(); }); }); @@ -117,11 +117,11 @@ describe('readUserLabels', () => { it('should query user labels by user id', async () => { await service.readUserLabels({ id: userId }); - expect(MockModel.query).toHaveBeenCalledTimes(1); - expect(MockModel.query).toHaveBeenCalledWith(); - expect(MockModel.where).toHaveBeenCalledTimes(1); - expect(MockModel.where).toHaveBeenCalledWith('userId', userId); - expect(MockModel.select).toHaveBeenCalledWith('labelText'); + expect(MockModel.query).toBeCalledTimes(1); + expect(MockModel.query).toBeCalledWith(); + expect(MockModel.where).toBeCalledTimes(1); + expect(MockModel.where).toBeCalledWith('userId', userId); + expect(MockModel.select).toBeCalledWith('labelText'); }); }); @@ -133,16 +133,16 @@ describe('updateUserLabels', () => { await service.updateUserLabels({ id: userId }, body); - expect(MockModel.startTransaction).toHaveBeenCalledTimes(1); - expect(MockModel.query).toHaveBeenCalledTimes(body.length * 2 + 1); - expect(MockModel.query).toHaveBeenCalledWith(expect.anything()); - expect(MockModel.insert).toHaveBeenCalledTimes(body.length); - expect(MockModel.insert).toHaveBeenCalledWith({ + expect(MockModel.startTransaction).toBeCalledTimes(1); + expect(MockModel.query).toBeCalledTimes(body.length * 2 + 1); + expect(MockModel.query).toBeCalledWith(expect.anything()); + expect(MockModel.insert).toBeCalledTimes(body.length); + expect(MockModel.insert).toBeCalledWith({ id: expect.any(String), userId: userId, labelText: expect.any(String), }); - expect(MockTransaction.commit).toHaveBeenCalledTimes(1); + expect(MockTransaction.commit).toBeCalledTimes(1); }); it('should throw when invalid options are provided', () => { @@ -151,17 +151,17 @@ describe('updateUserLabels', () => { expect(fn({ id: userId }, undefined)).rejects.toThrow(); expect(fn({ id: userId }, {})).rejects.toThrow(); - expect(MockModel.startTransaction).toHaveBeenCalledTimes(0); - expect(MockModel.query).toHaveBeenCalledTimes(0); + expect(MockModel.startTransaction).toBeCalledTimes(0); + expect(MockModel.query).toBeCalledTimes(0); }); it('should handle empty label body', async () => { await service.updateUserLabels({ id: userId }, []); - expect(MockModel.startTransaction).toHaveBeenCalledTimes(1); - expect(MockModel.query).toHaveBeenCalledTimes(1); - expect(MockModel.insert).not.toHaveBeenCalled(); - expect(MockTransaction.commit).toHaveBeenCalledTimes(1); + expect(MockModel.startTransaction).toBeCalledTimes(1); + expect(MockModel.query).toBeCalledTimes(1); + expect(MockModel.insert).not.toBeCalled(); + expect(MockTransaction.commit).toBeCalledTimes(1); }); }); @@ -189,8 +189,8 @@ describe('updateUserPreferences', () => { expect(fn({ id: userId }, undefined)).rejects.toThrow(); expect(fn({ id: userId }, {})).rejects.toThrow(); expect(fn({ id: userId }, { forms: {} })).rejects.toThrow(); - expect(MockModel.startTransaction).toHaveBeenCalledTimes(0); - expect(MockModel.query).toHaveBeenCalledTimes(0); + expect(MockModel.startTransaction).toBeCalledTimes(0); + expect(MockModel.query).toBeCalledTimes(0); }); it('should insert preferences', async () => { @@ -200,17 +200,17 @@ describe('updateUserPreferences', () => { await service.updateUserPreferences({ id: userId }, body); - expect(MockModel.startTransaction).toHaveBeenCalledTimes(1); - expect(MockModel.query).toHaveBeenCalledTimes(1); - expect(MockModel.query).toHaveBeenCalledWith(expect.anything()); - expect(MockModel.insert).toHaveBeenCalledTimes(1); - expect(MockModel.insert).toHaveBeenCalledWith({ + expect(MockModel.startTransaction).toBeCalledTimes(1); + expect(MockModel.query).toBeCalledTimes(1); + expect(MockModel.query).toBeCalledWith(expect.anything()); + expect(MockModel.insert).toBeCalledTimes(1); + expect(MockModel.insert).toBeCalledWith({ userId: userId, formId: formId, preferences: preferences, createdBy: undefined, }); - expect(MockTransaction.commit).toHaveBeenCalledTimes(1); + expect(MockTransaction.commit).toBeCalledTimes(1); }); it('should update preferences', async () => { @@ -220,15 +220,15 @@ describe('updateUserPreferences', () => { await service.updateUserPreferences({ id: userId }, body); - expect(MockModel.startTransaction).toHaveBeenCalledTimes(1); - expect(MockModel.query).toHaveBeenCalledTimes(1); - expect(MockModel.query).toHaveBeenCalledWith(expect.anything()); - expect(MockModel.patchAndFetchById).toHaveBeenCalledTimes(1); - expect(MockModel.patchAndFetchById).toHaveBeenCalledWith([userId, formId], { + expect(MockModel.startTransaction).toBeCalledTimes(1); + expect(MockModel.query).toBeCalledTimes(1); + expect(MockModel.query).toBeCalledWith(expect.anything()); + expect(MockModel.patchAndFetchById).toBeCalledTimes(1); + expect(MockModel.patchAndFetchById).toBeCalledWith([userId, formId], { preferences: preferences, updatedBy: undefined, }); - expect(MockTransaction.commit).toHaveBeenCalledTimes(1); + expect(MockTransaction.commit).toBeCalledTimes(1); }); }); @@ -247,17 +247,17 @@ describe('updateUserFormPreferences', () => { readUserFormPreferencesSpy.mockResolvedValue(undefined); await service.updateUserFormPreferences({ id: userId }, formId, preferences); - expect(MockModel.startTransaction).toHaveBeenCalledTimes(1); - expect(MockModel.query).toHaveBeenCalledTimes(1); - expect(MockModel.query).toHaveBeenCalledWith(expect.anything()); - expect(MockModel.insertAndFetch).toHaveBeenCalledTimes(1); - expect(MockModel.insertAndFetch).toHaveBeenCalledWith({ + expect(MockModel.startTransaction).toBeCalledTimes(1); + expect(MockModel.query).toBeCalledTimes(1); + expect(MockModel.query).toBeCalledWith(expect.anything()); + expect(MockModel.insertAndFetch).toBeCalledTimes(1); + expect(MockModel.insertAndFetch).toBeCalledWith({ userId: userId, formId: formId, preferences: preferences, createdBy: undefined, }); - expect(MockTransaction.commit).toHaveBeenCalledTimes(1); + expect(MockTransaction.commit).toBeCalledTimes(1); }); it('should update preferences', async () => { @@ -266,15 +266,15 @@ describe('updateUserFormPreferences', () => { await service.updateUserFormPreferences({ id: userId }, formId, preferences); - expect(MockModel.startTransaction).toHaveBeenCalledTimes(1); - expect(MockModel.query).toHaveBeenCalledTimes(1); - expect(MockModel.query).toHaveBeenCalledWith(expect.anything()); - expect(MockModel.patchAndFetchById).toHaveBeenCalledTimes(1); - expect(MockModel.patchAndFetchById).toHaveBeenCalledWith([userId, formId], { + expect(MockModel.startTransaction).toBeCalledTimes(1); + expect(MockModel.query).toBeCalledTimes(1); + expect(MockModel.query).toBeCalledWith(expect.anything()); + expect(MockModel.patchAndFetchById).toBeCalledTimes(1); + expect(MockModel.patchAndFetchById).toBeCalledWith([userId, formId], { preferences: preferences, updatedBy: undefined, }); - expect(MockTransaction.commit).toHaveBeenCalledTimes(1); + expect(MockTransaction.commit).toBeCalledTimes(1); }); it('should handle errors gracefully', async () => { @@ -286,7 +286,7 @@ describe('updateUserFormPreferences', () => { const fn = () => service.updateUserFormPreferences({ id: userId }, formId, preferences); await expect(fn()).rejects.toThrow(); - expect(MockModel.startTransaction).toHaveBeenCalledTimes(0); - expect(MockModel.query).toHaveBeenCalledTimes(0); + expect(MockModel.startTransaction).toBeCalledTimes(0); + expect(MockModel.query).toBeCalledTimes(0); }); }); diff --git a/app/tests/unit/routes/v1/admin.spec.js b/app/tests/unit/routes/v1/admin.spec.js index 476562752..2160fe638 100644 --- a/app/tests/unit/routes/v1/admin.spec.js +++ b/app/tests/unit/routes/v1/admin.spec.js @@ -315,7 +315,7 @@ describe(`${basePath}/forms/:formId/addUser`, () => { const response = await appRequest.put(path).query({ userId: '123' }).send({ userId: '123' }); - expect(rbacService.setFormUsers).toHaveBeenCalledWith(':formId', '123', { userId: '123' }, undefined); + expect(rbacService.setFormUsers).toBeCalledWith(':formId', '123', { userId: '123' }, undefined); expect(response.statusCode).toBe(200); expect(response.body).toBeTruthy(); }); diff --git a/app/tests/unit/routes/v1/form.spec.js b/app/tests/unit/routes/v1/form.spec.js index 40415fa4a..ffdb78efc 100644 --- a/app/tests/unit/routes/v1/form.spec.js +++ b/app/tests/unit/routes/v1/form.spec.js @@ -1344,8 +1344,8 @@ describe(`${basePath}/:formId/versions/:formVersionId/submissions`, () => { const response = await appRequest.post(path).set('Authorization', bearerAuth); - expect(emailService.submissionReceived).toHaveBeenCalledTimes(1); - expect(fileService.moveSubmissionFiles).toHaveBeenCalledTimes(1); + expect(emailService.submissionReceived).toBeCalledTimes(1); + expect(fileService.moveSubmissionFiles).toBeCalledTimes(1); expect(response.statusCode).toBe(201); expect(response.body).toBeTruthy(); }); @@ -1358,8 +1358,8 @@ describe(`${basePath}/:formId/versions/:formVersionId/submissions`, () => { const response = await appRequest.post(path).send({ draft: true }).set('Authorization', bearerAuth); - expect(emailService.submissionReceived).toHaveBeenCalledTimes(0); - expect(fileService.moveSubmissionFiles).toHaveBeenCalledTimes(1); + expect(emailService.submissionReceived).toBeCalledTimes(0); + expect(fileService.moveSubmissionFiles).toBeCalledTimes(1); expect(response.statusCode).toBe(201); expect(response.body).toBeTruthy(); }); @@ -1372,8 +1372,8 @@ describe(`${basePath}/:formId/versions/:formVersionId/submissions`, () => { const response = await appRequest.post(path).send({ draft: false }).set('Authorization', bearerAuth); - expect(emailService.submissionReceived).toHaveBeenCalledTimes(1); - expect(fileService.moveSubmissionFiles).toHaveBeenCalledTimes(1); + expect(emailService.submissionReceived).toBeCalledTimes(1); + expect(fileService.moveSubmissionFiles).toBeCalledTimes(1); expect(response.statusCode).toBe(201); expect(response.body).toBeTruthy(); }); diff --git a/app/tests/unit/routes/v1/submission.spec.js b/app/tests/unit/routes/v1/submission.spec.js index 4df8e4b15..a2871efb7 100644 --- a/app/tests/unit/routes/v1/submission.spec.js +++ b/app/tests/unit/routes/v1/submission.spec.js @@ -545,7 +545,7 @@ describe(`${basePath}/:formSubmissionId/status`, () => { expect(response.statusCode).toBe(200); expect(response.body).toBeTruthy(); - expect(emailService.statusAssigned).toHaveBeenCalledTimes(0); + expect(emailService.statusAssigned).toBeCalledTimes(0); }); it('should handle 401', async () => {