UNPKG

camunda-external-task-client-js

Version:

Implement your [BPMN Service Task](https://docs.camunda.org/manual/latest/user-guide/process-engine/external-tasks/) in NodeJS.

527 lines (445 loc) 15.9 kB
/* * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH * under one or more contributor license agreements. See the NOTICE file * distributed with this work for additional information regarding copyright * ownership. Camunda licenses this file to you under the Apache License, * Version 2.0; you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { jest } from "@jest/globals"; import Client from "./Client.js"; import TaskService from "./TaskService.js"; import Variables from "./Variables.js"; import { WRONG_INTERCEPTOR, MISSING_BASE_URL, ALREADY_REGISTERED, MISSING_HANDLER, WRONG_MIDDLEWARES, } from "./__internal/errors.js"; // workaround for https://github.com/facebook/jest/issues/2157 const flushPromises = () => new Promise(jest.requireActual("timers").setImmediate); const advanceTimersByTime = async (msToRun) => { await flushPromises(); return jest.advanceTimersByTime(msToRun); }; jest.mock("got"); jest.mock("../lib/Variables"); const customClientOptions = { baseUrl: "http://localhost:XXXX/engine-rest/", workerId: "foobarId", maxTasks: 3, interval: 100, lockDuration: 30000, sorting: [ { sortBy: Client.SortBy.CreateTime, sortOrder: Client.SortOrder.ASC }, ], }; describe("Client", () => { describe("poll ", () => { jest.useFakeTimers(); describe("when autoPoll is false", () => { let pollSpy, client, engineService; beforeEach(() => { client = new Client({ ...customClientOptions, autoPoll: false }); engineService = client.engineService; pollSpy = jest.spyOn(client, "poll"); }); test("calling start should call poll", () => { // when client.start(); // then expect(pollSpy).toHaveBeenCalled(); }); test("should call itself again after timeout when there are no topic subscriptions", async () => { // when client.start(); await advanceTimersByTime(customClientOptions.interval); // then expect(pollSpy).toHaveBeenCalledTimes(2); }); test("should not call itself again after timeout when there are registered client", async () => { // given client.subscribe("foo", () => {}); // when client.start(); await advanceTimersByTime(customClientOptions.interval); // then expect(pollSpy).toHaveBeenCalledTimes(2); }); test("should fetchAndLock and then call itself again when there are registered client", async () => { // given const fetchAndLockSpy = jest.spyOn(engineService, "fetchAndLock"); client.subscribe("foo", () => {}); // when client.start(); await advanceTimersByTime(customClientOptions.interval); // then expect(fetchAndLockSpy).toBeCalled(); expect(pollSpy).toHaveBeenCalledTimes(2); }); }); describe("when autoPoll is true", () => { it("should call itself automatically when autoPoll is true", async () => { // given const client = new Client({ ...customClientOptions, autoPoll: true }); const pollSpy = jest.spyOn(client, "poll"); // when await advanceTimersByTime(2 * customClientOptions.interval); // then expect(pollSpy).toHaveBeenCalledTimes(1); }); }); describe("usePriority", () => { test("should use Priority by default", () => { const client = new Client({ ...customClientOptions, autoPoll: false, }); const fetchSpy = jest.spyOn(client.engineService, "fetchAndLock"); client.subscribe("foo", {}, function () {}); client.start(); expect(fetchSpy).toHaveBeenCalled(); const args = fetchSpy.mock.calls[0][0]; expect(args.usePriority).toBeTruthy(); }); test("should configure priority use", () => { const client = new Client({ ...customClientOptions, usePriority: false, autoPoll: false, }); const fetchSpy = jest.spyOn(client.engineService, "fetchAndLock"); client.subscribe("foo", {}, function () {}); client.start(); expect(fetchSpy).toHaveBeenCalled(); const args = fetchSpy.mock.calls[0][0]; expect(args.usePriority).toBeFalsy(); }); }); describe("when maxParallelExecutions is configured", () => { test("should not call fetchAndLock if maximum parallel executions limit exceeded", async () => { // given const client = new Client({ ...customClientOptions, autoPoll: false, maxParallelExecutions: 1, }); const pollSpy = jest.spyOn(client, "poll"); const engineService = client.engineService; engineService.fetchAndLock = jest .fn() .mockResolvedValue([{ topicName: "foo", variables: {} }]); client.subscribe("foo", async () => { await new Promise((resolve) => setTimeout(resolve, customClientOptions.interval) ); await new Promise((resolve) => setTimeout(resolve, 0.5 * customClientOptions.interval) ); }); // when client.start(); // then expect(engineService.fetchAndLock).toBeCalled(); await advanceTimersByTime(customClientOptions.interval); expect(pollSpy).toHaveBeenCalledTimes(2); expect(engineService.fetchAndLock).toHaveBeenCalledTimes(1); await advanceTimersByTime(customClientOptions.interval); expect(pollSpy).toHaveBeenCalledTimes(3); expect(engineService.fetchAndLock).toHaveBeenCalledTimes(1); }); }); describe("stop", () => { it("should stop polling", async () => { // given: we advance time twice then call stop const client = new Client(customClientOptions); await advanceTimersByTime(2 * customClientOptions.interval); client.stop(); const pollSpy = jest.spyOn(client, "poll"); // when await advanceTimersByTime(2 * customClientOptions.interval); // then expect(pollSpy).not.toHaveBeenCalled(); }); }); }); describe("subscribe", () => { test("should throw error if api baseUrl wasn't passed as parameter", () => { expect(() => { new Client(); }).toThrowError(MISSING_BASE_URL); }); let client, fooWork, customConfig; beforeEach(() => { client = new Client(customClientOptions); fooWork = () => "foobar"; customConfig = { lockDuration: 3000, variables: ["fooVariable", "barVariable"], processDefinitionId: "processId", processDefinitionIdIn: ["processId", "processId2"], processDefinitionKey: "processKey", processDefinitionKeyIn: ["processKey", "processKey2"], processDefinitionVersionTag: "versionTag", tenantIdIn: ["tenantId"], withoutTenantId: true, localVariables: true, includeExtensionProperties: true, }; }); test("should overwrite default with custom client config", () => { expect(client.options.workerId).toBe(customClientOptions.workerId); expect(client.options.maxTasks).toBe(customClientOptions.maxTasks); expect(client.options.interval).toBe(customClientOptions.interval); expect(client.options.lockDuration).toBe( customClientOptions.lockDuration ); }); test("should subscribe to topic without custom config ", () => { // given const footopicSubscription = client.subscribe("foo", fooWork); // then expect(footopicSubscription.handler).not.toBeUndefined(); }); test("should subscribe to topic with custom lockDuration config ", () => { // given const footopicSubscription = client.subscribe( "foo", customConfig, fooWork ); // then expect(footopicSubscription.lockDuration).toBe(customConfig.lockDuration); }); test("should subscribe to topic with custom variable subset ", () => { // given const footopicSubscription = client.subscribe( "foo", customConfig, fooWork ); // then expect(footopicSubscription.variables).toBe(customConfig.variables); }); test("should subscribe to topic based on processDefinitionId ", () => { // given const footopicSubscription = client.subscribe( "foo", customConfig, fooWork ); // then expect(footopicSubscription.processDefinitionId).toBe( customConfig.processDefinitionId ); }); test("should subscribe to topic based on processDefinitionIdIn ", () => { // given const footopicSubscription = client.subscribe( "foo", customConfig, fooWork ); // then expect(footopicSubscription.processDefinitionIdIn).toBe( customConfig.processDefinitionIdIn ); }); test("should subscribe to topic based on processDefinitionKey ", () => { // given const footopicSubscription = client.subscribe( "foo", customConfig, fooWork ); // then expect(footopicSubscription.processDefinitionKey).toBe( customConfig.processDefinitionKey ); }); test("should subscribe to topic based on processDefinitionKeyIn ", () => { // given const footopicSubscription = client.subscribe( "foo", customConfig, fooWork ); // then expect(footopicSubscription.processDefinitionKeyIn).toBe( customConfig.processDefinitionKeyIn ); }); test("should subscribe to topic based on processDefinitionVersionTag ", () => { // given const footopicSubscription = client.subscribe( "foo", customConfig, fooWork ); // then expect(footopicSubscription.processDefinitionVersionTag).toBe( customConfig.processDefinitionVersionTag ); }); test("should subscribe to topic based on tenantIdIn ", () => { // given const footopicSubscription = client.subscribe( "foo", customConfig, fooWork ); // then expect(footopicSubscription.tenantIdIn).toBe(customConfig.tenantIdIn); }); test("should subscribe to topic based on withoutTenantId ", () => { // given const footopicSubscription = client.subscribe( "foo", customConfig, fooWork ); // then expect(footopicSubscription.withoutTenantId).toBe( customConfig.withoutTenantId ); }); test("should subscribe to topic based on localVariables ", () => { // given const footopicSubscription = client.subscribe( "foo", customConfig, fooWork ); // then expect(footopicSubscription.localVariables).toBe( customConfig.localVariables ); }); test("should subscribe to topic with includeExtensionProperties = true", () => { // given const footopicSubscription = client.subscribe( "foo", customConfig, fooWork ); // then expect(footopicSubscription.includeExtensionProperties).toBe( customConfig.includeExtensionProperties ); }); test("should call the API with the custom configs", () => { // given const fetchAnLockSpy = jest.spyOn(client.engineService, "fetchAndLock"); client.subscribe("foo", customConfig, fooWork); client.poll(); // then expect(fetchAnLockSpy).toBeCalled(); const args = fetchAnLockSpy.mock.calls[0]; expect(args).toMatchSnapshot(); }); test("should throw error if try to subscribe twice", () => { // given client.subscribe("foo", fooWork); // then expect(() => { client.subscribe("foo", fooWork); }).toThrowError(ALREADY_REGISTERED); }); test("should throw error if handler is not passed", () => { expect(() => { client.subscribe("foo2"); }).toThrowError(MISSING_HANDLER); }); test("should allow to unsubscribe from a topic", () => { // given const footopicSubscription = client.subscribe("foo", fooWork); // when footopicSubscription.unsubscribe(); // then expect(client.topicSubscriptions["foo"]).toBeUndefined(); }); }); describe("interceptors", () => { it("should not add interceptors if they are not provided as a function or array of functions", () => { // given const options = { ...customClientOptions, interceptors: [] }; // then expect(() => new Client(options)).toThrowError(WRONG_INTERCEPTOR); }); it("should add interceptors if they are provided as a function", () => { // given const foo = () => {}; const expectedInterceptors = [foo]; const options = { ...customClientOptions, interceptors: foo }; const client = new Client(options); // then expect(client.engineService.interceptors).toEqual(expectedInterceptors); }); it("should add interceptors if they are provided as an array of functions", () => { // given const foo = () => {}; const expectedInterceptors = [foo]; const options = { ...customClientOptions, interceptors: [foo] }; const client = new Client(options); // then expect(client.engineService.interceptors).toEqual(expectedInterceptors); }); }); describe("middlewares", () => { it("should not add middlewares if they are not provided as a function or array of functions", () => { // given const options = { ...customClientOptions, use: [] }; // then expect(() => new Client(options)).toThrowError(WRONG_MIDDLEWARES); }); it("should accept middleware function", () => { // givena const middleware = jest.fn(); const options = { ...customClientOptions, use: middleware }; const client = new Client(options); // then expect(middleware).toBeCalledWith(client); }); it("should accept middlewares array", () => { // given const middlewares = [jest.fn(), jest.fn()]; const options = { ...customClientOptions, use: middlewares }; const client = new Client(options); // then middlewares.forEach((middleware) => expect(middleware).toBeCalledWith(client) ); }); }); it("executeTask should call handler with task and taskService", () => { // given Variables.mockClear(); let client = new Client({ ...customClientOptions, autoPoll: false }); const handler = jest.fn(); client.subscribe("foo", handler); const expectedTask = { topicName: "foo", variables: {} }; // when client.executeTask(expectedTask); // then // Variable service is called with specific parameters (c.f. snapshot) const VariablesCall = Variables.mock.calls[0]; expect(VariablesCall).toMatchSnapshot(); // handler should be called with specific parameters expect(handler).toBeCalled(); const { task, taskService } = handler.mock.calls[0][0]; expect(taskService).toBeInstanceOf(TaskService); expect(task).toBeDefined(); expect(task.topicName).toBe(expectedTask.topicName); expect(task.variables).toBeInstanceOf(Variables); }); });