UNPKG

redis-workflow

Version:

Simple Promise based multi-channel workflow rules engine using Redis backing

539 lines (457 loc) 17.8 kB
/// <reference path="../global.d.ts" /> import { EventEmitter } from "events"; import * as redis from "redis"; import { ActionType, DelayedAction, IAction, ImmediateAction, IRule, ITrigger, IWorkflow, IWorkflowManager, RedisConfig, RedisWorkflowManager, Rule, Trigger, Util, Workflow, WorkflowEvents, } from "../index"; describe("RedisWorkflowManager", () => { const config: RedisConfig = new RedisConfig( "localhost", 6379, null, null, ); const manager: IWorkflowManager = new RedisWorkflowManager(config); const testKey: string = "test123"; const testKey2: string = "test234"; const testKey3: string = "test456"; const testEmptyKey: string = "testEmptyKey999"; const testKillMessage: string = "WFKILL"; // keep in sync with class const testEventName: string = "test_event_888"; const testEventName2: string = "test_event_2"; const testEvent: string = JSON.stringify( {event: testEventName, context: {age: 77}} ); const testEvent2: string = JSON.stringify( {event: testEventName2, context: {age: 55}} ); const testInvalidEvent: string = JSON.stringify( {event: testEventName2, context: {age: 22}} ); const testActionName: string = "test_action_999"; const testActionName2: string = "test_action_000"; const testRuleName: string = "Is retired"; const testRuleName2: string = "Is a grandparent"; const testRuleExpression: string = "age == 77"; const testRuleExpression2: string = "age == 55"; const testWorkflowName: string = "test_workflow_1"; const testWorkflowName2: string = "test_workflow_2"; const testTrigger1: ITrigger = new Trigger(testEventName); // immediate const testTrigger2: ITrigger = new Trigger(testEventName2); // delayed const testRule1: IRule = new Rule(testRuleName, testRuleExpression); const testRule2: IRule = new Rule(testRuleName, testRuleExpression2); const testAction1: IAction = new ImmediateAction(testActionName); const testAction2: IAction = new DelayedAction(testActionName).delay(1, "day").repeat(3); const testWorkflow1: IWorkflow = new Workflow(testWorkflowName, testTrigger1, [testRule1], [testAction1]); const testWorkflow2: IWorkflow = new Workflow(testWorkflowName2, testTrigger2, [testRule2], [testAction2]); const client: any = redis.createClient(); // for confirming app TODO: mock beforeAll((done) => { jest.setTimeout(5000); // 5 second timeout // add test workflows manager.setWorkflows({ [testKey]: [testWorkflow1, testWorkflow2], [testKey2]: [testWorkflow1, testWorkflow2], [testKey3]: [testWorkflow1, testWorkflow2], [testEmptyKey]: [], }); // add default workflow this.workflows = {}; done(); }); afterAll((done) => { // TODO: remove workflow set for channels, and workflows done(); }); it("instantiates a WorkflowManager", () => { expect(manager).toBeInstanceOf(RedisWorkflowManager); }); // constructor it("uses existing RedisClient if passed", () => { const workflowWithClient: IWorkflowManager = new RedisWorkflowManager(null, client); expect(workflowWithClient).toBeInstanceOf(RedisWorkflowManager); }); // constructor it("loads workflows from database if channels provided", (done) => { // arrange const testDict: Dictionary = { actions: [ { name: testActionName, type: ActionType.Immediate, }, ], name: testWorkflowName, rules: [ { expression: testRuleExpression, name: testRuleName, }, ], trigger: { name: testEventName, }, }; // create key channel:hash (hash: number = hash(workflow.getName())) const channelWfHashKey: string = [testKey3, 2695549310].join(":"); // store workflow key in set for channelName:workflows client.sadd([testKey3, "workflows"].join(":"), channelWfHashKey, (saddError: Error, saddReply: number) => { // save serialized workflow client.set(channelWfHashKey, JSON.stringify(testDict), (setError: Error, setReply: number) => { // instantiate manager with channel(s) const pManager: IWorkflowManager = new RedisWorkflowManager(config, null, [testKey3]); pManager.on(WorkflowEvents.Ready, () => { const result: IWorkflow[] = pManager.getWorkflowsForChannel(testKey3); expect(result).toBeDefined(); expect(result).toEqual([testWorkflow1]); done(); }); pManager.on(WorkflowEvents.Error, (error) => { done.fail(error); }); }); }); }); // constructor describe("getWorkflows", () => { it("returns dictionary of workflows", (done) => { const result: Dictionary = manager.getWorkflows(); // assert expect(result[testKey].length).toEqual(2); expect(result[testKey][0]).toBeInstanceOf(Workflow); expect(result[testKey2].length).toEqual(2); expect(result[testKey2][0]).toBeInstanceOf(Workflow); done(); }); }); // getWorkflows describe("setWorkflows", () => { it("replaces workflows with provided dictionary", (done) => { // act manager.setWorkflows({ [testKey]: [testWorkflow1, testWorkflow2], [testKey2]: [testWorkflow1, testWorkflow2], [testEmptyKey]: [], }); const result: Dictionary = manager.getWorkflows(); // assert expect(result[testKey].length).toEqual(2); expect(result[testKey][0]).toBeInstanceOf(Workflow); done(); }); }); // getWorkflows describe("getWorkflowsForChannel", () => { it("returns array of workflows", (done) => { const result: IWorkflow[] = manager.getWorkflowsForChannel(testKey); // assert expect(result.length).toEqual(2); expect(result[0]).toBeInstanceOf(Workflow); done(); }); }); // getWorkflows describe("setWorkflowsForChannel", () => { it("replaces workflows with provided array", (done) => { // act manager.setWorkflowsForChannel(testKey, [testWorkflow1, testWorkflow2]); const result: IWorkflow[] = manager.getWorkflowsForChannel(testKey); // assert expect(result.length).toEqual(2); expect(result[0]).toBeInstanceOf(Workflow); done(); }); }); // getWorkflows describe("addWorkflow", () => { beforeAll((done) => { // arrange: (delete workflows for channel from db) client.del([testKey, "workflows"].join(":"), (err: Error, reply: string) => { if (err !== null) { done.fail(err); } done(); }); }); it("returns a Promise", () => { expect(manager.addWorkflow(testEmptyKey, testWorkflow1)).toBeInstanceOf(Promise); }); it("emits an EventEmitter event", (done) => { // arrange manager.on(WorkflowEvents.Add, () => { // assert done(); }); manager.on(WorkflowEvents.Error, (error) => { done.fail(error); }); // act manager.addWorkflow(testEmptyKey, testWorkflow1); }); it("adds a workflow to the array of workflows", (done) => { // act manager.addWorkflow(testKey, testWorkflow1) .then(() => { const result: IWorkflow[] = manager.getWorkflowsForChannel(testKey); // assert expect(result.length).toEqual(3); expect(result[1]).toBeInstanceOf(Workflow); done(); }) .catch((error) => { done.fail(error); }); }); it("saves updated workflows to database", (done) => { client.smembers([testKey, "workflows"].join(":"), (err: Error, reply: string[]) => { if (err !== null) { done.fail(err); } else { expect(reply).toBeDefined(); expect(reply.length).toBeGreaterThan(0); done(); } }); }); }); // addWorkflow describe("removeWorkflow", () => { it("returns a Promise", () => { expect(manager.removeWorkflow(testEmptyKey, testWorkflow1.getName())).toBeInstanceOf(Promise); }); it("emits an EventEmitter event", (done) => { // arrange manager.on(WorkflowEvents.Remove, () => { // assert done(); }); manager.on(WorkflowEvents.Error, (error) => { done.fail(error); }); // act manager.removeWorkflow(testEmptyKey, testWorkflow1.getName()); }); it("removes workflow from the array of workflows", (done) => { // act manager.removeWorkflow(testKey, testWorkflow1.getName()) .then(() => { const channelFlows: IWorkflow[] = manager.getWorkflowsForChannel(testKey); // assert channelFlows.filter((flow: IWorkflow) => flow.getName() === name); expect(channelFlows.length).toEqual(1); done(); }) .catch((error) => { done.fail(error); }); }); }); // removeWorkflow describe("start", () => { beforeAll((done) => { // replace workflows manager.setWorkflows({ [testKey]: [testWorkflow1, testWorkflow2], [testKey2]: [testWorkflow1, testWorkflow2], [testKey3]: [testWorkflow1, testWorkflow2], [testEmptyKey]: [], }); done(); }); it("emits an EventEmitter event", (done) => { // arrange manager.on(WorkflowEvents.Start, () => { // assert done(); }); manager.on(WorkflowEvents.Error, (error) => { done.fail(error); }); // act manager.start(testEmptyKey); }); it("starts a pubsub listener and applies workflows to messages", (done) => { // arrange manager.on(testActionName, (context) => { // assert done(); }); manager.on(WorkflowEvents.Error, (error) => { done.fail(error); }); // act manager.start(testKey) .then(() => { setTimeout(() => { client.publish(testKey, testEvent, (pubErr: Error, _1: number) => { // now kill it setTimeout(() => { client.publish(testKey, testKillMessage, (killErr: Error, _2: number) => { // do nothing }); }, 1000); }); }, 500); }) .catch((error) => { done.fail(error); }); }); it("starts a pubsub listener and emits delayed actions", (done) => { // arrange manager.on(WorkflowEvents.Schedule, (action) => { // assert done(); }); manager.on(WorkflowEvents.Error, (error) => { done.fail(error); }); // act manager.start(testKey2) .then(() => { setTimeout(() => { client.publish(testKey2, testEvent2, (pubErr: Error, _1: number) => { // now kill it setTimeout(() => { client.publish(testKey2, testKillMessage, (killErr: Error, _2: number) => { // do nothing }); }, 1000); }); }, 500); }) .catch((error) => { done.fail(error); }); }); it("starts a pubsub listener and emits immediate actions", (done) => { // arrange manager.on(WorkflowEvents.Immediate, (context) => { // assert done(); }); manager.on(WorkflowEvents.Error, (error) => { done.fail(error); }); // act manager.start(testKey3) .then(() => { setTimeout(() => { client.publish(testKey3, testEvent, (pubErr: Error, _1: number) => { // now kill it setTimeout(() => { client.publish(testKey3, testKillMessage, (killErr: Error, _2: number) => { // do nothing }); }, 1000); }); }, 500); }) .catch((error) => { done.fail(error); }); }); it("starts a pubsub listener and emits invalid workflows", (done) => { // arrange manager.on(WorkflowEvents.Invalid, (message) => { // assert expect(message.event).toBeDefined(); expect(message.context).toBeDefined(); done(); }); manager.on(WorkflowEvents.Error, (error) => { done.fail(error); }); // act manager.start(testKey3) .then(() => { setTimeout(() => { client.publish(testKey3, testInvalidEvent, (pubErr: Error, _1: number) => { // now kill it setTimeout(() => { client.publish(testKey3, testKillMessage, (killErr: Error, _2: number) => { // do nothing }); }, 1000); }); }, 500); }) .catch((error) => { done.fail(error); }); }); }); // start describe("stop", () => { it("returns a Promise", () => { expect(manager.stop(testEmptyKey)).toBeInstanceOf(Promise); }); it("emits an EventEmitter event", (done) => { // arrange manager.on(WorkflowEvents.Stop, () => { // assert done(); }); manager.on(WorkflowEvents.Error, (error) => { done.fail(error); }); // act manager.stop(testEmptyKey); }); }); // stop describe("reload", () => { it("returns a Promise", () => { expect(manager.reload([testKey2])).toBeInstanceOf(Promise); }); it("emits an EventEmitter event", (done) => { // arrange manager.on(WorkflowEvents.Ready, () => { // assert done(); }); manager.on(WorkflowEvents.Error, (error) => { done.fail(error); }); // act manager.reload([testKey]); }); // already tested db with constructor tests }); // reload describe("save", () => { it("returns a Promise", () => { expect(manager.save([testKey2])).toBeInstanceOf(Promise); }); it("emits an EventEmitter event", (done) => { // arrange manager.on(WorkflowEvents.Save, () => { // assert done(); }); manager.on(WorkflowEvents.Error, (error) => { done.fail(error); }); // act manager.save([testKey]); }); // database tested in reset }); describe("reset", () => { it("returns a Promise", () => { expect(manager.reset()).toBeInstanceOf(Promise); }); it("emits an EventEmitter event", (done) => { // arrange manager.on(WorkflowEvents.Reset, () => { // assert done(); }); manager.on(WorkflowEvents.Error, (error) => { done.fail(error); }); // act manager.reset(); }); it("removes workflow from memory", () => { expect(this.workflows).toEqual({}); }); }); // reset }); // redis workflow