@vida-global/core 2.3.6 → 2.4.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/index.js +10 -8
- package/lib/appDependencies/index.js +48 -0
- package/lib/http/README.md +1 -1
- package/lib/http/client.js +2 -2
- package/lib/jobQueue/README.md +56 -17
- package/lib/jobQueue/abstractJob.js +39 -3
- package/lib/jobQueue/queue.js +28 -20
- package/lib/jobQueue/worker.js +69 -22
- package/lib/logger/README.md +12 -0
- package/lib/logger/index.js +24 -8
- package/lib/server/README.md +32 -1
- package/lib/server/controllerMixins/renderer.js +11 -0
- package/lib/server/doc/renderer.md +23 -0
- package/package.json +1 -1
- package/test/appDependencies/appDependencies.test.js +93 -0
- package/test/http/client.test.js +21 -0
- package/test/jobQueue/abstractJob.test.js +64 -5
- package/test/jobQueue/helpers/bullmqMock.js +19 -16
- package/test/jobQueue/helpers/fixtureJobs/defaultJobForBeta.js +18 -0
- package/test/jobQueue/helpers/loggerMock.js +16 -5
- package/test/jobQueue/helpers/queue.js +2 -4
- package/test/jobQueue/helpers/worker.js +10 -3
- package/test/jobQueue/queue.test.js +80 -34
- package/test/jobQueue/worker.test.js +147 -21
- package/test/logger/index.test.js +113 -0
- package/test/server/controllerMixins/renderer.test.js +59 -0
|
@@ -110,7 +110,7 @@ describe('Queue', () => {
|
|
|
110
110
|
});
|
|
111
111
|
|
|
112
112
|
it ('constructs the BullMQ queue with fullQueueName and connection options on first use', async () => {
|
|
113
|
-
await queue.
|
|
113
|
+
await queue.getJobCounts();
|
|
114
114
|
|
|
115
115
|
expect(bullmqMock.instances.queues).toHaveLength(1);
|
|
116
116
|
const instance = bullmqMock.instances.queues[0];
|
|
@@ -119,15 +119,15 @@ describe('Queue', () => {
|
|
|
119
119
|
});
|
|
120
120
|
|
|
121
121
|
it ('reuses the same BullMQ queue across multiple method calls', async () => {
|
|
122
|
-
await queue.
|
|
123
|
-
await queue.
|
|
122
|
+
await queue.getJobCounts();
|
|
123
|
+
await queue.pause();
|
|
124
124
|
await queue.close();
|
|
125
125
|
|
|
126
126
|
expect(bullmqMock.instances.queues).toHaveLength(1);
|
|
127
127
|
});
|
|
128
128
|
|
|
129
129
|
it ('registers an error handler on the BullMQ queue', async () => {
|
|
130
|
-
await queue.
|
|
130
|
+
await queue.getJobCounts();
|
|
131
131
|
|
|
132
132
|
const instance = bullmqMock.instances.queues[0];
|
|
133
133
|
expect(instance.on).toHaveBeenCalledWith('error', expect.any(Function));
|
|
@@ -136,9 +136,7 @@ describe('Queue', () => {
|
|
|
136
136
|
|
|
137
137
|
|
|
138
138
|
describe('#queueJob', () => {
|
|
139
|
-
it ('adds the job to the BullMQ queue with
|
|
140
|
-
const id = TestHelpers.Faker.Text.randomString();
|
|
141
|
-
queue.idGenerator.mockReturnValue(id);
|
|
139
|
+
it ('adds the job to the BullMQ queue with its args and settings', async () => {
|
|
142
140
|
const args = [TestHelpers.Faker.Text.randomString(), TestHelpers.Faker.Math.randomNumber()];
|
|
143
141
|
const settings = { attempts: TestHelpers.Faker.Math.randomNumber(10) };
|
|
144
142
|
const jobClass = { name: TestHelpers.Faker.Text.randomString() };
|
|
@@ -147,7 +145,7 @@ describe('Queue', () => {
|
|
|
147
145
|
|
|
148
146
|
const instance = bullmqMock.instances.queues[0];
|
|
149
147
|
expect(instance.add).toHaveBeenCalledTimes(1);
|
|
150
|
-
expect(instance.add).toHaveBeenCalledWith(jobClass.name, {
|
|
148
|
+
expect(instance.add).toHaveBeenCalledWith(jobClass.name, { args }, settings);
|
|
151
149
|
});
|
|
152
150
|
});
|
|
153
151
|
|
|
@@ -166,26 +164,20 @@ describe('Queue', () => {
|
|
|
166
164
|
});
|
|
167
165
|
|
|
168
166
|
|
|
169
|
-
describe('
|
|
170
|
-
it
|
|
171
|
-
|
|
172
|
-
[
|
|
173
|
-
|
|
174
|
-
])('#%s delegates to bullmq queue.%s and returns its value', async (method, bullMethod) => {
|
|
175
|
-
const expectedCount = TestHelpers.Faker.Math.randomNumber();
|
|
176
|
-
const queueInstance = (() => { queue[method](); return bullmqMock.instances.queues[0]; })();
|
|
177
|
-
queueInstance[bullMethod].mockResolvedValue(expectedCount);
|
|
167
|
+
describe('#getJobCounts', () => {
|
|
168
|
+
it ('returns what bullmq reports', async () => {
|
|
169
|
+
const counts = { waiting: 1, active: 2, completed: 3, failed: 4, delayed: 5, paused: 6 };
|
|
170
|
+
const queueInstance = (() => { queue.getJobCounts(); return bullmqMock.instances.queues[0]; })();
|
|
171
|
+
queueInstance.getJobCounts.mockResolvedValue(counts);
|
|
178
172
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
expect(result).toEqual(expectedCount);
|
|
173
|
+
await expect(queue.getJobCounts()).resolves.toEqual(counts);
|
|
182
174
|
});
|
|
183
175
|
});
|
|
184
176
|
|
|
185
177
|
|
|
186
178
|
describe('#numWorkers', () => {
|
|
187
179
|
it ('returns the length of bullmq queue.getWorkers()', async () => {
|
|
188
|
-
await queue.
|
|
180
|
+
await queue.getJobCounts();
|
|
189
181
|
const instance = bullmqMock.instances.queues[0];
|
|
190
182
|
const workers = Array.from({ length: TestHelpers.Faker.Math.randomNumber(10) }, () => ({}));
|
|
191
183
|
instance.getWorkers.mockResolvedValue(workers);
|
|
@@ -195,22 +187,29 @@ describe('Queue', () => {
|
|
|
195
187
|
});
|
|
196
188
|
|
|
197
189
|
|
|
198
|
-
describe('
|
|
199
|
-
it ('
|
|
200
|
-
|
|
190
|
+
describe('#clean', () => {
|
|
191
|
+
it ('passes the grace period, limit and type through in bullmq order', async () => {
|
|
192
|
+
const grace = TestHelpers.Faker.Math.randomNumber(100000);
|
|
193
|
+
const limit = TestHelpers.Faker.Math.randomNumber(100);
|
|
194
|
+
|
|
195
|
+
await queue.clean(grace, limit, 'failed');
|
|
201
196
|
|
|
202
197
|
const instance = bullmqMock.instances.queues[0];
|
|
203
|
-
expect(instance.clean).toHaveBeenCalledWith(
|
|
198
|
+
expect(instance.clean).toHaveBeenCalledWith(grace, limit, 'failed');
|
|
204
199
|
});
|
|
205
200
|
|
|
206
|
-
it ('
|
|
207
|
-
|
|
201
|
+
it ('returns the jobs bullmq removed', async () => {
|
|
202
|
+
const removed = [TestHelpers.Faker.Text.randomString()];
|
|
203
|
+
const queueInstance = (() => { queue.clean(0, 1, 'failed'); return bullmqMock.instances.queues[0]; })();
|
|
204
|
+
queueInstance.clean.mockResolvedValue(removed);
|
|
208
205
|
|
|
209
|
-
|
|
210
|
-
expect(instance.clean).toHaveBeenCalledWith(0, null, 'completed');
|
|
206
|
+
await expect(queue.clean(0, 1, 'failed')).resolves.toEqual(removed);
|
|
211
207
|
});
|
|
208
|
+
});
|
|
212
209
|
|
|
213
|
-
|
|
210
|
+
|
|
211
|
+
describe('#clearQueuedJobs', () => {
|
|
212
|
+
it ('calls bullmq drain()', async () => {
|
|
214
213
|
await queue.clearQueuedJobs();
|
|
215
214
|
|
|
216
215
|
const instance = bullmqMock.instances.queues[0];
|
|
@@ -219,6 +218,33 @@ describe('Queue', () => {
|
|
|
219
218
|
});
|
|
220
219
|
|
|
221
220
|
|
|
221
|
+
describe('maximum concurrency', () => {
|
|
222
|
+
it ('sets the queue maximum', async () => {
|
|
223
|
+
const maximum = TestHelpers.Faker.Math.randomNumber(20);
|
|
224
|
+
|
|
225
|
+
await queue.setMaximumConcurrency(maximum);
|
|
226
|
+
|
|
227
|
+
const instance = bullmqMock.instances.queues[0];
|
|
228
|
+
expect(instance.setGlobalConcurrency).toHaveBeenCalledWith(maximum);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it ('reads the queue maximum back', async () => {
|
|
232
|
+
const maximum = TestHelpers.Faker.Math.randomNumber(20);
|
|
233
|
+
const queueInstance = (() => { queue.maximumConcurrency(); return bullmqMock.instances.queues[0]; })();
|
|
234
|
+
queueInstance.getGlobalConcurrency.mockResolvedValue(maximum);
|
|
235
|
+
|
|
236
|
+
await expect(queue.maximumConcurrency()).resolves.toEqual(maximum);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it ('removes the queue maximum', async () => {
|
|
240
|
+
await queue.removeMaximumConcurrency();
|
|
241
|
+
|
|
242
|
+
const instance = bullmqMock.instances.queues[0];
|
|
243
|
+
expect(instance.removeGlobalConcurrency).toHaveBeenCalled();
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
|
|
222
248
|
describe('job-list queries', () => {
|
|
223
249
|
function buildBullJob(overrides = {}) {
|
|
224
250
|
return {
|
|
@@ -251,7 +277,7 @@ describe('Queue', () => {
|
|
|
251
277
|
it ('#getQueuedJobs maps getJobs(["paused","wait","prioritized","delayed"]) to formatted shape', async () => {
|
|
252
278
|
const bullJob1 = buildBullJob();
|
|
253
279
|
const bullJob2 = buildBullJob();
|
|
254
|
-
await queue.
|
|
280
|
+
await queue.getJobCounts();
|
|
255
281
|
const instance = bullmqMock.instances.queues[0];
|
|
256
282
|
instance.getJobs.mockResolvedValue([bullJob1, bullJob2]);
|
|
257
283
|
|
|
@@ -263,7 +289,7 @@ describe('Queue', () => {
|
|
|
263
289
|
|
|
264
290
|
it ('#getActiveJobs maps getActive() to formatted shape', async () => {
|
|
265
291
|
const bullJob = buildBullJob();
|
|
266
|
-
await queue.
|
|
292
|
+
await queue.getJobCounts();
|
|
267
293
|
const instance = bullmqMock.instances.queues[0];
|
|
268
294
|
instance.getActive.mockResolvedValue([bullJob]);
|
|
269
295
|
|
|
@@ -274,7 +300,7 @@ describe('Queue', () => {
|
|
|
274
300
|
|
|
275
301
|
it ('#getFailedJobs maps getFailed() to formatted shape', async () => {
|
|
276
302
|
const bullJob = buildBullJob();
|
|
277
|
-
await queue.
|
|
303
|
+
await queue.getJobCounts();
|
|
278
304
|
const instance = bullmqMock.instances.queues[0];
|
|
279
305
|
instance.getFailed.mockResolvedValue([bullJob]);
|
|
280
306
|
|
|
@@ -282,12 +308,32 @@ describe('Queue', () => {
|
|
|
282
308
|
|
|
283
309
|
expect(result).toEqual([expectedShape(bullJob)]);
|
|
284
310
|
});
|
|
311
|
+
|
|
312
|
+
// The way a single failed job gets fetched for inspection or replay.
|
|
313
|
+
it ('#getJob maps one job by id to the formatted shape', async () => {
|
|
314
|
+
const bullJob = buildBullJob();
|
|
315
|
+
await queue.getJobCounts();
|
|
316
|
+
const instance = bullmqMock.instances.queues[0];
|
|
317
|
+
instance.getJob.mockResolvedValue(bullJob);
|
|
318
|
+
|
|
319
|
+
const result = await queue.getJob(bullJob.id);
|
|
320
|
+
|
|
321
|
+
expect(instance.getJob).toHaveBeenCalledWith(bullJob.id);
|
|
322
|
+
expect(result).toEqual(expectedShape(bullJob));
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it ('#getJob returns null when there is no such job', async () => {
|
|
326
|
+
await queue.getJobCounts();
|
|
327
|
+
bullmqMock.instances.queues[0].getJob.mockResolvedValue(undefined);
|
|
328
|
+
|
|
329
|
+
await expect(queue.getJob('nope')).resolves.toBeNull();
|
|
330
|
+
});
|
|
285
331
|
});
|
|
286
332
|
|
|
287
333
|
|
|
288
334
|
describe('error handler', () => {
|
|
289
335
|
it ('forwards arguments from the bullmq error event to logger.error', async () => {
|
|
290
|
-
await queue.
|
|
336
|
+
await queue.getJobCounts();
|
|
291
337
|
const instance = bullmqMock.instances.queues[0];
|
|
292
338
|
const errorListener = bullmqMock.listenerFor(instance, 'error');
|
|
293
339
|
const err = new Error(TestHelpers.Faker.Text.randomString());
|
|
@@ -66,9 +66,32 @@ describe('Worker', () => {
|
|
|
66
66
|
|
|
67
67
|
expect(worker.concurrency).toEqual(value);
|
|
68
68
|
});
|
|
69
|
+
|
|
70
|
+
it ('takes the constructor option over the environment', () => {
|
|
71
|
+
process.env.WORKER_CONCURRENCY = '2';
|
|
72
|
+
const value = TestHelpers.Faker.Math.randomNumber(50);
|
|
73
|
+
const worker = new Worker('q', { concurrency: value });
|
|
74
|
+
|
|
75
|
+
expect(worker.concurrency).toEqual(value);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
describe('#rateLimit', () => {
|
|
81
|
+
it ('is null when the option is absent', () => {
|
|
82
|
+
expect(new Worker('q').rateLimit).toBeNull();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it ('returns the configured limit', () => {
|
|
86
|
+
const rateLimit = { max: 100, duration: 60000 };
|
|
87
|
+
|
|
88
|
+
expect(new Worker('q', { rateLimit }).rateLimit).toBe(rateLimit);
|
|
89
|
+
});
|
|
69
90
|
});
|
|
70
91
|
|
|
71
92
|
|
|
93
|
+
|
|
94
|
+
|
|
72
95
|
describe('#fullQueueName', () => {
|
|
73
96
|
it ('appends the current NODE_ENV', () => {
|
|
74
97
|
const name = TestHelpers.Faker.Text.randomString();
|
|
@@ -113,6 +136,17 @@ describe('Worker', () => {
|
|
|
113
136
|
},
|
|
114
137
|
});
|
|
115
138
|
});
|
|
139
|
+
|
|
140
|
+
it ('passes a configured rate limit to BullMQ as its limiter', () => {
|
|
141
|
+
const rateLimit = { max: 100, duration: 60000 };
|
|
142
|
+
const worker = new Worker('q', { rateLimit });
|
|
143
|
+
|
|
144
|
+
expect(worker.workerOptions.limiter).toBe(rateLimit);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it ('omits the limiter when no rate limit is configured', () => {
|
|
148
|
+
expect(new Worker('q').workerOptions).not.toHaveProperty('limiter');
|
|
149
|
+
});
|
|
116
150
|
});
|
|
117
151
|
|
|
118
152
|
|
|
@@ -147,6 +181,52 @@ describe('Worker', () => {
|
|
|
147
181
|
const events = bullWorker.on.mock.calls.map(([event]) => event);
|
|
148
182
|
expect(events).toEqual(expect.arrayContaining(['completed', 'progress', 'failed', 'error']));
|
|
149
183
|
});
|
|
184
|
+
|
|
185
|
+
it ('applies a configured maximum concurrency to the queue', async () => {
|
|
186
|
+
const maximumConcurrency = TestHelpers.Faker.Math.randomNumber(20);
|
|
187
|
+
const worker = buildWorker(Worker, 'queueX', { maximumConcurrency });
|
|
188
|
+
|
|
189
|
+
await worker.listen();
|
|
190
|
+
|
|
191
|
+
expect(worker.queue.setMaximumConcurrency).toHaveBeenCalledWith(maximumConcurrency);
|
|
192
|
+
expect(worker.queue.removeMaximumConcurrency).not.toHaveBeenCalled();
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it ('removes the queue maximum concurrency when none is configured', async () => {
|
|
196
|
+
const worker = buildWorker(Worker, 'queueX');
|
|
197
|
+
|
|
198
|
+
await worker.listen();
|
|
199
|
+
|
|
200
|
+
expect(worker.queue.removeMaximumConcurrency).toHaveBeenCalled();
|
|
201
|
+
expect(worker.queue.setMaximumConcurrency).not.toHaveBeenCalled();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it ('logs the maximum concurrency and the rate limit it applied', async () => {
|
|
205
|
+
const worker = buildWorker(Worker, 'queueX', {
|
|
206
|
+
maximumConcurrency: 8,
|
|
207
|
+
rateLimit: { max: 100, duration: 60000 },
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
await worker.listen();
|
|
211
|
+
|
|
212
|
+
const info = worker.loggerLibrary.worker.info.mock.calls.map(([message]) => message);
|
|
213
|
+
expect(info).toEqual(expect.arrayContaining([
|
|
214
|
+
'Queue queueX-test maximum concurrency is 8',
|
|
215
|
+
'Queue queueX-test rate limit is 100 per 60000ms',
|
|
216
|
+
]));
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
describe('#close', () => {
|
|
222
|
+
it ('closes the BullMQ worker', async () => {
|
|
223
|
+
const worker = buildWorker(Worker);
|
|
224
|
+
await worker.listen();
|
|
225
|
+
|
|
226
|
+
await worker.close();
|
|
227
|
+
|
|
228
|
+
expect(bullmqMock.instances.workers[0].close).toHaveBeenCalled();
|
|
229
|
+
});
|
|
150
230
|
});
|
|
151
231
|
|
|
152
232
|
|
|
@@ -177,6 +257,46 @@ describe('Worker', () => {
|
|
|
177
257
|
expect(afterCount).toEqual(beforeCount);
|
|
178
258
|
});
|
|
179
259
|
|
|
260
|
+
|
|
261
|
+
it ('falls back to the job that opted in as default for its queue', async () => {
|
|
262
|
+
const worker = buildWorker(Worker, 'beta');
|
|
263
|
+
await worker.listen();
|
|
264
|
+
const processor = bullmqMock.instances.workers[0].processor;
|
|
265
|
+
|
|
266
|
+
// The name bull writes for an unnamed job, which matches no registered job class.
|
|
267
|
+
const legacy = buildFakeBullJob({ name: '__default__', data: 'raw payload' });
|
|
268
|
+
|
|
269
|
+
await expect(processor(legacy)).resolves.toBeGreaterThanOrEqual(0);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
// The job scan is memoized for the process, but the default job is per worker. Assigning it
|
|
274
|
+
// inside the memo left every worker after the first without one, so only the first queue in
|
|
275
|
+
// a process could consume a legacy payload.
|
|
276
|
+
it ('gives a second worker in the process its own default job', async () => {
|
|
277
|
+
const first = buildWorker(Worker, 'alpha');
|
|
278
|
+
await first.listen();
|
|
279
|
+
|
|
280
|
+
const second = buildWorker(Worker, 'beta');
|
|
281
|
+
await second.listen();
|
|
282
|
+
const processor = bullmqMock.instances.workers[1].processor;
|
|
283
|
+
|
|
284
|
+
const legacy = buildFakeBullJob({ name: '__default__', data: 'raw payload' });
|
|
285
|
+
|
|
286
|
+
await expect(processor(legacy)).resolves.toBeGreaterThanOrEqual(0);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
it ('leaves a worker whose queue has no default job rejecting unknown names', async () => {
|
|
291
|
+
const worker = buildWorker(Worker, 'alpha');
|
|
292
|
+
await worker.listen();
|
|
293
|
+
const processor = bullmqMock.instances.workers[0].processor;
|
|
294
|
+
|
|
295
|
+
const legacy = buildFakeBullJob({ name: '__default__', data: 'raw payload' });
|
|
296
|
+
|
|
297
|
+
await expect(processor(legacy)).rejects.toEqual('Unknown Job: __default__');
|
|
298
|
+
});
|
|
299
|
+
|
|
180
300
|
it ('logs each registered job via verbose when NODE_ENV is not test', async () => {
|
|
181
301
|
process.env.NODE_ENV = 'development';
|
|
182
302
|
const worker = buildWorker(Worker);
|
|
@@ -215,12 +335,29 @@ describe('Worker', () => {
|
|
|
215
335
|
const args = [TestHelpers.Faker.Text.randomString()];
|
|
216
336
|
const id = TestHelpers.Faker.Text.randomString();
|
|
217
337
|
const updateProgress = jest.fn();
|
|
218
|
-
const fakeBull = buildFakeBullJob({ name: 'TestJobA', data: {
|
|
338
|
+
const fakeBull = buildFakeBullJob({ name: 'TestJobA', id, data: { args }, updateProgress });
|
|
219
339
|
|
|
220
340
|
await processor(fakeBull);
|
|
221
341
|
|
|
222
342
|
expect(runSpy).toHaveBeenCalledTimes(1);
|
|
223
|
-
expect(runSpy).toHaveBeenCalledWith(id, args, expect.any(Function));
|
|
343
|
+
expect(runSpy).toHaveBeenCalledWith(id, args, expect.any(Function), expect.any(Object));
|
|
344
|
+
runSpy.mockRestore();
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it ('tells the job which attempt it is on', async () => {
|
|
348
|
+
const worker = buildWorker(Worker);
|
|
349
|
+
const processor = await listenAndGetProcessor(worker);
|
|
350
|
+
const runSpy = jest.spyOn(TestJobA.prototype, '_run').mockResolvedValue(undefined);
|
|
351
|
+
const fakeBull = buildFakeBullJob({ attemptsMade: 2, opts: { attempts: 3 } });
|
|
352
|
+
|
|
353
|
+
await processor(fakeBull);
|
|
354
|
+
|
|
355
|
+
expect(runSpy).toHaveBeenCalledWith(
|
|
356
|
+
expect.anything(),
|
|
357
|
+
expect.anything(),
|
|
358
|
+
expect.any(Function),
|
|
359
|
+
{ attempt: 3, maxAttempts: 3 },
|
|
360
|
+
);
|
|
224
361
|
runSpy.mockRestore();
|
|
225
362
|
});
|
|
226
363
|
|
|
@@ -296,7 +433,8 @@ describe('Worker', () => {
|
|
|
296
433
|
const id = TestHelpers.Faker.Text.randomString();
|
|
297
434
|
const fakeBull = buildFakeBullJob({
|
|
298
435
|
name: 'TestJobA',
|
|
299
|
-
|
|
436
|
+
id,
|
|
437
|
+
data: { args: [] },
|
|
300
438
|
attemptsMade: 1,
|
|
301
439
|
opts: { attempts: 5 },
|
|
302
440
|
});
|
|
@@ -345,7 +483,8 @@ describe('Worker', () => {
|
|
|
345
483
|
const id = TestHelpers.Faker.Text.randomString();
|
|
346
484
|
const fakeBull = buildFakeBullJob({
|
|
347
485
|
name: 'TestJobA',
|
|
348
|
-
|
|
486
|
+
id,
|
|
487
|
+
data: { args: [] },
|
|
349
488
|
attemptsMade: 0,
|
|
350
489
|
opts: { attempts: 3 },
|
|
351
490
|
});
|
|
@@ -401,19 +540,6 @@ describe('Worker', () => {
|
|
|
401
540
|
expect(worker.apm.recordCustomEvent).not.toHaveBeenCalledWith('JobCompleted', expect.anything());
|
|
402
541
|
expect(worker.apm.recordMetric).not.toHaveBeenCalled();
|
|
403
542
|
});
|
|
404
|
-
|
|
405
|
-
/*
|
|
406
|
-
it ('registers a new logger scope for the job name', async () => {
|
|
407
|
-
const worker = buildWorker(Worker);
|
|
408
|
-
const processor = await listenAndGetProcessor(worker);
|
|
409
|
-
jest.spyOn(TestJobA.prototype, '_run').mockResolvedValue(undefined);
|
|
410
|
-
const fakeBull = buildFakeBullJob({ name: 'TestJobA' });
|
|
411
|
-
|
|
412
|
-
await processor(fakeBull);
|
|
413
|
-
|
|
414
|
-
expect(logger.addScope).toHaveBeenCalledWith('TestJobA');
|
|
415
|
-
});
|
|
416
|
-
*/
|
|
417
543
|
});
|
|
418
544
|
|
|
419
545
|
|
|
@@ -431,7 +557,7 @@ describe('Worker', () => {
|
|
|
431
557
|
const id = TestHelpers.Faker.Text.randomString();
|
|
432
558
|
const duration = TestHelpers.Faker.Math.randomNumber(10000);
|
|
433
559
|
|
|
434
|
-
handler({ name: 'TestJobA',
|
|
560
|
+
handler({ name: 'TestJobA', id }, duration);
|
|
435
561
|
|
|
436
562
|
const child = worker.loggerLibrary.worker.child.mock.results[0].value;
|
|
437
563
|
expect(child.debug).toHaveBeenCalledWith(`Completed in ${duration}ms`);
|
|
@@ -444,7 +570,7 @@ describe('Worker', () => {
|
|
|
444
570
|
const id = TestHelpers.Faker.Text.randomString();
|
|
445
571
|
const payload = { step: TestHelpers.Faker.Text.randomString() };
|
|
446
572
|
|
|
447
|
-
handler({ name: 'TestJobA',
|
|
573
|
+
handler({ name: 'TestJobA', id }, payload);
|
|
448
574
|
|
|
449
575
|
const child = worker.loggerLibrary.worker.child.mock.results[0].value;
|
|
450
576
|
expect(child.debug).toHaveBeenCalledWith(payload);
|
|
@@ -456,7 +582,7 @@ describe('Worker', () => {
|
|
|
456
582
|
const handler = bullmqMock.listenerFor(bullWorker, 'failed');
|
|
457
583
|
const job = {
|
|
458
584
|
name: 'TestJobA',
|
|
459
|
-
|
|
585
|
+
id: 't1',
|
|
460
586
|
attemptsMade: 1,
|
|
461
587
|
opts: { attempts: 3 },
|
|
462
588
|
};
|
|
@@ -473,7 +599,7 @@ describe('Worker', () => {
|
|
|
473
599
|
const handler = bullmqMock.listenerFor(bullWorker, 'failed');
|
|
474
600
|
const job = {
|
|
475
601
|
name: 'TestJobA',
|
|
476
|
-
|
|
602
|
+
id: 't1',
|
|
477
603
|
attemptsMade: 3,
|
|
478
604
|
opts: { attempts: 3 },
|
|
479
605
|
};
|
|
@@ -43,6 +43,69 @@ describe('Logger', () => {
|
|
|
43
43
|
|
|
44
44
|
expect(root[childScope].id).toEqual(id);
|
|
45
45
|
});
|
|
46
|
+
|
|
47
|
+
it ('returns the scoped logger it registered', () => {
|
|
48
|
+
const childScope = randomScope();
|
|
49
|
+
const root = new Logger(randomScope());
|
|
50
|
+
|
|
51
|
+
expect(root.addScope(childScope)).toBe(root[childScope]);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it ('returns the same logger when the scope is already registered', () => {
|
|
55
|
+
const childScope = randomScope();
|
|
56
|
+
const root = new Logger(randomScope());
|
|
57
|
+
|
|
58
|
+
expect(root.addScope(childScope)).toBe(root.addScope(childScope));
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it ('throws when the name is already taken by something that is not a logger', () => {
|
|
62
|
+
const root = new Logger(randomScope());
|
|
63
|
+
|
|
64
|
+
expect(() => root.addScope('child')).toThrow(/child/);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
describe('#scope', () => {
|
|
70
|
+
it ('registers and returns a scope whose name is not lowercase', () => {
|
|
71
|
+
const root = new Logger(randomScope());
|
|
72
|
+
const name = `S_${TestHelpers.Faker.Text.randomString()}`;
|
|
73
|
+
|
|
74
|
+
const scoped = root.scope(name);
|
|
75
|
+
|
|
76
|
+
expect(scoped.scopes).toContain(name.toLowerCase());
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it ('reaches the scope by the name it was given and by the lowercased one', () => {
|
|
80
|
+
const root = new Logger(randomScope());
|
|
81
|
+
const name = `S_${TestHelpers.Faker.Text.randomString()}`;
|
|
82
|
+
|
|
83
|
+
const scoped = root.scope(name);
|
|
84
|
+
|
|
85
|
+
expect(root[name]).toBe(scoped);
|
|
86
|
+
expect(root[name.toLowerCase()]).toBe(scoped);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it ('returns the same logger on every call', () => {
|
|
90
|
+
const root = new Logger(randomScope());
|
|
91
|
+
const name = `S_${TestHelpers.Faker.Text.randomString()}`;
|
|
92
|
+
|
|
93
|
+
expect(root.scope(name)).toBe(root.scope(name));
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it ('returns the existing scope when the same name arrives in another casing', () => {
|
|
97
|
+
const root = new Logger(randomScope());
|
|
98
|
+
const name = `S_${TestHelpers.Faker.Text.randomString()}`;
|
|
99
|
+
|
|
100
|
+
expect(root.scope(name.toUpperCase())).toBe(root.scope(name));
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it ('tags the scope lowercased whichever spelling registered it', () => {
|
|
104
|
+
const name = `S_${TestHelpers.Faker.Text.randomString()}`;
|
|
105
|
+
const root = new Logger(randomScope());
|
|
106
|
+
|
|
107
|
+
expect(root.scope(name).scopes).not.toContain(name);
|
|
108
|
+
});
|
|
46
109
|
});
|
|
47
110
|
|
|
48
111
|
|
|
@@ -83,5 +146,55 @@ describe('Logger', () => {
|
|
|
83
146
|
|
|
84
147
|
expect(result.message).toEqual(`[TS][INFO][${scopeA.toUpperCase()}][${scopeB.toUpperCase()}] hello`);
|
|
85
148
|
});
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
// Winston hands a logged Error through as `info`, so assigning onto it rewrote the caller's
|
|
152
|
+
// error with the whole formatted line.
|
|
153
|
+
it ('leaves the object it was given untouched', () => {
|
|
154
|
+
const logger = new Logger(randomScope());
|
|
155
|
+
const info = { level: 'info', timestamp: 'TS', message: 'hello' };
|
|
156
|
+
|
|
157
|
+
logger.messageFormatter(info);
|
|
158
|
+
|
|
159
|
+
expect(info.message).toEqual('hello');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it ('does not rewrite the message of an error it formats', () => {
|
|
163
|
+
const logger = new Logger(randomScope());
|
|
164
|
+
const error = new Error('boom');
|
|
165
|
+
error.level = 'error';
|
|
166
|
+
error.timestamp = 'TS';
|
|
167
|
+
|
|
168
|
+
const result = logger.messageFormatter(error);
|
|
169
|
+
|
|
170
|
+
expect(error.message).toEqual('boom');
|
|
171
|
+
expect(result.message).toContain('boom');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// Formatting the same error twice used to compound the prefix, because the first call had
|
|
175
|
+
// already written its output back onto the error.
|
|
176
|
+
it ('formats the same error the same way every time', () => {
|
|
177
|
+
const logger = new Logger(randomScope());
|
|
178
|
+
const error = new Error('boom');
|
|
179
|
+
error.level = 'error';
|
|
180
|
+
error.timestamp = 'TS';
|
|
181
|
+
|
|
182
|
+
const first = logger.messageFormatter(error);
|
|
183
|
+
const second = logger.messageFormatter(error);
|
|
184
|
+
|
|
185
|
+
expect(second.message).toEqual(first.message);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// The transports read these, so a copy that dropped them would stop formatting output.
|
|
189
|
+
it ("carries winston's own symbol properties onto the copy", () => {
|
|
190
|
+
const logger = new Logger(randomScope());
|
|
191
|
+
const marker = Symbol.for('level');
|
|
192
|
+
const info = { level: 'info', timestamp: 'TS', message: 'hello' };
|
|
193
|
+
info[marker] = 'info';
|
|
194
|
+
|
|
195
|
+
const result = logger.messageFormatter(info);
|
|
196
|
+
|
|
197
|
+
expect(result[marker]).toEqual('info');
|
|
198
|
+
});
|
|
86
199
|
});
|
|
87
200
|
});
|
|
@@ -751,6 +751,65 @@ describe('VidaServerController', () => {
|
|
|
751
751
|
});
|
|
752
752
|
|
|
753
753
|
|
|
754
|
+
describe('#renderBinary', () => {
|
|
755
|
+
let request;
|
|
756
|
+
let response;
|
|
757
|
+
let controller;
|
|
758
|
+
|
|
759
|
+
|
|
760
|
+
beforeEach(() => {
|
|
761
|
+
request = {};
|
|
762
|
+
response = {
|
|
763
|
+
setHeader: jest.fn(),
|
|
764
|
+
attachment: jest.fn(),
|
|
765
|
+
send: jest.fn(),
|
|
766
|
+
};
|
|
767
|
+
controller = new FooController(request, response);
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
|
|
771
|
+
it ('sends the body untouched with the given content type', () => {
|
|
772
|
+
const body = Buffer.from([0x00, 0x01, 0x02]);
|
|
773
|
+
|
|
774
|
+
controller.renderBinary(body, { contentType: 'audio/wav' });
|
|
775
|
+
|
|
776
|
+
expect(response.setHeader).toHaveBeenCalledWith('Content-Type', 'audio/wav');
|
|
777
|
+
expect(response.send).toHaveBeenCalledWith(body);
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
it ('marks the response rendered', () => {
|
|
782
|
+
controller.renderBinary(Buffer.from('x'), { contentType: 'audio/wav' });
|
|
783
|
+
|
|
784
|
+
expect(controller.rendered).toBe(true);
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
|
|
788
|
+
it ('sets a download filename when one is given', () => {
|
|
789
|
+
controller.renderBinary(Buffer.from('x'), { contentType: 'audio/wav', filename: 'call.wav' });
|
|
790
|
+
|
|
791
|
+
expect(response.attachment).toHaveBeenCalledWith('call.wav');
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
// Without a filename the body is displayed rather than downloaded.
|
|
796
|
+
it ('sets no filename when none is given', () => {
|
|
797
|
+
controller.renderBinary(Buffer.from('x'), { contentType: 'audio/wav' });
|
|
798
|
+
|
|
799
|
+
expect(response.attachment).not.toHaveBeenCalled();
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
|
|
803
|
+
it ('is a no-op once something has already been rendered', () => {
|
|
804
|
+
controller.markRendered();
|
|
805
|
+
|
|
806
|
+
controller.renderBinary(Buffer.from('x'), { contentType: 'audio/wav' });
|
|
807
|
+
|
|
808
|
+
expect(response.send).not.toHaveBeenCalled();
|
|
809
|
+
});
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
|
|
754
813
|
describe('#renderFile', () => {
|
|
755
814
|
let request;
|
|
756
815
|
let response;
|