diff --git a/lib/component-repository/package.json b/lib/component-repository/package.json index d0aab8095..26364bfc6 100644 --- a/lib/component-repository/package.json +++ b/lib/component-repository/package.json @@ -7,9 +7,9 @@ "node": ">=12" }, "scripts": { - "lint": "eslint index.js src spec", + "lint": "eslint ./src/**/*.js", "pretest": "npm run lint", - "test": "mocha spec --recursive" + "test": "mocha spec --recursive" }, "dependencies": { "@openintegrationhub/iam-utils": "*", @@ -23,6 +23,7 @@ "swagger-ui-express": "4.0.2" }, "devDependencies": { + "@faker-js/faker": "^6.0.0-alpha.6", "bunyan": "1.8.14", "chai": "4.3.4", "eslint": "7.32.0", diff --git a/lib/component-repository/spec/integration/routes/virtual-components/__mocks__/iamMiddleware.js b/lib/component-repository/spec/integration/routes/virtual-components/__mocks__/iamMiddleware.js new file mode 100644 index 000000000..6909ab250 --- /dev/null +++ b/lib/component-repository/spec/integration/routes/virtual-components/__mocks__/iamMiddleware.js @@ -0,0 +1,26 @@ +const { can, hasOneOf } = require('@openintegrationhub/iam-utils'); +const tokens = require('./tokens'); + +const userWithComponents = { + sub: '61f13960e88b288fd905c8ab', + tenant: '61f13992a4a0ad3c113c7c65', + permissions: [], +}; + +const iam = { + middleware(req, _, next) { + const tokenName = req.headers.authorization; + if (!tokens[tokenName]) { + throw new Error('User not valid'); + } + req.user = tokens[tokenName].value; + return next(); + }, + can, + hasOneOf, +}; + +module.exports = { + iam, + userWithComponents, +}; diff --git a/lib/component-repository/spec/integration/routes/virtual-components/__mocks__/insertData.js b/lib/component-repository/spec/integration/routes/virtual-components/__mocks__/insertData.js new file mode 100644 index 000000000..1992e0e3f --- /dev/null +++ b/lib/component-repository/spec/integration/routes/virtual-components/__mocks__/insertData.js @@ -0,0 +1,70 @@ +const VirtualComponent = require('../../../../../src/models/VirtualComponent'); +const Component = require('../../../../../src/models/Component'); +const ComponentVersion = require('../../../../../src/models/ComponentVersion'); +const ComponentConfig = require('../../../../../src/models/ComponentConfig'); + +const { + component1, + component2, + component3, + inactiveComponent, + componentNotDefault, + virtualComponent1, + virtualComponent2, + virtualComponent3, + inactiveVirtualComponent, + componentVersion1, + componentVersion2, + componentVersion3, + inactiveComponentVersion, + componentVersionNotDefaultVersion, + componentConfig1, + componentConfig2, + componentConfig3, + componentΝοConfig, + componentVersionNoConfig, + virtualComponentNoConfig, +} = require('./virtualComponentsData'); + +const insertDatainDb = async () => { + await VirtualComponent.insertMany([ + virtualComponent1, + virtualComponent2, + virtualComponent3, + inactiveVirtualComponent, + virtualComponentNoConfig, + ]); + await Component.insertMany([ + component1, + component2, + component3, + componentNotDefault, + inactiveComponent, + componentΝοConfig, + ]); + await ComponentVersion.insertMany([ + componentVersion1, + componentVersion2, + componentVersion3, + inactiveComponentVersion, + componentVersionNotDefaultVersion, + componentVersionNoConfig, + ]); + await ComponentConfig.insertMany([ + componentConfig1, + componentConfig2, + componentConfig3, + ]); +}; + +const deleteAllData = async () => { + await VirtualComponent.deleteMany({}); + await Component.deleteMany({}); + await ComponentVersion.deleteMany({}); + await ComponentConfig.deleteMany({}); +}; + +module.exports = { + insertDatainDb, + deleteAllData, +}; diff --git a/lib/component-repository/spec/integration/routes/virtual-components/__mocks__/logger.js b/lib/component-repository/spec/integration/routes/virtual-components/__mocks__/logger.js new file mode 100644 index 000000000..400503005 --- /dev/null +++ b/lib/component-repository/spec/integration/routes/virtual-components/__mocks__/logger.js @@ -0,0 +1,11 @@ +const logger = { + info: () => {}, + debug: () => {}, + warn: () => {}, + error: () => {}, + trace: () => {}, +}; + +module.exports = { + logger, +}; diff --git a/lib/component-repository/spec/integration/routes/virtual-components/__mocks__/tokens.js b/lib/component-repository/spec/integration/routes/virtual-components/__mocks__/tokens.js new file mode 100644 index 000000000..ccb2c35ea --- /dev/null +++ b/lib/component-repository/spec/integration/routes/virtual-components/__mocks__/tokens.js @@ -0,0 +1,74 @@ +const config = require('../../../../../src/config'); + +const tenantId = '60f922418ced69c612df63ff'; +const tenantWithoutComponents = '60f922418ced69c612df63fg'; + +module.exports = { + tenantId, + adminToken: { + token: 'adminToken', + value: { + sub: 'TestAdmin', + username: 'admin@example.com', + role: 'ADMIN', + permissions: ['all'], + iat: 1337, + isAdmin: true, + }, + }, + + permitToken: { + token: 'permitToken', + value: { + sub: 'PermitGuy', + username: 'admin@example.com', + permissions: [ + config.componentsCreatePermission, + config.componentsUpdatePermission, + config.componentDeletePermission, + config.componentWritePermission, + ], + tenant: tenantId, + iat: 1337, + }, + }, + + unpermitToken: { + token: 'unpermitToken', + value: { + sub: 'UnpermitGuy', + username: 'guest@example.com', + tenant: tenantId, + permissions: ['müsli.riegel', 'schoko.riegel'], + iat: 1337, + }, + }, + + partpermitToken: { + token: 'partpermitToken', + value: { + sub: 'PartpermitGuy', + username: 'guest@example.com', + tenant: tenantId, + permissions: [ + config.componentsCreatePermission, + config.componentsUpdatePermission, + ], + iat: 1337, + }, + }, + + otherTenantToken: { + token: 'otherTenantToken', + value: { + sub: 'OtherTenantToken', + username: 'guest@example.com', + tenant: tenantWithoutComponents, + permissions: [ + config.componentsCreatePermission, + config.componentsUpdatePermission, + ], + iat: 1337, + }, + }, +}; diff --git a/lib/component-repository/spec/integration/routes/virtual-components/__mocks__/virtualComponentsData.js b/lib/component-repository/spec/integration/routes/virtual-components/__mocks__/virtualComponentsData.js new file mode 100644 index 000000000..c035738e2 --- /dev/null +++ b/lib/component-repository/spec/integration/routes/virtual-components/__mocks__/virtualComponentsData.js @@ -0,0 +1,314 @@ +const { faker } = require('@faker-js/faker'); +const mongoose = require('mongoose'); +const { + permitToken: { value: userWithComponents }, +} = require('./tokens'); +const { ObjectId } = mongoose.Types; + +const virtualComponent1Id = new ObjectId(); +const component1Id = new ObjectId(); +const componentNotDefaultId = new ObjectId(); +const componentVersion1Id = new ObjectId(); +const componentVersionNotDefaultId = new ObjectId(); + +const virtualComponent2Id = new ObjectId(); +const component2Id = new ObjectId(); +const componentVersion2Id = new ObjectId(); + +const virtualComponent3Id = new ObjectId(); +const component3Id = new ObjectId(); +const componentVersion3Id = new ObjectId(); + +const virtualComponent4Id = new ObjectId(); +const component4Id = new ObjectId(); +const componentVersion4Id = new ObjectId(); + +const componentConfig1Id = new ObjectId(); +const componentConfig2Id = new ObjectId(); +const componentConfig3Id = new ObjectId(); +const componentIdNoConfig = new ObjectId(); +const virtualComponentIdNoConfig = new ObjectId(); +const componentVersiontIdNoConfig = new ObjectId(); + +/* Public virtual component */ +const virtualComponent1 = { + _id: virtualComponent1Id, + name: faker.name.firstName(), + access: 'public', + defaultVersionId: componentVersion1Id, + versions: [ + { id: componentVersion1Id, version: '1.0.0' }, + { id: componentVersionNotDefaultId, version: '1.0.1' }, + ], +}; +const virtualComponentNoConfig = { + _id: virtualComponentIdNoConfig, + name: faker.name.firstName(), + access: 'public', + defaultVersionId: componentVersiontIdNoConfig, + versions: [{ id: componentVersiontIdNoConfig, version: '1.0.0' }], +}; + +const component1 = { + _id: component1Id, + name: faker.name.firstName(), + isGlobal: true, + active: true, + description: faker.name.jobDescriptor(), + logo: faker.internet.url(), + distribution: { + image: faker.name.firstName(), + }, +}; +const componentΝοConfig = { + _id: componentIdNoConfig, + name: faker.name.firstName(), + isGlobal: true, + active: true, + description: faker.name.jobDescriptor(), + logo: faker.internet.url(), + distribution: { + image: faker.name.firstName(), + }, +}; +const componentNotDefault = { + _id: componentNotDefaultId, + name: faker.name.firstName(), + isGlobal: true, + active: true, + description: faker.name.jobDescriptor(), + logo: faker.internet.url(), + distribution: { + image: faker.name.firstName(), + }, +}; +const componentVersion1 = { + _id: componentVersion1Id, + name: faker.name.firstName(), + virtualComponentId: virtualComponent1Id, + componentId: component1Id, + authorization: { + authType: 'API_KEY', + }, + actions: [], + triggers: [], +}; +const componentVersionNoConfig = { + _id: componentVersiontIdNoConfig, + name: faker.name.firstName(), + virtualComponentId: virtualComponentIdNoConfig, + componentId: componentIdNoConfig, + authorization: { + authType: 'API_KEY', + }, + actions: [], + triggers: [], +}; +const componentVersionNotDefaultVersion = { + _id: componentVersionNotDefaultId, + name: faker.name.firstName(), + virtualComponentId: virtualComponent1Id, + componentId: componentNotDefaultId, + authorization: { + authType: 'API_KEY', + }, + actions: [], + triggers: [], +}; +/* END Public virtual component */ + +/* Private virtual component that owns to the mock userWithComponent */ +const virtualComponent2 = { + _id: virtualComponent2Id, + name: faker.name.firstName(), + access: 'private', + defaultVersionId: componentVersion2Id, + versions: [{ id: componentVersion2Id, version: '1.0.0' }], + tenant: userWithComponents.tenant, + owners: [{ type: 'user', id: userWithComponents.sub }], +}; + +const component2 = { + _id: component2Id, + name: faker.name.firstName(), + isGlobal: true, + active: true, + description: faker.name.jobDescriptor(), + logo: faker.internet.url(), + tenant: userWithComponents.tenant, + owners: [{ type: 'user', id: userWithComponents.sub }], + distribution: { + image: faker.name.firstName(), + }, +}; +const componentVersion2 = { + _id: componentVersion2Id, + name: faker.name.firstName(), + virtualComponentId: virtualComponent2Id, + componentId: component2Id, + authorization: { + authType: 'API_KEY', + }, + actions: [ + { + name: 'name1', + title: 'title1', + description: 'description1', + function: 'getName', + }, + { + name: 'name2', + title: 'title2', + description: 'description2', + function: 'getName2', + }, + ], + triggers: [ + { + name: 'name3', + title: 'title1', + description: 'description1', + function: 'getOrgs', + }, + { + name: 'name4', + title: 'title2', + description: 'description2', + function: 'getOrgs2', + }, + ], +}; +/* END Private virtual component that owns to the mock userWithComponent */ + +/* Private virtual component that owns to the mock userWithComponent */ +const virtualComponent3 = { + _id: virtualComponent3Id, + name: faker.name.firstName(), + access: 'private', + defaultVersionId: componentVersion3Id, + versions: [{ id: componentVersion3Id, version: '1.0.0' }], +}; +const componentConfig1 = { + _id: componentConfig1Id, + componentVersionId: componentVersion1Id, + authClientId: '71efeb5220a6cb9b4c076eae', + tenant: userWithComponents.tenant, +}; +const componentConfig2 = { + _id: componentConfig2Id, + componentVersionId: componentVersion2Id, + authClientId: '71efeb5220a6cb9b4c076eae', + tenant: userWithComponents.tenant, +}; +const componentConfig3 = { + _id: componentConfig3Id, + componentVersionId: componentVersionNotDefaultId, + authClientId: '71efeb5220a6cb9b4c076eae', + tenant: new ObjectId(), +}; + +const component3 = { + _id: component3Id, + name: faker.name.firstName(), + isGlobal: true, + active: true, + description: faker.name.jobDescriptor(), + logo: faker.internet.url(), + distribution: { + image: faker.name.firstName(), + }, +}; +const componentVersion3 = { + _id: componentVersion3Id, + name: faker.name.firstName(), + virtualComponentId: virtualComponent3Id, + componentId: component3Id, + authorization: { + authType: 'API_KEY', + }, + actions: [ + { + name: 'name1', + title: 'title1', + description: 'description1', + function: 'getName', + }, + { + name: 'name2', + title: 'title2', + description: 'description2', + function: 'getName2', + }, + ], + triggers: [ + { + name: 'name1', + title: 'title1', + description: 'description1', + function: 'getOrgs', + }, + { + name: 'name2', + title: 'title2', + description: 'description2', + function: 'getOrgs2', + }, + ], +}; +/* END Private virtual component that owns to the mock userWithComponent */ + +/* Inactive component */ +const inactiveVirtualComponent = { + _id: virtualComponent4Id, + name: faker.name.firstName(), + active: false, + access: 'public', + defaultVersionId: componentVersion4Id, + versions: [{ id: componentVersion4Id, version: '1.0.0' }], +}; +const inactiveComponent = { + _id: component4Id, + name: faker.name.firstName(), + isGlobal: true, + active: true, + description: faker.name.jobDescriptor(), + logo: faker.internet.url(), + distribution: { + image: faker.name.firstName(), + }, +}; +const inactiveComponentVersion = { + _id: componentVersion4Id, + name: faker.name.firstName(), + virtualComponentId: virtualComponent4Id, + componentId: component4Id, + authorization: { + authType: 'API_KEY', + }, + actions: [], + triggers: [], +}; +/* END Inactive component */ + +module.exports = { + virtualComponent1, + component1, + componentNotDefault, + componentVersion1, + componentVersionNotDefaultVersion, + virtualComponent2, + component2, + componentVersion2, + virtualComponent3, + component3, + componentVersion3, + inactiveVirtualComponent, + inactiveComponent, + inactiveComponentVersion, + componentConfig1, + componentConfig2, + componentConfig3, + virtualComponentNoConfig, + componentVersionNoConfig, + componentΝοConfig, +}; diff --git a/lib/component-repository/spec/integration/routes/virtual-components/createComponentConfig.spec.js b/lib/component-repository/spec/integration/routes/virtual-components/createComponentConfig.spec.js new file mode 100644 index 000000000..40cacb22b --- /dev/null +++ b/lib/component-repository/spec/integration/routes/virtual-components/createComponentConfig.spec.js @@ -0,0 +1,69 @@ +const Server = require('../../../../src/Server'); +const request = require('supertest'); +const mongoose = require('mongoose'); +const { expect } = require('chai'); +const EventBusMock = require('../../EventBusMock'); +const { iam } = require('./__mocks__/iamMiddleware'); +const { logger } = require('./__mocks__/logger'); +const { insertDatainDb, deleteAllData } = require('./__mocks__/insertData'); +const { ObjectId } = mongoose.Types; +const { + virtualComponent2, + componentVersion2, + virtualComponentNoConfig, +} = require('./__mocks__/virtualComponentsData'); + +describe('Create Component Config', () => { + let server; + + beforeEach(async () => { + const eventBus = new EventBusMock(); + const config = { + get(key) { + return this[key]; + }, + MONGODB_URI: process.env.MONGODB_URI + ? process.env.MONGODB_URI + : 'mongodb://localhost/test', + }; + + server = new Server({ config, logger, iam, eventBus }); + await server.start(); + await insertDatainDb(); + }); + + afterEach(async () => { + await deleteAllData(); + await server.stop(); + }); + + it('should return 400 error, component Version is required', async () => { + const message = 'This component version has already been set.'; + const { body, statusCode } = await request(server.getApp()) + .post( + `/virtual-components/${virtualComponent2._id}/${componentVersion2._id}/config` + ) + .send({ + authClientId: new ObjectId(), + }) + .set('Authorization', 'permitToken'); + + expect(statusCode).to.equal(400); + expect(body.errors).length(1); + expect(body.errors[0].message).to.equal(message); + }); + it('should return 400 error, invalid component version Id', async () => { + const message = 'Invalid id'; + const { body, statusCode } = await request(server.getApp()) + .post( + `/virtual-components/${virtualComponentNoConfig._id}/7da8sgdsad8agsadu/config` + ) + .send({ + authClientId: new ObjectId(), + }) + .set('Authorization', 'permitToken'); + expect(body.errors).length(1); + expect(body.errors[0].message).to.equal(message); + expect(statusCode).to.equal(400); + }); +}); diff --git a/lib/component-repository/spec/integration/routes/virtual-components/createComponentVersion.spec.js b/lib/component-repository/spec/integration/routes/virtual-components/createComponentVersion.spec.js new file mode 100644 index 000000000..c19064e92 --- /dev/null +++ b/lib/component-repository/spec/integration/routes/virtual-components/createComponentVersion.spec.js @@ -0,0 +1,245 @@ +const Server = require('../../../../src/Server'); +const request = require('supertest'); +const mongoose = require('mongoose'); +const { expect } = require('chai'); +const EventBusMock = require('../../EventBusMock'); +const { iam } = require('./__mocks__/iamMiddleware'); +const { logger } = require('./__mocks__/logger'); +const { insertDatainDb, deleteAllData } = require('./__mocks__/insertData'); +const { + virtualComponent2, + component2, +} = require('./__mocks__/virtualComponentsData'); + +const { ObjectId } = mongoose.Types; + +describe('Create Component Version', () => { + let server; + + beforeEach(async () => { + const eventBus = new EventBusMock(); + const config = { + get(key) { + return this[key]; + }, + MONGODB_URI: process.env.MONGODB_URI + ? process.env.MONGODB_URI + : 'mongodb://localhost/test', + }; + + server = new Server({ config, logger, iam, eventBus }); + await server.start(); + await insertDatainDb(); + }); + + afterEach(async () => { + await deleteAllData(); + await server.stop(); + }); + + it('should return 404 error, virtualComponent not found', async () => { + const { statusCode } = await request(server.getApp()) + .post(`/virtual-components/${new ObjectId()}`) + .send({ + description: '', + }) + .set('Authorization', 'permitToken'); + + expect(statusCode).to.equal(404); + }); + + it('should return 403 error, user without permissions', async () => { + const { statusCode } = await request(server.getApp()) + .post(`/virtual-components/${virtualComponent2._id}`) + .send({ + description: '', + }) + .set('Authorization', 'unpermitToken'); + + expect(statusCode).to.equal(403); + }); + + it('should return 403 error, component that does not belong to the user', async () => { + const { statusCode } = await request(server.getApp()) + .post(`/virtual-components/${virtualComponent2._id}`) + .send({ + description: '', + }) + .set('Authorization', 'otherTenantToken'); + + expect(statusCode).to.equal(404); + }); + + it('should return 400 error, some fields are required', async () => { + const { body, statusCode } = await request(server.getApp()) + .post(`/virtual-components/${virtualComponent2._id}`) + .send({ + description: '', + }) + .set('Authorization', 'permitToken'); + + const errors = ['name', 'componentId', 'authorization.authType']; + expect(statusCode).to.equal(400); + expect(body.errors).length(errors.length); + body.errors.forEach(({ message }) => { + const isPropertyInErrors = errors.some((error) => + message.includes(error) + ); + expect(isPropertyInErrors).to.true; + }); + }); + + it('should return 404, the componentId does not exist', async () => { + const newname = 'new version'; + const authorizationType = 'API_KEY'; + const { statusCode } = await request(server.getApp()) + .post(`/virtual-components/${virtualComponent2._id}`) + .send({ + name: newname, + authorization: { + authType: authorizationType, + }, + componentId: new ObjectId(), + }) + .set('Authorization', 'permitToken'); + + expect(statusCode).to.equal(404); + }); + + it('should create a new component version', async () => { + const newname = 'new version'; + const authorizationType = 'API_KEY'; + const { + body: { + data: { name, authorization, componentId, virtualComponentId }, + }, + statusCode, + } = await request(server.getApp()) + .post(`/virtual-components/${virtualComponent2._id}`) + .send({ + name: newname, + authorization: { + authType: authorizationType, + }, + componentId: component2._id, + }) + .set('Authorization', 'permitToken'); + + expect(statusCode).to.equal(201); + expect(name).to.equal(newname); + expect(authorization.authType).to.equal(authorizationType); + expect(component2._id.equals(componentId)).to.true; + expect(virtualComponent2._id.equals(virtualComponentId)).to.true; + }); + + it('should create a new component version with triggers and actions already formatted', async () => { + const newname = 'new version'; + const authorizationType = 'API_KEY'; + const functionExample = { + active: false, + name: 'alarm', + title: 'test', + function: 'https://millie.com', + }; + const { + body: { + data: { triggers, actions }, + }, + statusCode, + } = await request(server.getApp()) + .post(`/virtual-components/${virtualComponent2._id}`) + .send({ + name: newname, + authorization: { + authType: authorizationType, + }, + componentId: component2._id, + triggers: [functionExample, functionExample], + actions: [functionExample, functionExample], + }) + .set('Authorization', 'permitToken'); + + expect(statusCode).to.equal(201); + expect(triggers).to.length(2); + expect(actions).to.length(2); + }); + + it('should create a new component version with triggers and actions with component format', async () => { + const newname = 'new version'; + const authorizationType = 'API_KEY'; + const firstFunctionNotFormatted = { + getNextCustomerNumber: { + main: './lib/actions/getNextCustomerNumber.js', + title: 'Get next free customer number', + description: + 'Retrieves the next available customer number. Avoids duplicates.', + fields: { + verbose: { + viewClass: 'CheckBoxView', + label: 'Debug this step (log more data)', + }, + }, + metadata: { + in: './lib/schemas/getNextCustomerNumber.in.json', + out: './lib/schemas/getNextCustomerNumber.out.json', + }, + }, + }; + const secondFunctionNotFormatted = { + newFunction: { + main: './lib/actions/newFunction.js', + title: 'Get next free customer number', + description: + 'Retrieves the next available customer number. Avoids duplicates.', + fields: { + verbose: { + viewClass: 'CheckBoxView', + label: 'Debug this step (log more data)', + }, + }, + metadata: { + in: './lib/schemas/newFunction.in.json', + out: './lib/schemas/newFunction.out.json', + }, + }, + }; + const { + body: { + data: { triggers, actions }, + }, + statusCode, + } = await request(server.getApp()) + .post(`/virtual-components/${virtualComponent2._id}`) + .send({ + name: newname, + authorization: { + authType: authorizationType, + }, + componentId: component2._id, + triggers: { + ...firstFunctionNotFormatted, + ...secondFunctionNotFormatted, + }, + actions: { + ...firstFunctionNotFormatted, + ...secondFunctionNotFormatted, + }, + }) + .set('Authorization', 'permitToken'); + + expect(statusCode).to.equal(201); + expect(triggers).to.length(2); + expect(actions).to.length(2); + }); + + it('should return not authorize to a user without permissions', async () => { + const { statusCode } = await request(server.getApp()) + .post(`/virtual-components/${virtualComponent2._id}`) + .send({ + name: 'not get this point', + }) + .set('Authorization', 'unpermitToken'); + + expect(statusCode).to.equal(403); + }); +}); diff --git a/lib/component-repository/spec/integration/routes/virtual-components/createVirtualComponents.spec.js b/lib/component-repository/spec/integration/routes/virtual-components/createVirtualComponents.spec.js new file mode 100644 index 000000000..58554afbb --- /dev/null +++ b/lib/component-repository/spec/integration/routes/virtual-components/createVirtualComponents.spec.js @@ -0,0 +1,117 @@ +const Server = require('../../../../src/Server'); +const request = require('supertest'); +const mongoose = require('mongoose'); +const { expect } = require('chai'); +const EventBusMock = require('../../EventBusMock'); +const { iam } = require('./__mocks__/iamMiddleware'); +const { logger } = require('./__mocks__/logger'); +const { insertDatainDb, deleteAllData } = require('./__mocks__/insertData'); +const { permitToken } = require('./__mocks__/tokens'); +const { ObjectId } = mongoose.Types; + +describe('Create Virtual Component', () => { + let server; + + beforeEach(async () => { + const eventBus = new EventBusMock(); + const config = { + get(key) { + return this[key]; + }, + MONGODB_URI: process.env.MONGODB_URI + ? process.env.MONGODB_URI + : 'mongodb://localhost/test', + }; + + server = new Server({ config, logger, iam, eventBus }); + await server.start(); + await insertDatainDb(); + }); + + afterEach(async () => { + await deleteAllData(); + await server.stop(); + }); + + it('should return 400 error, name is required', async () => { + const { body, statusCode } = await request(server.getApp()) + .post('/virtual-components') + .send({ + access: 'public', + }) + .set('Authorization', 'permitToken'); + + expect(statusCode).to.equal(400); + expect(body.errors).length(1); + const { message } = body.errors[0]; + expect(message).contain('name'); + }); + + it('should create a new virtual component', async () => { + const newName = 'test'; + const newAccess = 'public'; + const { body, statusCode } = await request(server.getApp()) + .post('/virtual-components') + .send({ + name: newName, + access: newAccess, + owners: [{ id: new ObjectId(), type: 'sub' }], + tenant: new ObjectId(), + }) + .set('Authorization', 'permitToken'); + + expect(statusCode).to.equal(201); + const { tenant, owners, access, name } = body.data; + expect(name).to.equal(newName); + expect(tenant).to.equal(permitToken.value.tenant); + expect(access).to.equal(newAccess); + expect(owners[0].id).to.equal(permitToken.value.sub); + }); + + it('should return 403 unauthorized', async () => { + const newName = 'test'; + const newAccess = 'public'; + const { statusCode } = await request(server.getApp()) + .post('/virtual-components') + .send({ + name: newName, + access: newAccess, + }) + .set('Authorization', 'unpermitToken'); + + expect(statusCode).to.equal(403); + }); + + it('should create a new virtual component with tenant and owners from body', async () => { + const newName = 'testing'; + const newAccess = 'public'; + const ownerId = new ObjectId(); + const tenantId = new ObjectId(); + const componentVersion = '1.0.0'; + const apiVersion = '3.0.0 slack'; + const { body, statusCode } = await request(server.getApp()) + .post('/virtual-components') + .send({ + name: newName, + access: newAccess, + owners: [{ id: ownerId, type: 'sub' }], + tenant: tenantId, + versions: [ + { + id: new ObjectId(), + componentVersion, + apiVersion, + }, + ], + }) + .set('Authorization', 'adminToken'); + expect(statusCode).to.equal(201); + const { tenant, owners, access, name, versions } = body.data; + expect(name).to.equal(newName); + expect(tenantId.equals(tenant)).to.true; + expect(access).to.equal(newAccess); + expect(ownerId.equals(owners[0].id)).to.true; + expect(versions[0].componentVersion).to.equal(componentVersion); + expect(versions[0].apiVersion).to.equal(apiVersion); + }); +}); diff --git a/lib/component-repository/spec/integration/routes/virtual-components/deleteComponentVersion.spec.js b/lib/component-repository/spec/integration/routes/virtual-components/deleteComponentVersion.spec.js new file mode 100644 index 000000000..5fbf0307f --- /dev/null +++ b/lib/component-repository/spec/integration/routes/virtual-components/deleteComponentVersion.spec.js @@ -0,0 +1,78 @@ +const Server = require('../../../../src/Server'); +const request = require('supertest'); +const { expect } = require('chai'); +const EventBusMock = require('../../EventBusMock'); +const { iam } = require('./__mocks__/iamMiddleware'); +const { logger } = require('./__mocks__/logger'); +const { insertDatainDb, deleteAllData } = require('./__mocks__/insertData'); +const { + virtualComponent2, + virtualComponent1, + componentVersion2, + componentVersionNotDefaultVersion, +} = require('./__mocks__/virtualComponentsData'); + +describe('Delete Component Version', () => { + let server; + + beforeEach(async () => { + const eventBus = new EventBusMock(); + const config = { + get(key) { + return this[key]; + }, + MONGODB_URI: process.env.MONGODB_URI + ? process.env.MONGODB_URI + : 'mongodb://localhost/test', + }; + + server = new Server({ config, logger, iam, eventBus }); + await server.start(); + await insertDatainDb(); + }); + + afterEach(async () => { + await deleteAllData(); + await server.stop(); + }); + + it('should return 400, when trying to delete the default component version', async () => { + const { body, statusCode } = await request(server.getApp()) + .delete( + `/virtual-components/${virtualComponent2._id}/${componentVersion2._id}` + ) + .set('Authorization', 'permitToken'); + const message = 'Cannot delete the default component Version'; + expect(body.errors[0].message).to.equal(message); + expect(statusCode).to.equal(400); + }); + + it('should return 204, when trying to delete a non default component version', async () => { + const object = await request(server.getApp()) + .get(`/virtual-components/${virtualComponent1._id}`) + .set('Authorization', 'adminToken'); + const versionLength = object.body.data.versions.length; + const { statusCode } = await request(server.getApp()) + .delete( + `/virtual-components/${virtualComponent1._id}/${componentVersionNotDefaultVersion._id}` + ) + .set('Authorization', 'adminToken'); + const object2 = await request(server.getApp()) + .get(`/virtual-components/${virtualComponent1._id}`) + .set('Authorization', 'adminToken'); + const versionLength2 = object2.body.data.versions.length; + + expect(versionLength2).to.equal(versionLength - 1); + expect(statusCode).to.equal(204); + }); + + it('should return 400 error, invalid component version Id', async () => { + const { body, statusCode } = await request(server.getApp()) + .delete(`/virtual-components/${virtualComponent2._id}/bbd9oasbd8asdodasj`) + .set('Authorization', 'permitToken'); + const message = 'Invalid component version id'; + expect(body.errors).length(1); + expect(body.errors[0].message).to.equal(message); + expect(statusCode).to.equal(400); + }); +}); diff --git a/lib/component-repository/spec/integration/routes/virtual-components/deleteVirtualComponent.spec.js b/lib/component-repository/spec/integration/routes/virtual-components/deleteVirtualComponent.spec.js new file mode 100644 index 000000000..6ab769313 --- /dev/null +++ b/lib/component-repository/spec/integration/routes/virtual-components/deleteVirtualComponent.spec.js @@ -0,0 +1,116 @@ +const Server = require('../../../../src/Server'); +const request = require('supertest'); +const mongoose = require('mongoose'); +const { expect } = require('chai'); +const EventBusMock = require('../../EventBusMock'); +const { iam } = require('./__mocks__/iamMiddleware'); +const { logger } = require('./__mocks__/logger'); +const { insertDatainDb, deleteAllData } = require('./__mocks__/insertData'); +const { + virtualComponent1, + virtualComponent2, + componentVersion2, + componentConfig2, +} = require('./__mocks__/virtualComponentsData'); +const ComponentConfig = require('../../../../src/models/ComponentConfig'); +const ComponentVersion = require('../../../../src/models/ComponentVersion'); + +const { ObjectId } = mongoose.Types; + +describe('Delete Virtual Component', () => { + let server; + + beforeEach(async () => { + const eventBus = new EventBusMock(); + const config = { + get(key) { + return this[key]; + }, + MONGODB_URI: process.env.MONGODB_URI + ? process.env.MONGODB_URI + : 'mongodb://localhost/test', + }; + + server = new Server({ config, logger, iam, eventBus }); + await server.start(); + await insertDatainDb(); + }); + + afterEach(async () => { + await deleteAllData(); + await server.stop(); + }); + + it('should return 404 error, virtualComponent not found', async () => { + const { statusCode } = await request(server.getApp()) + .delete(`/virtual-components/${new ObjectId()}`) + .set('Authorization', 'permitToken'); + + expect(statusCode).to.equal(404); + }); + + it('should return 403 error, user without permissions', async () => { + const { statusCode } = await request(server.getApp()) + .delete(`/virtual-components/${virtualComponent2._id}`) + .set('Authorization', 'unpermitToken'); + + expect(statusCode).to.equal(403); + }); + + it('should return 403 error, component that does not belong to the user', async () => { + const { statusCode } = await request(server.getApp()) + .delete(`/virtual-components/${virtualComponent2._id}`) + .set('Authorization', 'otherTenantToken'); + + expect(statusCode).to.equal(403); + }); + + it('should return not authorize to a user without permissions', async () => { + const { statusCode } = await request(server.getApp()) + .delete(`/virtual-components/${virtualComponent2._id}`) + .set('Authorization', 'unpermitToken'); + + expect(statusCode).to.equal(403); + }); + + it('should delete the virtual component and component versions/configurations', async () => { + const { statusCode } = await request(server.getApp()) + .delete(`/virtual-components/${virtualComponent2._id}`) + .set('Authorization', 'permitToken'); + + expect(statusCode).to.equal(204); + }); + + it('should delete a private virtual component and component versions/configurations with an admin user', async () => { + const { statusCode } = await request(server.getApp()) + .delete(`/virtual-components/${virtualComponent2._id}`) + .set('Authorization', 'adminToken'); + + expect(statusCode).to.equal(204); + }); + + it('should delete a public virtual component and component versions/configurations with an admin user', async () => { + const { statusCode } = await request(server.getApp()) + .delete(`/virtual-components/${virtualComponent1._id}`) + .set('Authorization', 'adminToken'); + + expect(statusCode).to.equal(204); + }); + + it('should delete a public virtual component and component versions/configurations with an owner user', async () => { + const { statusCode } = await request(server.getApp()) + .delete(`/virtual-components/${virtualComponent2._id}`) + .set('Authorization', 'adminToken'); + + expect(statusCode).to.equal(204); + + const componentVersionCount = await ComponentVersion.countDocuments({ + _id: componentVersion2._id, + }); + const componentConfigCount = await ComponentConfig.countDocuments({ + _id: componentConfig2._id, + }); + expect(componentVersionCount).eq(0); + expect(componentConfigCount).eq(0); + }); +}); diff --git a/lib/component-repository/spec/integration/routes/virtual-components/getDefaultVirtualComponents.spec.js b/lib/component-repository/spec/integration/routes/virtual-components/getDefaultVirtualComponents.spec.js new file mode 100644 index 000000000..3e0540c31 --- /dev/null +++ b/lib/component-repository/spec/integration/routes/virtual-components/getDefaultVirtualComponents.spec.js @@ -0,0 +1,63 @@ +const Server = require('../../../../src/Server'); +const request = require('supertest'); +const { expect } = require('chai'); +const EventBusMock = require('../../EventBusMock'); +const { iam } = require('./__mocks__/iamMiddleware'); +const { logger } = require('./__mocks__/logger'); +const { insertDatainDb, deleteAllData } = require('./__mocks__/insertData'); +const { + componentVersionNotDefaultVersion, +} = require('./__mocks__/virtualComponentsData'); + +describe('Get Default Component versions', () => { + let server; + + beforeEach(async () => { + const eventBus = new EventBusMock(); + const config = { + get(key) { + return this[key]; + }, + MONGODB_URI: process.env.MONGODB_URI + ? process.env.MONGODB_URI + : 'mongodb://localhost/test', + }; + + server = new Server({ config, logger, iam, eventBus }); + await server.start(); + await insertDatainDb(); + }); + + afterEach(async () => { + await deleteAllData(); + await server.stop(); + }); + + it('should return three components version to the permitToken user', async () => { + const { body, statusCode } = await request(server.getApp()) + .get('/virtual-components/defaults') + .set('Authorization', 'permitToken'); + + expect(body.data).lengthOf(3); + const notDefaultId = componentVersionNotDefaultVersion._id; + const isNotDefaultInResult = body.data.some( + ({ _id }) => _id === notDefaultId + ); + expect(isNotDefaultInResult).to.be.false; + expect(statusCode).to.equal(200); + }); + + it('should return 5 components version to the admin user', async () => { + const { body, statusCode } = await request(server.getApp()) + .get('/virtual-components/defaults') + .set('Authorization', 'adminToken'); + + expect(body.data).lengthOf(5); + const notDefaultId = componentVersionNotDefaultVersion._id; + const isNotDefaultInResult = body.data.some( + ({ _id }) => _id === notDefaultId + ); + expect(isNotDefaultInResult).to.be.false; + expect(statusCode).to.equal(200); + }); +}); diff --git a/lib/component-repository/spec/integration/routes/virtual-components/getVirtualComponentAction.spec.js b/lib/component-repository/spec/integration/routes/virtual-components/getVirtualComponentAction.spec.js new file mode 100644 index 000000000..0f12ae84a --- /dev/null +++ b/lib/component-repository/spec/integration/routes/virtual-components/getVirtualComponentAction.spec.js @@ -0,0 +1,82 @@ +const Server = require('../../../../src/Server'); +const request = require('supertest'); +const { expect } = require('chai'); +const EventBusMock = require('../../EventBusMock'); +const { iam } = require('./__mocks__/iamMiddleware'); +const { logger } = require('./__mocks__/logger'); +const { insertDatainDb, deleteAllData } = require('./__mocks__/insertData'); +const { + virtualComponent2, + componentVersion2, +} = require('./__mocks__/virtualComponentsData'); +const mongoose = require('mongoose'); +const { ObjectId } = mongoose.Types; +const validId = new ObjectId(); + +describe('Get action from a component version', () => { + let server; + + beforeEach(async () => { + const eventBus = new EventBusMock(); + const config = { + get(key) { + return this[key]; + }, + MONGODB_URI: process.env.MONGODB_URI + ? process.env.MONGODB_URI + : 'mongodb://localhost/test', + }; + + server = new Server({ config, logger, iam, eventBus }); + await server.start(); + await insertDatainDb(); + }); + + afterEach(async () => { + await deleteAllData(); + await server.stop(); + }); + + it('should return the action to the permitToken user', async () => { + const { statusCode } = await request(server.getApp()) + .get( + `/virtual-components/${virtualComponent2._id}/${componentVersion2._id}/actions/name1` + ) + .set('Authorization', 'permitToken'); + + expect(statusCode).to.equal(200); + }); + it('should return could not find the action to the permitToken user', async () => { + const message = 'Invalid id'; + const { body, statusCode } = await request(server.getApp()) + .get( + `/virtual-components/${virtualComponent2._id}/76fadvsdad87sad7as7g9i/actions/name1` + ) + .set('Authorization', 'permitToken'); + + expect(body.errors[0].message).to.equal(message); + expect(statusCode).to.equal(400); + }); + it('should return could not find the component Version to the permitToken user', async () => { + const message = 'Component version or action could not be found!'; + const { body, statusCode } = await request(server.getApp()) + .get( + `/virtual-components/${virtualComponent2._id}/${validId}/actions/name1` + ) + .set('Authorization', 'permitToken'); + + expect(body.errors[0].message).to.equal(message); + expect(statusCode).to.equal(404); + }); + it('should return could not find the action to the permitToken user', async () => { + const message = 'Component version or action could not be found!'; + const { body, statusCode } = await request(server.getApp()) + .get( + `/virtual-components/${virtualComponent2._id}/${componentVersion2._id}/actions/nam310` + ) + .set('Authorization', 'permitToken'); + + expect(body.errors[0].message).to.equal(message); + expect(statusCode).to.equal(404); + }); +}); diff --git a/lib/component-repository/spec/integration/routes/virtual-components/getVirtualComponentById.spec.js b/lib/component-repository/spec/integration/routes/virtual-components/getVirtualComponentById.spec.js new file mode 100644 index 000000000..079d90f2d --- /dev/null +++ b/lib/component-repository/spec/integration/routes/virtual-components/getVirtualComponentById.spec.js @@ -0,0 +1,91 @@ +const Server = require('../../../../src/Server'); +const request = require('supertest'); +const mongoose = require('mongoose'); +const { expect } = require('chai'); +const EventBusMock = require('../../EventBusMock'); +const { iam } = require('./__mocks__/iamMiddleware'); +const { logger } = require('./__mocks__/logger'); +const { insertDatainDb, deleteAllData } = require('./__mocks__/insertData'); +const { virtualComponent2 } = require('./__mocks__/virtualComponentsData'); + +const { ObjectId } = mongoose.Types; + +describe('Get Virtual Components', () => { + let server; + + beforeEach(async () => { + const eventBus = new EventBusMock(); + const config = { + get(key) { + return this[key]; + }, + MONGODB_URI: process.env.MONGODB_URI + ? process.env.MONGODB_URI + : 'mongodb://localhost/test', + }; + + server = new Server({ config, logger, iam, eventBus }); + await server.start(); + await insertDatainDb(); + }); + + afterEach(async () => { + await deleteAllData(); + await server.stop(); + }); + + it('should return 404 not found for a not existing id', async () => { + const notExistingId = new ObjectId(); + const message = 'VirtualComponent is not found'; + + const { body, statusCode } = await request(server.getApp()) + .get(`/virtual-components/${notExistingId}`) + .set('Authorization', 'permitToken'); + + expect(statusCode).to.equal(404); + expect(body.errors[0].message).to.equal(message); + }); + + it('should return 404 not found for a not authorized virtualComponent', async () => { + const message = 'VirtualComponent is not found'; + + const { body, statusCode } = await request(server.getApp()) + .get(`/virtual-components/${virtualComponent2._id}`) + .set('Authorization', 'otherTenantToken'); + + expect(statusCode).to.equal(404); + expect(body.errors[0].message).to.equal(message); + }); + + it('should return the virtualComponent with the same tenant', async () => { + const { body, statusCode } = await request(server.getApp()) + .get(`/virtual-components/${virtualComponent2._id}`) + .set('Authorization', 'partpermitToken'); + + expect(statusCode).to.equal(200); + const { _id, name } = body.data; + expect(virtualComponent2._id.equals(_id)).to.true; + expect(virtualComponent2.name).to.equals(name); + }); + + it('should return the virtualComponent with the userId in the owners', async () => { + const { body, statusCode } = await request(server.getApp()) + .get(`/virtual-components/${virtualComponent2._id}`) + .set('Authorization', 'permitToken'); + + expect(statusCode).to.equal(200); + const { _id, name } = body.data; + expect(virtualComponent2._id.equals(_id)).to.true; + expect(virtualComponent2.name).to.equals(name); + }); + + it('should return the virtualComponent to an admin user', async () => { + const { body, statusCode } = await request(server.getApp()) + .get(`/virtual-components/${virtualComponent2._id}`) + .set('Authorization', 'adminToken'); + expect(statusCode).to.equal(200); + const { _id, name } = body.data; + expect(virtualComponent2._id.equals(_id)).to.true; + expect(virtualComponent2.name).to.equals(name); + }); +}); diff --git a/lib/component-repository/spec/integration/routes/virtual-components/getVirtualComponentTrigger.spec.js b/lib/component-repository/spec/integration/routes/virtual-components/getVirtualComponentTrigger.spec.js new file mode 100644 index 000000000..a8e9d5494 --- /dev/null +++ b/lib/component-repository/spec/integration/routes/virtual-components/getVirtualComponentTrigger.spec.js @@ -0,0 +1,82 @@ +const Server = require('../../../../src/Server'); +const request = require('supertest'); +const { expect } = require('chai'); +const EventBusMock = require('../../EventBusMock'); +const { iam } = require('./__mocks__/iamMiddleware'); +const { logger } = require('./__mocks__/logger'); +const { insertDatainDb, deleteAllData } = require('./__mocks__/insertData'); +const { + virtualComponent2, + componentVersion2, +} = require('./__mocks__/virtualComponentsData'); +const mongoose = require('mongoose'); +const { ObjectId } = mongoose.Types; +const validId = new ObjectId(); + +describe('Get action from a component version', () => { + let server; + + beforeEach(async () => { + const eventBus = new EventBusMock(); + const config = { + get(key) { + return this[key]; + }, + MONGODB_URI: process.env.MONGODB_URI + ? process.env.MONGODB_URI + : 'mongodb://localhost/test', + }; + + server = new Server({ config, logger, iam, eventBus }); + await server.start(); + await insertDatainDb(); + }); + + afterEach(async () => { + await deleteAllData(); + await server.stop(); + }); + + it('should return the trigger to the permitToken user', async () => { + const { statusCode } = await request(server.getApp()) + .get( + `/virtual-components/${virtualComponent2._id}/${componentVersion2._id}/triggers/name3` + ) + .set('Authorization', 'permitToken'); + + expect(statusCode).to.equal(200); + }); + it('should return could not find the trigger to the permitToken user', async () => { + const message = 'Invalid id'; + const { body, statusCode } = await request(server.getApp()) + .get( + `/virtual-components/${virtualComponent2._id}/76fadvsdad87sad7as7g9i/triggers/name1` + ) + .set('Authorization', 'permitToken'); + + expect(body.errors[0].message).to.equal(message); + expect(statusCode).to.equal(400); + }); + it('should return could not find the component Version to the permitToken user', async () => { + const message = 'Component version or trigger could not be found!'; + const { body, statusCode } = await request(server.getApp()) + .get( + `/virtual-components/${virtualComponent2._id}/${validId}/triggers/name3` + ) + .set('Authorization', 'permitToken'); + + expect(body.errors[0].message).to.equal(message); + expect(statusCode).to.equal(404); + }); + it('should return could not find the trigger to the permitToken user', async () => { + const message = 'Component version or trigger could not be found!'; + const { body, statusCode } = await request(server.getApp()) + .get( + `/virtual-components/${virtualComponent2._id}/${componentVersion2._id}/triggers/nam310` + ) + .set('Authorization', 'permitToken'); + + expect(body.errors[0].message).to.equal(message); + expect(statusCode).to.equal(404); + }); +}); diff --git a/lib/component-repository/spec/integration/routes/virtual-components/getVirtualComponents.spec.js b/lib/component-repository/spec/integration/routes/virtual-components/getVirtualComponents.spec.js new file mode 100644 index 000000000..a295b6354 --- /dev/null +++ b/lib/component-repository/spec/integration/routes/virtual-components/getVirtualComponents.spec.js @@ -0,0 +1,50 @@ +const Server = require('../../../../src/Server'); +const request = require('supertest'); +const { expect } = require('chai'); +const EventBusMock = require('../../EventBusMock'); +const { iam } = require('./__mocks__/iamMiddleware'); +const { logger } = require('./__mocks__/logger'); +const { insertDatainDb, deleteAllData } = require('./__mocks__/insertData'); + +describe('Get Virtual Components', () => { + let server; + + beforeEach(async () => { + const eventBus = new EventBusMock(); + const config = { + get(key) { + return this[key]; + }, + MONGODB_URI: process.env.MONGODB_URI + ? process.env.MONGODB_URI + : 'mongodb://localhost/test', + }; + + server = new Server({ config, logger, iam, eventBus }); + await server.start(); + await insertDatainDb(); + }); + + afterEach(async () => { + await deleteAllData(); + await server.stop(); + }); + + it('should return two virtual components to the permitToken user', async () => { + const { body, statusCode } = await request(server.getApp()) + .get('/virtual-components') + .set('Authorization', 'permitToken'); + + expect(body.data).lengthOf(3); + expect(statusCode).to.equal(200); + }); + + it('should return four virtual components to the permitToken user', async () => { + const { body, statusCode } = await request(server.getApp()) + .get('/virtual-components') + .set('Authorization', 'adminToken'); + + expect(body.data).lengthOf(5); + expect(statusCode).to.equal(200); + }); +}); diff --git a/lib/component-repository/spec/integration/routes/virtual-components/getVirtualComponentsDefaultsConfig.spec.js b/lib/component-repository/spec/integration/routes/virtual-components/getVirtualComponentsDefaultsConfig.spec.js new file mode 100644 index 000000000..903b18581 --- /dev/null +++ b/lib/component-repository/spec/integration/routes/virtual-components/getVirtualComponentsDefaultsConfig.spec.js @@ -0,0 +1,49 @@ +const Server = require('../../../../src/Server'); +const request = require('supertest'); +const { expect } = require('chai'); +const EventBusMock = require('../../EventBusMock'); +const { iam } = require('./__mocks__/iamMiddleware'); +const { logger } = require('./__mocks__/logger'); +const { insertDatainDb, deleteAllData } = require('./__mocks__/insertData'); + +describe('Get Default Component versions configs', () => { + let server; + + beforeEach(async () => { + const eventBus = new EventBusMock(); + const config = { + get(key) { + return this[key]; + }, + MONGODB_URI: process.env.MONGODB_URI + ? process.env.MONGODB_URI + : 'mongodb://localhost/test', + }; + + server = new Server({ config, logger, iam, eventBus }); + await server.start(); + await insertDatainDb(); + }); + + afterEach(async () => { + await deleteAllData(); + await server.stop(); + }); + + it('should return two components version configurations to the permitToken user', async () => { + const { body, statusCode } = await request(server.getApp()) + .get('/virtual-components/defaults/config') + .set('Authorization', 'permitToken'); + + expect(body.data).lengthOf(2); + expect(statusCode).to.equal(200); + }); + it('should return three components version configurations to the admin', async () => { + const { body, statusCode } = await request(server.getApp()) + .get('/virtual-components/defaults/config') + .set('Authorization', 'adminToken'); + + expect(body.data).lengthOf(3); + expect(statusCode).to.equal(200); + }); +}); diff --git a/lib/component-repository/spec/integration/routes/virtual-components/patchVirtualComponent.spec.js b/lib/component-repository/spec/integration/routes/virtual-components/patchVirtualComponent.spec.js new file mode 100644 index 000000000..ebadf606b --- /dev/null +++ b/lib/component-repository/spec/integration/routes/virtual-components/patchVirtualComponent.spec.js @@ -0,0 +1,178 @@ +const request = require('supertest'); +const { faker } = require('@faker-js/faker'); +const mongoose = require('mongoose'); +const { expect } = require('chai'); +const EventBusMock = require('../../EventBusMock'); +const { iam } = require('./__mocks__/iamMiddleware'); +const { logger } = require('./__mocks__/logger'); +const Server = require('../../../../src/Server'); +const { insertDatainDb, deleteAllData } = require('./__mocks__/insertData'); +const { virtualComponent2 } = require('./__mocks__/virtualComponentsData'); + +const { ObjectId } = mongoose.Types; + +describe('Patch Virtual Component', () => { + let server; + + beforeEach(async () => { + const eventBus = new EventBusMock(); + const config = { + get(key) { + return this[key]; + }, + MONGODB_URI: process.env.MONGODB_URI + ? process.env.MONGODB_URI + : 'mongodb://localhost/test', + }; + + server = new Server({ config, logger, iam, eventBus }); + await server.start(); + await insertDatainDb(); + }); + + afterEach(async () => { + await deleteAllData(); + await server.stop(); + }); + + it('should update a new virtual component', async () => { + const newName = 'test'; + const newAccess = 'private'; + + // create a new virtual component to update it + const { + body: { + data: { _id: newComponentId, versions: newVersions, name: savedName }, + }, + } = await request(server.getApp()) + .post('/virtual-components') + .send({ + name: newName, + access: newAccess, + owners: [{ id: new ObjectId(), type: 'sub' }], + tenant: new ObjectId(), + versions: [ + { + id: new ObjectId(), + componentVersion: '001', + apiVersion: '3.0.0' + }, + ], + defaultVersionId: new ObjectId(), + }) + .set('Authorization', 'permitToken'); + + expect(newVersions).to.length(1); + expect(savedName).to.equal(savedName); + + const updatedName = faker.name.firstName(); + + const { + body: { + data: { + _id, + versions: updatedVersions, + name: updatedSavedName, + defaultVersionId: updatedDefaultVersionId, + }, + }, + } = await request(server.getApp()) + .patch(`/virtual-components/${newComponentId}`) + .send({ + name: updatedName, + access: 'private', + owners: [{ id: new ObjectId(), type: 'sub' }], + tenant: new ObjectId(), + versions: [], + defaultVersionId: null, + }) + .set('Authorization', 'permitToken'); + + expect(updatedSavedName).to.equal(updatedName); + expect(newComponentId).to.equal(_id); + expect(updatedVersions).to.length(0); + expect(updatedDefaultVersionId).to.null; + }); + + it('should return 404 error, virtual component not found', async () => { + const objectId = new ObjectId(); + const { statusCode } = await request(server.getApp()) + .patch(`/virtual-components/${objectId}`) + .send({ + access: 'public', + }) + .set('Authorization', 'permitToken'); + + expect(statusCode).to.equal(404); + }); + + it('should return 404 error, virtual component without permissions', async () => { + const { statusCode } = await request(server.getApp()) + .patch(`/virtual-components/${virtualComponent2._id}`) + .send({ + versions: [], + }) + .set('Authorization', 'otherTenantToken'); + + expect(statusCode).to.equal(404); + }); + + it('should return updated private virtual component that belongs to the token user ', async () => { + const { + statusCode, + body: { + data: { versions }, + }, + } = await request(server.getApp()) + .patch(`/virtual-components/${virtualComponent2._id}`) + .send({ + versions: [], + }) + .set('Authorization', 'permitToken'); + + expect(statusCode).to.equal(200); + expect(versions).to.length(0); + }); + + it('should return updated private virtual component with an admin user that belongs to other user ', async () => { + const { + statusCode, + body: { + data: { versions }, + }, + } = await request(server.getApp()) + .patch(`/virtual-components/${virtualComponent2._id}`) + .send({ + versions: [], + }) + .set('Authorization', 'adminToken'); + + expect(statusCode).to.equal(200); + expect(versions).to.length(0); + }); + + it('should return 400 error, name is required', async () => { + const { statusCode, body } = await request(server.getApp()) + .patch(`/virtual-components/${virtualComponent2._id}`) + .send({ + name: '', + }) + .set('Authorization', 'adminToken'); + + expect(statusCode).to.equal(400); + expect(body.errors).length(1); + const { message } = body.errors[0]; + expect(message).contain('name'); + }); + + it('should return not authorize to a user without permissions', async () => { + const { statusCode } = await request(server.getApp()) + .patch(`/virtual-components/${virtualComponent2._id}`) + .send({ + versions: [], + }) + .set('Authorization', 'unpermitToken'); + + expect(statusCode).to.equal(403); + }); +}); diff --git a/lib/component-repository/spec/integration/routes/virtual-components/patchVirtualComponentVersion.spec.js b/lib/component-repository/spec/integration/routes/virtual-components/patchVirtualComponentVersion.spec.js new file mode 100644 index 000000000..9bc4937ee --- /dev/null +++ b/lib/component-repository/spec/integration/routes/virtual-components/patchVirtualComponentVersion.spec.js @@ -0,0 +1,146 @@ +const Server = require('../../../../src/Server'); +const request = require('supertest'); +const mongoose = require('mongoose'); +const { expect } = require('chai'); +const EventBusMock = require('../../EventBusMock'); +const { iam } = require('./__mocks__/iamMiddleware'); +const { logger } = require('./__mocks__/logger'); +const { insertDatainDb, deleteAllData } = require('./__mocks__/insertData'); +const { + virtualComponent2, + componentVersion2, +} = require('./__mocks__/virtualComponentsData'); + +const { ObjectId } = mongoose.Types; + +describe('Patch Virtual Component', () => { + let server; + + beforeEach(async () => { + const eventBus = new EventBusMock(); + const config = { + get(key) { + return this[key]; + }, + MONGODB_URI: process.env.MONGODB_URI + ? process.env.MONGODB_URI + : 'mongodb://localhost/test', + }; + + server = new Server({ config, logger, iam, eventBus }); + await server.start(); + await insertDatainDb(); + }); + + afterEach(async () => { + await deleteAllData(); + await server.stop(); + }); + + it('should return 404 error, virtualComponent not found', async () => { + const { statusCode } = await request(server.getApp()) + .patch(`/virtual-components/${new ObjectId()}/${componentVersion2._id}`) + .send({ + description: '', + }) + .set('Authorization', 'permitToken'); + + expect(statusCode).to.equal(404); + }); + + it('should return 403 error, user without permissions', async () => { + const { statusCode } = await request(server.getApp()) + .patch( + `/virtual-components/${virtualComponent2._id}/${componentVersion2._id}` + ) + .send({ + description: '', + }) + .set('Authorization', 'unpermitToken'); + + expect(statusCode).to.equal(403); + }); + + it('should return 403 error, component that does not belong to the user', async () => { + const { statusCode } = await request(server.getApp()) + .patch( + `/virtual-components/${virtualComponent2._id}/${componentVersion2._id}` + ) + .send({ + description: '', + }) + .set('Authorization', 'otherTenantToken'); + + expect(statusCode).to.equal(404); + }); + + it('should return 400 error, some fields are required', async () => { + const { body, statusCode } = await request(server.getApp()) + .patch( + `/virtual-components/${virtualComponent2._id}/${componentVersion2._id}` + ) + .send({ + name: '', + authorization: { authType: '' }, + description: '', + componentId: null, + }) + .set('Authorization', 'permitToken'); + + const errors = ['name', 'componentId', 'authorization.authType']; + expect(statusCode).to.equal(400); + expect(body.errors).length(errors.length); + body.errors.forEach(({ message }) => { + const isPropertyInErrors = errors.some((error) => + message.includes(error) + ); + expect(isPropertyInErrors).to.true; + }); + }); + + it('should return not authorize to a user without permissions', async () => { + const { statusCode } = await request(server.getApp()) + .patch( + `/virtual-components/${virtualComponent2._id}/${componentVersion2._id}` + ) + .send({ + name: 'not get this point', + }) + .set('Authorization', 'unpermitToken'); + + expect(statusCode).to.equal(403); + }); + + it('should update a component version', async () => { + const newName = 'not get this point'; + const newDescription = 'new description'; + const { body, statusCode } = await request(server.getApp()) + .patch( + `/virtual-components/${virtualComponent2._id}/${componentVersion2._id}` + ) + .send({ + name: newName, + description: newDescription, + actions: [ + { + name: 'name1', + title: 'title1', + description: 'description1', + function: 'getName', + }, + { + name: 'name2', + title: 'title2', + description: 'description2', + function: 'getName2', + }, + ], + }) + .set('Authorization', 'permitToken'); + + expect(statusCode).to.equal(200); + expect(body.data.actions).lengthOf(2); + expect(body.data.name).to.equal(newName); + expect(body.data.description).to.equal(newDescription); + }); +}); diff --git a/lib/component-repository/spec/integration/routes/virtual-components/updateComponentConfig.spec.js b/lib/component-repository/spec/integration/routes/virtual-components/updateComponentConfig.spec.js new file mode 100644 index 000000000..f9055f7a3 --- /dev/null +++ b/lib/component-repository/spec/integration/routes/virtual-components/updateComponentConfig.spec.js @@ -0,0 +1,84 @@ +const Server = require('../../../../src/Server'); +const request = require('supertest'); +const mongoose = require('mongoose'); +const { expect } = require('chai'); +const EventBusMock = require('../../EventBusMock'); +const { iam } = require('./__mocks__/iamMiddleware'); +const { logger } = require('./__mocks__/logger'); +const { insertDatainDb, deleteAllData } = require('./__mocks__/insertData'); +const { ObjectId } = mongoose.Types; +const { + virtualComponent2, + componentVersion2, + virtualComponentNoConfig, +} = require('./__mocks__/virtualComponentsData'); + +describe('Update Component Config', () => { + let server; + + beforeEach(async () => { + const eventBus = new EventBusMock(); + const config = { + get(key) { + return this[key]; + }, + MONGODB_URI: process.env.MONGODB_URI + ? process.env.MONGODB_URI + : 'mongodb://localhost/test', + }; + + server = new Server({ config, logger, iam, eventBus }); + await server.start(); + await insertDatainDb(); + }); + + afterEach(async () => { + await deleteAllData(); + await server.stop(); + }); + + it('should return 400 error, invalid authClientId is required', async () => { + const message = 'Invalid auth Client id'; + const { body, statusCode } = await request(server.getApp()) + .patch( + `/virtual-components/${virtualComponent2._id}/${componentVersion2._id}/config` + ) + .send({ + authClientId: 'z8bda8bsad67as8dz', + }) + .set('Authorization', 'permitToken'); + + expect(statusCode).to.equal(400); + expect(body.errors).length(1); + expect(body.errors[0].message).to.equal(message); + }); + it('should return 400 error, invalid component version Id', async () => { + const message = 'Invalid id'; + const { body, statusCode } = await request(server.getApp()) + .patch( + `/virtual-components/${virtualComponentNoConfig._id}/7da8sgdsad8agsadu/config` + ) + .send({ + authClientId: new ObjectId(), + }) + .set('Authorization', 'permitToken'); + + expect(body.errors).length(1); + expect(body.errors[0].message).to.equal(message); + expect(statusCode).to.equal(400); + }); + it('should return 200 , succesfully updated config', async () => { + const newAuthClientId = new ObjectId(); + const { body, statusCode } = await request(server.getApp()) + .patch( + `/virtual-components/${virtualComponentNoConfig._id}/${componentVersion2._id}/config` + ) + .send({ + authClientId: newAuthClientId, + }) + .set('Authorization', 'permitToken'); + + expect(body.data.authClientId).to.equal(`${newAuthClientId}`); + expect(statusCode).to.equal(200); + }); +}); diff --git a/lib/component-repository/src/Server.js b/lib/component-repository/src/Server.js index 7bd0e3c82..629a651fa 100644 --- a/lib/component-repository/src/Server.js +++ b/lib/component-repository/src/Server.js @@ -24,6 +24,7 @@ class Server { app.get('/', (req, res) => res.send('Component Repository')); app.use('/healthcheck', require('./routes/healthcheck')); app.use('/components', setCors({whitelist, logger}), require('./routes/components')({iam, eventBus, logger })); + app.use('/virtual-components', setCors({whitelist, logger}), require('./routes/virtual-components')({iam, eventBus, logger })); app.use(formatAndRespond()); app.use(errorHandler()); app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument, { explorer: true })); @@ -39,7 +40,7 @@ class Server { }; await mongoose.connect(this._config.get('MONGODB_URI'), mongooseOptions); await this._subscribeToEvents(); - const server = await this._app.listen(this._config.get('PORT')); + const server = this._app.listen(this._config.get('PORT')); this._logger.info(`Listening on port ${server.address().port}`); this._server = server; return server; diff --git a/lib/component-repository/src/config/index.js b/lib/component-repository/src/config/index.js new file mode 100644 index 000000000..7506ca762 --- /dev/null +++ b/lib/component-repository/src/config/index.js @@ -0,0 +1,9 @@ +const configuration = { + componentsCreatePermission: process.env.COMPONENT_CREATE_PERMISSION || 'components.create', + componentsUpdatePermission: process.env.COMPONENT_UPDATE_PERMISSION || 'components.update', + componentDeletePermission: process.env.COMPONENT_DELETE_PERMISSION || 'components.delete', + componentWritePermission: process.env.COMPONENT_WRITE_PERMISSION || 'components.write', + adminPermission: process.env.ADMIN_PERMISSION || 'all' +}; + +module.exports = configuration; diff --git a/lib/component-repository/src/constants/index.js b/lib/component-repository/src/constants/index.js new file mode 100644 index 000000000..9c51285a9 --- /dev/null +++ b/lib/component-repository/src/constants/index.js @@ -0,0 +1,12 @@ +module.exports = { + AUTH_TYPE: { + NO_AUTH: 'NO_AUTH', + API_KEY: 'API_KEY', + OA1_TWO_LEGGED: 'OA1_TWO_LEGGED', + OA1_THREE_LEGGED: 'OA1_THREE_LEGGED', + OA2_AUTHORIZATION_CODE: 'OA2_AUTHORIZATION_CODE', + SIMPLE: 'SIMPLE', + MIXED: 'MIXED', + SESSION_AUTH: 'SESSION_AUTH', + }, +}; diff --git a/lib/component-repository/src/models/ComponentConfig.js b/lib/component-repository/src/models/ComponentConfig.js new file mode 100644 index 000000000..05ceefceb --- /dev/null +++ b/lib/component-repository/src/models/ComponentConfig.js @@ -0,0 +1,23 @@ +const mongoose = require('mongoose'); + +const Schema = mongoose.Schema; + +// Define schema +const componentConfig = new Schema( + { + componentVersionId: { + type: Schema.Types.ObjectId, + required: [true, 'Configuration requires a Component Version Id'], + }, + authClientId: { type: Schema.Types.ObjectId }, + tenant: { + type: String, + required: [true, 'Configuration requires a Tenant'], + }, + }, + { timestamps: true }, +); + +componentConfig.index({ componentVersionId: 1, tenant: 1 }, { unique: true }); + +module.exports = mongoose.model('ComponentConfig', componentConfig); diff --git a/lib/component-repository/src/models/ComponentVersion.js b/lib/component-repository/src/models/ComponentVersion.js new file mode 100644 index 000000000..8c187a34d --- /dev/null +++ b/lib/component-repository/src/models/ComponentVersion.js @@ -0,0 +1,78 @@ +const mongoose = require('mongoose'); + +const Schema = mongoose.Schema; +const { AUTH_TYPE } = require('../constants'); + +const func = new Schema( + { + name: { + type: String, + required: true, + }, + title: { + type: String, + }, + description: { + type: String, + maxLength: 300, + }, + function: { + type: String, + required: true, + }, + fields: { + type: Schema.Types.Mixed, + }, + schemas: { + in: { type: Schema.Types.Mixed }, + out: { type: Schema.Types.Mixed }, + }, + active: { + type: Boolean, + default: true, + }, + // FUTURE: Add an "Address" or "Implementation" field containing the repo id or the function location + }, + { _id: false } +); + +// Define schema +const componentVersion = new Schema( + { + name: { + type: String, + maxLength: 50, + required: true, + }, + description: { + type: String, + maxLength: 300, + }, + componentId: { + type: Schema.Types.ObjectId, + ref: 'Component', + required: true, + }, + authorization: { + authType: { + type: String, + enum: Object.keys(AUTH_TYPE), + required: true, + }, + authSetupLink: { + type: String, + }, + }, + actions: { type: [func] }, + triggers: { type: [func] }, + virtualComponentId: { + type: Schema.Types.ObjectId, + ref: 'VirtualComponent', + required: true, + }, + logo: String, + }, + { timestamps: true } +); + +module.exports = mongoose.model('ComponentVersion', componentVersion); diff --git a/lib/component-repository/src/models/VirtualComponent.js b/lib/component-repository/src/models/VirtualComponent.js new file mode 100644 index 000000000..0c4b3e560 --- /dev/null +++ b/lib/component-repository/src/models/VirtualComponent.js @@ -0,0 +1,53 @@ +const mongoose = require('mongoose'); + +const Schema = mongoose.Schema; + +const ownersSchema = new Schema({ + id: { + type: String, + required: true, + }, + type: { + type: String, + required: true, + }, + _id: false, +}); + +const versionsSchema = new Schema({ + id: { + type: Schema.Types.ObjectId, + ref: 'ComponentVersion', + }, + componentVersion: String, + apiVersion: String, + _id: false, +}); + +// Define schema +const virtualComponent = new Schema( + { + name: { type: String, required: true }, + defaultVersionId: { type: Schema.Types.ObjectId, ref: 'ComponentVersion' }, + versions: { + type: [versionsSchema], + }, + tenant: { type: String }, + access: { + type: String, + enum: ['private', 'public'], + default: 'private', + }, + owners: { + type: [ownersSchema], + required: true, + }, + active: { + type: Boolean, + default: true, + }, + }, + { timestamps: true } +); + +module.exports = mongoose.model('VirtualComponent', virtualComponent); diff --git a/lib/component-repository/src/routes/components/index.js b/lib/component-repository/src/routes/components/index.js index 20ea4562f..ea14f07c9 100644 --- a/lib/component-repository/src/routes/components/index.js +++ b/lib/component-repository/src/routes/components/index.js @@ -3,6 +3,7 @@ const asyncHandler = require('express-async-handler'); const bodyParser = require('body-parser').json(); const { parsePagedQuery } = require('../../middleware'); +const config = require('../../config'); const Component = require('../../models/Component'); async function loadComponent(req, res, next) { @@ -94,15 +95,15 @@ module.exports = ({ iam }) => { }); router.get('/', parsePagedQuery(), GetList); - router.post('/', can('components.create'), bodyParser, Create); + router.post('/', can(config.componentsCreatePermission), bodyParser, Create); router.get('/:id', loadComp, canRead, GetOne); - router.patch('/:id', can('components.update'), loadComp, canWrite, bodyParser, PatchOne); - router.delete('/:id', can('components.delete'), loadComp, canWrite, Delete); + router.patch('/:id', can(config.componentsUpdatePermission), loadComp, canWrite, bodyParser, PatchOne); + router.delete('/:id', can(config.componentDeletePermission), loadComp, canWrite, Delete); - router.post('/global/:id/start', can('all'), loadComp, GlobalConnectorStart); - router.post('/global/:id/stop', can('all'), loadComp, GlobalConnectorStop); + router.post('/global/:id/start', can(config.adminPermission), loadComp, GlobalConnectorStart); + router.post('/global/:id/stop', can(config.adminPermission), loadComp, GlobalConnectorStop); - router.patch('/enrich/:id', can('components.update'), loadComp, canWrite, Enrich); + router.patch('/enrich/:id', can(config.componentsUpdatePermission), loadComp, canWrite, Enrich); return router; }; diff --git a/lib/component-repository/src/routes/virtual-components/CreateComponentConfig.js b/lib/component-repository/src/routes/virtual-components/CreateComponentConfig.js new file mode 100644 index 000000000..ad91489f9 --- /dev/null +++ b/lib/component-repository/src/routes/virtual-components/CreateComponentConfig.js @@ -0,0 +1,43 @@ +const ComponentConfig = require('../../models/ComponentConfig'); +const mongoose = require('mongoose'); +const { validate } = require('../../utils/validator'); + +module.exports = async function (req, res, next) { + const { authClientId } = req.body; + const { componentVersionId } = req.params; + const tenant = req.user.isAdmin ? req.body.tenant : req.user.tenant; + + if (!mongoose.Types.ObjectId.isValid(componentVersionId)) { + return res + .status(400) + .send({ errors: [{ message: 'Invalid id', code: 400 }] }); + } + const currentVersionCompConfig = await ComponentConfig.findOne({ + componentVersionId, + tenant, + }); + + if (currentVersionCompConfig) { + return res.status(400).send({ + errors: [ + { message: 'This component version has already been set.', code: 400 }, + ], + }); + } + + const newConfig = new ComponentConfig({ + authClientId, + componentVersionId, + tenant + }); + const errors = validate(newConfig); + + if (errors && errors.length > 0) { + return res.status(400).send({ errors }); + } + + const savedConfig = await newConfig.save(); + res.statusCode = 201; + res.send({ data: savedConfig }); + return next(); +}; diff --git a/lib/component-repository/src/routes/virtual-components/CreateComponentVersion.js b/lib/component-repository/src/routes/virtual-components/CreateComponentVersion.js new file mode 100644 index 000000000..74dcd4158 --- /dev/null +++ b/lib/component-repository/src/routes/virtual-components/CreateComponentVersion.js @@ -0,0 +1,44 @@ +const ComponentVersion = require('../../models/ComponentVersion'); +const Component = require('../../models/Component'); +const { validate } = require('../../utils/validator'); +const { buildFunctionList } = require('../../utils/parserFunctions'); + +module.exports = async function (req, res, next) { + const { body, virtualComponent } = req; + const data = body; + + if (data._id) { + delete data._id; + } + + data.virtualComponentId = virtualComponent._id; + + if (data.triggers && !Array.isArray(data.triggers)) { + data.triggers = buildFunctionList(data.triggers); + } + if (data.actions && !Array.isArray(data.actions)) { + data.actions = buildFunctionList(data.actions); + } + + const componentVersion = new ComponentVersion(data); + const errors = validate(componentVersion); + + if (errors && errors.length > 0) { + return res.status(400).send({ errors }); + } + + const component = await Component.count({ + _id: componentVersion.componentId, + }); + + if (!component) { + return res + .status(404) + .send({ errors: { mesage: 'Component not found', code: 404 } }); + } + + res.data = await ComponentVersion.create(data); + res.statusCode = 201; + + return next(); +}; diff --git a/lib/component-repository/src/routes/virtual-components/CreateVirtualComponent.js b/lib/component-repository/src/routes/virtual-components/CreateVirtualComponent.js new file mode 100644 index 000000000..f8ce03f1d --- /dev/null +++ b/lib/component-repository/src/routes/virtual-components/CreateVirtualComponent.js @@ -0,0 +1,38 @@ +const VirtualComponent = require('../../models/VirtualComponent'); +const { validate } = require('../../utils/validator'); + +module.exports = async function (req, res, next) { + const { body, user } = req; + const data = body; + + if (data._id) { + delete data._id; + } + + data.owners = user.isAdmin + ? data.owners + : [ + { + id: user.sub, + type: 'user', + }, + ]; + + if (user.isAdmin && body.tenant) { + data.tenant = body.tenant; + } else { + data.tenant = user.tenant; + } + + const virtualComponent = new VirtualComponent(data); + const errors = validate(virtualComponent); + + if (errors && errors.length > 0) { + return res.status(400).send({ errors }); + } + + res.data = await VirtualComponent.create(data); + res.statusCode = 201; + + return next(); +}; diff --git a/lib/component-repository/src/routes/virtual-components/DeleteComponentVersion.js b/lib/component-repository/src/routes/virtual-components/DeleteComponentVersion.js new file mode 100644 index 000000000..ca28a9800 --- /dev/null +++ b/lib/component-repository/src/routes/virtual-components/DeleteComponentVersion.js @@ -0,0 +1,48 @@ +const ComponentVersion = require('../../models/ComponentVersion'); +const mongoose = require('mongoose'); +const VirtualComponent = require('../../models/VirtualComponent'); +const ComponentConfig = require('../../models/ComponentConfig'); +const { ObjectId } = mongoose.Types; + +const areTheSameObjectIds = (a, b) => { + return a.equals(b); +}; + +module.exports = async function (req, res, next) { + const componentVersionId = req.params.componentVersionId; + const { virtualComponent } = req; + + if (!mongoose.Types.ObjectId.isValid(componentVersionId)) { + return res.status(400).send({ + errors: [{ message: 'Invalid component version id', code: 400 }], + }); + } + + if ( + !areTheSameObjectIds( + new ObjectId(componentVersionId), + virtualComponent.defaultVersionId + ) + ) { + await ComponentVersion.deleteOne({ componentVersionId }); + + const newVersions = virtualComponent.versions.filter( + (nV) => !areTheSameObjectIds(nV.id, componentVersionId) + ); + await VirtualComponent.findOneAndUpdate( + { _id: virtualComponent._id }, + { + versions: newVersions, + } + ).lean(); + await ComponentConfig.deleteMany({ componentVersionId }); + res.statusCode = 204; + } else { + return res.status(400).send({ + errors: [ + { message: 'Cannot delete the default component Version', code: 400 }, + ], + }); + } + return next(); +}; diff --git a/lib/component-repository/src/routes/virtual-components/DeleteVirtualComponent.js b/lib/component-repository/src/routes/virtual-components/DeleteVirtualComponent.js new file mode 100644 index 000000000..af8e60181 --- /dev/null +++ b/lib/component-repository/src/routes/virtual-components/DeleteVirtualComponent.js @@ -0,0 +1,35 @@ +const ComponentVersion = require('../../models/ComponentVersion'); +const ComponentConfig = require('../../models/ComponentConfig'); + +module.exports = async function (req, res) { + const { virtualComponent } = req; + await virtualComponent.remove(); + + const componentVersions = await ComponentVersion.find({ + virtualComponentId: virtualComponent._id, + }).lean(); + + if (componentVersions.length) { + const componentVersionIds = componentVersions.map(({ _id }) => _id); + + const deletedManyVersion = await ComponentVersion.deleteMany({ + _id: { + $in: componentVersionIds, + }, + }); + + const deletedManyConfig = await ComponentConfig.deleteMany({ + componentVersionId: { + $in: componentVersionIds, + }, + }); + + req.logger.debug({ + deletedComponentVersions: deletedManyVersion.deletedCount, + deletedComponentConfigs: deletedManyConfig.deletedCount, + }); + } + + res.statusCode = 204; + return res.end(); +}; diff --git a/lib/component-repository/src/routes/virtual-components/GetAction.js b/lib/component-repository/src/routes/virtual-components/GetAction.js new file mode 100644 index 000000000..2f1d67ff1 --- /dev/null +++ b/lib/component-repository/src/routes/virtual-components/GetAction.js @@ -0,0 +1,37 @@ +const mongoose = require('mongoose'); +const ComponentVersion = require('../../models/ComponentVersion'); + +module.exports = async function (req, res, next) { + const { componentVersionId, actionName } = req.params; + + if (!mongoose.Types.ObjectId.isValid(componentVersionId)) { + return res + .status(400) + .send({ errors: [{ message: 'Invalid id', code: 400 }] }); + } + + const componentVersion = await ComponentVersion.findOne( + { + _id: componentVersionId, + 'actions.name': actionName, + }, + { + 'actions.$': 1, + } + ).lean(); + + if (!componentVersion) { + return res.status(404).send({ + errors: [ + { + message: 'Component version or action could not be found!', + code: 404, + }, + ], + }); + } + + req.logger.debug(componentVersion.actions[0]); + res.data = componentVersion.actions[0]; + return next(); +}; diff --git a/lib/component-repository/src/routes/virtual-components/GetComponentVersion.js b/lib/component-repository/src/routes/virtual-components/GetComponentVersion.js new file mode 100644 index 000000000..18b540235 --- /dev/null +++ b/lib/component-repository/src/routes/virtual-components/GetComponentVersion.js @@ -0,0 +1,25 @@ +const ComponentVersion = require('../../models/ComponentVersion'); +const mongoose = require('mongoose'); +module.exports = async function (req, res, next) { + const componentVersionId = req.params.componentVersionId; + + if (!mongoose.Types.ObjectId.isValid(componentVersionId)) { + return res + .status(400) + .send({ errors: [{ message: 'Invalid id', code: 400 }] }); + } + + const componentVersionData = await ComponentVersion.findOne({ + _id: componentVersionId, + }).lean(); + + if (!componentVersionData) { + return res.status(404).send({ + errors: [{ message: 'Component version could not be found', code: 404 }], + }); + } + + req.logger.debug(componentVersionData); + res.data = componentVersionData; + return next(); +}; diff --git a/lib/component-repository/src/routes/virtual-components/GetDefaultComponentsVersion.js b/lib/component-repository/src/routes/virtual-components/GetDefaultComponentsVersion.js new file mode 100644 index 000000000..b74389404 --- /dev/null +++ b/lib/component-repository/src/routes/virtual-components/GetDefaultComponentsVersion.js @@ -0,0 +1,83 @@ +const VirtualComponent = require('../../models/VirtualComponent'); +const ComponentVersion = require('../../models/ComponentVersion'); + +const lightProjectionFields = { + name: 1, + authorization: 1, + logo: 1, + description: 1, + componentId: 1, + virtualComponentId: 1, + 'triggers.name': 1, + 'triggers.title': 1, + 'triggers.description': 1, + 'triggers.active': 1, + 'actions.title': 1, + 'actions.name': 1, + 'actions.description': 1, + 'actions.active': 1, +}; + +module.exports = async function (req, res) { + const { verbose, filterActiveFunctions } = req.query; + const { user } = req; + const meta = {}; + + let query = {}; + if (user.isAdmin) { + query = {}; + } else { + query = { + $and: [ + { + active: true, + }, + { + $or: [ + { access: 'public' }, + { 'owners.id': user.sub }, + { tenant: user.tenant }, + ], + }, + ], + }; + } + + const virtualComponents = await VirtualComponent.find(query, { + _id: 0, + defaultVersionId: 1, + }).lean(); + + const virtualComponentIds = virtualComponents.map( + (virtualComponent) => virtualComponent.defaultVersionId + ); + + let projection = {}; + + if (verbose === 'false') { + projection = lightProjectionFields; + } + + const componentVersions = await ComponentVersion.find( + { + _id: { + $in: virtualComponentIds, + }, + }, + projection + ); + + if (filterActiveFunctions) { + componentVersions.forEach((component) => { + component.triggers = component.triggers.filter(({ active }) => active); + component.actions = component.actions.filter(({ active }) => active); + }); + } + + res.data = componentVersions; + + return res.send({ + data: componentVersions, + meta, + }); +}; diff --git a/lib/component-repository/src/routes/virtual-components/GetDefaultVirtualComponentsConfig.js b/lib/component-repository/src/routes/virtual-components/GetDefaultVirtualComponentsConfig.js new file mode 100644 index 000000000..653798417 --- /dev/null +++ b/lib/component-repository/src/routes/virtual-components/GetDefaultVirtualComponentsConfig.js @@ -0,0 +1,53 @@ +const VirtualComponent = require('../../models/VirtualComponent'); +const ComponentConfig = require('../../models/ComponentConfig'); + +module.exports = async function (req, res) { + const { user } = req; + const meta = {}; + let query = {}; + if (user.isAdmin) { + query = {}; + } else { + query = { + $and: [ + { + active: true, + }, + { + $or: [ + { access: 'public' }, + { 'owners.id': user.sub }, + { tenant: user.tenant }, + ], + }, + ], + }; + } + + const virtualComponents = await VirtualComponent.find(query).lean(); + const defaultVersionIds = virtualComponents.map( + (virtualComponent) => virtualComponent.defaultVersionId + ); + + let configQuery = user.isAdmin + ? {} + : { + $and: [ + { + tenant: user.tenant, + }, + { + componentVersionId: { $in: defaultVersionIds }, + }, + ], + }; + + const componentConfigs = await ComponentConfig.find(configQuery) + .lean() + .exec(); + + return res.send({ + data: componentConfigs, + meta, + }); +}; diff --git a/lib/component-repository/src/routes/virtual-components/GetOneVirtualComponent.js b/lib/component-repository/src/routes/virtual-components/GetOneVirtualComponent.js new file mode 100644 index 000000000..1379133bd --- /dev/null +++ b/lib/component-repository/src/routes/virtual-components/GetOneVirtualComponent.js @@ -0,0 +1,5 @@ +module.exports = async function (req, res, next) { + const { virtualComponent } = req; + res.data = virtualComponent; + return next(); +}; diff --git a/lib/component-repository/src/routes/virtual-components/GetTrigger.js b/lib/component-repository/src/routes/virtual-components/GetTrigger.js new file mode 100644 index 000000000..90c2ac164 --- /dev/null +++ b/lib/component-repository/src/routes/virtual-components/GetTrigger.js @@ -0,0 +1,36 @@ +const mongoose = require('mongoose'); +const ComponentVersion = require('../../models/ComponentVersion'); + +module.exports = async function (req, res, next) { + const { componentVersionId, triggerName } = req.params; + + if (!mongoose.Types.ObjectId.isValid(componentVersionId)) { + return res + .status(400) + .send({ errors: [{ message: 'Invalid id', code: 400 }] }); + } + + const componentVersion = await ComponentVersion.findOne( + { + _id: componentVersionId, + 'triggers.name': triggerName, + }, + { + 'triggers.$': 1, + } + ).lean(); + if (!componentVersion) { + return res.status(404).send({ + errors: [ + { + message: 'Component version or trigger could not be found!', + code: 404, + }, + ], + }); + } + + req.logger.debug(componentVersion.triggers[0]); + res.data = componentVersion.triggers[0]; + return next(); +}; diff --git a/lib/component-repository/src/routes/virtual-components/GetVirtualComponents.js b/lib/component-repository/src/routes/virtual-components/GetVirtualComponents.js new file mode 100644 index 000000000..47a1f2648 --- /dev/null +++ b/lib/component-repository/src/routes/virtual-components/GetVirtualComponents.js @@ -0,0 +1,30 @@ +const VirtualComponent = require('../../models/VirtualComponent'); + +module.exports = async function (req, res, next) { + const { user } = req; + + let query; + if (user.isAdmin) { + query = {}; + } else { + query = { + $and: [ + { + active: true, + }, + { + $or: [ + { access: 'public' }, + { 'owners.id': user.sub }, + { tenant: user.tenant }, + ], + }, + ], + }; + } + + const versions = await VirtualComponent.find(query).lean(); + + res.data = versions; + return next(); +}; diff --git a/lib/component-repository/src/routes/virtual-components/UpdateComponentConfig.js b/lib/component-repository/src/routes/virtual-components/UpdateComponentConfig.js new file mode 100644 index 000000000..772ada90a --- /dev/null +++ b/lib/component-repository/src/routes/virtual-components/UpdateComponentConfig.js @@ -0,0 +1,35 @@ +const ComponentConfig = require('../../models/ComponentConfig'); +const mongoose = require('mongoose'); + +module.exports = async function (req, res, next) { + const { authClientId } = req.body; + const { componentVersionId } = req.params; + const tenant = req.user.isAdmin ? req.body.tenant : req.user.tenant; + if (!mongoose.Types.ObjectId.isValid(authClientId)) { + return res + .status(400) + .send({ errors: [{ message: 'Invalid auth Client id', code: 400 }] }); + } + if (!mongoose.Types.ObjectId.isValid(componentVersionId)) { + return res + .status(400) + .send({ errors: [{ message: 'Invalid id', code: 400 }] }); + } + + const qry = { componentVersionId, tenant }; + const currentVersionComponentConfig = await ComponentConfig.findOne(qry); + + if (!currentVersionComponentConfig) { + return res.status(404).send({ + errors: [ + { message: 'This component version has not been found.', code: 404 }, + ], + }); + } + currentVersionComponentConfig.authClientId = authClientId; + await currentVersionComponentConfig.save(); + res.data = currentVersionComponentConfig; + res.statusCode = 200; + + return next(); +}; diff --git a/lib/component-repository/src/routes/virtual-components/UpdateComponentVersion.js b/lib/component-repository/src/routes/virtual-components/UpdateComponentVersion.js new file mode 100644 index 000000000..13740752c --- /dev/null +++ b/lib/component-repository/src/routes/virtual-components/UpdateComponentVersion.js @@ -0,0 +1,35 @@ +const ComponentVersion = require('../../models/ComponentVersion'); +const { validate } = require('../../utils/validator'); + +module.exports = async function (req, res, next) { + const { body } = req; + const { componentVersionId } = req.params; + + const componentVersion = await ComponentVersion.findOne({ + _id: componentVersionId, + }); + + if (!componentVersion) { + const error = new Error('ComponentVersion is not found'); + error.statusCode = 404; + throw error; + } + + const data = { + ...componentVersion._doc, + ...body, + }; + + const newComponentVersion = new ComponentVersion(data); + const errors = validate(newComponentVersion); + + if (errors && errors.length > 0) { + return res.status(400).send({ errors }); + } + + Object.assign(componentVersion, data); + res.data = await componentVersion.save(); + res.statusCode = 200; + + return next(); +}; diff --git a/lib/component-repository/src/routes/virtual-components/UpdateVirtualComponent.js b/lib/component-repository/src/routes/virtual-components/UpdateVirtualComponent.js new file mode 100644 index 000000000..9c231d43c --- /dev/null +++ b/lib/component-repository/src/routes/virtual-components/UpdateVirtualComponent.js @@ -0,0 +1,29 @@ +const VirtualComponent = require('../../models/VirtualComponent'); +const { validate } = require('../../utils/validator'); + +module.exports = async function (req, res, next) { + const { body, user, virtualComponent } = req; + const data = { + ...virtualComponent._doc, + ...body, + }; + + if (data.owners && !data.owners.length) { + data.owners.push({ + id: user.sub, + type: 'user', + }); + } + + const newVirtualComponent = new VirtualComponent(data); + const errors = validate(newVirtualComponent); + + if (errors && errors.length > 0) { + return res.status(400).send({ errors }); + } + Object.assign(virtualComponent, data); + res.data = await virtualComponent.save(); + res.statusCode = 200; + + return next(); +}; diff --git a/lib/component-repository/src/routes/virtual-components/index.js b/lib/component-repository/src/routes/virtual-components/index.js new file mode 100644 index 000000000..cb00a3ade --- /dev/null +++ b/lib/component-repository/src/routes/virtual-components/index.js @@ -0,0 +1,178 @@ +const express = require('express'); +const asyncHandler = require('express-async-handler'); +const bodyParser = express.json(); + +const config = require('../../config'); +const VirtualComponent = require('../../models/VirtualComponent'); + +const isValidVirtualComponent = (virtualComponent, user) => { + if (virtualComponent.access === 'public' || user.isAdmin) { + return true; + } + if ( + virtualComponent.access === 'private' && + (user.tenant === virtualComponent.tenant || + virtualComponent.owners.some(({ id }) => id === user.sub)) + ) { + return true; + } + return false; +}; + +const loadVirtualComponent = async (req, _, next) => { + const { id } = req.params; + const { user } = req; + const virtualComponent = await VirtualComponent.findById(id); + + if (!virtualComponent || !isValidVirtualComponent(virtualComponent, user)) { + const error = new Error('VirtualComponent is not found'); + error.statusCode = 404; + throw error; + } + req.virtualComponent = virtualComponent; + + return next(); +}; + +const canWriteVComponent = (req, _, next) => { + const { virtualComponent, user } = req; + + if (virtualComponent.access === 'public' && !user.isAdmin) { + throw Object.assign(new Error('Not authorized'), { statusCode: 403 }); + } + + next(); +}; + +module.exports = ({ iam }) => { + const { can } = iam; + const loadVirtualComp = asyncHandler(loadVirtualComponent); + + const GetVirtualComponents = asyncHandler(require('./GetVirtualComponents')); + const GetDefaultComponentsVersion = asyncHandler( + require('./GetDefaultComponentsVersion') + ); + const GetDefaultVirtualComponentsConfig = asyncHandler( + require('./GetDefaultVirtualComponentsConfig') + ); + const CreateVirtualComponent = asyncHandler( + require('./CreateVirtualComponent') + ); + const UpdateVirtualComponent = asyncHandler( + require('./UpdateVirtualComponent') + ); + const UpdateComponentVersion = asyncHandler( + require('./UpdateComponentVersion') + ); + const GetVirtualComponent = asyncHandler(require('./GetOneVirtualComponent')); + const CreateComponentVersion = asyncHandler( + require('./CreateComponentVersion') + ); + const DeleteComponentVersion = asyncHandler( + require('./DeleteComponentVersion') + ); + + const GetAction = asyncHandler(require('./GetAction')); + + const GetTrigger = asyncHandler(require('./GetTrigger')); + + const GetComponentVersion = asyncHandler(require('./GetComponentVersion')); + const CreateComponentConfig = asyncHandler( + require('./CreateComponentConfig') + ); + const UpdateComponentConfig = asyncHandler( + require('./UpdateComponentConfig') + ); + + const DeleteVirtualComponent = asyncHandler( + require('./DeleteVirtualComponent') + ); + + const router = express.Router(); + router.use(asyncHandler(iam.middleware)); + router.use((req, _, next) => { + req.logger.trace({ user: req.user }, 'Resolved IAM user'); + return next(); + }); + + // Virtual Components + router.get('/', GetVirtualComponents); + router.get('/defaults', GetDefaultComponentsVersion); + router.get('/defaults/config', GetDefaultVirtualComponentsConfig); + router.post( + '/', + can(config.componentsCreatePermission), + bodyParser, + CreateVirtualComponent + ); + router.get('/:id', loadVirtualComp, GetVirtualComponent); + router.patch( + '/:id', + can(config.componentsUpdatePermission), + loadVirtualComp, + canWriteVComponent, + bodyParser, + UpdateVirtualComponent + ); + router.delete( + '/:id', + can(config.componentDeletePermission), + loadVirtualComp, + canWriteVComponent, + DeleteVirtualComponent + ); + + // Component versions + router.post( + '/:id', + can(config.componentsCreatePermission), + loadVirtualComp, + canWriteVComponent, + bodyParser, + CreateComponentVersion + ); + router.patch( + '/:id/:componentVersionId', + can(config.componentsUpdatePermission), + loadVirtualComp, + canWriteVComponent, + bodyParser, + UpdateComponentVersion + ); + + router.delete( + '/:id/:componentVersionId', + can(config.componentDeletePermission), + loadVirtualComp, + canWriteVComponent, + DeleteComponentVersion + ); + + router.get('/:id/:componentVersionId', loadVirtualComp, GetComponentVersion); + router.post( + '/:id/:componentVersionId/config', + can(config.componentWritePermission), + loadVirtualComp, + bodyParser, + CreateComponentConfig + ); + router.patch( + '/:id/:componentVersionId/config', + can(config.componentWritePermission), + loadVirtualComp, + bodyParser, + UpdateComponentConfig + ); + router.get( + '/:id/:componentVersionId/actions/:actionName', + loadVirtualComp, + GetAction + ); + router.get( + '/:id/:componentVersionId/triggers/:triggerName', + loadVirtualComp, + GetTrigger + ); + + return router; +}; diff --git a/lib/component-repository/src/utils/parserFunctions.js b/lib/component-repository/src/utils/parserFunctions.js new file mode 100644 index 000000000..fb5548989 --- /dev/null +++ b/lib/component-repository/src/utils/parserFunctions.js @@ -0,0 +1,24 @@ +const buildFunctionList = (repoFunctions) => { + const outFunctions = []; + for (const [key,value] of Object.entries(repoFunctions)) { + + const pushObj = { + name: key, + title: value.title, + function: value.main, + description: value.description, + fields: value.fields, + schemas: { + in: value.metadata ? value.metadata.in : undefined, + out: value.metadata ? value.metadata.out : undefined, + }, + }; + outFunctions.push(pushObj); + } + + return outFunctions; +} + +module.exports = { + buildFunctionList, +} \ No newline at end of file diff --git a/lib/component-repository/src/utils/validator.js b/lib/component-repository/src/utils/validator.js new file mode 100644 index 000000000..0ba29b4cb --- /dev/null +++ b/lib/component-repository/src/utils/validator.js @@ -0,0 +1,25 @@ +function validate(collection) { + const errors = []; + + // Check for missing required attributes and length using the mongoose validation + const validateErrors = collection.validateSync(); + if (validateErrors) { + const validateErrorsKeys = Object.keys(validateErrors.errors); + for (let i = 0; i < validateErrorsKeys.length; i += 1) { + if ( + !validateErrors.errors[validateErrorsKeys[i]].message.startsWith( + 'Validation failed', + ) + ) { + errors.push({ + message: validateErrors.errors[validateErrorsKeys[i]].message, + code: 400, + }); + } + } + } + + return errors; + } + + module.exports = { validate }; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index c4816356e..15d00a572 100644 --- a/package-lock.json +++ b/package-lock.json @@ -467,6 +467,7 @@ "swagger-ui-express": "4.0.2" }, "devDependencies": { + "@faker-js/faker": "^6.0.0-alpha.6", "bunyan": "1.8.14", "chai": "4.3.4", "eslint": "7.32.0", @@ -4093,6 +4094,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@faker-js/faker": { + "version": "6.0.0-alpha.6", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-6.0.0-alpha.6.tgz", + "integrity": "sha512-+jatKq8wYwOgCpVQ0wE4t9BglS16qJH/Nu4WjPU+ABTywivTY8BCsD1XIZhw9iXDq3t1InyiXmz+R70TcUMbrg==", + "dev": true, + "engines": { + "node": ">=14.0.0", + "npm": ">=7.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.5.0", "dev": true, @@ -34585,7 +34596,7 @@ } }, "services/governance-service": { - "version": "1.4.2", + "version": "1.4.4", "license": "Apache-2.0", "dependencies": { "@openintegrationhub/event-bus": "*", @@ -39203,7 +39214,7 @@ "license": "MIT" }, "services/web-ui": { - "version": "0.4.0", + "version": "0.4.3", "license": "Apache-2.0", "dependencies": { "@basaas/node-logger": "1.1.5", @@ -41042,6 +41053,12 @@ } } }, + "@faker-js/faker": { + "version": "6.0.0-alpha.6", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-6.0.0-alpha.6.tgz", + "integrity": "sha512-+jatKq8wYwOgCpVQ0wE4t9BglS16qJH/Nu4WjPU+ABTywivTY8BCsD1XIZhw9iXDq3t1InyiXmz+R70TcUMbrg==", + "dev": true + }, "@humanwhocodes/config-array": { "version": "0.5.0", "dev": true, @@ -41499,6 +41516,7 @@ "@openintegrationhub/component-repository": { "version": "file:lib/component-repository", "requires": { + "@faker-js/faker": "^6.0.0-alpha.6", "@openintegrationhub/iam-utils": "*", "body-parser": "1.19.0", "bunyan": "1.8.14", diff --git a/services/component-repository/src/App.js b/services/component-repository/src/App.js index 956face56..4ceb6f726 100644 --- a/services/component-repository/src/App.js +++ b/services/component-repository/src/App.js @@ -21,7 +21,6 @@ class ComponentRepositoryApp extends App { .singleton() .inject(() => ({iam: undefined, eventClass: Event})) //use default iam middleware }); - const server = container.resolve('server'); await server.start(); } diff --git a/services/iam/package.json b/services/iam/package.json index 447ebb4a9..a0328410b 100644 --- a/services/iam/package.json +++ b/services/iam/package.json @@ -9,6 +9,7 @@ "watch-scss": "npm run build --watch", "watch-test": "jest --watch --runInBand", "start": "node src/index.js", + "start:nodemon": "NODE_ENV=development nodemon -r dotenv/config src/index.js", "build": "sass ./src/views/style/basic.scss:./src/views/style/basic.css", "lint": "eslint src/ test/", "task": "node",