@emartech/program-executor 3.11.0 → 3.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/.env.example +9 -1
  2. package/.github/CODEOWNERS +1 -1
  3. package/.github/dependabot.yml +0 -7
  4. package/.github/workflows/main.yaml +17 -5
  5. package/docker-compose.yml +11 -0
  6. package/eslint.config.js +1 -5
  7. package/jest.config.js +13 -0
  8. package/package.json +13 -16
  9. package/src/create-test-databases.js +84 -0
  10. package/src/execution-time-exceeded-error/{index.spec.js → index.test.js} +3 -3
  11. package/src/graphql/{schema.spec.js → schema.test.js} +69 -24
  12. package/src/ignorable-error/{index.spec.js → index.test.js} +5 -5
  13. package/src/index-pubsub-e2e.test.js +94 -0
  14. package/src/index-pubsub.js +78 -0
  15. package/src/index-pubsub.test.js +154 -0
  16. package/src/{index.spec.js → index.test.js} +42 -38
  17. package/src/job-data-handler/{index.spec.js → index.test.js} +15 -21
  18. package/src/program-executor-processor/{index.spec.js → index.test.js} +62 -49
  19. package/src/program-handler/index.js +7 -1
  20. package/src/program-handler/{index.spec.js → index.test.js} +58 -34
  21. package/src/queue-manager/{index.spec.js → index.test.js} +16 -19
  22. package/src/queue-manager-pubsub/index.js +33 -0
  23. package/src/queue-manager-pubsub/index.test.js +44 -0
  24. package/src/repositories/{programs.spec.js → programs.test.js} +54 -37
  25. package/src/retryable-error/{index.spec.js → index.test.js} +5 -5
  26. package/src/runid-generator/{index.spec.js → index.test.js} +2 -2
  27. package/src/test-helper/get-test-db-config.js +29 -0
  28. package/src/testSetup.js +23 -0
  29. package/src/testTeardown.js +13 -0
  30. package/src/setup.spec.js +0 -50
@@ -0,0 +1,154 @@
1
+ 'use strict';
2
+
3
+ const ProgramExecutor = require('./index-pubsub');
4
+ const ProgramHandler = require('./program-handler');
5
+ const ProgramsRepository = require('./repositories/programs');
6
+ const QueueManager = require('./queue-manager-pubsub');
7
+ const ProgramExecutorProcessor = require('./program-executor-processor');
8
+ const Consumer = require('@emartech/pubsub-client-js').Consumer;
9
+
10
+ const testJobLibrary = {
11
+ firstJob: {},
12
+ secondJob: {}
13
+ };
14
+
15
+ describe('ProgramExecutor', function () {
16
+ let config;
17
+
18
+ beforeEach(async function () {
19
+ config = {
20
+ knex: this.db,
21
+ tableName: 'programs',
22
+ topicName: 'program-executor',
23
+ projectId: 'amqp://guest:guest@localhost:9999'
24
+ };
25
+
26
+
27
+ jest.spyOn(ProgramHandler, 'create');
28
+ ProgramHandler.prototype.createProgram = jest.fn().mockReturnValue(true);
29
+
30
+ jest.spyOn(ProgramsRepository, 'create');
31
+ jest.spyOn(QueueManager, 'create');
32
+ jest.spyOn(ProgramExecutorProcessor, 'create');
33
+
34
+ ProgramExecutorProcessor.prototype.process = jest.fn().mockReturnValue(true);
35
+ jest.spyOn(Consumer, 'create');
36
+ Consumer.prototype.process = jest.fn().mockReturnValue(true);
37
+
38
+
39
+ });
40
+
41
+ describe('#createProgram', function () {
42
+ it('should create program handler and call createProgram with given data', async function () {
43
+ await ProgramExecutor.create(config).createProgram({
44
+ jobs: ['current_program', 'next_program']
45
+ });
46
+
47
+ expect(ProgramHandler.create).toHaveBeenCalledWith(
48
+ expect.any(ProgramsRepository),
49
+ expect.any(QueueManager)
50
+ );
51
+ expect(ProgramsRepository.create).toHaveBeenCalledWith(config.knex, config.tableName);
52
+ expect(QueueManager.create).toHaveBeenCalledWith(config.topicName, config.projectId);
53
+ expect(ProgramHandler.prototype.createProgram).toHaveBeenCalledWith({
54
+ jobs: ['current_program', 'next_program']
55
+ });
56
+ });
57
+ });
58
+
59
+ describe('#processPrograms', function () {
60
+ it('should create program executor processor with job library', async function () {
61
+ await ProgramExecutor.create(config).processPrograms(testJobLibrary);
62
+
63
+ expect(ProgramExecutorProcessor.create).toHaveBeenCalledWith(
64
+ expect.any(Object), //TODO check
65
+ expect.any(QueueManager),
66
+ testJobLibrary
67
+ );
68
+
69
+ expect(ProgramsRepository.create).toHaveBeenCalledWith(config.knex, config.tableName);
70
+ expect(QueueManager.create).toHaveBeenCalledWith(config.topicName, config.projectId);
71
+ });
72
+
73
+ it('should create consumer with the given pubsub config', async function () {
74
+ await ProgramExecutor.create(config).processPrograms(testJobLibrary);
75
+
76
+ expect(Consumer.create.mock.calls[0][0]).toEqual(config.topicName);
77
+ });
78
+
79
+ it('should create consumer to consume the given topic', async function () {
80
+ await ProgramExecutor.create(config).processPrograms(testJobLibrary);
81
+
82
+ expect(Consumer.create.mock.calls[0][1]).toEqual(expect.objectContaining({
83
+ MaxMessages: 1,
84
+ MaxStreams: 1,
85
+ logger: 'program-executor-consumer' }));
86
+ });
87
+
88
+ it('should create consumer with a logger based on the given topic name', async function () {
89
+ await ProgramExecutor.create(config).processPrograms(testJobLibrary);
90
+
91
+ expect(Consumer.create.mock.calls[0][1]).toEqual(
92
+ expect.objectContaining({ logger: `${config.topicName}-consumer` }));
93
+ });
94
+
95
+ it('should config consumer with given flow control settings', async function () {
96
+ await ProgramExecutor.create(config).processPrograms(testJobLibrary);
97
+
98
+ expect(Consumer.create.mock.calls[0][1]).toEqual(
99
+ expect.objectContaining({ MaxMessages: 1,
100
+ MaxStreams: 1 }));
101
+ });
102
+
103
+ it('should call the created executor when message callback fires', async function () {
104
+ await ProgramExecutor.create(config).processPrograms(testJobLibrary);
105
+
106
+ const onMessageFunction = Consumer.create.mock.calls[0][1].onMessage;
107
+
108
+ await onMessageFunction({ random: 'message' });
109
+
110
+ expect(ProgramExecutorProcessor.prototype.process).toHaveBeenCalledWith({ random: 'message' });
111
+ });
112
+
113
+ it('should truncate log message to avoid splitting by heroku', async function () {
114
+ const veryLongError = new Error('1'.repeat(100000));
115
+
116
+ ProgramExecutorProcessor.prototype.process = jest.fn().mockRejectedValue(veryLongError);
117
+
118
+ let caughtError;
119
+ try {
120
+ await ProgramExecutor.create(config).processPrograms(testJobLibrary);
121
+ const onMessageFunction = Consumer.create.mock.calls[0][1].onMessage;
122
+ await onMessageFunction({ random: 'message' });
123
+ } catch (error) {
124
+ caughtError = error;
125
+ }
126
+
127
+ expect(caughtError).not.toBe(undefined);
128
+ expect(caughtError.message.length).toEqual(255);
129
+ });
130
+
131
+ it('should emit an error event', function (done) {
132
+ const sampleError = new Error('Error to be emitted');
133
+ ProgramExecutorProcessor.prototype.process = jest.fn().mockRejectedValue(sampleError);
134
+
135
+ const programExecutor = ProgramExecutor.create(config);
136
+
137
+ programExecutor.on('programError', function ({ error, message }) {
138
+ expect(message).toEqual({ random: 'message' });
139
+ expect(error.message).toEqual('Error to be emitted');
140
+ done();
141
+ });
142
+
143
+ programExecutor.processPrograms(testJobLibrary);
144
+ const onMessageFunction = Consumer.create.mock.calls[0][1].onMessage;
145
+ onMessageFunction({ random: 'message' }).catch(() => {});
146
+ });
147
+
148
+ it('should start processing', async function () {
149
+ await ProgramExecutor.create(config).processPrograms(testJobLibrary);
150
+
151
+ expect(Consumer.prototype.process).toHaveBeenCalled();
152
+ });
153
+ });
154
+ });
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const ProgramExecutor = require('./');
3
+ const ProgramExecutor = require('.');
4
4
  const ProgramHandler = require('./program-handler');
5
5
  const ProgramsRepository = require('./repositories/programs');
6
6
  const QueueManager = require('./queue-manager');
@@ -23,125 +23,129 @@ describe('ProgramExecutor', function () {
23
23
  amqpUrl: 'amqp://guest:guest@localhost:9999'
24
24
  };
25
25
 
26
- this.sandbox.spy(ProgramHandler, 'create');
27
- this.sandbox.stub(ProgramHandler.prototype, 'createProgram');
28
26
 
29
- this.sandbox.spy(ProgramsRepository, 'create');
30
- this.sandbox.spy(QueueManager, 'create');
27
+ jest.spyOn(ProgramHandler, 'create');
28
+ ProgramHandler.prototype.createProgram = jest.fn().mockReturnValue(true);
29
+
30
+ jest.spyOn(ProgramsRepository, 'create');
31
+ jest.spyOn(QueueManager, 'create');
32
+ jest.spyOn(ProgramExecutorProcessor, 'create');
33
+
34
+ ProgramExecutorProcessor.prototype.process = jest.fn().mockReturnValue(true);
35
+ jest.spyOn(Consumer, 'create');
36
+ Consumer.prototype.process = jest.fn().mockReturnValue(true);
31
37
 
32
- this.sandbox.spy(ProgramExecutorProcessor, 'create');
33
- this.sandbox.stub(ProgramExecutorProcessor.prototype, 'process');
34
38
 
35
- this.sandbox.spy(Consumer, 'create');
36
- this.sandbox.stub(Consumer.prototype, 'process');
37
39
  });
38
40
 
39
- describe('#createProgram', async function () {
41
+ describe('#createProgram', function () {
40
42
  it('should create program handler and call createProgram with given data', async function () {
41
43
  await ProgramExecutor.create(config).createProgram({
42
44
  jobs: ['current_program', 'next_program']
43
45
  });
44
46
 
45
- expect(ProgramHandler.create).to.have.been.calledWith(
46
- this.sinon.match.instanceOf(ProgramsRepository),
47
- this.sinon.match.instanceOf(QueueManager)
47
+ expect(ProgramHandler.create).toHaveBeenCalledWith(
48
+ expect.any(ProgramsRepository),
49
+ expect.any(QueueManager)
48
50
  );
49
- expect(ProgramsRepository.create).to.have.been.calledWith(config.knex, config.tableName);
50
- expect(QueueManager.create).to.have.been.calledWith(config.amqpUrl, config.queueName);
51
- expect(ProgramHandler.prototype.createProgram).to.have.been.calledWith({
51
+ expect(ProgramsRepository.create).toHaveBeenCalledWith(config.knex, config.tableName);
52
+ expect(QueueManager.create).toHaveBeenCalledWith(config.amqpUrl, config.queueName);
53
+ expect(ProgramHandler.prototype.createProgram).toHaveBeenCalledWith({
52
54
  jobs: ['current_program', 'next_program']
53
55
  });
54
56
  });
55
57
  });
56
58
 
57
- describe('#processPrograms', async function () {
59
+ describe('#processPrograms', function () {
58
60
  it('should create program executor processor with job library', async function () {
59
61
  await ProgramExecutor.create(config).processPrograms(testJobLibrary);
60
62
 
61
- expect(ProgramExecutorProcessor.create).to.have.been.calledWith(
62
- this.sinon.match.instanceOf(ProgramHandler),
63
- this.sinon.match.instanceOf(QueueManager),
63
+ expect(ProgramExecutorProcessor.create).toHaveBeenCalledWith(
64
+ expect.any(Object), //TODO check
65
+ expect.any(QueueManager),
64
66
  testJobLibrary
65
67
  );
66
68
 
67
- expect(ProgramsRepository.create).to.have.been.calledWith(config.knex, config.tableName);
68
- expect(QueueManager.create).to.have.been.calledWith(config.amqpUrl, config.queueName);
69
+ expect(ProgramsRepository.create).toHaveBeenCalledWith(config.knex, config.tableName);
70
+ expect(QueueManager.create).toHaveBeenCalledWith(config.amqpUrl, config.queueName);
69
71
  });
70
72
 
71
73
  it('should create consumer with the given rabbitMq config', async function () {
72
74
  await ProgramExecutor.create(config).processPrograms(testJobLibrary);
73
75
 
74
- expect(Consumer.create.lastCall.args[0]).to.eql({ default: { url: config.amqpUrl } });
76
+ expect(Consumer.create.mock.calls[0][0]).toEqual({ default: { url: config.amqpUrl } });
75
77
  });
76
78
 
77
79
  it('should create consumer to consume the given queue', async function () {
78
80
  await ProgramExecutor.create(config).processPrograms(testJobLibrary);
79
81
 
80
- expect(Consumer.create.lastCall.args[1]).to.containSubset({ channel: config.queueName });
82
+ expect(Consumer.create.mock.calls[0][1]).toEqual(
83
+ expect.objectContaining({ channel: config.queueName }));
81
84
  });
82
85
 
83
86
  it('should create consumer with a logger based on the given queue name', async function () {
84
87
  await ProgramExecutor.create(config).processPrograms(testJobLibrary);
85
88
 
86
- expect(Consumer.create.lastCall.args[1]).to.containSubset({ logger: `${config.queueName}-consumer` });
89
+ expect(Consumer.create.mock.calls[0][1]).toEqual(
90
+ expect.objectContaining({ logger: `${config.queueName}-consumer` }));
87
91
  });
88
92
 
89
93
  it('should config consumer with prefecth count and a retry time', async function () {
90
94
  await ProgramExecutor.create(config).processPrograms(testJobLibrary);
91
95
 
92
- expect(Consumer.create.lastCall.args[1]).to.containSubset({ prefetchCount: 1, retryTime: 1000 });
96
+ expect(Consumer.create.mock.calls[0][1]).toEqual(
97
+ expect.objectContaining({ prefetchCount: 1, retryTime: 1000 }));
93
98
  });
94
99
 
95
100
  it('should call the created executor when message callback fires', async function () {
96
101
  await ProgramExecutor.create(config).processPrograms(testJobLibrary);
97
102
 
98
- const onMessageFunction = Consumer.create.lastCall.args[1].onMessage;
103
+ const onMessageFunction = Consumer.create.mock.calls[0][1].onMessage;
99
104
 
100
105
  await onMessageFunction({ random: 'message' });
101
106
 
102
- expect(ProgramExecutorProcessor.prototype.process).to.have.been.calledWith({ random: 'message' });
107
+ expect(ProgramExecutorProcessor.prototype.process).toHaveBeenCalledWith({ random: 'message' });
103
108
  });
104
109
 
105
110
  it('should truncate log message to avoid splitting by heroku', async function () {
106
111
  const veryLongError = new Error('1'.repeat(100000));
107
112
 
108
- ProgramExecutorProcessor.prototype.process.rejects(veryLongError);
113
+ ProgramExecutorProcessor.prototype.process = jest.fn().mockRejectedValue(veryLongError);
109
114
 
110
115
  let caughtError;
111
116
  try {
112
117
  await ProgramExecutor.create(config).processPrograms(testJobLibrary);
113
- const onMessageFunction = Consumer.create.lastCall.args[1].onMessage;
118
+ const onMessageFunction = Consumer.create.mock.calls[0][1].onMessage;
114
119
  await onMessageFunction({ random: 'message' });
115
120
  } catch (error) {
116
121
  caughtError = error;
117
122
  }
118
123
 
119
- expect(caughtError).not.to.be.undefined;
120
- expect(caughtError.message.length).to.eql(255);
124
+ expect(caughtError).not.toBe(undefined);
125
+ expect(caughtError.message.length).toEqual(255);
121
126
  });
122
127
 
123
128
  it('should emit an error event', function (done) {
124
129
  const sampleError = new Error('Error to be emitted');
125
-
126
- ProgramExecutorProcessor.prototype.process.rejects(sampleError);
130
+ ProgramExecutorProcessor.prototype.process = jest.fn().mockRejectedValue(sampleError);
127
131
 
128
132
  const programExecutor = ProgramExecutor.create(config);
129
133
 
130
134
  programExecutor.on('programError', function ({ error, message }) {
131
- expect(message).to.eql({ random: 'message' });
132
- expect(error.message).to.eql('Error to be emitted');
135
+ expect(message).toEqual({ random: 'message' });
136
+ expect(error.message).toEqual('Error to be emitted');
133
137
  done();
134
138
  });
135
139
 
136
140
  programExecutor.processPrograms(testJobLibrary);
137
- const onMessageFunction = Consumer.create.lastCall.args[1].onMessage;
141
+ const onMessageFunction = Consumer.create.mock.calls[0][1].onMessage;
138
142
  onMessageFunction({ random: 'message' }).catch(() => {});
139
143
  });
140
144
 
141
145
  it('should start processing', async function () {
142
146
  await ProgramExecutor.create(config).processPrograms(testJobLibrary);
143
147
 
144
- expect(Consumer.prototype.process).to.have.been.called;
148
+ expect(Consumer.prototype.process).toHaveBeenCalled();
145
149
  });
146
150
  });
147
151
  });
@@ -1,15 +1,16 @@
1
1
  'use strict';
2
2
 
3
- const JobHandler = require('./');
3
+ const JobHandler = require('.');
4
4
  const ExecutionTimeExceededError = require('../execution-time-exceeded-error');
5
+ jest.useFakeTimers();
5
6
 
6
7
  describe('JobDataHandler', function () {
7
8
  let programHandlerStub;
8
9
 
9
10
  beforeEach(function () {
10
11
  programHandlerStub = {
11
- getJobData: this.sandbox.stub().resolves(true),
12
- updateJobData: this.sandbox.stub().resolves(true)
12
+ getJobData: jest.fn().mockResolvedValue(true),
13
+ updateJobData: jest.fn().mockResolvedValue(true)
13
14
  };
14
15
  });
15
16
 
@@ -20,7 +21,7 @@ describe('JobDataHandler', function () {
20
21
 
21
22
  await JobHandler.create(programHandlerStub, runId, program).get();
22
23
 
23
- expect(programHandlerStub.getJobData).to.have.been.calledWith(runId, program);
24
+ expect(programHandlerStub.getJobData).toHaveBeenCalledWith(runId, program);
24
25
  });
25
26
  });
26
27
 
@@ -31,7 +32,7 @@ describe('JobDataHandler', function () {
31
32
 
32
33
  await JobHandler.create(programHandlerStub, runId, program).set({ test: 'data' });
33
34
 
34
- expect(programHandlerStub.updateJobData).to.have.been.calledWith(runId, program, { test: 'data' });
35
+ expect(programHandlerStub.updateJobData).toHaveBeenCalledWith(runId, program, { test: 'data' });
35
36
  });
36
37
  });
37
38
 
@@ -42,7 +43,7 @@ describe('JobDataHandler', function () {
42
43
 
43
44
  await JobHandler.create(programHandlerStub, runId, program).merge({ test: 'data' });
44
45
 
45
- expect(programHandlerStub.updateJobData).to.have.been.calledWith(runId, program, { test: 'data' }, true);
46
+ expect(programHandlerStub.updateJobData).toHaveBeenCalledWith(runId, program, { test: 'data' }, true);
46
47
  });
47
48
  });
48
49
 
@@ -53,18 +54,17 @@ describe('JobDataHandler', function () {
53
54
 
54
55
  await JobHandler.create(programHandlerStub, runId, program).checkpoint({ test: 'data' });
55
56
 
56
- expect(programHandlerStub.updateJobData).to.have.been.calledWith(runId, program, { test: 'data' }, true);
57
+ expect(programHandlerStub.updateJobData).toHaveBeenCalledWith(runId, program, { test: 'data' }, true);
57
58
  });
58
59
 
59
60
  it('should throw an ExecutionTimeExceededError if execution takes longer than given duration', async function () {
60
- const clock = this.sandbox.useFakeTimers();
61
61
  const runId = '1';
62
62
  const program = 'product_sync';
63
63
  const maxExecutionTime = 1000;
64
64
 
65
65
  const jobHandler = JobHandler.create(programHandlerStub, runId, program);
66
66
 
67
- clock.tick(maxExecutionTime + 1);
67
+ jest.advanceTimersByTime(maxExecutionTime + 1);
68
68
 
69
69
  let expectedError;
70
70
  try {
@@ -73,20 +73,17 @@ describe('JobDataHandler', function () {
73
73
  expectedError = error;
74
74
  }
75
75
 
76
- expect(expectedError).to.be.an.instanceof(ExecutionTimeExceededError);
77
- expect(expectedError.executionTimeExceeded).to.be.true;
78
- clock.restore();
76
+ expect(expectedError).toBeInstanceOf(ExecutionTimeExceededError);
77
+ expect(expectedError.executionTimeExceeded).toBe(true);
79
78
  });
80
79
 
81
80
  it('should not throw an exception if execution time is shorter than given duration', async function () {
82
- const clock = this.sandbox.useFakeTimers();
83
81
  const runId = '1';
84
82
  const program = 'product_sync';
85
83
  const maxExecutionTime = 1000;
86
84
 
87
85
  const jobHandler = JobHandler.create(programHandlerStub, runId, program);
88
-
89
- clock.tick(maxExecutionTime - 1);
86
+ jest.advanceTimersByTime(maxExecutionTime - 1);
90
87
 
91
88
  let expectedError;
92
89
  try {
@@ -95,19 +92,17 @@ describe('JobDataHandler', function () {
95
92
  expectedError = error;
96
93
  }
97
94
 
98
- expect(expectedError).to.be.undefined;
99
- clock.restore();
95
+ expect(expectedError).toBe(undefined);
100
96
  });
101
97
 
102
98
  [0, null, undefined].forEach((executionTime) => {
103
99
  it('should not throw an exception if no max execution time given', async function () {
104
- const clock = this.sandbox.useFakeTimers();
105
100
  const runId = '1';
106
101
  const program = 'product_sync';
107
102
 
108
103
  const jobHandler = JobHandler.create(programHandlerStub, runId, program);
109
104
 
110
- clock.tick(99999);
105
+ jest.advanceTimersByTime(99999);
111
106
 
112
107
  let expectedError;
113
108
  try {
@@ -116,8 +111,7 @@ describe('JobDataHandler', function () {
116
111
  expectedError = error;
117
112
  }
118
113
 
119
- expect(expectedError).to.be.undefined;
120
- clock.restore();
114
+ expect(expectedError).toBe(undefined);
121
115
  });
122
116
  });
123
117
  });