diff --git a/app/src/forms/common/utils.js b/app/src/forms/common/utils.js index b729c51d3..456b8ab88 100644 --- a/app/src/forms/common/utils.js +++ b/app/src/forms/common/utils.js @@ -6,7 +6,13 @@ const _ = require('lodash'); const setupMount = (type, app, routes) => { const p = `/${type}`; - app.use(p, routes); + if (Array.isArray(routes)) { + for (let r of routes) { + app.use(p, r); + } + } else { + app.use(p, routes); + } return p; }; diff --git a/app/src/forms/form/controller.js b/app/src/forms/form/controller.js index cb80a84c8..23833a987 100644 --- a/app/src/forms/form/controller.js +++ b/app/src/forms/form/controller.js @@ -387,52 +387,4 @@ module.exports = { next(error); } }, - listExternalAPIs: async (req, res, next) => { - try { - const response = await service.listExternalAPIs(req.params.formId); - res.status(200).json(response); - } catch (error) { - next(error); - } - }, - listExternalAPIAlgorithms: async (req, res, next) => { - try { - const response = await service.listExternalAPIAlgorithms(); - res.status(200).json(response); - } catch (error) { - next(error); - } - }, - listExternalAPIStatusCodes: async (req, res, next) => { - try { - const response = await service.listExternalAPIStatusCodes(); - res.status(200).json(response); - } catch (error) { - next(error); - } - }, - createExternalAPI: async (req, res, next) => { - try { - const response = await service.createExternalAPI(req.params.formId, req.body, req.currentUser); - res.status(201).json(response); - } catch (error) { - next(error); - } - }, - updateExternalAPI: async (req, res, next) => { - try { - const response = await service.updateExternalAPI(req.params.formId, req.params.externalAPIId, req.body, req.currentUser); - res.status(200).json(response); - } catch (error) { - next(error); - } - }, - deleteExternalAPI: async (req, res, next) => { - try { - await service.deleteExternalAPI(req.params.formId, req.params.externalAPIId); - res.status(204).send(); - } catch (error) { - next(error); - } - }, }; diff --git a/app/src/forms/form/externalApi/controller.js b/app/src/forms/form/externalApi/controller.js new file mode 100644 index 000000000..324a0cbbb --- /dev/null +++ b/app/src/forms/form/externalApi/controller.js @@ -0,0 +1,52 @@ +const service = require('./service'); + +module.exports = { + listExternalAPIs: async (req, res, next) => { + try { + const response = await service.listExternalAPIs(req.params.formId); + res.status(200).json(response); + } catch (error) { + next(error); + } + }, + listExternalAPIAlgorithms: async (req, res, next) => { + try { + const response = await service.listExternalAPIAlgorithms(); + res.status(200).json(response); + } catch (error) { + next(error); + } + }, + listExternalAPIStatusCodes: async (req, res, next) => { + try { + const response = await service.listExternalAPIStatusCodes(); + res.status(200).json(response); + } catch (error) { + next(error); + } + }, + createExternalAPI: async (req, res, next) => { + try { + const response = await service.createExternalAPI(req.params.formId, req.body, req.currentUser); + res.status(201).json(response); + } catch (error) { + next(error); + } + }, + updateExternalAPI: async (req, res, next) => { + try { + const response = await service.updateExternalAPI(req.params.formId, req.params.externalAPIId, req.body, req.currentUser); + res.status(200).json(response); + } catch (error) { + next(error); + } + }, + deleteExternalAPI: async (req, res, next) => { + try { + await service.deleteExternalAPI(req.params.formId, req.params.externalAPIId); + res.status(204).send(); + } catch (error) { + next(error); + } + }, +}; diff --git a/app/src/forms/form/externalApi/index.js b/app/src/forms/form/externalApi/index.js new file mode 100644 index 000000000..e69de29bb diff --git a/app/src/forms/form/externalApi/routes.js b/app/src/forms/form/externalApi/routes.js new file mode 100644 index 000000000..85bb24376 --- /dev/null +++ b/app/src/forms/form/externalApi/routes.js @@ -0,0 +1,36 @@ +const routes = require('express').Router(); +const { currentUser, hasFormPermissions } = require('../../auth/middleware/userAccess'); +const validateParameter = require('../../common/middleware/validateParameter'); +const P = require('../../common/constants').Permissions; + +const controller = require('./controller'); + +routes.use(currentUser); + +routes.param('formId', validateParameter.validateFormId); + +routes.get('/:formId/externalAPIs', hasFormPermissions([P.FORM_READ, P.FORM_UPDATE]), async (req, res, next) => { + await controller.listExternalAPIs(req, res, next); +}); + +routes.post('/:formId/externalAPIs', hasFormPermissions([P.FORM_READ, P.FORM_UPDATE]), async (req, res, next) => { + await controller.createExternalAPI(req, res, next); +}); + +routes.get('/:formId/externalAPIs/algorithms', hasFormPermissions([P.FORM_READ, P.FORM_UPDATE]), async (req, res, next) => { + await controller.listExternalAPIAlgorithms(req, res, next); +}); + +routes.get('/:formId/externalAPIs/statusCodes', hasFormPermissions([P.FORM_READ, P.FORM_UPDATE]), async (req, res, next) => { + await controller.listExternalAPIStatusCodes(req, res, next); +}); + +routes.put('/:formId/externalAPIs/:externalAPIId', hasFormPermissions([P.FORM_READ, P.FORM_UPDATE]), async (req, res, next) => { + await controller.updateExternalAPI(req, res, next); +}); + +routes.delete('/:formId/externalAPIs/:externalAPIId', hasFormPermissions([P.FORM_READ, P.FORM_UPDATE]), async (req, res, next) => { + await controller.deleteExternalAPI(req, res, next); +}); + +module.exports = routes; diff --git a/app/src/forms/form/externalApi/service.js b/app/src/forms/form/externalApi/service.js new file mode 100644 index 000000000..49da60ec4 --- /dev/null +++ b/app/src/forms/form/externalApi/service.js @@ -0,0 +1,120 @@ +const Problem = require('api-problem'); + +const { v4: uuidv4 } = require('uuid'); +const { ExternalAPIStatuses } = require('../../common/constants'); + +const { ExternalAPI, ExternalAPIStatusCode } = require('../../common/models'); + +const { ENCRYPTION_ALGORITHMS } = require('../../../components/encryptionService'); + +const service = { + // ----------------------------------------------------------------------------- + // External API + // ----------------------------------------------------------------------------- + + listExternalAPIs: (formId) => { + return ExternalAPI.query().modify('filterFormId', formId); + }, + + listExternalAPIAlgorithms: () => { + return Object.values(ENCRYPTION_ALGORITHMS).map((x) => ({ + code: x, + display: x, + })); + }, + + listExternalAPIStatusCodes: () => { + return ExternalAPIStatusCode.query(); + }, + + validateExternalAPI: (data) => { + if (!data) { + throw new Problem(422, `'externalAPI record' cannot be empty.`); + } + if (data.sendApiKey) { + if (!data.apiKeyHeader || !data.apiKey) { + throw new Problem(422, `'apiKeyHeader' and 'apiKey' are required when 'sendApiKey' is true.`); + } + } + if (data.sendUserToken) { + if (!data.userTokenHeader) { + throw new Problem(422, `'userTokenHeader' is required when 'sendUserToken' is true.`); + } + } + if (data.sendUserInfo) { + if (data.userInfoEncrypted && !data.userInfoHeader) { + throw new Problem(422, `'userInfoHeader' is required when 'sendUserInfo' and 'userInfoEncrypted' are true.`); + } + if (data.userInfoEncrypted) { + if (!Object.values(ENCRYPTION_ALGORITHMS).includes(data.userInfoEncryptionAlgo)) { + throw new Problem(422, `'${data.userInfoEncryptionAlgo}' is not a valid Encryption Algorithm.`); + } + if (!data.userInfoEncryptionKey) { + throw new Problem(422, `'userInfoEncryptionKey' is required when 'userInfoEncrypted' is true.`); + } + } + } + }, + + createExternalAPI: async (formId, data, currentUser) => { + service.validateExternalAPI(data); + + let trx; + try { + trx = await ExternalAPI.startTransaction(); + data.id = uuidv4(); + // set status to SUBMITTED + data.code = ExternalAPIStatuses.SUBMITTED; + await ExternalAPI.query(trx).insert({ + ...data, + createdBy: currentUser.usernameIdp, + }); + + await trx.commit(); + return ExternalAPI.query().findById(data.id); + } catch (err) { + if (trx) await trx.rollback(); + throw err; + } + }, + + updateExternalAPI: async (formId, externalAPIId, data, currentUser) => { + service.validateExternalAPI(data); + + let trx; + try { + const existing = await ExternalAPI.query().modify('findByIdAndFormId', externalAPIId, formId).first().throwIfNotFound(); + trx = await ExternalAPI.startTransaction(); + // let's use a different method for the administrators to update status code. + // this method should not change the status code. + data.code = existing.code; + await ExternalAPI.query(trx) + .modify('findByIdAndFormId', externalAPIId, formId) + .update({ + ...data, + updatedBy: currentUser.usernameIdp, + }); + + await trx.commit(); + return ExternalAPI.query().findById(externalAPIId); + } catch (err) { + if (trx) await trx.rollback(); + throw err; + } + }, + + deleteExternalAPI: async (formId, externalAPIId) => { + let trx; + try { + await ExternalAPI.query().modify('findByIdAndFormId', externalAPIId, formId).first().throwIfNotFound(); + trx = await ExternalAPI.startTransaction(); + await ExternalAPI.query().deleteById(externalAPIId); + await trx.commit(); + } catch (err) { + if (trx) await trx.rollback(); + throw err; + } + }, +}; + +module.exports = service; diff --git a/app/src/forms/form/index.js b/app/src/forms/form/index.js index 7a5f2c998..06c842dbe 100644 --- a/app/src/forms/form/index.js +++ b/app/src/forms/form/index.js @@ -1,6 +1,9 @@ const routes = require('./routes'); const setupMount = require('../common/utils').setupMount; +const externalApiRoutes = require('./externalApi/routes'); + module.exports.mount = (app) => { - return setupMount('forms', app, routes); + const p = setupMount('forms', app, [routes, externalApiRoutes]); + return p; }; diff --git a/app/src/forms/form/routes.js b/app/src/forms/form/routes.js index b66e57c28..0dfff16d0 100644 --- a/app/src/forms/form/routes.js +++ b/app/src/forms/form/routes.js @@ -171,28 +171,4 @@ routes.put('/:formId/subscriptions', hasFormPermissions([P.FORM_READ, P.FORM_UPD await controller.createOrUpdateSubscriptionDetails(req, res, next); }); -routes.get('/:formId/externalAPIs', hasFormPermissions([P.FORM_READ, P.FORM_UPDATE]), async (req, res, next) => { - await controller.listExternalAPIs(req, res, next); -}); - -routes.post('/:formId/externalAPIs', hasFormPermissions([P.FORM_READ, P.FORM_UPDATE]), async (req, res, next) => { - await controller.createExternalAPI(req, res, next); -}); - -routes.get('/:formId/externalAPIs/algorithms', hasFormPermissions([P.FORM_READ, P.FORM_UPDATE]), async (req, res, next) => { - await controller.listExternalAPIAlgorithms(req, res, next); -}); - -routes.get('/:formId/externalAPIs/statusCodes', hasFormPermissions([P.FORM_READ, P.FORM_UPDATE]), async (req, res, next) => { - await controller.listExternalAPIStatusCodes(req, res, next); -}); - -routes.put('/:formId/externalAPIs/:externalAPIId', hasFormPermissions([P.FORM_READ, P.FORM_UPDATE]), async (req, res, next) => { - await controller.updateExternalAPI(req, res, next); -}); - -routes.delete('/:formId/externalAPIs/:externalAPIId', hasFormPermissions([P.FORM_READ, P.FORM_UPDATE]), async (req, res, next) => { - await controller.deleteExternalAPI(req, res, next); -}); - module.exports = routes; diff --git a/app/src/forms/form/service.js b/app/src/forms/form/service.js index bce20344b..b65f3316d 100644 --- a/app/src/forms/form/service.js +++ b/app/src/forms/form/service.js @@ -1,7 +1,7 @@ const Problem = require('api-problem'); const { ref } = require('objection'); const { v4: uuidv4 } = require('uuid'); -const { EmailTypes, ExternalAPIStatuses } = require('../common/constants'); +const { EmailTypes } = require('../common/constants'); const eventService = require('../event/eventService'); const moment = require('moment'); const { @@ -22,13 +22,10 @@ const { SubmissionMetadata, FormComponentsProactiveHelp, FormSubscription, - ExternalAPI, - ExternalAPIStatusCode, } = require('../common/models'); const { falsey, queryUtils, checkIsFormExpired, validateScheduleObject, typeUtils } = require('../common/utils'); const { Permissions, Roles, Statuses } = require('../common/constants'); const Rolenames = [Roles.OWNER, Roles.TEAM_MANAGER, Roles.FORM_DESIGNER, Roles.SUBMISSION_REVIEWER, Roles.FORM_SUBMITTER, Roles.SUBMISSION_APPROVER]; -const { ENCRYPTION_ALGORITHMS } = require('../../components/encryptionService'); const service = { _findFileIds: (schema, data) => { @@ -1043,114 +1040,6 @@ const service = { throw error; } }, - - // ----------------------------------------------------------------------------- - // External API - // ----------------------------------------------------------------------------- - - listExternalAPIs: (formId) => { - return ExternalAPI.query().modify('filterFormId', formId); - }, - - listExternalAPIAlgorithms: () => { - return Object.values(ENCRYPTION_ALGORITHMS).map((x) => ({ - code: x, - display: x, - })); - }, - - listExternalAPIStatusCodes: () => { - return ExternalAPIStatusCode.query(); - }, - - validateExternalAPI: (data) => { - if (!data) { - throw new Problem(422, `'externalAPI record' cannot be empty.`); - } - if (data.sendApiKey) { - if (!data.apiKeyHeader || !data.apiKey) { - throw new Problem(422, `'apiKeyHeader' and 'apiKey' are required when 'sendApiKey' is true.`); - } - } - if (data.sendUserToken) { - if (!data.userTokenHeader) { - throw new Problem(422, `'userTokenHeader' is required when 'sendUserToken' is true.`); - } - } - if (data.sendUserInfo) { - if (data.userInfoEncrypted && !data.userInfoHeader) { - throw new Problem(422, `'userInfoHeader' is required when 'sendUserInfo' and 'userInfoEncrypted' are true.`); - } - if (data.userInfoEncrypted) { - if (!Object.values(ENCRYPTION_ALGORITHMS).includes(data.userInfoEncryptionAlgo)) { - throw new Problem(422, `'${data.userInfoEncryptionAlgo}' is not a valid Encryption Algorithm.`); - } - if (!data.userInfoEncryptionKey) { - throw new Problem(422, `'userInfoEncryptionKey' is required when 'userInfoEncrypted' is true.`); - } - } - } - }, - - createExternalAPI: async (formId, data, currentUser) => { - service.validateExternalAPI(data); - - let trx; - try { - trx = await ExternalAPI.startTransaction(); - data.id = uuidv4(); - // set status to SUBMITTED - data.code = ExternalAPIStatuses.SUBMITTED; - await ExternalAPI.query(trx).insert({ - ...data, - createdBy: currentUser.usernameIdp, - }); - - await trx.commit(); - return ExternalAPI.query().findById(data.id); - } catch (err) { - if (trx) await trx.rollback(); - throw err; - } - }, - - updateExternalAPI: async (formId, externalAPIId, data, currentUser) => { - service.validateExternalAPI(data); - - let trx; - try { - const existing = await ExternalAPI.query().modify('findByIdAndFormId', externalAPIId, formId).first().throwIfNotFound(); - trx = await ExternalAPI.startTransaction(); - // let's use a different method for the administrators to update status code. - // this method should not change the status code. - data.code = existing.code; - await ExternalAPI.query(trx) - .modify('findByIdAndFormId', externalAPIId, formId) - .update({ - ...data, - updatedBy: currentUser.usernameIdp, - }); - - await trx.commit(); - return ExternalAPI.query().findById(externalAPIId); - } catch (err) { - if (trx) await trx.rollback(); - throw err; - } - }, - - deleteExternalAPI: async (formId, externalAPIId) => { - let trx; - try { - await ExternalAPI.query().modify('findByIdAndFormId', externalAPIId, formId).first().throwIfNotFound(); - trx = await ExternalAPI.startTransaction(); - await ExternalAPI.query().deleteById(externalAPIId); - await trx.commit(); - } catch (err) { - if (trx) await trx.rollback(); - throw err; - } - }, }; module.exports = service; diff --git a/app/tests/unit/forms/form/controller.spec.js b/app/tests/unit/forms/form/controller.spec.js index 6b2019fb9..78e70f65c 100644 --- a/app/tests/unit/forms/form/controller.spec.js +++ b/app/tests/unit/forms/form/controller.spec.js @@ -1,13 +1,9 @@ const { getMockReq, getMockRes } = require('@jest-mock/express'); const { v4: uuidv4 } = require('uuid'); -const { ENCRYPTION_ALGORITHMS } = require('../../../../src/components/encryptionService'); - const controller = require('../../../../src/forms/form/controller'); const exportService = require('../../../../src/forms/form/exportService'); const service = require('../../../../src/forms/form/service'); -const { ExternalAPI } = require('../../../../src/forms/common/models'); -const ExternalAPIStatuses = require('../../../../src/forms/common/constants'); const currentUser = { usernameIdp: 'TESTER', @@ -417,214 +413,3 @@ describe('readFormOptions', () => { expect(next).toBeCalledWith(error); }); }); - -describe('listExternalAPIs', () => { - describe('200 response when', () => { - it('has a successful query call', async () => { - service.listExternalAPIs = jest.fn().mockReturnValue([]); - const req = getMockReq({ - params: { - formId: '123', - }, - }); - const { res, next } = getMockRes(); - - await controller.listExternalAPIs(req, res, next); - - expect(service.listExternalAPIs).toBeCalledWith('123'); - expect(res.status).toBeCalledWith(200); - expect(next).not.toBeCalled(); - }); - }); -}); - -describe('createExternalAPI', () => { - let returnValue = null; - beforeEach(() => { - returnValue = null; - ExternalAPI.mockReturnValue = (value) => { - returnValue = value; - }; - ExternalAPI.query = jest.fn().mockReturnThis(); - ExternalAPI.where = jest.fn().mockReturnThis(); - ExternalAPI.modify = jest.fn().mockReturnThis(); - ExternalAPI.first = jest.fn().mockReturnThis(); - ExternalAPI.startTransaction = jest.fn(); - ExternalAPI.throwIfNotFound = jest.fn().mockReturnThis(); - ExternalAPI.then = jest.fn((done) => { - done(returnValue); - }); - }); - - const formId = uuidv4(); - const externalApi = { - id: uuidv4(), - formId: formId, - name: 'test_api', - endpointUrl: 'http://external.api/', - sendApiKey: true, - apiKeyHeader: 'X-API-KEY', - apiKey: 'my-api-key', - sendUserToken: true, - userTokenHeader: 'Authorization', - userTokenBearer: true, - sendUserInfo: true, - userInfoHeader: 'X-API-USER', - userInfoEncrypted: true, - userInfoEncryptionKey: '0489aa2a7882dc53be7c76db43be1800e56627c31a88a0011d85ccc255b79d00', - userInfoEncryptionAlgo: ENCRYPTION_ALGORITHMS.AES_256_GCM, - code: ExternalAPIStatuses.SUBMITTED, - }; - const validRequest = { - body: { - ...externalApi, - }, - currentUser: currentUser, - params: { - formId: formId, - }, - }; - - describe('error response when', () => { - it('has no current user', async () => { - const invalidRequest = { ...validRequest }; - delete invalidRequest.currentUser; - const req = getMockReq(invalidRequest); - const { res, next } = getMockRes(); - - await controller.createExternalAPI(req, res, next); - - expect(res.json).not.toBeCalled(); - expect(res.status).not.toBeCalled(); - expect(next).toBeCalledWith(expect.any(TypeError)); - }); - - it('has an unsuccessful service call', async () => { - service.createExternalAPI = jest.fn().mockRejectedValue(error); - const req = getMockReq(validRequest); - const { res, next } = getMockRes(); - - await controller.createExternalAPI(req, res, next); - - expect(service.createExternalAPI).toBeCalledWith(validRequest.params.formId, validRequest.body, validRequest.currentUser); - expect(res.json).not.toBeCalled(); - expect(res.status).not.toBeCalled(); - expect(next).toBeCalledWith(error); - }); - }); - - describe('200 response when', () => { - it('has a successful service call', async () => { - service.createExternalAPI = jest.fn().mockResolvedValue(externalApi); - const req = getMockReq(validRequest); - const { res, next } = getMockRes(); - - await controller.createExternalAPI(req, res, next); - - expect(service.createExternalAPI).toBeCalledWith(validRequest.params.formId, validRequest.body, validRequest.currentUser); - expect(res.json).toBeCalledWith( - expect.objectContaining({ - ...externalApi, - }) - ); - expect(res.status).toBeCalledWith(200); - expect(next).not.toBeCalled(); - }); - }); -}); -describe('updateExternalAPI', () => { - let returnValue = null; - beforeEach(() => { - returnValue = null; - ExternalAPI.mockReturnValue = (value) => { - returnValue = value; - }; - ExternalAPI.query = jest.fn().mockReturnThis(); - ExternalAPI.where = jest.fn().mockReturnThis(); - ExternalAPI.modify = jest.fn().mockReturnThis(); - ExternalAPI.first = jest.fn().mockReturnThis(); - ExternalAPI.startTransaction = jest.fn(); - ExternalAPI.throwIfNotFound = jest.fn().mockReturnThis(); - ExternalAPI.then = jest.fn((done) => { - done(returnValue); - }); - }); - - const formId = uuidv4(); - const externalAPIId = uuidv4(); - const externalApi = { - id: externalAPIId, - formId: formId, - name: 'test_api', - endpointUrl: 'http://external.api/', - sendApiKey: true, - apiKeyHeader: 'X-API-KEY', - apiKey: 'my-api-key', - sendUserToken: true, - userTokenHeader: 'Authorization', - userTokenBearer: true, - sendUserInfo: true, - userInfoHeader: 'X-API-USER', - userInfoEncrypted: true, - userInfoEncryptionKey: '0489aa2a7882dc53be7c76db43be1800e56627c31a88a0011d85ccc255b79d00', - userInfoEncryptionAlgo: ENCRYPTION_ALGORITHMS.AES_256_GCM, - code: ExternalAPIStatuses.SUBMITTED, - }; - const validRequest = { - body: { - ...externalApi, - }, - currentUser: currentUser, - params: { - formId: formId, - externalAPIId: externalAPIId, - }, - }; - - describe('error response when', () => { - it('has no current user', async () => { - const invalidRequest = { ...validRequest }; - delete invalidRequest.currentUser; - const req = getMockReq(invalidRequest); - const { res, next } = getMockRes(); - - await controller.updateExternalAPI(req, res, next); - - expect(res.json).not.toBeCalled(); - expect(res.status).not.toBeCalled(); - expect(next).toBeCalledWith(expect.any(TypeError)); - }); - - it('has an unsuccessful service call', async () => { - service.updateExternalAPI = jest.fn().mockRejectedValue(error); - const req = getMockReq(validRequest); - const { res, next } = getMockRes(); - - await controller.updateExternalAPI(req, res, next); - - expect(service.updateExternalAPI).toBeCalledWith(validRequest.params.formId, validRequest.params.externalAPIId, validRequest.body, validRequest.currentUser); - expect(res.json).not.toBeCalled(); - expect(res.status).not.toBeCalled(); - expect(next).toBeCalledWith(error); - }); - }); - - describe('200 response when', () => { - it('has a successful service call', async () => { - service.updateExternalAPI = jest.fn().mockResolvedValue(externalApi); - const req = getMockReq(validRequest); - const { res, next } = getMockRes(); - - await controller.updateExternalAPI(req, res, next); - - expect(service.updateExternalAPI).toBeCalledWith(validRequest.params.formId, validRequest.params.externalAPIId, validRequest.body, validRequest.currentUser); - expect(res.json).toBeCalledWith( - expect.objectContaining({ - ...externalApi, - }) - ); - expect(res.status).toBeCalledWith(200); - expect(next).not.toBeCalled(); - }); - }); -}); diff --git a/app/tests/unit/forms/form/externalApi/controller.spec.js b/app/tests/unit/forms/form/externalApi/controller.spec.js new file mode 100644 index 000000000..b83245b8f --- /dev/null +++ b/app/tests/unit/forms/form/externalApi/controller.spec.js @@ -0,0 +1,343 @@ +const { getMockReq, getMockRes } = require('@jest-mock/express'); +const { v4: uuidv4 } = require('uuid'); + +const { ENCRYPTION_ALGORITHMS } = require('../../../../../src/components/encryptionService'); + +const controller = require('../../../../../src/forms/form/externalApi/controller'); +const service = require('../../../../../src/forms/form/externalApi/service'); +const { ExternalAPI } = require('../../../../../src/forms/common/models'); +const ExternalAPIStatuses = require('../../../../../src/forms/common/constants'); + +const currentUser = { + usernameIdp: 'TESTER', +}; + +const error = new Error('error'); + +afterEach(() => { + jest.restoreAllMocks(); +}); + +describe('listExternalAPIs', () => { + describe('200 response when', () => { + it('has a successful query call', async () => { + service.listExternalAPIs = jest.fn().mockReturnValue([]); + const req = getMockReq({ + params: { + formId: '123', + }, + }); + const { res, next } = getMockRes(); + + await controller.listExternalAPIs(req, res, next); + + expect(service.listExternalAPIs).toBeCalledWith('123'); + expect(res.status).toBeCalledWith(200); + expect(next).not.toBeCalled(); + }); + }); +}); + +describe('listExternalAPIAlgorithms', () => { + describe('200 response when', () => { + it('has a successful query call', async () => { + service.listExternalAPIAlgorithms = jest.fn().mockReturnValue([]); + const req = getMockReq({ + params: { + formId: '123', + }, + }); + const { res, next } = getMockRes(); + + await controller.listExternalAPIAlgorithms(req, res, next); + + expect(service.listExternalAPIAlgorithms).toBeCalled(); // service doesn't use form id + expect(res.status).toBeCalledWith(200); + expect(next).not.toBeCalled(); + }); + }); +}); + +describe('listExternalAPIStatusCodes', () => { + describe('200 response when', () => { + it('has a successful query call', async () => { + service.listExternalAPIStatusCodes = jest.fn().mockReturnValue([]); + const req = getMockReq({ + params: { + formId: '123', + }, + }); + const { res, next } = getMockRes(); + + await controller.listExternalAPIStatusCodes(req, res, next); + + expect(service.listExternalAPIStatusCodes).toBeCalled(); // service doesn't use form id + expect(res.status).toBeCalledWith(200); + expect(next).not.toBeCalled(); + }); + }); +}); + +describe('createExternalAPI', () => { + let returnValue = null; + beforeEach(() => { + returnValue = null; + ExternalAPI.mockReturnValue = (value) => { + returnValue = value; + }; + ExternalAPI.query = jest.fn().mockReturnThis(); + ExternalAPI.where = jest.fn().mockReturnThis(); + ExternalAPI.modify = jest.fn().mockReturnThis(); + ExternalAPI.first = jest.fn().mockReturnThis(); + ExternalAPI.startTransaction = jest.fn(); + ExternalAPI.throwIfNotFound = jest.fn().mockReturnThis(); + ExternalAPI.then = jest.fn((done) => { + done(returnValue); + }); + }); + + const formId = uuidv4(); + const externalApi = { + id: uuidv4(), + formId: formId, + name: 'test_api', + endpointUrl: 'http://external.api/', + sendApiKey: true, + apiKeyHeader: 'X-API-KEY', + apiKey: 'my-api-key', + sendUserToken: true, + userTokenHeader: 'Authorization', + userTokenBearer: true, + sendUserInfo: true, + userInfoHeader: 'X-API-USER', + userInfoEncrypted: true, + userInfoEncryptionKey: '0489aa2a7882dc53be7c76db43be1800e56627c31a88a0011d85ccc255b79d00', + userInfoEncryptionAlgo: ENCRYPTION_ALGORITHMS.AES_256_GCM, + code: ExternalAPIStatuses.SUBMITTED, + }; + const validRequest = { + body: { + ...externalApi, + }, + currentUser: currentUser, + params: { + formId: formId, + }, + }; + + describe('error response when', () => { + it('has no current user', async () => { + const invalidRequest = { ...validRequest }; + delete invalidRequest.currentUser; + const req = getMockReq(invalidRequest); + const { res, next } = getMockRes(); + + await controller.createExternalAPI(req, res, next); + + expect(res.json).not.toBeCalled(); + expect(res.status).not.toBeCalled(); + expect(next).toBeCalledWith(expect.any(TypeError)); + }); + + it('has an unsuccessful service call', async () => { + service.createExternalAPI = jest.fn().mockRejectedValue(error); + const req = getMockReq(validRequest); + const { res, next } = getMockRes(); + + await controller.createExternalAPI(req, res, next); + + expect(service.createExternalAPI).toBeCalledWith(validRequest.params.formId, validRequest.body, validRequest.currentUser); + expect(res.json).not.toBeCalled(); + expect(res.status).not.toBeCalled(); + expect(next).toBeCalledWith(error); + }); + }); + + describe('200 response when', () => { + it('has a successful service call', async () => { + service.createExternalAPI = jest.fn().mockResolvedValue(externalApi); + const req = getMockReq(validRequest); + const { res, next } = getMockRes(); + + await controller.createExternalAPI(req, res, next); + + expect(service.createExternalAPI).toBeCalledWith(validRequest.params.formId, validRequest.body, validRequest.currentUser); + expect(res.json).toBeCalledWith( + expect.objectContaining({ + ...externalApi, + }) + ); + expect(res.status).toBeCalledWith(201); + expect(next).not.toBeCalled(); + }); + }); +}); + +describe('updateExternalAPI', () => { + let returnValue = null; + beforeEach(() => { + returnValue = null; + ExternalAPI.mockReturnValue = (value) => { + returnValue = value; + }; + ExternalAPI.query = jest.fn().mockReturnThis(); + ExternalAPI.where = jest.fn().mockReturnThis(); + ExternalAPI.modify = jest.fn().mockReturnThis(); + ExternalAPI.first = jest.fn().mockReturnThis(); + ExternalAPI.startTransaction = jest.fn(); + ExternalAPI.throwIfNotFound = jest.fn().mockReturnThis(); + ExternalAPI.then = jest.fn((done) => { + done(returnValue); + }); + }); + + const formId = uuidv4(); + const externalAPIId = uuidv4(); + const externalApi = { + id: externalAPIId, + formId: formId, + name: 'test_api', + endpointUrl: 'http://external.api/', + sendApiKey: true, + apiKeyHeader: 'X-API-KEY', + apiKey: 'my-api-key', + sendUserToken: true, + userTokenHeader: 'Authorization', + userTokenBearer: true, + sendUserInfo: true, + userInfoHeader: 'X-API-USER', + userInfoEncrypted: true, + userInfoEncryptionKey: '0489aa2a7882dc53be7c76db43be1800e56627c31a88a0011d85ccc255b79d00', + userInfoEncryptionAlgo: ENCRYPTION_ALGORITHMS.AES_256_GCM, + code: ExternalAPIStatuses.SUBMITTED, + }; + const validRequest = { + body: { + ...externalApi, + }, + currentUser: currentUser, + params: { + formId: formId, + externalAPIId: externalAPIId, + }, + }; + + describe('error response when', () => { + it('has no current user', async () => { + const invalidRequest = { ...validRequest }; + delete invalidRequest.currentUser; + const req = getMockReq(invalidRequest); + const { res, next } = getMockRes(); + + await controller.updateExternalAPI(req, res, next); + + expect(res.json).not.toBeCalled(); + expect(res.status).not.toBeCalled(); + expect(next).toBeCalledWith(expect.any(TypeError)); + }); + + it('has an unsuccessful service call', async () => { + service.updateExternalAPI = jest.fn().mockRejectedValue(error); + const req = getMockReq(validRequest); + const { res, next } = getMockRes(); + + await controller.updateExternalAPI(req, res, next); + + expect(service.updateExternalAPI).toBeCalledWith(validRequest.params.formId, validRequest.params.externalAPIId, validRequest.body, validRequest.currentUser); + expect(res.json).not.toBeCalled(); + expect(res.status).not.toBeCalled(); + expect(next).toBeCalledWith(error); + }); + }); + + describe('200 response when', () => { + it('has a successful service call', async () => { + service.updateExternalAPI = jest.fn().mockResolvedValue(externalApi); + const req = getMockReq(validRequest); + const { res, next } = getMockRes(); + + await controller.updateExternalAPI(req, res, next); + + expect(service.updateExternalAPI).toBeCalledWith(validRequest.params.formId, validRequest.params.externalAPIId, validRequest.body, validRequest.currentUser); + expect(res.json).toBeCalledWith( + expect.objectContaining({ + ...externalApi, + }) + ); + expect(res.status).toBeCalledWith(200); + expect(next).not.toBeCalled(); + }); + }); +}); + +describe('deleteExternalAPI', () => { + let returnValue = null; + beforeEach(() => { + returnValue = null; + ExternalAPI.mockReturnValue = (value) => { + returnValue = value; + }; + ExternalAPI.query = jest.fn().mockReturnThis(); + ExternalAPI.where = jest.fn().mockReturnThis(); + ExternalAPI.modify = jest.fn().mockReturnThis(); + ExternalAPI.first = jest.fn().mockReturnThis(); + ExternalAPI.startTransaction = jest.fn(); + ExternalAPI.throwIfNotFound = jest.fn().mockReturnThis(); + ExternalAPI.then = jest.fn((done) => { + done(returnValue); + }); + }); + + const formId = uuidv4(); + const externalAPIId = uuidv4(); + + const validRequest = { + currentUser: currentUser, + params: { + formId: formId, + externalAPIId: externalAPIId, + }, + }; + + describe('error response when', () => { + it('has no current user', async () => { + const invalidRequest = { ...validRequest }; + delete invalidRequest.currentUser; + const req = getMockReq(invalidRequest); + const { res, next } = getMockRes(); + + await controller.deleteExternalAPI(req, res, next); + + expect(res.json).not.toBeCalled(); + expect(res.status).not.toBeCalled(); + expect(next).toBeCalledWith(expect.any(TypeError)); + }); + + it('has an unsuccessful service call', async () => { + service.deleteExternalAPI = jest.fn().mockRejectedValue(error); + const req = getMockReq(validRequest); + const { res, next } = getMockRes(); + + await controller.deleteExternalAPI(req, res, next); + + expect(service.deleteExternalAPI).toBeCalledWith(validRequest.params.formId, validRequest.params.externalAPIId); + expect(res.json).not.toBeCalled(); + expect(res.status).not.toBeCalled(); + expect(next).toBeCalledWith(error); + }); + }); + + describe('200 response when', () => { + it('has a successful service call', async () => { + service.deleteExternalAPI = jest.fn().mockResolvedValue(); + const req = getMockReq(validRequest); + const { res, next } = getMockRes(); + + await controller.deleteExternalAPI(req, res, next); + + expect(service.deleteExternalAPI).toBeCalledWith(validRequest.params.formId, validRequest.params.externalAPIId); + expect(res.status).toBeCalledWith(204); + expect(next).not.toBeCalled(); + }); + }); +}); diff --git a/app/tests/unit/forms/form/externalApi/routes.spec.js b/app/tests/unit/forms/form/externalApi/routes.spec.js new file mode 100644 index 000000000..283496888 --- /dev/null +++ b/app/tests/unit/forms/form/externalApi/routes.spec.js @@ -0,0 +1,167 @@ +const request = require('supertest'); +const { v4: uuidv4 } = require('uuid'); + +const { expressHelper } = require('../../../../common/helper'); + +const apiAccess = require('../../../../../src/forms/auth/middleware/apiAccess'); +const userAccess = require('../../../../../src/forms/auth/middleware/userAccess'); +const rateLimiter = require('../../../../../src/forms/common/middleware/rateLimiter'); +const validateParameter = require('../../../../../src/forms/common/middleware/validateParameter'); +const controller = require('../../../../../src/forms/form/externalApi/controller'); + +// +// Mock out all the middleware - we're testing that the routes are set up +// correctly, not the functionality of the middleware. +// +// +// mock middleware +// +const jwtService = require('../../../../../src/components/jwtService'); + +// +// test assumes that caller has appropriate token, we are not testing middleware here... +// +jwtService.protect = jest.fn(() => { + return jest.fn((_req, _res, next) => { + next(); + }); +}); + +jest.mock('../../../../../src/forms/auth/middleware/apiAccess'); +apiAccess.mockImplementation( + jest.fn((_req, _res, next) => { + next(); + }) +); + +controller.documentTemplateCreate = jest.fn((_req, _res, next) => { + next(); +}); +controller.documentTemplateDelete = jest.fn((_req, _res, next) => { + next(); +}); +controller.documentTemplateList = jest.fn((_req, _res, next) => { + next(); +}); +controller.documentTemplateRead = jest.fn((_req, _res, next) => { + next(); +}); + +rateLimiter.apiKeyRateLimiter = jest.fn((_req, _res, next) => { + next(); +}); + +const hasFormPermissionsMock = jest.fn((_req, _res, next) => { + next(); +}); + +userAccess.currentUser = jest.fn((_req, _res, next) => { + next(); +}); + +userAccess.hasFormPermissions = jest.fn(() => { + return hasFormPermissionsMock; +}); + +validateParameter.validateDocumentTemplateId = jest.fn((_req, _res, next) => { + next(); +}); +validateParameter.validateFormId = jest.fn((_req, _res, next) => { + next(); +}); + +const formId = uuidv4(); + +// +// Create the router and a simple Express server. +// + +const router = require('../../../../../src/forms/form/externalApi/routes'); +const basePath = '/form'; +const app = expressHelper(basePath, router); +const appRequest = request(app); + +afterEach(() => { + jest.clearAllMocks(); +}); + +describe(`${basePath}/:formId/externalAPIs`, () => { + const path = `${basePath}/${formId}/externalAPIs`; + controller.listExternalAPIs = jest.fn((_req, _res, next) => { + next(); + }); + controller.createExternalAPI = jest.fn((_req, _res, next) => { + next(); + }); + + it('should return 404 for DELETE', async () => { + const response = await appRequest.delete(path); + + expect(response.statusCode).toBe(404); + }); + + it('should return 404 for PUT', async () => { + const response = await appRequest.put(path); + + expect(response.statusCode).toBe(404); + }); + + it('should have correct middleware for GET', async () => { + await appRequest.get(path); + + expect(apiAccess).toBeCalledTimes(0); + expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(0); + expect(hasFormPermissionsMock).toBeCalledTimes(1); + expect(controller.listExternalAPIs).toBeCalledTimes(1); + }); + + it('should have correct middleware for POST', async () => { + await appRequest.post(path); + + expect(apiAccess).toBeCalledTimes(0); + expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(0); + expect(hasFormPermissionsMock).toBeCalledTimes(1); + expect(controller.createExternalAPI).toBeCalledTimes(1); + }); +}); + +describe(`${basePath}/:formId/externalAPIs/:externalAPIId`, () => { + const externalAPIId = uuidv4(); + const path = `${basePath}/${formId}/externalAPIs/${externalAPIId}`; + controller.updateExternalAPI = jest.fn((_req, _res, next) => { + next(); + }); + controller.deleteExternalAPI = jest.fn((_req, _res, next) => { + next(); + }); + + it('should return 404 for POST', async () => { + const response = await appRequest.post(path); + + expect(response.statusCode).toBe(404); + }); + + it('should return 404 for GET', async () => { + const response = await appRequest.get(path); + + expect(response.statusCode).toBe(404); + }); + + it('should have correct middleware for PUT', async () => { + await appRequest.put(path); + + expect(apiAccess).toBeCalledTimes(0); + expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(0); + expect(hasFormPermissionsMock).toBeCalledTimes(1); + expect(controller.updateExternalAPI).toBeCalledTimes(1); + }); + + it('hould have correct middleware for DELETE', async () => { + await appRequest.delete(path); + + expect(apiAccess).toBeCalledTimes(0); + expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(0); + expect(hasFormPermissionsMock).toBeCalledTimes(1); + expect(controller.deleteExternalAPI).toBeCalledTimes(1); + }); +}); diff --git a/app/tests/unit/forms/form/externalApi/service.spec.js b/app/tests/unit/forms/form/externalApi/service.spec.js new file mode 100644 index 000000000..2a8b22fad --- /dev/null +++ b/app/tests/unit/forms/form/externalApi/service.spec.js @@ -0,0 +1,197 @@ +const { MockModel, MockTransaction } = require('../../../../common/dbHelper'); + +const { v4: uuidv4 } = require('uuid'); + +const { ExternalAPIStatuses } = require('../../../../../src/forms/common/constants'); +const service = require('../../../../../src/forms/form/externalApi/service'); +const { ENCRYPTION_ALGORITHMS } = require('../../../../../src/components/encryptionService'); + +jest.mock('../../../../../src/forms/common/models/tables/externalAPI', () => MockModel); + +beforeEach(() => { + MockModel.mockReset(); + MockTransaction.mockReset(); +}); + +afterEach(() => { + jest.restoreAllMocks(); +}); + +describe('validateExternalAPI', () => { + let validData = null; + beforeEach(() => { + validData = { + id: uuidv4(), + formId: uuidv4(), + name: 'test_api', + endpointUrl: 'http://external.api/', + sendApiKey: true, + apiKeyHeader: 'X-API-KEY', + apiKey: 'my-api-key', + sendUserToken: true, + userTokenHeader: 'Authorization', + userTokenBearer: true, + sendUserInfo: true, + userInfoHeader: 'X-API-USER', + userInfoEncrypted: true, + userInfoEncryptionKey: '0489aa2a7882dc53be7c76db43be1800e56627c31a88a0011d85ccc255b79d00', + userInfoEncryptionAlgo: ENCRYPTION_ALGORITHMS.AES_256_GCM, + code: ExternalAPIStatuses.SUBMITTED, + }; + }); + it('should not throw errors with valid data', () => { + service.validateExternalAPI(validData); + }); + + it('should throw 422 with no data', () => { + expect(() => service.validateExternalAPI(undefined)).toThrow(); + }); + + it('should throw 422 when userInfo encryption options are invalid', () => { + validData.userInfoEncryptionKey = null; + expect(() => service.validateExternalAPI(undefined)).toThrow(); + }); + + it('should throw 422 when userInfoEncryptionAlgo is invalid', () => { + validData.userInfoEncryptionAlgo = 'not valid!'; + expect(() => service.validateExternalAPI(undefined)).toThrow(); + }); + + it('should throw 422 when sendApiKey is true with no header', () => { + validData.apiKeyHeader = null; + expect(() => service.validateExternalAPI(undefined)).toThrow(); + }); + + it('should throw 422 when sendApiKey is true with no key', () => { + validData.apiKey = null; + expect(() => service.validateExternalAPI(undefined)).toThrow(); + }); + + it('should throw 422 when sendUserInfo (encrypted) is true with no header', () => { + validData.userInfoHeader = null; + expect(() => service.validateExternalAPI(undefined)).toThrow(); + }); + + it('should throw 422 when sendUserInfo (encrypted) is true with no header', () => { + validData.userInfoHeader = null; + expect(() => service.validateExternalAPI(undefined)).toThrow(); + }); +}); + +describe('createExternalAPI', () => { + const user = { usernameIdp: 'username' }; + let validData = null; + + beforeEach(() => { + MockModel.mockReset(); + MockTransaction.mockReset(); + validData = { + id: uuidv4(), + formId: uuidv4(), + name: 'test_api', + endpointUrl: 'http://external.api/', + sendApiKey: true, + apiKeyHeader: 'X-API-KEY', + apiKey: 'my-api-key', + sendUserToken: true, + userTokenHeader: 'Authorization', + userTokenBearer: true, + sendUserInfo: true, + userInfoHeader: 'X-API-USER', + userInfoEncrypted: true, + userInfoEncryptionKey: '0489aa2a7882dc53be7c76db43be1800e56627c31a88a0011d85ccc255b79d00', + userInfoEncryptionAlgo: ENCRYPTION_ALGORITHMS.AES_256_GCM, + code: ExternalAPIStatuses.SUBMITTED, + }; + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should insert valid data', async () => { + validData.id = null; + validData.code = null; + await service.createExternalAPI(validData.formId, validData, user); + expect(MockModel.insert).toBeCalledTimes(1); + expect(MockModel.insert).toBeCalledWith({ + createdBy: user.usernameIdp, + code: ExternalAPIStatuses.SUBMITTED, + ...validData, + }); + expect(MockTransaction.commit).toBeCalledTimes(1); + }); + + it('should rollback on error', async () => { + MockModel.insert = jest.fn().mockRejectedValueOnce(new Error('SQL Error')); + + await expect(service.createExternalAPI(validData.formId, validData, user)).rejects.toThrow(); + + expect(MockTransaction.rollback).toBeCalledTimes(1); + }); +}); + +describe('updateExternalAPI', () => { + const user = { usernameIdp: 'username' }; + let validData = null; + + beforeEach(() => { + MockModel.mockReset(); + MockTransaction.mockReset(); + validData = { + id: uuidv4(), + formId: uuidv4(), + name: 'test_api', + endpointUrl: 'http://external.api/', + sendApiKey: true, + apiKeyHeader: 'X-API-KEY', + apiKey: 'my-api-key', + sendUserToken: true, + userTokenHeader: 'Authorization', + userTokenBearer: true, + sendUserInfo: true, + userInfoHeader: 'X-API-USER', + userInfoEncrypted: true, + userInfoEncryptionKey: '0489aa2a7882dc53be7c76db43be1800e56627c31a88a0011d85ccc255b79d00', + userInfoEncryptionAlgo: ENCRYPTION_ALGORITHMS.AES_256_GCM, + code: ExternalAPIStatuses.SUBMITTED, + }; + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should update valid data', async () => { + MockModel.throwIfNotFound = jest.fn().mockResolvedValueOnce(Object.assign({}, validData)); + + // we do not update (status) code - must stay SUBMITTED + validData.code = ExternalAPIStatuses.APPROVED; + + await service.updateExternalAPI(validData.formId, validData.id, validData, user); + expect(MockModel.update).toBeCalledTimes(1); + expect(MockModel.update).toBeCalledWith({ + updatedBy: user.usernameIdp, + code: ExternalAPIStatuses.SUBMITTED, + ...validData, + }); + expect(MockTransaction.commit).toBeCalledTimes(1); + }); + + it('should not commit when not found', async () => { + MockModel.throwIfNotFound = jest.fn().mockRejectedValueOnce(new Error('SQL Error')); + + await expect(service.updateExternalAPI(validData.formId, validData.id, validData, user)).rejects.toThrow(); + // shouldn't start the transaction + expect(MockModel.startTransaction).toBeCalledTimes(0); + expect(MockTransaction.commit).toBeCalledTimes(0); + }); + + it('should rollback on error', async () => { + MockModel.update = jest.fn().mockRejectedValueOnce(new Error('SQL Error')); + + await expect(service.updateExternalAPI(validData.formId, validData.id, validData, user)).rejects.toThrow(); + + expect(MockTransaction.rollback).toBeCalledTimes(1); + }); +}); diff --git a/app/tests/unit/forms/form/routes.spec.js b/app/tests/unit/forms/form/routes.spec.js index d2fe5bb3e..242958cd8 100644 --- a/app/tests/unit/forms/form/routes.spec.js +++ b/app/tests/unit/forms/form/routes.spec.js @@ -135,84 +135,3 @@ describe(`${basePath}/:formId/documentTemplates/:documentTemplateId`, () => { expect(controller.documentTemplateRead).toBeCalledTimes(1); }); }); - -describe(`${basePath}/:formId/externalAPIs`, () => { - const path = `${basePath}/${formId}/externalAPIs`; - controller.listExternalAPIs = jest.fn((_req, _res, next) => { - next(); - }); - controller.createExternalAPI = jest.fn((_req, _res, next) => { - next(); - }); - - it('should return 404 for DELETE', async () => { - const response = await appRequest.delete(path); - - expect(response.statusCode).toBe(404); - }); - - it('should return 404 for PUT', async () => { - const response = await appRequest.put(path); - - expect(response.statusCode).toBe(404); - }); - - it('should have correct middleware for GET', async () => { - await appRequest.get(path); - - expect(apiAccess).toBeCalledTimes(0); - expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(0); - expect(hasFormPermissionsMock).toBeCalledTimes(1); - expect(controller.listExternalAPIs).toBeCalledTimes(1); - }); - - it('should have correct middleware for POST', async () => { - await appRequest.post(path); - - expect(apiAccess).toBeCalledTimes(0); - expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(0); - expect(hasFormPermissionsMock).toBeCalledTimes(1); - expect(controller.createExternalAPI).toBeCalledTimes(1); - }); -}); - -describe(`${basePath}/:formId/externalAPIs/:externalAPIId`, () => { - const externalAPIId = uuidv4(); - const path = `${basePath}/${formId}/externalAPIs/${externalAPIId}`; - controller.updateExternalAPI = jest.fn((_req, _res, next) => { - next(); - }); - controller.deleteExternalAPI = jest.fn((_req, _res, next) => { - next(); - }); - - it('should return 404 for POST', async () => { - const response = await appRequest.post(path); - - expect(response.statusCode).toBe(404); - }); - - it('should return 404 for GET', async () => { - const response = await appRequest.get(path); - - expect(response.statusCode).toBe(404); - }); - - it('should have correct middleware for PUT', async () => { - await appRequest.put(path); - - expect(apiAccess).toBeCalledTimes(0); - expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(0); - expect(hasFormPermissionsMock).toBeCalledTimes(1); - expect(controller.updateExternalAPI).toBeCalledTimes(1); - }); - - it('hould have correct middleware for DELETE', async () => { - await appRequest.delete(path); - - expect(apiAccess).toBeCalledTimes(0); - expect(rateLimiter.apiKeyRateLimiter).toBeCalledTimes(0); - expect(hasFormPermissionsMock).toBeCalledTimes(1); - expect(controller.deleteExternalAPI).toBeCalledTimes(1); - }); -}); diff --git a/app/tests/unit/forms/form/service.spec.js b/app/tests/unit/forms/form/service.spec.js index 418e76ad0..8e91daa48 100644 --- a/app/tests/unit/forms/form/service.spec.js +++ b/app/tests/unit/forms/form/service.spec.js @@ -2,14 +2,12 @@ const { MockModel, MockTransaction } = require('../../../common/dbHelper'); const { v4: uuidv4 } = require('uuid'); -const { EmailTypes, ExternalAPIStatuses } = require('../../../../src/forms/common/constants'); +const { EmailTypes } = require('../../../../src/forms/common/constants'); const service = require('../../../../src/forms/form/service'); -const { ENCRYPTION_ALGORITHMS } = require('../../../../src/components/encryptionService'); jest.mock('../../../../src/forms/common/models/tables/documentTemplate', () => MockModel); jest.mock('../../../../src/forms/common/models/tables/formEmailTemplate', () => MockModel); jest.mock('../../../../src/forms/common/models/views/submissionMetadata', () => MockModel); -jest.mock('../../../../src/forms/common/models/tables/externalAPI', () => MockModel); const documentTemplateId = uuidv4(); const formId = uuidv4(); @@ -818,182 +816,3 @@ describe('createOrUpdateEmailTemplates', () => { expect(MockTransaction.rollback).toBeCalledTimes(1); }); }); - -describe('validateExternalAPI', () => { - let validData = null; - beforeEach(() => { - validData = { - id: uuidv4(), - formId: uuidv4(), - name: 'test_api', - endpointUrl: 'http://external.api/', - sendApiKey: true, - apiKeyHeader: 'X-API-KEY', - apiKey: 'my-api-key', - sendUserToken: true, - userTokenHeader: 'Authorization', - userTokenBearer: true, - sendUserInfo: true, - userInfoHeader: 'X-API-USER', - userInfoEncrypted: true, - userInfoEncryptionKey: '0489aa2a7882dc53be7c76db43be1800e56627c31a88a0011d85ccc255b79d00', - userInfoEncryptionAlgo: ENCRYPTION_ALGORITHMS.AES_256_GCM, - code: ExternalAPIStatuses.SUBMITTED, - }; - }); - it('should not throw errors with valid data', () => { - service.validateExternalAPI(validData); - }); - - it('should throw 422 with no data', () => { - expect(() => service.validateExternalAPI(undefined)).toThrow(); - }); - - it('should throw 422 when userInfo encryption options are invalid', () => { - validData.userInfoEncryptionKey = null; - expect(() => service.validateExternalAPI(undefined)).toThrow(); - }); - - it('should throw 422 when userInfoEncryptionAlgo is invalid', () => { - validData.userInfoEncryptionAlgo = 'not valid!'; - expect(() => service.validateExternalAPI(undefined)).toThrow(); - }); - - it('should throw 422 when sendApiKey is true with no header', () => { - validData.apiKeyHeader = null; - expect(() => service.validateExternalAPI(undefined)).toThrow(); - }); - - it('should throw 422 when sendApiKey is true with no key', () => { - validData.apiKey = null; - expect(() => service.validateExternalAPI(undefined)).toThrow(); - }); - - it('should throw 422 when sendUserInfo (encrypted) is true with no header', () => { - validData.userInfoHeader = null; - expect(() => service.validateExternalAPI(undefined)).toThrow(); - }); - - it('should throw 422 when sendUserInfo (encrypted) is true with no header', () => { - validData.userInfoHeader = null; - expect(() => service.validateExternalAPI(undefined)).toThrow(); - }); -}); - -describe('createExternalAPI', () => { - const user = { usernameIdp: 'username' }; - let validData = null; - - beforeEach(() => { - MockModel.mockReset(); - MockTransaction.mockReset(); - validData = { - id: uuidv4(), - formId: uuidv4(), - name: 'test_api', - endpointUrl: 'http://external.api/', - sendApiKey: true, - apiKeyHeader: 'X-API-KEY', - apiKey: 'my-api-key', - sendUserToken: true, - userTokenHeader: 'Authorization', - userTokenBearer: true, - sendUserInfo: true, - userInfoHeader: 'X-API-USER', - userInfoEncrypted: true, - userInfoEncryptionKey: '0489aa2a7882dc53be7c76db43be1800e56627c31a88a0011d85ccc255b79d00', - userInfoEncryptionAlgo: ENCRYPTION_ALGORITHMS.AES_256_GCM, - code: ExternalAPIStatuses.SUBMITTED, - }; - }); - - afterEach(() => { - jest.restoreAllMocks(); - }); - - it('should insert valid data', async () => { - validData.id = null; - validData.code = null; - await service.createExternalAPI(validData.formId, validData, user); - expect(MockModel.insert).toBeCalledTimes(1); - expect(MockModel.insert).toBeCalledWith({ - createdBy: user.usernameIdp, - code: ExternalAPIStatuses.SUBMITTED, - ...validData, - }); - expect(MockTransaction.commit).toBeCalledTimes(1); - }); - - it('should rollback on error', async () => { - MockModel.insert = jest.fn().mockRejectedValueOnce(new Error('SQL Error')); - - await expect(service.createExternalAPI(validData.formId, validData, user)).rejects.toThrow(); - - expect(MockTransaction.rollback).toBeCalledTimes(1); - }); -}); - -describe('updateExternalAPI', () => { - const user = { usernameIdp: 'username' }; - let validData = null; - - beforeEach(() => { - MockModel.mockReset(); - MockTransaction.mockReset(); - validData = { - id: uuidv4(), - formId: uuidv4(), - name: 'test_api', - endpointUrl: 'http://external.api/', - sendApiKey: true, - apiKeyHeader: 'X-API-KEY', - apiKey: 'my-api-key', - sendUserToken: true, - userTokenHeader: 'Authorization', - userTokenBearer: true, - sendUserInfo: true, - userInfoHeader: 'X-API-USER', - userInfoEncrypted: true, - userInfoEncryptionKey: '0489aa2a7882dc53be7c76db43be1800e56627c31a88a0011d85ccc255b79d00', - userInfoEncryptionAlgo: ENCRYPTION_ALGORITHMS.AES_256_GCM, - code: ExternalAPIStatuses.SUBMITTED, - }; - }); - - afterEach(() => { - jest.restoreAllMocks(); - }); - - it('should update valid data', async () => { - MockModel.throwIfNotFound = jest.fn().mockResolvedValueOnce(Object.assign({}, validData)); - - // we do not update (status) code - must stay SUBMITTED - validData.code = ExternalAPIStatuses.APPROVED; - - await service.updateExternalAPI(validData.formId, validData.id, validData, user); - expect(MockModel.update).toBeCalledTimes(1); - expect(MockModel.update).toBeCalledWith({ - updatedBy: user.usernameIdp, - code: ExternalAPIStatuses.SUBMITTED, - ...validData, - }); - expect(MockTransaction.commit).toBeCalledTimes(1); - }); - - it('should not commit when not found', async () => { - MockModel.throwIfNotFound = jest.fn().mockRejectedValueOnce(new Error('SQL Error')); - - await expect(service.updateExternalAPI(validData.formId, validData.id, validData, user)).rejects.toThrow(); - // shouldn't start the transaction - expect(MockModel.startTransaction).toBeCalledTimes(0); - expect(MockTransaction.commit).toBeCalledTimes(0); - }); - - it('should rollback on error', async () => { - MockModel.update = jest.fn().mockRejectedValueOnce(new Error('SQL Error')); - - await expect(service.updateExternalAPI(validData.formId, validData.id, validData, user)).rejects.toThrow(); - - expect(MockTransaction.rollback).toBeCalledTimes(1); - }); -});