From 320e513508ca0b73d0e3b6fcae2d77b9dd73024c Mon Sep 17 00:00:00 2001 From: akshatdubeysf <77672713+akshatdubeysf@users.noreply.github.com> Date: Tue, 21 Sep 2021 11:36:44 +0530 Subject: [PATCH] feat(bpmn-service): add unit and acceptance tests (#289) GH-0 --- services/bpmn-service/.eslintignore | 1 + services/bpmn-service/.nycrc | 7 + services/bpmn-service/.prettierignore | 1 + services/bpmn-service/package-lock.json | 9 + services/bpmn-service/package.json | 8 +- .../src/__tests__/acceptance/helper.ts | 69 +++++ .../workflow.controller.acceptance.ts | 291 ++++++++++++++++++ .../src/__tests__/commands/test.command.ts | 11 + services/bpmn-service/src/__tests__/const.ts | 18 ++ .../datasources/workflowdb.datasource.ts | 34 ++ .../bpmn-service/src/__tests__/mock-engine.ts | 155 ++++++++++ .../provider/bearer-token-verify.provider.ts | 16 + .../provider/workflow-helper-mock.provider.ts | 66 ++++ .../workflow-mock-implementation.provider.ts | 35 +++ services/bpmn-service/src/__tests__/types.ts | 13 + .../__tests__/unit/register-worker.unit.ts | 60 ++++ .../src/controllers/workflow.controller.ts | 42 ++- .../execution-input-validator.provider.ts | 8 +- services/scheduler-service/.nycrc | 2 +- 19 files changed, 818 insertions(+), 28 deletions(-) create mode 100644 services/bpmn-service/.nycrc create mode 100644 services/bpmn-service/src/__tests__/acceptance/helper.ts create mode 100644 services/bpmn-service/src/__tests__/acceptance/workflow.controller.acceptance.ts create mode 100644 services/bpmn-service/src/__tests__/commands/test.command.ts create mode 100644 services/bpmn-service/src/__tests__/const.ts create mode 100644 services/bpmn-service/src/__tests__/datasources/workflowdb.datasource.ts create mode 100644 services/bpmn-service/src/__tests__/mock-engine.ts create mode 100644 services/bpmn-service/src/__tests__/provider/bearer-token-verify.provider.ts create mode 100644 services/bpmn-service/src/__tests__/provider/workflow-helper-mock.provider.ts create mode 100644 services/bpmn-service/src/__tests__/provider/workflow-mock-implementation.provider.ts create mode 100644 services/bpmn-service/src/__tests__/types.ts create mode 100644 services/bpmn-service/src/__tests__/unit/register-worker.unit.ts diff --git a/services/bpmn-service/.eslintignore b/services/bpmn-service/.eslintignore index af366873dc..2ea907d884 100644 --- a/services/bpmn-service/.eslintignore +++ b/services/bpmn-service/.eslintignore @@ -2,6 +2,7 @@ node_modules/ dist/ coverage/ migrations/ +.nyc_output .eslintrc.js migration.js \ No newline at end of file diff --git a/services/bpmn-service/.nycrc b/services/bpmn-service/.nycrc new file mode 100644 index 0000000000..ced77aacf3 --- /dev/null +++ b/services/bpmn-service/.nycrc @@ -0,0 +1,7 @@ +{ + "include": ["dist"], + "exclude": ["dist/__tests__/"], + "extension": [".js", ".ts"], + "reporter": ["text", "html"], + "exclude-after-remap": false +} \ No newline at end of file diff --git a/services/bpmn-service/.prettierignore b/services/bpmn-service/.prettierignore index c6911da9e1..bfc57b89fa 100644 --- a/services/bpmn-service/.prettierignore +++ b/services/bpmn-service/.prettierignore @@ -1,2 +1,3 @@ dist *.json +coverage \ No newline at end of file diff --git a/services/bpmn-service/package-lock.json b/services/bpmn-service/package-lock.json index 493e2188d9..713bdda0de 100644 --- a/services/bpmn-service/package-lock.json +++ b/services/bpmn-service/package-lock.json @@ -962,6 +962,15 @@ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz", "integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==" }, + "@types/jsonwebtoken": { + "version": "8.5.4", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.5.4.tgz", + "integrity": "sha512-4L8msWK31oXwdtC81RmRBAULd0ShnAHjBuKT9MRQpjP0piNrZdXyTRcKY9/UIfhGeKIT4PvF5amOOUbbT/9Wpg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/keyv": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.1.tgz", diff --git a/services/bpmn-service/package.json b/services/bpmn-service/package.json index acc229e546..d7ac758beb 100644 --- a/services/bpmn-service/package.json +++ b/services/bpmn-service/package.json @@ -24,7 +24,9 @@ "openapi-spec": "node ./dist/openapi-spec", "apidocs": "./node_modules/.bin/widdershins --search false --language_tabs 'javascript:JavaScript:request' 'javascript--nodejs:Node.JS' --summary openapi.json -o openapi.md", "pretest": "npm run clean && npm run build", - "test": "echo \"No tests !\"", + "test": "lb-mocha --allow-console-logs \"dist/__tests__\"", + "coverage": "lb-nyc npm run test", + "coverage:ci": "lb-nyc report --reporter=text-lcov | coveralls", "posttest": "npm run lint", "test:dev": "lb-mocha --allow-console-logs dist/__tests__/**/*.js && npm run posttest", "clean": "lb-clean dist *.tsbuildinfo .eslintcache", @@ -62,6 +64,7 @@ "camunda-external-task-client-js": "^2.1.0", "dotenv": "^8.2.0", "dotenv-extended": "^2.9.0", + "jsonwebtoken": "^8.5.1", "loopback-connector-postgresql": "^5.3.0", "loopback4-authentication": "^4.6.0", "loopback4-authorization": "^3.2.0", @@ -76,6 +79,7 @@ "@types/camunda-external-task-client-js": "^1.3.1", "@types/lodash": "^4.14.169", "@types/node": "^10.17.60", + "@types/jsonwebtoken": "^8.5.4", "db-migrate": "^0.11.12", "db-migrate-pg": "^1.2.2", "eslint": "^7.25.0", @@ -87,4 +91,4 @@ "registry": "https://registry.npmjs.org/", "access": "public" } -} +} \ No newline at end of file diff --git a/services/bpmn-service/src/__tests__/acceptance/helper.ts b/services/bpmn-service/src/__tests__/acceptance/helper.ts new file mode 100644 index 0000000000..3be786dcf6 --- /dev/null +++ b/services/bpmn-service/src/__tests__/acceptance/helper.ts @@ -0,0 +1,69 @@ +import { + Client, + createRestAppClient, + givenHttpServerConfig, +} from '@loopback/testlab'; +import {BearerTokenVerifyProvider} from '../provider/bearer-token-verify.provider'; +import {Strategies} from 'loopback4-authentication'; +import {WorkflowServiceApplication} from '../../application'; +import {BPMTask, WorkflowCacheSourceName} from '../../types'; +import {WorkflowDbDatasource} from '../datasources/workflowdb.datasource'; +import {WorkflowServiceBindings} from '../../keys'; +import {WorkflowMockProvider} from '../provider/workflow-helper-mock.provider'; +import {firstTestBpmn, MOCK_CAMUNDA} from '../const'; +import {MOCK_BPMN_ENGINE_KEY} from '../types'; +import {MockEngine} from '../mock-engine'; +import {BindingScope} from '@loopback/context'; +import {WorkerMockImplementationProvider} from '../provider/workflow-mock-implementation.provider'; +import {TestBpmnCommand} from '../commands/test.command'; + +export async function setUpApplication(): Promise { + const app = new WorkflowServiceApplication({ + rest: givenHttpServerConfig(), + }); + + app.dataSource(WorkflowDbDatasource); + app.bind(`datasources.config.${WorkflowCacheSourceName}`).to({ + name: 'pgdb', + connector: 'memory', + }); + app + .bind(Strategies.Passport.BEARER_TOKEN_VERIFIER) + .toProvider(BearerTokenVerifyProvider); + app + .bind(WorkflowServiceBindings.WorkflowManager) + .toProvider(WorkflowMockProvider); + app + .bind(WorkflowServiceBindings.WorkerImplementationFunction) + .toProvider(WorkerMockImplementationProvider); + app.bind(WorkflowServiceBindings.Config).to({ + workflowEngineBaseUrl: MOCK_CAMUNDA, + useCustomSequence: false, + }); + app + .bind(MOCK_BPMN_ENGINE_KEY) + .toClass(MockEngine) + .inScope(BindingScope.SINGLETON); + const registerFn = await app.getValueOrPromise( + WorkflowServiceBindings.RegisterWorkerFunction, + ); + if (registerFn) { + const cmd = new TestBpmnCommand(); + await registerFn(firstTestBpmn.name, 'topic1', new BPMTask(cmd)); + await registerFn(firstTestBpmn.name, 'topic2', new BPMTask(cmd)); + } else { + throw new Error('No worker register function in the context'); + } + await app.boot(); + + await app.start(); + + const client = createRestAppClient(app); + + return {app, client}; +} + +export interface AppWithClient { + app: WorkflowServiceApplication; + client: Client; +} diff --git a/services/bpmn-service/src/__tests__/acceptance/workflow.controller.acceptance.ts b/services/bpmn-service/src/__tests__/acceptance/workflow.controller.acceptance.ts new file mode 100644 index 0000000000..995a4184bc --- /dev/null +++ b/services/bpmn-service/src/__tests__/acceptance/workflow.controller.acceptance.ts @@ -0,0 +1,291 @@ +import {WorkflowServiceApplication} from '../../application'; +import {Client, expect} from '@loopback/testlab'; +import * as jwt from 'jsonwebtoken'; +import {setUpApplication} from './helper'; +import {firstTestBpmnInput, firstTestBpmn} from '../const'; +import {MockEngine} from '../mock-engine'; +import {MOCK_BPMN_ENGINE_KEY} from '../types'; +import { + WorkflowRepository, + WorkflowVersionRepository, +} from '../../repositories'; + +describe('Workflow Controller', () => { + let app: WorkflowServiceApplication; + let client: Client; + let mockBpmnEngine: MockEngine; + let workflowRepo: WorkflowRepository; + let workflowVersionRepo: WorkflowVersionRepository; + const basePath = '/workflow'; + const pass = 'test_password'; + const testUser = { + id: 1, + username: 'test_user', + password: pass, + permissions: [ + 'ViewWorkflow', + 'CreateWorkflow', + 'UpdateWorkflow', + 'DeleteWorkflow', + ], + }; + + const token = jwt.sign(testUser, 'kdskssdkdfs', { + expiresIn: 180000, + issuer: 'sf', + }); + + before('setupApplication', async () => { + ({app, client} = await setUpApplication()); + }); + before(givenRepositories); + before(givenMockEngine); + + after(async () => app.stop()); + + afterEach(deleteMockData); + + describe('/workflows GET', () => { + it('gives status 401 when no token is passed', async () => { + const response = await client.get(basePath).expect(401); + expect(response).to.have.property('error'); + }); + + it('gives status 200 when token is passed, with no existing workflow', async () => { + const response = await client + .get(basePath) + .set('authorization', `Bearer ${token}`) + .expect(200); + + expect(response.body).to.have.length(0); + }); + + it('gives status 200 when token is passed, with an existing workflow', async () => { + await client + .post(basePath) + .send(firstTestBpmn) + .set('authorization', `Bearer ${token}`); + + const response = await client + .get(basePath) + .set('authorization', `Bearer ${token}`) + .expect(200); + + expect(response.body).to.have.length(1); + expect(response.body[0]) + .to.have.property('name') + .equal(firstTestBpmn.name); + const dataInEngine = mockBpmnEngine.get(firstTestBpmn.name); + expect(dataInEngine) + .to.have.property('file') + .equal(firstTestBpmn.bpmnFile); + }); + }); + + describe('/workflows POST', () => { + it('gives status 200 when creating a workflow using correct payload', async () => { + await client + .post(basePath) + .send(firstTestBpmn) + .set('authorization', `Bearer ${token}`) + .expect(200); + }); + }); + + describe('/workflow/:id PATCH', () => { + it('gives status 404 when token is passed, with non-existant workflow id', async () => { + const updatedFile = JSON.stringify(['topic1']); + const updatedFirstTestBpmn = { + ...firstTestBpmn, + bpmnFile: updatedFile, + }; + + await client + .patch(`${basePath}/0`) + .send(updatedFirstTestBpmn) + .set('authorization', `Bearer ${token}`) + .expect(404); + }); + + it('gives status 204 when token is passed, with an existing workflow id, and workflow is updated', async () => { + const saved = await client + .post(basePath) + .send(firstTestBpmn) + .set('authorization', `Bearer ${token}`); + + const updatedFile = JSON.stringify(['topic1']); + const updatedFirstTestBpmn = { + ...firstTestBpmn, + bpmnFile: updatedFile, + }; + + await client + .patch(`${basePath}/${saved.body.id}`) + .send(updatedFirstTestBpmn) + .set('authorization', `Bearer ${token}`) + .expect(204); + + const updated = await client + .get(basePath) + .set('authorization', `Bearer ${token}`) + .expect(200); + + expect(updated.body).to.have.length(1); + expect(updated.body[0]) + .to.have.property('name') + .equal(firstTestBpmn.name); + const dataInEngine = mockBpmnEngine.get(updatedFirstTestBpmn.name); + expect(dataInEngine).to.have.property('file').equal(updatedFile); + }); + }); + + describe('/workflows/:id DELETE', () => { + it('gives 204 when token is passed, with an existing workflow id', async () => { + const saved = await client + .post(basePath) + .send(firstTestBpmn) + .set('authorization', `Bearer ${token}`); + + await client + .delete(`${basePath}/${saved.body.id}`) + .set('authorization', `Bearer ${token}`) + .expect(204); + }); + + it('gives 404 when token is passed, with a non-existant workflow id', async () => { + await client + .delete(`${basePath}/0`) + .set('authorization', `Bearer ${token}`) + .expect(404); + }); + }); + + describe('/workflows/:id/version/:version DELETE', () => { + it('gives 204 when token is passed, with an existing non-latest workflow version id', async () => { + const saved = await client + .post(basePath) + .send(firstTestBpmn) + .set('authorization', `Bearer ${token}`); + + await client + .patch(`${basePath}/${saved.body.id}`) + .send(firstTestBpmn) + .set('authorization', `Bearer ${token}`); + + await client + .delete( + `${basePath}/${saved.body.id}/version/${saved.body.workflowVersion}`, + ) + .set('authorization', `Bearer ${token}`) + .expect(204); + }); + + it('gives 404 when token is passed, with a non-existant workflow version id', async () => { + const saved = await client + .post(basePath) + .send(firstTestBpmn) + .set('authorization', `Bearer ${token}`); + + await client + .delete( + `${basePath}/${saved.body.id}/version/${ + saved.body.workflowVersion + 1 + }`, + ) + .set('authorization', `Bearer ${token}`) + .expect(404); + }); + }); + + describe('/workflow/:id/execute POST', () => { + it('gives 400 when invalid input is passed', async () => { + const saved = await client + .post(basePath) + .send(firstTestBpmn) + .set('authorization', `Bearer ${token}`); + + const input = { + ...firstTestBpmnInput, + valueB: 123, + }; + + await client + .post(`${basePath}/${saved.body.id}/execute`) + .send({input}) + .set('authorization', `Bearer ${token}`) + .expect(400); + }); + + it('gives 200 when valid input is passed, and workflow is completed in the engine', async () => { + const saved = await client + .post(basePath) + .send(firstTestBpmn) + .set('authorization', `Bearer ${token}`); + + await client + .post(`${basePath}/${saved.body.id}/execute`) + .send({input: firstTestBpmnInput}) + .set('authorization', `Bearer ${token}`) + .expect(200); + + expect(mockBpmnEngine.instances[saved.body.name].complete).to.be.true(); + }); + + it('gives 404 when invalid workflow id is passed', async () => { + await client + .post(`${basePath}/0/execute`) + .send({input: firstTestBpmnInput}) + .set('authorization', `Bearer ${token}`) + .expect(404); + }); + + it('gives 404 when invalid workflow version is passed', async () => { + const saved = await client + .post(basePath) + .send(firstTestBpmn) + .set('authorization', `Bearer ${token}`); + + await client + .post(`${basePath}/${saved.body.id}/execute`) + .send({ + input: firstTestBpmnInput, + workflowVersion: saved.body.workflowVersion + 1, + }) + .set('authorization', `Bearer ${token}`) + .expect(404); + }); + + it('gives 200 when valid workflow version and input is passed, and workflow is completed in the engine', async () => { + const saved = await client + .post(basePath) + .send(firstTestBpmn) + .set('authorization', `Bearer ${token}`); + + await client + .post(`${basePath}/${saved.body.id}/execute`) + .send({ + input: firstTestBpmnInput, + workflowVersion: saved.body.workflowVersion, + }) + .set('authorization', `Bearer ${token}`) + .expect(200); + + expect(mockBpmnEngine.instances[saved.body.name].complete).to.be.true(); + }); + }); + + async function deleteMockData() { + await workflowRepo.deleteAllHard(); + await workflowVersionRepo.deleteAll(); + mockBpmnEngine.clear(); + } + + async function givenRepositories() { + workflowRepo = await app.getRepository(WorkflowRepository); + workflowVersionRepo = await app.getRepository(WorkflowVersionRepository); + } + + async function givenMockEngine() { + mockBpmnEngine = await app.get(MOCK_BPMN_ENGINE_KEY); + } +}); diff --git a/services/bpmn-service/src/__tests__/commands/test.command.ts b/services/bpmn-service/src/__tests__/commands/test.command.ts new file mode 100644 index 0000000000..dde08a90f1 --- /dev/null +++ b/services/bpmn-service/src/__tests__/commands/test.command.ts @@ -0,0 +1,11 @@ +import {AnyObject} from '@loopback/repository'; +import {ICommand} from '@sourceloop/core'; + +export class TestBpmnCommand implements ICommand { + parameters: AnyObject; + + async execute() { + const finish = this.parameters.finish; + finish(); + } +} diff --git a/services/bpmn-service/src/__tests__/const.ts b/services/bpmn-service/src/__tests__/const.ts new file mode 100644 index 0000000000..c2534d0015 --- /dev/null +++ b/services/bpmn-service/src/__tests__/const.ts @@ -0,0 +1,18 @@ +import {WorkflowDto} from '../models'; + +export const MOCK_CAMUNDA = 'https://mock-camunda.api/engine-rest'; + +export const firstTestBpmn: WorkflowDto = new WorkflowDto({ + name: 'first-bpmn', + bpmnFile: JSON.stringify(['topic1', 'topic2']), + inputSchema: { + type: 'object', + properties: {valueA: {type: 'string'}, valueB: {type: 'string'}}, + required: ['valueA', 'valueB'], + }, +}); + +export const firstTestBpmnInput = { + valueA: 'string', + valueB: 'string', +}; diff --git a/services/bpmn-service/src/__tests__/datasources/workflowdb.datasource.ts b/services/bpmn-service/src/__tests__/datasources/workflowdb.datasource.ts new file mode 100644 index 0000000000..56d8f7a7c3 --- /dev/null +++ b/services/bpmn-service/src/__tests__/datasources/workflowdb.datasource.ts @@ -0,0 +1,34 @@ +import { + inject, + lifeCycleObserver, + LifeCycleObserver, + ValueOrPromise, +} from '@loopback/core'; +import {juggler} from '@loopback/repository'; +import {WorkflowCacheSourceName} from '../..'; + +const config = { + name: WorkflowCacheSourceName, + connector: 'memory', + localStorage: '', + file: '', +}; + +@lifeCycleObserver('datasource') +export class WorkflowDbDatasource + extends juggler.DataSource + implements LifeCycleObserver +{ + static dataSourceName = WorkflowCacheSourceName; + + constructor( + @inject(`datasources.config.${WorkflowCacheSourceName}`, {optional: true}) + dsConfig: object = config, + ) { + super(dsConfig); + } + + stop(): ValueOrPromise { + return super.disconnect(); + } +} diff --git a/services/bpmn-service/src/__tests__/mock-engine.ts b/services/bpmn-service/src/__tests__/mock-engine.ts new file mode 100644 index 0000000000..e0606ff77f --- /dev/null +++ b/services/bpmn-service/src/__tests__/mock-engine.ts @@ -0,0 +1,155 @@ +import {AnyObject} from '@loopback/repository'; +import {HttpErrors} from '@loopback/rest'; +import {WorkflowDto} from '../models'; +import {MockCamundaWorkflow} from './types'; + +export class MockEngine { + workflowList: { + [id: string]: { + [version: number]: MockCamundaWorkflow; + }; + } = {}; + subscriptions: { + [topic: string]: (data: AnyObject, finish: () => void) => void; + } = {}; + + instances: { + [name: string]: { + topics: { + topic: string; + done: boolean; + }[]; + complete: boolean; + }; + } = {}; + + create(workflow: WorkflowDto) { + const workflowPrevList = this.workflowList[workflow.name]; + let version = 0; + if (workflowPrevList) { + version = Object.keys(workflowPrevList).length; + workflowPrevList[version] = { + workflowVersion: version, + externalIdentifier: workflow.name, + name: workflow.name, + provider: 'bpmn', + inputSchema: workflow.inputSchema, + workflowVersions: [], + file: workflow.bpmnFile, + }; + } else { + this.workflowList[workflow.name] = { + [version]: { + workflowVersion: version, + externalIdentifier: workflow.name, + name: workflow.name, + provider: 'bpmn', + inputSchema: workflow.inputSchema, + workflowVersions: [], + file: workflow.bpmnFile, + }, + }; + } + return this.workflowList[workflow.name][version]; + } + + update(workflow: WorkflowDto) { + if (this.workflowList?.[workflow.name]) { + const latest = Math.max( + ...Object.keys(this.workflowList[workflow.name]).map(s => Number(s)), + ); + this.workflowList[workflow.name][latest + 1] = { + workflowVersion: latest + 1, + externalIdentifier: workflow.name, + name: workflow.name, + provider: 'bpmn', + inputSchema: workflow.inputSchema, + workflowVersions: [], + file: workflow.bpmnFile, + }; + return this.workflowList[workflow.name][latest + 1]; + } else { + throw new HttpErrors.NotFound('Not Found'); + } + } + + get(name: string, version?: number) { + if (version) { + if (this.workflowList?.[name]?.[version]) { + return this.workflowList[name][version]; + } else { + throw new HttpErrors.NotFound('Not Found'); + } + } else { + if (this.workflowList?.[name]) { + const latest = Object.keys(this.workflowList?.[name]).length - 1; + if (latest > -1) { + return this.workflowList[name][latest]; + } else { + throw new HttpErrors.NotFound('Not Found'); + } + } else { + throw new HttpErrors.NotFound('Not Found'); + } + } + } + + delete(name: string) { + if (this.workflowList?.[name]) { + const latest = Math.max( + ...Object.keys(this.workflowList[name]).map(s => Number(s)), + ); + const workflowBackup = this.workflowList[name][latest]; + delete this.workflowList[name]; + return workflowBackup; + } else { + throw new HttpErrors.NotFound('Not Found'); + } + } + + deleteVersion(name: string, version: number) { + if (this.workflowList?.[name]?.[version]) { + delete this.workflowList[name][version]; + } else { + throw new HttpErrors.NotFound('Not Found'); + } + } + + clear() { + this.workflowList = {}; + } + + async start(input: AnyObject, name: string, version?: number) { + const workflow = this.get(name, version); + const topics = JSON.parse(workflow.file) as string[]; + this.instances[workflow.name] = { + topics: topics.map(t => ({topic: t, done: false})), + complete: false, + }; + this.triggerTask(name, topics[0], input); + return workflow; + } + + subscribe(topic: string, cb: (data: AnyObject, finish: () => void) => void) { + this.subscriptions[topic] = cb; + } + + triggerTask(name: string, topic: string, data: AnyObject) { + const finish = () => { + const instance = this.instances[name]; + const current = instance.topics.find(t => t.topic === topic); + if (current) { + current.done = true; + } else { + instance.complete = true; + } + const next = instance.topics.find(t => !t.done); + if (next && !instance.complete) { + this.triggerTask(name, next.topic, data); + } else { + instance.complete = true; + } + }; + this.subscriptions[topic](data, finish); + } +} diff --git a/services/bpmn-service/src/__tests__/provider/bearer-token-verify.provider.ts b/services/bpmn-service/src/__tests__/provider/bearer-token-verify.provider.ts new file mode 100644 index 0000000000..0c913e13f9 --- /dev/null +++ b/services/bpmn-service/src/__tests__/provider/bearer-token-verify.provider.ts @@ -0,0 +1,16 @@ +import {Provider} from '@loopback/context'; +import {IAuthUserWithPermissions} from '@sourceloop/core'; +import {verify} from 'jsonwebtoken'; +import {VerifyFunction} from 'loopback4-authentication'; + +export class BearerTokenVerifyProvider + implements Provider +{ + value(): VerifyFunction.BearerFn { + return async (token: string) => { + return verify(token, 'kdskssdkdfs', { + issuer: 'sf', + }) as IAuthUserWithPermissions; + }; + } +} diff --git a/services/bpmn-service/src/__tests__/provider/workflow-helper-mock.provider.ts b/services/bpmn-service/src/__tests__/provider/workflow-helper-mock.provider.ts new file mode 100644 index 0000000000..6cc41dcbe0 --- /dev/null +++ b/services/bpmn-service/src/__tests__/provider/workflow-helper-mock.provider.ts @@ -0,0 +1,66 @@ +import {bind, inject, BindingScope, Provider, Getter} from '@loopback/core'; +import {Workflow} from '../../models'; +import {WorflowManager} from '../../types'; +import {MockEngine} from '../mock-engine'; +import {MockCamundaWorkflow, MOCK_BPMN_ENGINE_KEY} from '../types'; + +@bind({scope: BindingScope.TRANSIENT}) +export class WorkflowMockProvider implements Provider { + workflowList: { + [id: string]: { + [version: string]: MockCamundaWorkflow; + }; + } = {}; + constructor( + @inject.getter(MOCK_BPMN_ENGINE_KEY) + private readonly engine: Getter, + ) {} + value(): WorflowManager { + return { + getWorkflowById: async (workflow: Workflow) => { + const engine = await this.engine(); + return new Workflow(engine.get(workflow.name)); + }, + startWorkflow: async (input, workflow, version) => { + const engine = await this.engine(); + return new Workflow( + await engine.start( + input, + workflow.name, + version?.version ?? workflow.workflowVersion, + ), + ); + }, + createWorkflow: async workflow => { + const engine = await this.engine(); + const mockWorkflow = engine.create(workflow); + return { + version: mockWorkflow.workflowVersion, + provider: 'bpmn', + processId: workflow.name, + externalId: workflow.name, + }; + }, + updateWorkflow: async workflow => { + const engine = await this.engine(); + const mockWorkflow = engine.update(workflow); + return { + version: mockWorkflow.workflowVersion, + provider: 'bpmn', + processId: workflow.name, + externalId: workflow.name, + }; + }, + deleteWorkflowById: async workflow => { + const engine = await this.engine(); + const mockWorkflow = engine.delete(workflow.name); + return new Workflow(mockWorkflow); + }, + deleteWorkflowVersionById: async version => { + const engine = await this.engine(); + engine.deleteVersion(version.externalWorkflowId, version.version); + return version; + }, + }; + } +} diff --git a/services/bpmn-service/src/__tests__/provider/workflow-mock-implementation.provider.ts b/services/bpmn-service/src/__tests__/provider/workflow-mock-implementation.provider.ts new file mode 100644 index 0000000000..d4d12ded66 --- /dev/null +++ b/services/bpmn-service/src/__tests__/provider/workflow-mock-implementation.provider.ts @@ -0,0 +1,35 @@ +import {inject, Provider} from '@loopback/context'; +import {ILogger, LOGGER} from '@sourceloop/core'; +import {IWorkflowServiceConfig, WorkerImplementationFn} from '../../types'; +import {WorkflowServiceBindings} from '../../keys'; +import {MOCK_BPMN_ENGINE_KEY} from '../types'; +import {MockEngine} from '../mock-engine'; + +export class WorkerMockImplementationProvider + implements Provider +{ + constructor( + @inject(WorkflowServiceBindings.Config) + config: IWorkflowServiceConfig, + @inject(LOGGER.LOGGER_INJECT) + private readonly ilogger: ILogger, + @inject(MOCK_BPMN_ENGINE_KEY) + private readonly engine: MockEngine, + ) {} + value(): WorkerImplementationFn { + return async worker => { + worker.running = true; + this.engine.subscribe(worker.topic, (data, finish) => { + worker.command.operation( + { + data, + finish, + }, + () => { + this.ilogger.info(`completed ${worker.topic}`); + }, + ); + }); + }; + } +} diff --git a/services/bpmn-service/src/__tests__/types.ts b/services/bpmn-service/src/__tests__/types.ts new file mode 100644 index 0000000000..494574ef99 --- /dev/null +++ b/services/bpmn-service/src/__tests__/types.ts @@ -0,0 +1,13 @@ +import {AnyObject} from '@loopback/repository'; + +export interface MockCamundaWorkflow { + workflowVersion: number; + externalIdentifier: string; + name: string; + provider: string; + inputSchema: AnyObject; + workflowVersions: []; + file: string; +} + +export const MOCK_BPMN_ENGINE_KEY = 'mock.bpmn.engine'; diff --git a/services/bpmn-service/src/__tests__/unit/register-worker.unit.ts b/services/bpmn-service/src/__tests__/unit/register-worker.unit.ts new file mode 100644 index 0000000000..c2d43642f6 --- /dev/null +++ b/services/bpmn-service/src/__tests__/unit/register-worker.unit.ts @@ -0,0 +1,60 @@ +import {WorkerRegisterFnProvider} from '../../providers/register-worker.service'; +import {expect} from '@loopback/testlab'; +import {BPMTask, WorkerMap} from '../../types'; +import {AnyObject} from '@loopback/repository'; + +describe('RegisterWorker unit', () => { + let map: WorkerMap = {}; + const getterStub = async () => map; + const setterStub = (value: AnyObject) => { + map = value; + }; + + beforeEach(() => { + map = {}; + setterStub({}); + }); + + it('sets a worker value in a map againts a workflow name', async () => { + const registerWorker = new WorkerRegisterFnProvider( + getterStub, + setterStub, + ).value(); + const task1 = new BPMTask(); + const task2 = new BPMTask(); + const task3 = new BPMTask(); + const task4 = new BPMTask(); + const workerMap = { + workflow1: [ + { + command: task1, + running: false, + topic: 'topic1', + }, + { + command: task2, + running: false, + topic: 'topic2', + }, + ], + workflow2: [ + { + command: task3, + running: false, + topic: 'topic3', + }, + { + command: task4, + running: false, + topic: 'topic4', + }, + ], + }; + await registerWorker('workflow1', 'topic1', task1); + await registerWorker('workflow1', 'topic2', task2); + await registerWorker('workflow2', 'topic3', task3); + await registerWorker('workflow2', 'topic4', task4); + + expect(map).to.deepEqual(workerMap); + }); +}); diff --git a/services/bpmn-service/src/controllers/workflow.controller.ts b/services/bpmn-service/src/controllers/workflow.controller.ts index fbce716279..adca3a8c22 100644 --- a/services/bpmn-service/src/controllers/workflow.controller.ts +++ b/services/bpmn-service/src/controllers/workflow.controller.ts @@ -126,33 +126,29 @@ export class WorkflowController { workflowDto: WorkflowDto, @param.path.string('id') id: string, ): Promise { - try { - const workflowResponse = await this.workflowManagerService.updateWorkflow( - workflowDto, - ); + const workflowResponse = await this.workflowManagerService.updateWorkflow( + workflowDto, + ); - const entity = new Workflow({ - workflowVersion: workflowResponse.version, - externalIdentifier: workflowResponse.processId, - name: workflowDto.name, - provider: workflowResponse.provider, - inputSchema: workflowDto.inputSchema, - }); + const entity = new Workflow({ + workflowVersion: workflowResponse.version, + externalIdentifier: workflowResponse.processId, + name: workflowDto.name, + provider: workflowResponse.provider, + inputSchema: workflowDto.inputSchema, + }); - await this.workflowRepository.updateById(id, entity); + await this.workflowRepository.updateById(id, entity); - const version = new WorkflowVersion({ - workflowId: id, - version: workflowResponse.version, - bpmnDiagram: workflowResponse.fileRef, - externalWorkflowId: workflowResponse.externalId, - inputSchema: workflowDto.inputSchema, - }); + const version = new WorkflowVersion({ + workflowId: id, + version: workflowResponse.version, + bpmnDiagram: workflowResponse.fileRef, + externalWorkflowId: workflowResponse.externalId, + inputSchema: workflowDto.inputSchema, + }); - await this.workflowVersionRepository.create(version); - } catch (e) { - throw new HttpErrors.BadRequest(e); - } + await this.workflowVersionRepository.create(version); } @authenticate(STRATEGY.BEARER) diff --git a/services/bpmn-service/src/providers/execution-input-validator.provider.ts b/services/bpmn-service/src/providers/execution-input-validator.provider.ts index 980a7aae76..624c980df1 100644 --- a/services/bpmn-service/src/providers/execution-input-validator.provider.ts +++ b/services/bpmn-service/src/providers/execution-input-validator.provider.ts @@ -11,8 +11,12 @@ export class ExecutionInputValidationProvider const ajv = new Ajv(); const validate = ajv.compile(schema); try { - validate(input); - return true; + const valid = validate(input); + if (valid) { + return true; + } else { + throw new Error('invalid'); + } } catch (e) { throw new HttpErrors.BadRequest(JSON.stringify(validate.errors)); } diff --git a/services/scheduler-service/.nycrc b/services/scheduler-service/.nycrc index ced77aacf3..9c0a841ad4 100644 --- a/services/scheduler-service/.nycrc +++ b/services/scheduler-service/.nycrc @@ -2,6 +2,6 @@ "include": ["dist"], "exclude": ["dist/__tests__/"], "extension": [".js", ".ts"], - "reporter": ["text", "html"], + "reporter": ["text"], "exclude-after-remap": false } \ No newline at end of file