@vida-global/core 1.4.6 → 2.0.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/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 +1 -1
- package/lib/logger/index.js +33 -15
- package/lib/logger/serverMiddleware.js +14 -12
- package/lib/redis/redisClient.js +32 -24
- package/lib/server/controllerImporter.js +9 -47
- package/lib/server/server.js +30 -13
- package/lib/server/serverController.js +100 -23
- package/lib/utils/abstractAutoImporter.js +64 -0
- package/package.json +4 -2
- 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/helpers/baseRecordMocks.js +6 -4
- package/test/activeRecord/helpers/connection.js +0 -3
- package/test/activeRecord/helpers/connectionConfiguration.js +0 -8
- 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 +6 -7
- package/test/server/serverController.test.js +226 -10
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
const { ConnectionConfiguration } = require('../../../lib/activeRecord/db/connectionConfiguration');
|
|
2
2
|
const Helpers = require('../helpers/connectionConfiguration');
|
|
3
|
+
const { snapshotEnv } = require('../../helpers/env');
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
let restoreEnv;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
restoreEnv = snapshotEnv(['DB_POOL_MIN', 'DB_POOL_MAX', 'NODE_ENV']);
|
|
10
|
+
});
|
|
3
11
|
|
|
4
12
|
|
|
5
13
|
afterEach(() => {
|
|
6
14
|
Helpers.fetchConfigsSpy.mockReset();
|
|
7
|
-
|
|
8
|
-
process.env.DB_POOL_MAX = Helpers.originalEnv.dbPoolMax;
|
|
9
|
-
process.env.NODE_ENV = Helpers.originalEnv.nodeEnv;
|
|
15
|
+
restoreEnv();
|
|
10
16
|
});
|
|
11
17
|
|
|
12
18
|
|
|
@@ -35,15 +35,17 @@ class MockSequelize {
|
|
|
35
35
|
static DataTypes = {
|
|
36
36
|
[dataTypeKey1]: {types: {postgres: [dataTypeValue1]}, key: dataTypeKey1},
|
|
37
37
|
[dataTypeKey2]: {types: {postgres: [dataTypeValue2]}, key: dataTypeKey2},
|
|
38
|
-
postgres: {}
|
|
38
|
+
postgres: {},
|
|
39
|
+
VIRTUAL: {key: 'VIRTUAL'},
|
|
39
40
|
}
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
|
|
43
44
|
const redisClient = {
|
|
44
|
-
del:
|
|
45
|
-
mGet:
|
|
46
|
-
mSet:
|
|
45
|
+
del: jest.fn(),
|
|
46
|
+
mGet: jest.fn(),
|
|
47
|
+
mSet: jest.fn(),
|
|
48
|
+
connect: jest.fn(),
|
|
47
49
|
}
|
|
48
50
|
const mockRedisClientFactory = () => redisClient;
|
|
49
51
|
|
|
@@ -6,8 +6,6 @@ const secondaryConfig = Fixtures.buildPostgresConfig();
|
|
|
6
6
|
const sslConfig = Fixtures.buildPostgresConfig({ ssl: true });
|
|
7
7
|
const sqliteConfig = { dialect: 'sqlite' };
|
|
8
8
|
|
|
9
|
-
const originalNodeEnv = process.env.NODE_ENV;
|
|
10
|
-
|
|
11
9
|
|
|
12
10
|
function buildConfigs() {
|
|
13
11
|
return {
|
|
@@ -23,6 +21,5 @@ module.exports = {
|
|
|
23
21
|
...Fixtures,
|
|
24
22
|
buildConfigs,
|
|
25
23
|
defaultConfig,
|
|
26
|
-
originalNodeEnv,
|
|
27
24
|
secondaryConfig,
|
|
28
25
|
};
|
|
@@ -7,13 +7,6 @@ const productionConfig = Fixtures.buildPostgresConfig();
|
|
|
7
7
|
const sqliteConfig = { dialect: 'sqlite' };
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
const originalEnv = {
|
|
11
|
-
dbPoolMin: process.env.DB_POOL_MIN,
|
|
12
|
-
dbPoolMax: process.env.DB_POOL_MAX,
|
|
13
|
-
nodeEnv: process.env.NODE_ENV,
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
|
|
17
10
|
const fetchConfigsSpy = jest.spyOn(ConnectionConfiguration, '_fetchAllConfigs');
|
|
18
11
|
function mockConfigs(configs) {
|
|
19
12
|
fetchConfigsSpy.mockImplementation(() => configs);
|
|
@@ -24,7 +17,6 @@ module.exports = {
|
|
|
24
17
|
fetchConfigsSpy,
|
|
25
18
|
...Fixtures,
|
|
26
19
|
mockConfigs,
|
|
27
|
-
originalEnv,
|
|
28
20
|
productionConfig,
|
|
29
21
|
sqliteConfig,
|
|
30
22
|
testConfig,
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const ORIGINAL_ENV = process.env;
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
beforeEach(() => {
|
|
5
|
+
jest.resetModules();
|
|
6
|
+
process.env = { ...ORIGINAL_ENV, NODE_ENV: 'test' };
|
|
7
|
+
delete process.env.NEW_RELIC_LICENSE_KEY;
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
process.env = ORIGINAL_ENV;
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
describe('apm/agent', () => {
|
|
17
|
+
describe('stub client (NEW_RELIC_LICENSE_KEY unset)', () => {
|
|
18
|
+
it ('reports enabled=false', () => {
|
|
19
|
+
const { enabled } = require('../../lib/apm/agent');
|
|
20
|
+
|
|
21
|
+
expect(enabled).toEqual(false);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it ('exposes no-op stubs that accept any arguments', () => {
|
|
25
|
+
const { client } = require('../../lib/apm/agent');
|
|
26
|
+
|
|
27
|
+
expect(() => client.addCustomAttribute('k', 'v')).not.toThrow();
|
|
28
|
+
expect(() => client.noticeError(new Error('x'), {})).not.toThrow();
|
|
29
|
+
expect(() => client.recordCustomEvent('Type', { a: 1 })).not.toThrow();
|
|
30
|
+
expect(() => client.recordMetric('name', 1)).not.toThrow();
|
|
31
|
+
expect(() => client.setControllerName('C', 'a')).not.toThrow();
|
|
32
|
+
expect(() => client.setUserID(1)).not.toThrow();
|
|
33
|
+
expect(() => client.instrumentLoadedModule('pkg', {})).not.toThrow();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it ('startBackgroundTransaction invokes the handler and returns its result', () => {
|
|
37
|
+
const { client } = require('../../lib/apm/agent');
|
|
38
|
+
const handler = jest.fn(() => 'bg-result');
|
|
39
|
+
|
|
40
|
+
const result = client.startBackgroundTransaction('Job', 'queue', handler);
|
|
41
|
+
|
|
42
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
43
|
+
expect(result).toEqual('bg-result');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it ('startSegment invokes the handler and returns its result', () => {
|
|
47
|
+
const { client } = require('../../lib/apm/agent');
|
|
48
|
+
const handler = jest.fn(() => 'seg-result');
|
|
49
|
+
|
|
50
|
+
const result = client.startSegment('seg', true, handler);
|
|
51
|
+
|
|
52
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
53
|
+
expect(result).toEqual('seg-result');
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
jest.mock('../../lib/apm/agent', () => ({
|
|
2
|
+
enabled: false,
|
|
3
|
+
client: {
|
|
4
|
+
addCustomAttribute: jest.fn(),
|
|
5
|
+
noticeError: jest.fn(),
|
|
6
|
+
recordCustomEvent: jest.fn(),
|
|
7
|
+
recordMetric: jest.fn(),
|
|
8
|
+
setControllerName: jest.fn(),
|
|
9
|
+
setUserID: jest.fn(),
|
|
10
|
+
startBackgroundTransaction: jest.fn((name, group, handler) => handler()),
|
|
11
|
+
startSegment: jest.fn((name, record, handler) => handler()),
|
|
12
|
+
},
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
let utils;
|
|
17
|
+
let agent;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
jest.resetModules();
|
|
21
|
+
utils = require('../../lib/apm/utils');
|
|
22
|
+
agent = require('../../lib/apm/agent');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
jest.clearAllMocks();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
describe('apm/utils', () => {
|
|
32
|
+
describe('addCustomAttribute', () => {
|
|
33
|
+
it ('forwards key and value to the client', () => {
|
|
34
|
+
utils.addCustomAttribute('foo', 'bar');
|
|
35
|
+
|
|
36
|
+
expect(agent.client.addCustomAttribute).toHaveBeenCalledWith('foo', 'bar');
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
describe('reportError', () => {
|
|
42
|
+
it ('forwards the error and details to noticeError', () => {
|
|
43
|
+
const err = new Error('boom');
|
|
44
|
+
const details = { a: 1 };
|
|
45
|
+
|
|
46
|
+
utils.reportError(err, details);
|
|
47
|
+
|
|
48
|
+
expect(agent.client.noticeError).toHaveBeenCalledWith(err, details);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it ('defaults details to an empty object', () => {
|
|
52
|
+
const err = new Error('boom');
|
|
53
|
+
|
|
54
|
+
utils.reportError(err);
|
|
55
|
+
|
|
56
|
+
expect(agent.client.noticeError).toHaveBeenCalledWith(err, {});
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
describe('recordCustomEvent', () => {
|
|
62
|
+
it ('forwards eventType and attributes to the client', () => {
|
|
63
|
+
const attrs = { jobName: 'X' };
|
|
64
|
+
|
|
65
|
+
utils.recordCustomEvent('JobCompleted', attrs);
|
|
66
|
+
|
|
67
|
+
expect(agent.client.recordCustomEvent).toHaveBeenCalledWith('JobCompleted', attrs);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
describe('recordMetric', () => {
|
|
73
|
+
it ('forwards name and value to the client', () => {
|
|
74
|
+
utils.recordMetric('Custom/Worker/foo', 42);
|
|
75
|
+
|
|
76
|
+
expect(agent.client.recordMetric).toHaveBeenCalledWith('Custom/Worker/foo', 42);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
describe('setControllerName', () => {
|
|
82
|
+
it ('forwards controller and action to the client', () => {
|
|
83
|
+
utils.setControllerName('Users', 'getIndex');
|
|
84
|
+
|
|
85
|
+
expect(agent.client.setControllerName).toHaveBeenCalledWith('Users', 'getIndex');
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
describe('setUserId', () => {
|
|
91
|
+
it ('forwards id to the client (note the casing difference)', () => {
|
|
92
|
+
utils.setUserId(123);
|
|
93
|
+
|
|
94
|
+
expect(agent.client.setUserID).toHaveBeenCalledWith(123);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
describe('startBackgroundTransaction', () => {
|
|
100
|
+
it ('forwards name, group, and handler to the client and returns its result', () => {
|
|
101
|
+
const handler = jest.fn(() => 'result');
|
|
102
|
+
|
|
103
|
+
const result = utils.startBackgroundTransaction('Job', 'queue', handler);
|
|
104
|
+
|
|
105
|
+
expect(agent.client.startBackgroundTransaction).toHaveBeenCalledWith('Job', 'queue', handler);
|
|
106
|
+
expect(result).toEqual('result');
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
describe('startSegment', () => {
|
|
112
|
+
it ('forwards name, record flag, and handler to the client and returns its result', () => {
|
|
113
|
+
const handler = jest.fn(() => 'segment-result');
|
|
114
|
+
|
|
115
|
+
const result = utils.startSegment('seg', true, handler);
|
|
116
|
+
|
|
117
|
+
expect(agent.client.startSegment).toHaveBeenCalledWith('seg', true, handler);
|
|
118
|
+
expect(result).toEqual('segment-result');
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/***************************************************************************************************
|
|
2
|
+
* ENV SNAPSHOT
|
|
3
|
+
*
|
|
4
|
+
* Captures the current value of the given process.env keys and returns a restore() function that
|
|
5
|
+
* puts them back to their original state. Use in beforeEach/afterEach pairs whenever a test needs
|
|
6
|
+
* to mutate environment variables without leaking changes to other tests.
|
|
7
|
+
*
|
|
8
|
+
* const restoreEnv = snapshotEnv(['NODE_ENV', 'REDIS_BULL_HOST']);
|
|
9
|
+
* process.env.NODE_ENV = 'production';
|
|
10
|
+
* // ...test body...
|
|
11
|
+
* restoreEnv();
|
|
12
|
+
***************************************************************************************************/
|
|
13
|
+
function snapshotEnv(keys) {
|
|
14
|
+
const original = {};
|
|
15
|
+
for (const key of keys) {
|
|
16
|
+
original[key] = process.env[key];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return function restore() {
|
|
20
|
+
for (const key of keys) {
|
|
21
|
+
if (original[key] === undefined) {
|
|
22
|
+
delete process.env[key];
|
|
23
|
+
} else {
|
|
24
|
+
process.env[key] = original[key];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
module.exports = {
|
|
32
|
+
snapshotEnv,
|
|
33
|
+
}
|
package/test/http/client.test.js
CHANGED
|
@@ -10,7 +10,7 @@ afterEach(() => {
|
|
|
10
10
|
|
|
11
11
|
beforeEach(() => {
|
|
12
12
|
Helpers.mockFetchSuccess();
|
|
13
|
-
logger.
|
|
13
|
+
logger.http.debug = jest.fn();
|
|
14
14
|
});
|
|
15
15
|
|
|
16
16
|
|
|
@@ -60,8 +60,8 @@ describe('HttpClient', () => {
|
|
|
60
60
|
it ('logs API requests', async () => {
|
|
61
61
|
await client[methodName](Helpers.apiUrl);
|
|
62
62
|
|
|
63
|
-
expect(logger.
|
|
64
|
-
expect(logger.
|
|
63
|
+
expect(logger.http.debug).toHaveBeenCalledTimes(1);
|
|
64
|
+
expect(logger.http.debug).toHaveBeenCalledWith(`API CALL: ${httpMethod} ${Helpers.apiUrl}`);
|
|
65
65
|
});
|
|
66
66
|
|
|
67
67
|
if (behavior.supportsBody) {
|
|
@@ -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
|
+
});
|