@vida-global/core 1.4.5 → 2.0.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.
- package/AGENTS.md +13 -0
- package/README.md +1 -0
- package/config/newrelic-config.js +118 -0
- package/index.js +9 -5
- package/lib/activeRecord/baseRecord.js +49 -3
- package/lib/apm/agent.js +22 -0
- package/lib/apm/index.js +54 -0
- package/lib/apm/utils.js +43 -0
- package/lib/http/client.js +16 -2
- package/lib/jobQueue/README.md +63 -0
- package/lib/jobQueue/abstractJob.js +103 -0
- package/lib/jobQueue/abstractJobComponent.js +37 -0
- package/lib/jobQueue/index.js +10 -0
- package/lib/jobQueue/jobImporter.js +19 -0
- package/lib/jobQueue/queue.js +161 -0
- package/lib/jobQueue/worker.js +273 -0
- package/lib/logger/README.md +33 -2
- package/lib/logger/index.js +117 -20
- package/lib/logger/serverMiddleware.js +41 -0
- package/lib/redis/redisClient.js +32 -24
- package/lib/server/controllerImporter.js +9 -47
- package/lib/server/server.js +34 -71
- package/lib/server/serverController.js +215 -23
- package/lib/utils/abstractAutoImporter.js +64 -0
- package/package.json +8 -6
- package/test/activeRecord/baseRecord.test.js +129 -0
- package/test/activeRecord/db/connection.test.js +4 -1
- package/test/activeRecord/db/connectionConfiguration.test.js +9 -3
- package/test/activeRecord/db/schema.test.js +217 -0
- package/test/activeRecord/helpers/baseRecordMocks.js +6 -4
- package/test/activeRecord/helpers/connection.js +0 -3
- package/test/activeRecord/helpers/connectionConfiguration.js +0 -8
- package/test/activeRecord/utils.test.js +72 -0
- package/test/apm/agent.test.js +56 -0
- package/test/apm/utils.test.js +121 -0
- package/test/helpers/env.js +33 -0
- package/test/http/client.test.js +3 -3
- package/test/jobQueue/abstractJob.test.js +307 -0
- package/test/jobQueue/abstractJobComponent.test.js +110 -0
- package/test/jobQueue/helpers/abstractJob.js +26 -0
- package/test/jobQueue/helpers/apmMock.js +20 -0
- package/test/jobQueue/helpers/bullmqMock.js +67 -0
- package/test/jobQueue/helpers/env.js +29 -0
- package/test/jobQueue/helpers/fixtureJobs/notAJob.js +8 -0
- package/test/jobQueue/helpers/fixtureJobs/testJobA.js +14 -0
- package/test/jobQueue/helpers/fixtureJobs/testJobB.js +14 -0
- package/test/jobQueue/helpers/loggerMock.js +43 -0
- package/test/jobQueue/helpers/worker.js +38 -0
- package/test/jobQueue/queue.test.js +320 -0
- package/test/jobQueue/worker.test.js +526 -0
- package/test/logger/index.test.js +61 -0
- package/test/logger/serverMiddleware.test.js +72 -0
- package/test/server/apiDocsGenerator.test.js +38 -0
- package/test/server/server.test.js +186 -4
- package/test/server/serverController.test.js +407 -5
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
const TestHelpers = require('@vida-global/test-helpers');
|
|
2
|
+
const { defineJobSubclass } = require('./helpers/abstractJob');
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
jest.mock('../../lib/jobQueue/queue', () => ({
|
|
6
|
+
getQueue: jest.fn(),
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
jest.mock('../../lib/logger', () => {
|
|
10
|
+
const { buildLoggerMock } = require('./helpers/loggerMock');
|
|
11
|
+
return { logger: buildLoggerMock() };
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
jest.mock('../../lib/apm', () => {
|
|
15
|
+
const { buildApmMock } = require('./helpers/apmMock');
|
|
16
|
+
return buildApmMock();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
let AbstractJob;
|
|
21
|
+
let getQueue;
|
|
22
|
+
let logger;
|
|
23
|
+
let APM;
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
jest.resetModules();
|
|
27
|
+
({ AbstractJob } = require('../../lib/jobQueue/abstractJob'));
|
|
28
|
+
({ getQueue } = require('../../lib/jobQueue/queue'));
|
|
29
|
+
({ logger } = require('../../lib/logger'));
|
|
30
|
+
APM = require('../../lib/apm');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
jest.clearAllMocks();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
describe('AbstractJob', () => {
|
|
40
|
+
describe('constructor', () => {
|
|
41
|
+
it ('throws when AbstractJob is instantiated directly', () => {
|
|
42
|
+
expect(() => new AbstractJob()).toThrow('Cannot create an instance of AbstractJob');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it ('throws when the subclass does not define a queueName', () => {
|
|
46
|
+
class NoQueueJob extends AbstractJob {
|
|
47
|
+
async run() {}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
expect(() => new NoQueueJob()).toThrow('No queue defined');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it ('throws when the subclass does not define a run method', () => {
|
|
54
|
+
class NoRunJob extends AbstractJob {
|
|
55
|
+
static queueName() { return 'q'; }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
expect(() => new NoRunJob()).toThrow('No run method defined');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it ('succeeds when the subclass defines both queueName and run', () => {
|
|
62
|
+
const Subclass = defineJobSubclass(AbstractJob);
|
|
63
|
+
|
|
64
|
+
expect(() => new Subclass()).not.toThrow();
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
describe('.queue', () => {
|
|
70
|
+
it ('returns the queue from getQueue invoked with the resolved queue name', () => {
|
|
71
|
+
const name = TestHelpers.Faker.Text.randomString();
|
|
72
|
+
const Subclass = defineJobSubclass(AbstractJob, { queueName: name });
|
|
73
|
+
const stubQueue = { queueJob: jest.fn() };
|
|
74
|
+
getQueue.mockReturnValue(stubQueue);
|
|
75
|
+
|
|
76
|
+
const result = Subclass.queue('arg1', 'arg2');
|
|
77
|
+
|
|
78
|
+
expect(getQueue).toHaveBeenCalledTimes(1);
|
|
79
|
+
expect(getQueue).toHaveBeenCalledWith(name);
|
|
80
|
+
expect(result).toBe(stubQueue);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it ('passes job args through to a dynamic queueName method', () => {
|
|
84
|
+
const arg1 = TestHelpers.Faker.Text.randomString();
|
|
85
|
+
const arg2 = TestHelpers.Faker.Math.randomNumber();
|
|
86
|
+
const queueNameSpy = jest.fn(() => 'dynamic-queue');
|
|
87
|
+
const Subclass = defineJobSubclass(AbstractJob, { queueNameFn: queueNameSpy });
|
|
88
|
+
getQueue.mockReturnValue({ queueJob: jest.fn() });
|
|
89
|
+
|
|
90
|
+
Subclass.queue(arg1, arg2);
|
|
91
|
+
|
|
92
|
+
expect(queueNameSpy).toHaveBeenCalledWith(arg1, arg2);
|
|
93
|
+
expect(getQueue).toHaveBeenCalledWith('dynamic-queue');
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
describe('.queueJob', () => {
|
|
99
|
+
it ('enqueues the job on the resolved queue with the args and settings', async () => {
|
|
100
|
+
const name = TestHelpers.Faker.Text.randomString();
|
|
101
|
+
const Subclass = defineJobSubclass(AbstractJob, { queueName: name });
|
|
102
|
+
const stubQueue = { queueJob: jest.fn().mockResolvedValue(undefined) };
|
|
103
|
+
getQueue.mockReturnValue(stubQueue);
|
|
104
|
+
const arg1 = TestHelpers.Faker.Text.randomString();
|
|
105
|
+
const arg2 = TestHelpers.Faker.Math.randomNumber();
|
|
106
|
+
|
|
107
|
+
await Subclass.queueJob(arg1, arg2);
|
|
108
|
+
|
|
109
|
+
expect(stubQueue.queueJob).toHaveBeenCalledTimes(1);
|
|
110
|
+
expect(stubQueue.queueJob).toHaveBeenCalledWith(
|
|
111
|
+
Subclass,
|
|
112
|
+
[arg1, arg2],
|
|
113
|
+
Subclass.queueSettings(arg1, arg2),
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it ('awaits the underlying queueJob call', async () => {
|
|
118
|
+
const Subclass = defineJobSubclass(AbstractJob);
|
|
119
|
+
let resolveQueueJob;
|
|
120
|
+
const queueJobPromise = new Promise((resolve) => { resolveQueueJob = resolve; });
|
|
121
|
+
getQueue.mockReturnValue({ queueJob: jest.fn().mockReturnValue(queueJobPromise) });
|
|
122
|
+
|
|
123
|
+
const callPromise = Subclass.queueJob();
|
|
124
|
+
let resolved = false;
|
|
125
|
+
callPromise.then(() => { resolved = true; });
|
|
126
|
+
await Promise.resolve();
|
|
127
|
+
expect(resolved).toBe(false);
|
|
128
|
+
|
|
129
|
+
resolveQueueJob();
|
|
130
|
+
await callPromise;
|
|
131
|
+
expect(resolved).toBe(true);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
describe('.queueSettings', () => {
|
|
137
|
+
it ('returns default retry settings', () => {
|
|
138
|
+
const Subclass = defineJobSubclass(AbstractJob);
|
|
139
|
+
|
|
140
|
+
expect(Subclass.queueSettings()).toEqual({
|
|
141
|
+
attempts: 3,
|
|
142
|
+
backoff: { type: 'exponential', delay: 2000 },
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it ('reflects subclass overrides for retry settings', () => {
|
|
147
|
+
const numRetries = TestHelpers.Faker.Math.randomNumber(20);
|
|
148
|
+
const retryBackoffType = TestHelpers.Faker.Text.randomString();
|
|
149
|
+
const retryBackoffDelay = TestHelpers.Faker.Math.randomNumber(10000);
|
|
150
|
+
const Subclass = defineJobSubclass(AbstractJob, {
|
|
151
|
+
numRetries,
|
|
152
|
+
retryBackoffType,
|
|
153
|
+
retryBackoffDelay,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
expect(Subclass.queueSettings()).toEqual({
|
|
157
|
+
attempts: numRetries,
|
|
158
|
+
backoff: { type: retryBackoffType, delay: retryBackoffDelay },
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
describe('default static getters', () => {
|
|
165
|
+
it ('.numRetries defaults to 3', () => expect(AbstractJob.numRetries).toEqual(3));
|
|
166
|
+
it ('.retryBackoffType defaults to "exponential"', () => expect(AbstractJob.retryBackoffType).toEqual('exponential'));
|
|
167
|
+
it ('.retryBackoffDelay defaults to 2000', () => expect(AbstractJob.retryBackoffDelay).toEqual(2000));
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
describe('#_run', () => {
|
|
172
|
+
it ('stores the id and exposes it via #id', async () => {
|
|
173
|
+
const Subclass = defineJobSubclass(AbstractJob);
|
|
174
|
+
const job = new Subclass();
|
|
175
|
+
const id = TestHelpers.Faker.Text.randomString();
|
|
176
|
+
|
|
177
|
+
await job._run(id, [], jest.fn());
|
|
178
|
+
|
|
179
|
+
expect(job.id).toEqual(id);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it ('calls run with the spread args', async () => {
|
|
183
|
+
const runSpy = jest.fn().mockResolvedValue(undefined);
|
|
184
|
+
const Subclass = defineJobSubclass(AbstractJob, { run: runSpy });
|
|
185
|
+
const job = new Subclass();
|
|
186
|
+
const args = [TestHelpers.Faker.Text.randomString(), TestHelpers.Faker.Math.randomNumber()];
|
|
187
|
+
|
|
188
|
+
await job._run('id', args, jest.fn());
|
|
189
|
+
|
|
190
|
+
expect(runSpy).toHaveBeenCalledTimes(1);
|
|
191
|
+
expect(runSpy).toHaveBeenCalledWith(...args);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it ('propagates rejections from run', async () => {
|
|
195
|
+
const error = new Error(TestHelpers.Faker.Text.randomString());
|
|
196
|
+
const Subclass = defineJobSubclass(AbstractJob, { run: jest.fn().mockRejectedValue(error) });
|
|
197
|
+
const job = new Subclass();
|
|
198
|
+
|
|
199
|
+
await expect(job._run('id', [], jest.fn())).rejects.toBe(error);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it ('stores the updateProgress callback for later use', async () => {
|
|
203
|
+
const Subclass = defineJobSubclass(AbstractJob);
|
|
204
|
+
const job = new Subclass();
|
|
205
|
+
const callback = jest.fn().mockResolvedValue(undefined);
|
|
206
|
+
|
|
207
|
+
await job._run('id', [], callback);
|
|
208
|
+
const payload = TestHelpers.Faker.Text.randomString();
|
|
209
|
+
await job.updateProgress(payload);
|
|
210
|
+
|
|
211
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
212
|
+
expect(callback).toHaveBeenCalledWith(payload);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
describe('#id', () => {
|
|
218
|
+
it ('returns undefined before _run has been invoked', () => {
|
|
219
|
+
const Subclass = defineJobSubclass(AbstractJob);
|
|
220
|
+
const job = new Subclass();
|
|
221
|
+
|
|
222
|
+
expect(job.id).toBeUndefined();
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
describe('#updateProgress', () => {
|
|
228
|
+
it ('forwards all arguments to the stored callback', async () => {
|
|
229
|
+
const Subclass = defineJobSubclass(AbstractJob);
|
|
230
|
+
const job = new Subclass();
|
|
231
|
+
const callback = jest.fn().mockResolvedValue(undefined);
|
|
232
|
+
await job._run('id', [], callback);
|
|
233
|
+
|
|
234
|
+
const arg1 = TestHelpers.Faker.Math.randomNumber();
|
|
235
|
+
const arg2 = TestHelpers.Faker.Text.randomString();
|
|
236
|
+
await job.updateProgress(arg1, arg2);
|
|
237
|
+
|
|
238
|
+
expect(callback).toHaveBeenCalledWith(arg1, arg2);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it ('awaits the callback', async () => {
|
|
242
|
+
const Subclass = defineJobSubclass(AbstractJob);
|
|
243
|
+
const job = new Subclass();
|
|
244
|
+
let resolveCallback;
|
|
245
|
+
const callback = jest.fn(() => new Promise((res) => { resolveCallback = res; }));
|
|
246
|
+
await job._run('id', [], callback);
|
|
247
|
+
|
|
248
|
+
const callPromise = job.updateProgress();
|
|
249
|
+
let resolved = false;
|
|
250
|
+
callPromise.then(() => { resolved = true; });
|
|
251
|
+
await Promise.resolve();
|
|
252
|
+
expect(resolved).toBe(false);
|
|
253
|
+
|
|
254
|
+
resolveCallback();
|
|
255
|
+
await callPromise;
|
|
256
|
+
expect(resolved).toBe(true);
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
describe('#addApmAttribute', () => {
|
|
262
|
+
it ('delegates to APM.addCustomAttribute', () => {
|
|
263
|
+
const Subclass = defineJobSubclass(AbstractJob);
|
|
264
|
+
const job = new Subclass();
|
|
265
|
+
const key = TestHelpers.Faker.Text.randomString();
|
|
266
|
+
const value = TestHelpers.Faker.Text.randomString();
|
|
267
|
+
|
|
268
|
+
job.addApmAttribute(key, value);
|
|
269
|
+
|
|
270
|
+
expect(APM.addCustomAttribute).toHaveBeenCalledTimes(1);
|
|
271
|
+
expect(APM.addCustomAttribute).toHaveBeenCalledWith(key, value);
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
describe('#startApmSegment', () => {
|
|
277
|
+
it ('delegates to APM.startSegment and returns its result', () => {
|
|
278
|
+
const Subclass = defineJobSubclass(AbstractJob);
|
|
279
|
+
const job = new Subclass();
|
|
280
|
+
const name = TestHelpers.Faker.Text.randomString();
|
|
281
|
+
const sentinel = TestHelpers.Faker.Text.randomString();
|
|
282
|
+
const handler = jest.fn(() => sentinel);
|
|
283
|
+
|
|
284
|
+
const result = job.startApmSegment(name, true, handler);
|
|
285
|
+
|
|
286
|
+
expect(APM.startSegment).toHaveBeenCalledTimes(1);
|
|
287
|
+
expect(APM.startSegment).toHaveBeenCalledWith(name, true, handler);
|
|
288
|
+
expect(result).toEqual(sentinel);
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
describe('#logger', () => {
|
|
294
|
+
it ('returns a child of the constructor-named scope keyed by job id', async () => {
|
|
295
|
+
const Subclass = defineJobSubclass(AbstractJob);
|
|
296
|
+
const job = new Subclass();
|
|
297
|
+
const id = TestHelpers.Faker.Text.randomString();
|
|
298
|
+
await job._run(id, [], jest.fn());
|
|
299
|
+
|
|
300
|
+
const scope = logger[Subclass.name];
|
|
301
|
+
const child = job.logger;
|
|
302
|
+
|
|
303
|
+
expect(scope.child).toHaveBeenCalledWith(id);
|
|
304
|
+
expect(child).toBe(scope.child.mock.results[0].value);
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
const { AbstractJobComponent } = require('../../lib/jobQueue/abstractJobComponent');
|
|
2
|
+
const TestHelpers = require('@vida-global/test-helpers');
|
|
3
|
+
const { snapshotEnv } = require('../helpers/env');
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
let restoreEnv;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
restoreEnv = snapshotEnv([
|
|
10
|
+
'NODE_ENV',
|
|
11
|
+
'REDIS_BULL_HOST',
|
|
12
|
+
'REDIS_BULL_PORT',
|
|
13
|
+
'REDIS_BULL_PASSWORD',
|
|
14
|
+
]);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
restoreEnv();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
describe('AbstractJobComponent', () => {
|
|
24
|
+
describe('.fullQueueName', () => {
|
|
25
|
+
it ('builds a name suffixed with the current NODE_ENV', () => {
|
|
26
|
+
const queueName = TestHelpers.Faker.Text.randomString();
|
|
27
|
+
const env = TestHelpers.Faker.Text.randomString();
|
|
28
|
+
process.env.NODE_ENV = env;
|
|
29
|
+
|
|
30
|
+
expect(AbstractJobComponent.fullQueueName(queueName)).toEqual(`${queueName}-${env}`);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it ('reflects changes to NODE_ENV between calls', () => {
|
|
34
|
+
const queueName = TestHelpers.Faker.Text.randomString();
|
|
35
|
+
const env1 = TestHelpers.Faker.Text.randomString();
|
|
36
|
+
const env2 = TestHelpers.Faker.Text.randomString();
|
|
37
|
+
|
|
38
|
+
process.env.NODE_ENV = env1;
|
|
39
|
+
const first = AbstractJobComponent.fullQueueName(queueName);
|
|
40
|
+
process.env.NODE_ENV = env2;
|
|
41
|
+
const second = AbstractJobComponent.fullQueueName(queueName);
|
|
42
|
+
|
|
43
|
+
expect(first).toEqual(`${queueName}-${env1}`);
|
|
44
|
+
expect(second).toEqual(`${queueName}-${env2}`);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
describe('.env', () => {
|
|
50
|
+
it ('returns the current NODE_ENV', () => {
|
|
51
|
+
const env = TestHelpers.Faker.Text.randomString();
|
|
52
|
+
process.env.NODE_ENV = env;
|
|
53
|
+
|
|
54
|
+
expect(AbstractJobComponent.env).toEqual(env);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it ('returns undefined when NODE_ENV is not set', () => {
|
|
58
|
+
delete process.env.NODE_ENV;
|
|
59
|
+
|
|
60
|
+
expect(AbstractJobComponent.env).toBeUndefined();
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
describe('#fullQueueName', () => {
|
|
66
|
+
it ('delegates to the static method using `this.queueName`', () => {
|
|
67
|
+
const queueName = TestHelpers.Faker.Text.randomString();
|
|
68
|
+
const env = TestHelpers.Faker.Text.randomString();
|
|
69
|
+
process.env.NODE_ENV = env;
|
|
70
|
+
|
|
71
|
+
class Concrete extends AbstractJobComponent {
|
|
72
|
+
get queueName() { return queueName; }
|
|
73
|
+
}
|
|
74
|
+
const instance = new Concrete();
|
|
75
|
+
|
|
76
|
+
expect(instance.fullQueueName).toEqual(`${queueName}-${env}`);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
describe('#redisConnectionDetails', () => {
|
|
82
|
+
it ('returns the connection details from REDIS_BULL_* environment variables', () => {
|
|
83
|
+
const host = TestHelpers.Faker.Text.randomString();
|
|
84
|
+
const port = String(TestHelpers.Faker.Math.randomNumber(65000));
|
|
85
|
+
const password = TestHelpers.Faker.Text.randomString();
|
|
86
|
+
process.env.REDIS_BULL_HOST = host;
|
|
87
|
+
process.env.REDIS_BULL_PORT = port;
|
|
88
|
+
process.env.REDIS_BULL_PASSWORD = password;
|
|
89
|
+
|
|
90
|
+
const instance = new AbstractJobComponent();
|
|
91
|
+
|
|
92
|
+
expect(instance.redisConnectionDetails).toEqual({ host, password, port });
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
describe('individual redis env getters', () => {
|
|
98
|
+
it.each([
|
|
99
|
+
['host', 'REDIS_BULL_HOST'],
|
|
100
|
+
['port', 'REDIS_BULL_PORT'],
|
|
101
|
+
['password', 'REDIS_BULL_PASSWORD'],
|
|
102
|
+
])('#%s returns process.env.%s', (getter, envVar) => {
|
|
103
|
+
const value = TestHelpers.Faker.Text.randomString();
|
|
104
|
+
process.env[envVar] = value;
|
|
105
|
+
const instance = new AbstractJobComponent();
|
|
106
|
+
|
|
107
|
+
expect(instance[getter]).toEqual(value);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const TestHelpers = require('@vida-global/test-helpers');
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
/***************************************************************************************************
|
|
5
|
+
* SUBCLASS FACTORY
|
|
6
|
+
***************************************************************************************************/
|
|
7
|
+
function defineJobSubclass(AbstractJob, { queueName, queueNameFn, run, ...overrides } = {}) {
|
|
8
|
+
const resolvedName = queueName || TestHelpers.Faker.Text.randomString();
|
|
9
|
+
const nameFunction = queueNameFn || (() => resolvedName);
|
|
10
|
+
const Subclass = class extends AbstractJob {
|
|
11
|
+
static queueName(...jobArgs) { return nameFunction(...jobArgs); }
|
|
12
|
+
async run(...args) {
|
|
13
|
+
if (run) return await run(...args);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
17
|
+
Object.defineProperty(Subclass, key, { value });
|
|
18
|
+
}
|
|
19
|
+
Subclass.expectedQueueName = resolvedName;
|
|
20
|
+
return Subclass;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
module.exports = {
|
|
25
|
+
defineJobSubclass,
|
|
26
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/***************************************************************************************************
|
|
2
|
+
* MOCKS
|
|
3
|
+
***************************************************************************************************/
|
|
4
|
+
function buildApmMock() {
|
|
5
|
+
return {
|
|
6
|
+
addCustomAttribute: jest.fn(),
|
|
7
|
+
recordCustomEvent: jest.fn(),
|
|
8
|
+
recordMetric: jest.fn(),
|
|
9
|
+
reportError: jest.fn(),
|
|
10
|
+
setControllerName: jest.fn(),
|
|
11
|
+
setUserId: jest.fn(),
|
|
12
|
+
startBackgroundTransaction: jest.fn((name, group, handler) => handler()),
|
|
13
|
+
startSegment: jest.fn((name, record, handler) => handler()),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
module.exports = {
|
|
19
|
+
buildApmMock,
|
|
20
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/***************************************************************************************************
|
|
2
|
+
* MOCKS
|
|
3
|
+
***************************************************************************************************/
|
|
4
|
+
const instances = {
|
|
5
|
+
queues: [],
|
|
6
|
+
workers: [],
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MockBullQueue {
|
|
11
|
+
constructor(name, options) {
|
|
12
|
+
this.name = name;
|
|
13
|
+
this.options = options;
|
|
14
|
+
|
|
15
|
+
this.add = jest.fn();
|
|
16
|
+
this.close = jest.fn();
|
|
17
|
+
this.pause = jest.fn();
|
|
18
|
+
this.resume = jest.fn();
|
|
19
|
+
this.count = jest.fn();
|
|
20
|
+
this.getJobs = jest.fn();
|
|
21
|
+
this.getActiveCount = jest.fn();
|
|
22
|
+
this.getActive = jest.fn();
|
|
23
|
+
this.getFailedCount = jest.fn();
|
|
24
|
+
this.getFailed = jest.fn();
|
|
25
|
+
this.clean = jest.fn();
|
|
26
|
+
this.drain = jest.fn();
|
|
27
|
+
this.getWorkers = jest.fn();
|
|
28
|
+
this.on = jest.fn();
|
|
29
|
+
|
|
30
|
+
instances.queues.push(this);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class MockBullWorker {
|
|
36
|
+
constructor(name, processor, options) {
|
|
37
|
+
this.name = name;
|
|
38
|
+
this.processor = processor;
|
|
39
|
+
this.options = options;
|
|
40
|
+
|
|
41
|
+
this.run = jest.fn();
|
|
42
|
+
this.on = jest.fn();
|
|
43
|
+
|
|
44
|
+
instances.workers.push(this);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
function reset() {
|
|
50
|
+
instances.queues.length = 0;
|
|
51
|
+
instances.workers.length = 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
function listenerFor(emitter, event) {
|
|
56
|
+
const call = emitter.on.mock.calls.find(([name]) => name === event);
|
|
57
|
+
return call ? call[1] : undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
module.exports = {
|
|
62
|
+
Queue: MockBullQueue,
|
|
63
|
+
Worker: MockBullWorker,
|
|
64
|
+
instances,
|
|
65
|
+
reset,
|
|
66
|
+
listenerFor,
|
|
67
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const TestHelpers = require('@vida-global/test-helpers');
|
|
2
|
+
const { snapshotEnv } = require('../../helpers/env');
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
/***************************************************************************************************
|
|
6
|
+
* ENV SETUP
|
|
7
|
+
***************************************************************************************************/
|
|
8
|
+
function setupJobQueueEnv(extraKeysToClear = []) {
|
|
9
|
+
const restore = snapshotEnv([
|
|
10
|
+
'NODE_ENV',
|
|
11
|
+
'REDIS_BULL_HOST',
|
|
12
|
+
'REDIS_BULL_PORT',
|
|
13
|
+
'REDIS_BULL_PASSWORD',
|
|
14
|
+
...extraKeysToClear,
|
|
15
|
+
]);
|
|
16
|
+
process.env.NODE_ENV = 'test';
|
|
17
|
+
process.env.REDIS_BULL_HOST = TestHelpers.Faker.Text.randomString();
|
|
18
|
+
process.env.REDIS_BULL_PORT = String(TestHelpers.Faker.Math.randomNumber(65000));
|
|
19
|
+
process.env.REDIS_BULL_PASSWORD = TestHelpers.Faker.Text.randomString();
|
|
20
|
+
for (const key of extraKeysToClear) {
|
|
21
|
+
delete process.env[key];
|
|
22
|
+
}
|
|
23
|
+
return restore;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
module.exports = {
|
|
28
|
+
setupJobQueueEnv,
|
|
29
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/***************************************************************************************************
|
|
2
|
+
* MOCKS
|
|
3
|
+
***************************************************************************************************/
|
|
4
|
+
function createLeaf(name) {
|
|
5
|
+
const leaf = {
|
|
6
|
+
name,
|
|
7
|
+
debug: jest.fn(),
|
|
8
|
+
info: jest.fn(),
|
|
9
|
+
error: jest.fn(),
|
|
10
|
+
warn: jest.fn(),
|
|
11
|
+
verbose: jest.fn(),
|
|
12
|
+
silly: jest.fn(),
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const children = {};
|
|
16
|
+
leaf.child = jest.fn((id) => {
|
|
17
|
+
if (!children[id]) children[id] = createLeaf(`${name}:${id}`);
|
|
18
|
+
return children[id];
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
return leaf;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
function buildLoggerMock() {
|
|
26
|
+
const scopes = {};
|
|
27
|
+
const root = createLeaf('root');
|
|
28
|
+
root.addScope = jest.fn();
|
|
29
|
+
|
|
30
|
+
return new Proxy(root, {
|
|
31
|
+
get(target, prop) {
|
|
32
|
+
if (typeof prop !== 'string') return target[prop];
|
|
33
|
+
if (Object.hasOwn(target, prop)) return target[prop];
|
|
34
|
+
if (!scopes[prop]) scopes[prop] = createLeaf(prop);
|
|
35
|
+
return scopes[prop];
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
module.exports = {
|
|
42
|
+
buildLoggerMock,
|
|
43
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const TestHelpers = require('@vida-global/test-helpers');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
const FIXTURE_JOBS_DIR = path.join(__dirname, 'fixtureJobs');
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
/***************************************************************************************************
|
|
9
|
+
* WORKER FACTORY
|
|
10
|
+
***************************************************************************************************/
|
|
11
|
+
function buildWorker(Worker, queueName) {
|
|
12
|
+
class TestWorker extends Worker {
|
|
13
|
+
get jobDirectories() { return [FIXTURE_JOBS_DIR]; }
|
|
14
|
+
}
|
|
15
|
+
return new TestWorker(queueName || TestHelpers.Faker.Text.randomString());
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
/***************************************************************************************************
|
|
20
|
+
* BULL JOB BUILDER
|
|
21
|
+
***************************************************************************************************/
|
|
22
|
+
function buildFakeBullJob(overrides = {}) {
|
|
23
|
+
return {
|
|
24
|
+
name: 'TestJobA',
|
|
25
|
+
data: { id: TestHelpers.Faker.Text.randomString(), args: [] },
|
|
26
|
+
updateProgress: jest.fn(),
|
|
27
|
+
attemptsMade: 0,
|
|
28
|
+
opts: { attempts: 3 },
|
|
29
|
+
...overrides,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
module.exports = {
|
|
35
|
+
FIXTURE_JOBS_DIR,
|
|
36
|
+
buildFakeBullJob,
|
|
37
|
+
buildWorker,
|
|
38
|
+
}
|