@emartech/program-executor 3.11.1 → 3.12.1
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.
- package/.env.example +9 -1
- package/.github/CODEOWNERS +1 -1
- package/.github/dependabot.yml +0 -7
- package/.github/workflows/main.yaml +17 -5
- package/docker-compose.yml +11 -0
- package/eslint.config.js +1 -5
- package/jest.config.js +13 -0
- package/package.json +12 -16
- package/src/create-test-databases.js +84 -0
- package/src/execution-time-exceeded-error/{index.spec.js → index.test.js} +3 -3
- package/src/graphql/{schema.spec.js → schema.test.js} +53 -24
- package/src/ignorable-error/{index.spec.js → index.test.js} +5 -5
- package/src/index-pubsub-e2e.test.js +209 -0
- package/src/index-pubsub.js +84 -0
- package/src/index-pubsub.test.js +160 -0
- package/src/{index.spec.js → index.test.js} +42 -38
- package/src/job-data-handler/{index.spec.js → index.test.js} +15 -21
- package/src/program-executor-processor/{index.spec.js → index.test.js} +59 -48
- package/src/program-handler/{index.spec.js → index.test.js} +58 -34
- package/src/queue-manager/{index.spec.js → index.test.js} +16 -19
- package/src/queue-manager-pubsub/index.js +33 -0
- package/src/queue-manager-pubsub/index.test.js +44 -0
- package/src/repositories/{programs.spec.js → programs.test.js} +54 -37
- package/src/retryable-error/{index.spec.js → index.test.js} +5 -5
- package/src/runid-generator/{index.spec.js → index.test.js} +2 -2
- package/src/test-helper/get-test-db-config.js +29 -0
- package/src/testSetup.js +23 -0
- package/src/testTeardown.js +13 -0
- package/src/setup.spec.js +0 -50
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const ProgramExecutor = require('./index-pubsub');
|
|
4
|
+
const ProgramsRepository = require('./repositories/programs');
|
|
5
|
+
const e2eTestReuse = require('@emartech/pubsub-client-js/e2e/e2e-test-resuse.js');
|
|
6
|
+
|
|
7
|
+
const knex = require('knex');
|
|
8
|
+
const DbCleaner = require('./test-helper/db-cleaner');
|
|
9
|
+
const { db } = require('./test-helper/get-test-db-config');
|
|
10
|
+
|
|
11
|
+
const customerId = 1234;
|
|
12
|
+
const topicName = 'program-executor';
|
|
13
|
+
|
|
14
|
+
class SuccessfulJob {
|
|
15
|
+
static get name() {
|
|
16
|
+
return 'successful_job';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
static create(programData) {
|
|
20
|
+
return new SuccessfulJob(programData);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// eslint-disable-next-line no-unused-vars
|
|
24
|
+
constructor(programData) {
|
|
25
|
+
/// ... initialize member variables based on programData if needed
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async execute(message, jobDataHandler) {
|
|
29
|
+
// eslint-disable-next-line no-unused-vars
|
|
30
|
+
const jobSpecificData = await jobDataHandler.get();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
class ProblematicJob {
|
|
35
|
+
static get name() {
|
|
36
|
+
return 'problematic_job';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
static create(programData) {
|
|
40
|
+
return new ProblematicJob(programData);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// eslint-disable-next-line no-unused-vars
|
|
44
|
+
constructor(programData) {
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// eslint-disable-next-line no-unused-vars
|
|
48
|
+
async execute(message, jobDataHandler) {
|
|
49
|
+
throw new Error('Some error happened in the job');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
const testJobLibrary = {
|
|
55
|
+
firstJob: {},
|
|
56
|
+
secondJob: {},
|
|
57
|
+
successful_job: SuccessfulJob,
|
|
58
|
+
problematic_job: ProblematicJob
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
describe('ProgramExecutor', function () {
|
|
62
|
+
let config;
|
|
63
|
+
let configFlowControl;
|
|
64
|
+
let dbConnection;
|
|
65
|
+
let mockConstructorSpy;
|
|
66
|
+
let publisher;
|
|
67
|
+
|
|
68
|
+
beforeAll(async function () {
|
|
69
|
+
jest.clearAllMocks();
|
|
70
|
+
dbConnection = knex({
|
|
71
|
+
client: 'pg',
|
|
72
|
+
connection: db.connection.connString
|
|
73
|
+
});
|
|
74
|
+
await DbCleaner.create(dbConnection).tearDown();
|
|
75
|
+
ProgramsRepository.create(dbConnection, 'programs');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
afterAll(async function () {
|
|
79
|
+
await DbCleaner.create(dbConnection).tearDown();
|
|
80
|
+
dbConnection.destroy();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
afterEach(async () => {
|
|
85
|
+
await e2eTestReuse.afterEach(mockConstructorSpy, publisher);
|
|
86
|
+
|
|
87
|
+
publisher = undefined;
|
|
88
|
+
jest.clearAllMocks();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
beforeEach(async function () {
|
|
93
|
+
|
|
94
|
+
config = {
|
|
95
|
+
knex: dbConnection,
|
|
96
|
+
tableName: 'programs',
|
|
97
|
+
topicName: topicName,
|
|
98
|
+
projectId: 'some-project-id'
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
configFlowControl = {
|
|
102
|
+
knex: dbConnection,
|
|
103
|
+
tableName: 'programs',
|
|
104
|
+
topicName: topicName,
|
|
105
|
+
projectId: 'some-project-id',
|
|
106
|
+
pubSubMaxExtensionMinutes: 20,
|
|
107
|
+
pubSubMinAckDeadline: 10,
|
|
108
|
+
pubSubMaxAckDeadline: {"millis":600000}
|
|
109
|
+
};
|
|
110
|
+
const result = await e2eTestReuse.beforeEach(topicName);
|
|
111
|
+
mockConstructorSpy = result.mockConstructorSpy;
|
|
112
|
+
publisher = result.publisher;
|
|
113
|
+
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('#Program Executor', function () {
|
|
117
|
+
it('should execute program and maintain status in DB: two skipped jobs', async function () {
|
|
118
|
+
const runId = await ProgramExecutor.create(config).createProgram(
|
|
119
|
+
{
|
|
120
|
+
programData: {
|
|
121
|
+
customerId: customerId
|
|
122
|
+
},
|
|
123
|
+
jobs: ['current_program', 'next_program']
|
|
124
|
+
}
|
|
125
|
+
);
|
|
126
|
+
await ProgramExecutor.create(config).processPrograms(testJobLibrary);
|
|
127
|
+
|
|
128
|
+
await e2eTestReuse.waitForMessages(1000);
|
|
129
|
+
|
|
130
|
+
const result = await dbConnection('programs').where({ run_id: runId }).first();
|
|
131
|
+
|
|
132
|
+
expect(result.run_id).toBe(runId);
|
|
133
|
+
expect(result.jobs).toEqual(['current_program', 'next_program']);
|
|
134
|
+
expect(result.step).toEqual(1);
|
|
135
|
+
expect(result.errored_at).toEqual(null);
|
|
136
|
+
expect(result.finished_at).not.toEqual(null);
|
|
137
|
+
expect(result.program_data).toEqual({ customerId: 1234 });
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should execute program and maintain status in DB: two skipped jobs (with flow control config)', async function () {
|
|
141
|
+
const runId = await ProgramExecutor.create(configFlowControl).createProgram(
|
|
142
|
+
{
|
|
143
|
+
programData: {
|
|
144
|
+
customerId: customerId
|
|
145
|
+
},
|
|
146
|
+
jobs: ['current_program', 'next_program']
|
|
147
|
+
}
|
|
148
|
+
);
|
|
149
|
+
await ProgramExecutor.create(configFlowControl).processPrograms(testJobLibrary);
|
|
150
|
+
|
|
151
|
+
await e2eTestReuse.waitForMessages(1000);
|
|
152
|
+
|
|
153
|
+
const result = await dbConnection('programs').where({ run_id: runId }).first();
|
|
154
|
+
|
|
155
|
+
expect(result.run_id).toBe(runId);
|
|
156
|
+
expect(result.jobs).toEqual(['current_program', 'next_program']);
|
|
157
|
+
expect(result.step).toEqual(1);
|
|
158
|
+
expect(result.errored_at).toEqual(null);
|
|
159
|
+
expect(result.finished_at).not.toEqual(null);
|
|
160
|
+
expect(result.program_data).toEqual({ customerId: 1234 });
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('should execute program and maintain status in DB: one executed job', async function () {
|
|
164
|
+
const runId = await ProgramExecutor.create(config).createProgram(
|
|
165
|
+
{
|
|
166
|
+
programData: {
|
|
167
|
+
customerId: customerId
|
|
168
|
+
},
|
|
169
|
+
jobs: ['successful_job']
|
|
170
|
+
}
|
|
171
|
+
);
|
|
172
|
+
await ProgramExecutor.create(config).processPrograms(testJobLibrary);
|
|
173
|
+
|
|
174
|
+
await e2eTestReuse.waitForMessages(1000);
|
|
175
|
+
|
|
176
|
+
const result = await dbConnection('programs').where({ run_id: runId }).first();
|
|
177
|
+
|
|
178
|
+
expect(result.run_id).toBe(runId);
|
|
179
|
+
expect(result.jobs).toEqual(['successful_job']);
|
|
180
|
+
expect(result.step).toEqual(0);
|
|
181
|
+
expect(result.errored_at).toEqual(null);
|
|
182
|
+
expect(result.finished_at).not.toEqual(null);
|
|
183
|
+
expect(result.program_data).toEqual({ customerId: 1234 });
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('should execute program and maintain status in DB: one failed job', async function () {
|
|
187
|
+
const runId = await ProgramExecutor.create(config).createProgram(
|
|
188
|
+
{
|
|
189
|
+
programData: {
|
|
190
|
+
customerId: customerId
|
|
191
|
+
},
|
|
192
|
+
jobs: ['problematic_job']
|
|
193
|
+
}
|
|
194
|
+
);
|
|
195
|
+
await ProgramExecutor.create(config).processPrograms(testJobLibrary);
|
|
196
|
+
|
|
197
|
+
await e2eTestReuse.waitForMessages(1000);
|
|
198
|
+
|
|
199
|
+
const result = await dbConnection('programs').where({ run_id: runId }).first();
|
|
200
|
+
|
|
201
|
+
expect(result.run_id).toBe(runId);
|
|
202
|
+
expect(result.jobs).toEqual(['problematic_job']);
|
|
203
|
+
expect(result.step).toEqual(0);
|
|
204
|
+
expect(result.errored_at).not.toEqual(null);
|
|
205
|
+
expect(result.finished_at).toEqual(null);
|
|
206
|
+
expect(result.program_data).toEqual({ customerId: 1234 });
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const consumer = require('@emartech/pubsub-client-js').Consumer;
|
|
4
|
+
const EventEmitter = require('events');
|
|
5
|
+
|
|
6
|
+
const ProgramHandler = require('./program-handler');
|
|
7
|
+
const ProgramsRepository = require('./repositories/programs');
|
|
8
|
+
const QueueManager = require('./queue-manager-pubsub');
|
|
9
|
+
|
|
10
|
+
class ProgramExecutor extends EventEmitter {
|
|
11
|
+
/**
|
|
12
|
+
* @param {object} config
|
|
13
|
+
* @param {object} config.knex - Connected Knex instance
|
|
14
|
+
* @param {string} config.projectId - GCP Project ID
|
|
15
|
+
* @param {string} config.tableName - Table name for bookkeeping
|
|
16
|
+
* @param {string} config.topicName - Topic name to publish to
|
|
17
|
+
* @param {string} config.pubSubMaxExtensionMinutes - Pubsub lease management: max extension minutes
|
|
18
|
+
* @param {string} config.pubSubMinAckDeadline - Pubsub lease management: Min Ack Dead line
|
|
19
|
+
* @param {string} config.pubSubMaxAckDeadline - Pubsub lease management: Max Ack Dead line
|
|
20
|
+
*/
|
|
21
|
+
constructor(config) {
|
|
22
|
+
super();
|
|
23
|
+
this._config = config;
|
|
24
|
+
this._programsRepository = ProgramsRepository.create(config.knex, config.tableName);
|
|
25
|
+
this._queueManager = QueueManager.create(config.topicName, config.projectId);
|
|
26
|
+
this._programHandler = ProgramHandler.create(this._programsRepository, this._queueManager);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {object} data
|
|
31
|
+
* @param {object} data.programData
|
|
32
|
+
* @param {array} data.jobs
|
|
33
|
+
* @param {object} data.jobsData
|
|
34
|
+
*/
|
|
35
|
+
createProgram(data) {
|
|
36
|
+
return this._programHandler.createProgram(data);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
processPrograms(jobLibrary) {
|
|
40
|
+
const eventEmitter = this; // eslint-disable-line consistent-this
|
|
41
|
+
|
|
42
|
+
const programExecutorProcessor = require('./program-executor-processor').create(
|
|
43
|
+
this._programHandler,
|
|
44
|
+
this._queueManager,
|
|
45
|
+
jobLibrary
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
consumer
|
|
49
|
+
.create(this._config.topicName,
|
|
50
|
+
{
|
|
51
|
+
logger: `${this._config.topicName}-consumer`,
|
|
52
|
+
MaxStreams: 1,
|
|
53
|
+
MaxMessages: 1,
|
|
54
|
+
MaxExtensionMinutes:this._config.pubSubMaxExtensionMinutes || 60,
|
|
55
|
+
MinAckDeadline:this._config.pubSubMinAckDeadline || undefined,
|
|
56
|
+
MaxAckDeadline:this._config.pubSubMaxAckDeadline || undefined,
|
|
57
|
+
onMessage: async (message) => {
|
|
58
|
+
try {
|
|
59
|
+
await programExecutorProcessor.process(message);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
eventEmitter.emit('programError', { message, error });
|
|
62
|
+
error.message = error.message.substring(0, 255);
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
{ projectId: this._config.projectId }
|
|
68
|
+
)
|
|
69
|
+
.process();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @param {object} config
|
|
74
|
+
* @param {object} config.knex - Connected Knex instance
|
|
75
|
+
* @param {string} config.projectId - GCP Project ID
|
|
76
|
+
* @param {string} config.tableName - Table name for bookkeeping
|
|
77
|
+
* @param {string} config.topicName - Topic name to publish to
|
|
78
|
+
*/
|
|
79
|
+
static create(config) {
|
|
80
|
+
return new ProgramExecutor(config);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
module.exports = ProgramExecutor;
|
|
@@ -0,0 +1,160 @@
|
|
|
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
|
+
pubSubMaxExtensionMinutes: 60,
|
|
25
|
+
pubSubMinAckDeadline: {'millis':600000},
|
|
26
|
+
pubSubMaxAckDeadline: {'millis':600000}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
jest.spyOn(ProgramHandler, 'create');
|
|
31
|
+
ProgramHandler.prototype.createProgram = jest.fn().mockReturnValue(true);
|
|
32
|
+
|
|
33
|
+
jest.spyOn(ProgramsRepository, 'create');
|
|
34
|
+
jest.spyOn(QueueManager, 'create');
|
|
35
|
+
jest.spyOn(ProgramExecutorProcessor, 'create');
|
|
36
|
+
|
|
37
|
+
ProgramExecutorProcessor.prototype.process = jest.fn().mockReturnValue(true);
|
|
38
|
+
jest.spyOn(Consumer, 'create');
|
|
39
|
+
Consumer.prototype.process = jest.fn().mockReturnValue(true);
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('#createProgram', function () {
|
|
45
|
+
it('should create program handler and call createProgram with given data', async function () {
|
|
46
|
+
await ProgramExecutor.create(config).createProgram({
|
|
47
|
+
jobs: ['current_program', 'next_program']
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
expect(ProgramHandler.create).toHaveBeenCalledWith(
|
|
51
|
+
expect.any(ProgramsRepository),
|
|
52
|
+
expect.any(QueueManager)
|
|
53
|
+
);
|
|
54
|
+
expect(ProgramsRepository.create).toHaveBeenCalledWith(config.knex, config.tableName);
|
|
55
|
+
expect(QueueManager.create).toHaveBeenCalledWith(config.topicName, config.projectId);
|
|
56
|
+
expect(ProgramHandler.prototype.createProgram).toHaveBeenCalledWith({
|
|
57
|
+
jobs: ['current_program', 'next_program']
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('#processPrograms', function () {
|
|
63
|
+
it('should create program executor processor with job library', async function () {
|
|
64
|
+
await ProgramExecutor.create(config).processPrograms(testJobLibrary);
|
|
65
|
+
|
|
66
|
+
expect(ProgramExecutorProcessor.create).toHaveBeenCalledWith(
|
|
67
|
+
expect.any(Object), //TODO check
|
|
68
|
+
expect.any(QueueManager),
|
|
69
|
+
testJobLibrary
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
expect(ProgramsRepository.create).toHaveBeenCalledWith(config.knex, config.tableName);
|
|
73
|
+
expect(QueueManager.create).toHaveBeenCalledWith(config.topicName, config.projectId);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should create consumer with the given pubsub config', async function () {
|
|
77
|
+
await ProgramExecutor.create(config).processPrograms(testJobLibrary);
|
|
78
|
+
|
|
79
|
+
expect(Consumer.create.mock.calls[0][0]).toEqual(config.topicName);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should create consumer to consume the given topic', async function () {
|
|
83
|
+
await ProgramExecutor.create(config).processPrograms(testJobLibrary);
|
|
84
|
+
|
|
85
|
+
expect(Consumer.create.mock.calls[0][1]).toEqual(expect.objectContaining({
|
|
86
|
+
MaxMessages: 1,
|
|
87
|
+
MaxStreams: 1,
|
|
88
|
+
MaxExtensionMinutes: 60,
|
|
89
|
+
MinAckDeadline:{'millis':600000},
|
|
90
|
+
MaxAckDeadline:{'millis':600000},
|
|
91
|
+
logger: 'program-executor-consumer' }));
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should create consumer with a logger based on the given topic name', async function () {
|
|
95
|
+
await ProgramExecutor.create(config).processPrograms(testJobLibrary);
|
|
96
|
+
|
|
97
|
+
expect(Consumer.create.mock.calls[0][1]).toEqual(
|
|
98
|
+
expect.objectContaining({ logger: `${config.topicName}-consumer` }));
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should config consumer with given flow control settings', async function () {
|
|
102
|
+
await ProgramExecutor.create(config).processPrograms(testJobLibrary);
|
|
103
|
+
|
|
104
|
+
expect(Consumer.create.mock.calls[0][1]).toEqual(
|
|
105
|
+
expect.objectContaining({ MaxMessages: 1,
|
|
106
|
+
MaxStreams: 1 }));
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should call the created executor when message callback fires', async function () {
|
|
110
|
+
await ProgramExecutor.create(config).processPrograms(testJobLibrary);
|
|
111
|
+
|
|
112
|
+
const onMessageFunction = Consumer.create.mock.calls[0][1].onMessage;
|
|
113
|
+
|
|
114
|
+
await onMessageFunction({ random: 'message' });
|
|
115
|
+
|
|
116
|
+
expect(ProgramExecutorProcessor.prototype.process).toHaveBeenCalledWith({ random: 'message' });
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should truncate log message to avoid splitting by heroku', async function () {
|
|
120
|
+
const veryLongError = new Error('1'.repeat(100000));
|
|
121
|
+
|
|
122
|
+
ProgramExecutorProcessor.prototype.process = jest.fn().mockRejectedValue(veryLongError);
|
|
123
|
+
|
|
124
|
+
let caughtError;
|
|
125
|
+
try {
|
|
126
|
+
await ProgramExecutor.create(config).processPrograms(testJobLibrary);
|
|
127
|
+
const onMessageFunction = Consumer.create.mock.calls[0][1].onMessage;
|
|
128
|
+
await onMessageFunction({ random: 'message' });
|
|
129
|
+
} catch (error) {
|
|
130
|
+
caughtError = error;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
expect(caughtError).not.toBe(undefined);
|
|
134
|
+
expect(caughtError.message.length).toEqual(255);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should emit an error event', function (done) {
|
|
138
|
+
const sampleError = new Error('Error to be emitted');
|
|
139
|
+
ProgramExecutorProcessor.prototype.process = jest.fn().mockRejectedValue(sampleError);
|
|
140
|
+
|
|
141
|
+
const programExecutor = ProgramExecutor.create(config);
|
|
142
|
+
|
|
143
|
+
programExecutor.on('programError', function ({ error, message }) {
|
|
144
|
+
expect(message).toEqual({ random: 'message' });
|
|
145
|
+
expect(error.message).toEqual('Error to be emitted');
|
|
146
|
+
done();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
programExecutor.processPrograms(testJobLibrary);
|
|
150
|
+
const onMessageFunction = Consumer.create.mock.calls[0][1].onMessage;
|
|
151
|
+
onMessageFunction({ random: 'message' }).catch(() => {});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should start processing', async function () {
|
|
155
|
+
await ProgramExecutor.create(config).processPrograms(testJobLibrary);
|
|
156
|
+
|
|
157
|
+
expect(Consumer.prototype.process).toHaveBeenCalled();
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
});
|
|
@@ -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
|
-
|
|
30
|
-
|
|
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',
|
|
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).
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
expect(ProgramHandler.create).toHaveBeenCalledWith(
|
|
48
|
+
expect.any(ProgramsRepository),
|
|
49
|
+
expect.any(QueueManager)
|
|
48
50
|
);
|
|
49
|
-
expect(ProgramsRepository.create).
|
|
50
|
-
expect(QueueManager.create).
|
|
51
|
-
expect(ProgramHandler.prototype.createProgram).
|
|
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',
|
|
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).
|
|
62
|
-
|
|
63
|
-
|
|
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).
|
|
68
|
-
expect(QueueManager.create).
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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).
|
|
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.
|
|
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.
|
|
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.
|
|
120
|
-
expect(caughtError.message.length).
|
|
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).
|
|
132
|
-
expect(error.message).
|
|
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.
|
|
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).
|
|
148
|
+
expect(Consumer.prototype.process).toHaveBeenCalled();
|
|
145
149
|
});
|
|
146
150
|
});
|
|
147
151
|
});
|