@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.
Files changed (55) hide show
  1. package/AGENTS.md +13 -0
  2. package/README.md +1 -0
  3. package/config/newrelic-config.js +118 -0
  4. package/index.js +9 -5
  5. package/lib/activeRecord/baseRecord.js +49 -3
  6. package/lib/apm/agent.js +22 -0
  7. package/lib/apm/index.js +54 -0
  8. package/lib/apm/utils.js +43 -0
  9. package/lib/http/client.js +16 -2
  10. package/lib/jobQueue/README.md +63 -0
  11. package/lib/jobQueue/abstractJob.js +103 -0
  12. package/lib/jobQueue/abstractJobComponent.js +37 -0
  13. package/lib/jobQueue/index.js +10 -0
  14. package/lib/jobQueue/jobImporter.js +19 -0
  15. package/lib/jobQueue/queue.js +161 -0
  16. package/lib/jobQueue/worker.js +273 -0
  17. package/lib/logger/README.md +33 -2
  18. package/lib/logger/index.js +117 -20
  19. package/lib/logger/serverMiddleware.js +41 -0
  20. package/lib/redis/redisClient.js +32 -24
  21. package/lib/server/controllerImporter.js +9 -47
  22. package/lib/server/server.js +34 -71
  23. package/lib/server/serverController.js +215 -23
  24. package/lib/utils/abstractAutoImporter.js +64 -0
  25. package/package.json +8 -6
  26. package/test/activeRecord/baseRecord.test.js +129 -0
  27. package/test/activeRecord/db/connection.test.js +4 -1
  28. package/test/activeRecord/db/connectionConfiguration.test.js +9 -3
  29. package/test/activeRecord/db/schema.test.js +217 -0
  30. package/test/activeRecord/helpers/baseRecordMocks.js +6 -4
  31. package/test/activeRecord/helpers/connection.js +0 -3
  32. package/test/activeRecord/helpers/connectionConfiguration.js +0 -8
  33. package/test/activeRecord/utils.test.js +72 -0
  34. package/test/apm/agent.test.js +56 -0
  35. package/test/apm/utils.test.js +121 -0
  36. package/test/helpers/env.js +33 -0
  37. package/test/http/client.test.js +3 -3
  38. package/test/jobQueue/abstractJob.test.js +307 -0
  39. package/test/jobQueue/abstractJobComponent.test.js +110 -0
  40. package/test/jobQueue/helpers/abstractJob.js +26 -0
  41. package/test/jobQueue/helpers/apmMock.js +20 -0
  42. package/test/jobQueue/helpers/bullmqMock.js +67 -0
  43. package/test/jobQueue/helpers/env.js +29 -0
  44. package/test/jobQueue/helpers/fixtureJobs/notAJob.js +8 -0
  45. package/test/jobQueue/helpers/fixtureJobs/testJobA.js +14 -0
  46. package/test/jobQueue/helpers/fixtureJobs/testJobB.js +14 -0
  47. package/test/jobQueue/helpers/loggerMock.js +43 -0
  48. package/test/jobQueue/helpers/worker.js +38 -0
  49. package/test/jobQueue/queue.test.js +320 -0
  50. package/test/jobQueue/worker.test.js +526 -0
  51. package/test/logger/index.test.js +61 -0
  52. package/test/logger/serverMiddleware.test.js +72 -0
  53. package/test/server/apiDocsGenerator.test.js +38 -0
  54. package/test/server/server.test.js +186 -4
  55. package/test/server/serverController.test.js +407 -5
@@ -0,0 +1,217 @@
1
+ jest.mock('../../../lib/activeRecord/db/importSchema', () => jest.fn());
2
+
3
+ jest.mock('../../../lib/activeRecord/db/connection', () => {
4
+ const clearConnectionsCache = jest.fn();
5
+ const allTables = jest.fn();
6
+ function Connection() {
7
+ this.queryInterface = { allTables };
8
+ }
9
+ Connection.clearConnectionsCache = clearConnectionsCache;
10
+ Connection.__allTables = allTables;
11
+ return { Connection };
12
+ });
13
+
14
+ jest.mock('../../../lib/activeRecord/db/connectionConfiguration', () => ({
15
+ ConnectionConfiguration: { configuredDatabaseIds: jest.fn() }
16
+ }));
17
+
18
+ jest.mock('../../../lib/activeRecord/utils', () => {
19
+ const actual = jest.requireActual('../../../lib/activeRecord/utils');
20
+ return { ...actual, ensureSchemasDirectory: jest.fn() };
21
+ });
22
+
23
+ const fs = require('node:fs');
24
+ const importSchema = require('../../../lib/activeRecord/db/importSchema');
25
+ const { Connection } = require('../../../lib/activeRecord/db/connection');
26
+ const { ConnectionConfiguration } = require('../../../lib/activeRecord/db/connectionConfiguration');
27
+ const utils = require('../../../lib/activeRecord/utils');
28
+ const { generateActiveRecordSchemaFiles, getActiveRecordSchema } = require('../../../lib/activeRecord/db/schema');
29
+ const { Sequelize } = require('sequelize');
30
+
31
+ afterEach(() => {
32
+ jest.clearAllMocks();
33
+ });
34
+
35
+
36
+ describe('activeRecord/db/schema', () => {
37
+ describe('.getActiveRecordSchema', () => {
38
+ it ('throws when no schema is found for the table', () => {
39
+ importSchema.mockImplementation(() => undefined);
40
+ expect(() => getActiveRecordSchema('users', 'main', 'postgres'))
41
+ .toThrow('No schema found for table main.users');
42
+ });
43
+
44
+
45
+ describe('type mapping (cleanSchema → dataTypeForColumn)', () => {
46
+ it ('maps INTEGER columns to Sequelize.DataTypes.INTEGER', () => {
47
+ importSchema.mockImplementation(() => ({
48
+ id: { type: 'INTEGER', primaryKey: true }
49
+ }));
50
+ const schema = getActiveRecordSchema('t', 'main', 'postgres');
51
+ expect(schema.id.type).toBe(Sequelize.DataTypes.INTEGER);
52
+ });
53
+
54
+ it ('maps BOOLEAN columns to Sequelize.DataTypes.BOOLEAN', () => {
55
+ importSchema.mockImplementation(() => ({
56
+ flag: { type: 'BOOLEAN' }
57
+ }));
58
+ const schema = getActiveRecordSchema('t', 'main', 'postgres');
59
+ expect(schema.flag.type).toBe(Sequelize.DataTypes.BOOLEAN);
60
+ });
61
+
62
+ it ('strips parameterized type modifiers before lookup', () => {
63
+ importSchema.mockImplementation(() => ({
64
+ name: { type: 'INTEGER(10)' }
65
+ }));
66
+ const schema = getActiveRecordSchema('t', 'main', 'postgres');
67
+ expect(schema.name.type).toBe(Sequelize.DataTypes.INTEGER);
68
+ });
69
+
70
+ it ('falls back to STRING when type contains CHAR but is not otherwise matched', () => {
71
+ importSchema.mockImplementation(() => ({
72
+ code: { type: 'NCHARFOO' }
73
+ }));
74
+ const schema = getActiveRecordSchema('t', 'main', 'postgres');
75
+ expect(schema.code.type).toBe(Sequelize.DataTypes.STRING);
76
+ });
77
+
78
+ it ('leaves the original type when no match is found', () => {
79
+ importSchema.mockImplementation(() => ({
80
+ weird: { type: 'COMPLETELY_UNKNOWN_TYPE' }
81
+ }));
82
+ const schema = getActiveRecordSchema('t', 'main', 'postgres');
83
+ expect(schema.weird.type).toBe('COMPLETELY_UNKNOWN_TYPE');
84
+ });
85
+ });
86
+
87
+
88
+ describe('autoIncrement resolution', () => {
89
+ it ('sets autoIncrement and removes defaultValue + allowNull when default is a PG sequence', () => {
90
+ importSchema.mockImplementation(() => ({
91
+ id: {
92
+ type: 'INTEGER',
93
+ primaryKey: true,
94
+ defaultValue: 'nextval(users_id_seq::regclass)',
95
+ allowNull: false
96
+ }
97
+ }));
98
+ const schema = getActiveRecordSchema('t', 'main', 'postgres');
99
+ expect(schema.id.autoIncrement).toBe(true);
100
+ expect(schema.id).not.toHaveProperty('defaultValue');
101
+ expect(schema.id).not.toHaveProperty('allowNull');
102
+ });
103
+
104
+ it ('does not modify columns whose defaultValue is not a sequence', () => {
105
+ importSchema.mockImplementation(() => ({
106
+ name: {
107
+ type: 'INTEGER',
108
+ defaultValue: 'literal',
109
+ allowNull: true
110
+ }
111
+ }));
112
+ const schema = getActiveRecordSchema('t', 'main', 'postgres');
113
+ expect(schema.name.autoIncrement).toBeUndefined();
114
+ expect(schema.name.defaultValue).toBe('literal');
115
+ expect(schema.name.allowNull).toBe(true);
116
+ });
117
+
118
+ it ('does nothing when there is no defaultValue', () => {
119
+ importSchema.mockImplementation(() => ({
120
+ name: { type: 'INTEGER' }
121
+ }));
122
+ const schema = getActiveRecordSchema('t', 'main', 'postgres');
123
+ expect(schema.name.autoIncrement).toBeUndefined();
124
+ });
125
+ });
126
+
127
+
128
+ describe('UUID resolution', () => {
129
+ it ('sets UUIDV4 default on UUID primary keys whose type stays as the string "UUID"', () => {
130
+ importSchema.mockImplementation(() => ({
131
+ id: { type: 'UUID', primaryKey: true }
132
+ }));
133
+ const schema = getActiveRecordSchema('t', 'main', 'postgres');
134
+ // Whether dataTypeForColumn rewrote the type or not, behavior is observable:
135
+ // if it stayed as 'UUID', resolveUUIDColumns sets the default.
136
+ if (schema.id.type === 'UUID') {
137
+ expect(schema.id.defaultValue).toBe(Sequelize.UUIDV4);
138
+ }
139
+ });
140
+
141
+ it ('does not set a UUIDV4 default on UUID columns that are not primary keys', () => {
142
+ importSchema.mockImplementation(() => ({
143
+ ref: { type: 'UUID' }
144
+ }));
145
+ const schema = getActiveRecordSchema('t', 'main', 'postgres');
146
+ expect(schema.ref.defaultValue).toBeUndefined();
147
+ });
148
+
149
+ it ('does not set a UUIDV4 default on non-UUID primary keys', () => {
150
+ importSchema.mockImplementation(() => ({
151
+ id: { type: 'INTEGER', primaryKey: true }
152
+ }));
153
+ const schema = getActiveRecordSchema('t', 'main', 'postgres');
154
+ expect(schema.id.defaultValue).toBeUndefined();
155
+ });
156
+ });
157
+ });
158
+
159
+
160
+ describe('.generateActiveRecordSchemaFiles', () => {
161
+ let writeFileSpy;
162
+
163
+ beforeEach(() => {
164
+ writeFileSpy = jest.spyOn(fs, 'writeFile').mockImplementation((p, c, cb) => cb && cb(null));
165
+ });
166
+
167
+ afterEach(() => {
168
+ writeFileSpy.mockRestore();
169
+ });
170
+
171
+ it ('ensures the schemas directory exists before writing', async () => {
172
+ ConnectionConfiguration.configuredDatabaseIds.mockReturnValue([]);
173
+ await generateActiveRecordSchemaFiles();
174
+ expect(utils.ensureSchemasDirectory).toHaveBeenCalledTimes(1);
175
+ });
176
+
177
+ it ('writes one schema file per configured database id', async () => {
178
+ ConnectionConfiguration.configuredDatabaseIds.mockReturnValue(['main', 'analytics']);
179
+ Connection.__allTables.mockResolvedValue({ users: { id: { type: 'INTEGER' } } });
180
+
181
+ await generateActiveRecordSchemaFiles();
182
+ expect(Connection.clearConnectionsCache).toHaveBeenCalledTimes(2);
183
+ expect(writeFileSpy).toHaveBeenCalledTimes(2);
184
+
185
+ const writtenPaths = writeFileSpy.mock.calls.map(c => c[0]);
186
+ expect(writtenPaths[0]).toEqual(`${utils.schemasDirectory}/main.js`);
187
+ expect(writtenPaths[1]).toEqual(`${utils.schemasDirectory}/analytics.js`);
188
+
189
+ const writtenBody = writeFileSpy.mock.calls[0][1];
190
+ expect(writtenBody).toContain('module.exports = ');
191
+ expect(writtenBody).toContain('"users"');
192
+ });
193
+
194
+ it ('logs an error when fs.writeFile fails (does not reject)', async () => {
195
+ const { logger } = require('../../../lib/logger');
196
+ const errSpy = jest.spyOn(logger, 'error').mockImplementation(() => {});
197
+ ConnectionConfiguration.configuredDatabaseIds.mockReturnValue(['main']);
198
+ Connection.__allTables.mockResolvedValue({});
199
+ writeFileSpy.mockImplementation((p, c, cb) => cb(new Error('disk full')));
200
+
201
+ await generateActiveRecordSchemaFiles();
202
+ expect(errSpy).toHaveBeenCalledWith(expect.stringContaining('Unable to write new schema file'));
203
+ errSpy.mockRestore();
204
+ });
205
+
206
+ it ('logs success when fs.writeFile succeeds', async () => {
207
+ const { logger } = require('../../../lib/logger');
208
+ const infoSpy = jest.spyOn(logger, 'info').mockImplementation(() => {});
209
+ ConnectionConfiguration.configuredDatabaseIds.mockReturnValue(['main']);
210
+ Connection.__allTables.mockResolvedValue({});
211
+
212
+ await generateActiveRecordSchemaFiles();
213
+ expect(infoSpy).toHaveBeenCalledWith(expect.stringContaining('New schemas file written'));
214
+ infoSpy.mockRestore();
215
+ });
216
+ });
217
+ });
@@ -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: jest.fn(),
45
- mGet: jest.fn(),
46
- mSet: jest.fn(),
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,72 @@
1
+ const fs = require('node:fs');
2
+ const utils = require('../../lib/activeRecord/utils');
3
+
4
+
5
+ describe('activeRecord/utils', () => {
6
+ describe('.toCamelCase', () => {
7
+ it ('converts kebab-case to camelCase', () => {
8
+ expect(utils.toCamelCase('foo-bar-baz')).toEqual('fooBarBaz');
9
+ });
10
+
11
+ it ('converts snake_case to camelCase', () => {
12
+ expect(utils.toCamelCase('foo_bar_baz')).toEqual('fooBarBaz');
13
+ });
14
+
15
+ it ('converts space-separated words to camelCase', () => {
16
+ expect(utils.toCamelCase('foo bar baz')).toEqual('fooBarBaz');
17
+ });
18
+
19
+ it ('handles mixed separators', () => {
20
+ expect(utils.toCamelCase('foo-bar_baz qux')).toEqual('fooBarBazQux');
21
+ });
22
+
23
+ it ('lowercases the first token even when capitalized', () => {
24
+ expect(utils.toCamelCase('Foo-Bar')).toEqual('fooBar');
25
+ });
26
+
27
+ it ('returns a single token lowercased', () => {
28
+ expect(utils.toCamelCase('FOO')).toEqual('foo');
29
+ });
30
+
31
+ it ('returns an empty string when given an empty string', () => {
32
+ expect(utils.toCamelCase('')).toEqual('');
33
+ });
34
+ });
35
+
36
+
37
+ describe('directory ensurers', () => {
38
+ let mkdirSpy;
39
+
40
+ beforeEach(() => {
41
+ mkdirSpy = jest.spyOn(fs, 'mkdirSync').mockImplementation(() => undefined);
42
+ });
43
+
44
+ afterEach(() => {
45
+ mkdirSpy.mockRestore();
46
+ });
47
+
48
+ describe('.ensureConfigDirectory', () => {
49
+ it ('creates the config directory recursively', () => {
50
+ utils.ensureConfigDirectory();
51
+ expect(mkdirSpy).toHaveBeenCalledTimes(1);
52
+ expect(mkdirSpy).toHaveBeenCalledWith(utils.configDirectory, { recursive: true });
53
+ });
54
+ });
55
+
56
+ describe('.ensureMigrationsDirectory', () => {
57
+ it ('creates the migrations directory recursively', () => {
58
+ utils.ensureMigrationsDirectory();
59
+ expect(mkdirSpy).toHaveBeenCalledTimes(1);
60
+ expect(mkdirSpy).toHaveBeenCalledWith(utils.migrationsDirectory, { recursive: true });
61
+ });
62
+ });
63
+
64
+ describe('.ensureSchemasDirectory', () => {
65
+ it ('creates the schemas directory recursively', () => {
66
+ utils.ensureSchemasDirectory();
67
+ expect(mkdirSpy).toHaveBeenCalledTimes(1);
68
+ expect(mkdirSpy).toHaveBeenCalledWith(utils.schemasDirectory, { recursive: true });
69
+ });
70
+ });
71
+ });
72
+ });
@@ -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
+ }
@@ -10,7 +10,7 @@ afterEach(() => {
10
10
 
11
11
  beforeEach(() => {
12
12
  Helpers.mockFetchSuccess();
13
- logger.info = jest.fn();
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.info).toHaveBeenCalledTimes(1);
64
- expect(logger.info).toHaveBeenCalledWith(`API CALL: ${httpMethod} ${Helpers.apiUrl}`);
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) {