@vida-global/core 2.4.4 → 2.5.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/index.js +2 -1
- package/lib/activeRecord/README.md +4 -2
- package/lib/activeRecord/db/migrationErrors.js +6 -0
- package/lib/activeRecord/db/migrator.js +27 -10
- package/lib/appDependencies/index.js +3 -2
- package/lib/http/README.md +18 -4
- package/lib/http/client.js +13 -9
- package/lib/jobQueue/README.md +4 -0
- package/lib/jobQueue/queue.js +7 -0
- package/lib/server/controllerMixins/validations.js +1 -0
- package/package.json +1 -1
- package/test/activeRecord/db/migrator.test.js +17 -5
- package/test/appDependencies/appDependencies.test.js +38 -1
- package/test/http/client.test.js +42 -0
- package/test/http/helpers/client.js +15 -1
- package/test/jobQueue/helpers/bullmqMock.js +1 -0
- package/test/jobQueue/queue.test.js +11 -0
package/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const ActiveRecord = require('./lib/activeRecord');
|
|
2
2
|
const httpLibs = require('./lib/http/client');
|
|
3
3
|
const APM = require('./lib/apm');
|
|
4
|
-
const { AppDependencies } = require('./lib/appDependencies');
|
|
4
|
+
const { AppDependencies, DependencyRegistry } = require('./lib/appDependencies');
|
|
5
5
|
const JobQueue = require('./lib/jobQueue');
|
|
6
6
|
const { logger } = require('./lib/logger');
|
|
7
7
|
const redisLibs = require('./lib/redis');
|
|
@@ -29,6 +29,7 @@ module.exports = {
|
|
|
29
29
|
...serverErrorsToExport,
|
|
30
30
|
ApiDocsGenerator,
|
|
31
31
|
AppDependencies,
|
|
32
|
+
DependencyRegistry,
|
|
32
33
|
VidaServer,
|
|
33
34
|
VidaServerController,
|
|
34
35
|
};
|
|
@@ -113,7 +113,7 @@ dropTable(tableName)
|
|
|
113
113
|
addColumn(tableName, columnName, columnDetails)
|
|
114
114
|
removeColumn(tableName, columnName)
|
|
115
115
|
addIndex(tableName, fields, { concurrently, unique, name, using, where })
|
|
116
|
-
removeIndex(tableName, indexNameOrAttributes, concurrently
|
|
116
|
+
removeIndex(tableName, indexNameOrAttributes, { concurrently })
|
|
117
117
|
renameColumn(tableName, oldName, newName)
|
|
118
118
|
changeColumn(tableName, columnName, dataTypeOrOptions)
|
|
119
119
|
```
|
|
@@ -141,13 +141,15 @@ module.exports = {
|
|
|
141
141
|
},
|
|
142
142
|
|
|
143
143
|
down: async function() {
|
|
144
|
-
await this.removeIndex('users', ['team_id'], true);
|
|
144
|
+
await this.removeIndex('users', ['team_id'], { concurrently: true });
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
147
|
```
|
|
148
148
|
|
|
149
149
|
An opted-out migration is **not** rolled back if it fails, so keep it to the single statement that needs it and put everything else in its own migration.
|
|
150
150
|
|
|
151
|
+
`concurrently` is off by default on both methods, because the default transaction rules it out. Asking for it from inside a transaction throws a `MigrationError` before the statement reaches the database, and the message tells you to set `transactional: false`.
|
|
152
|
+
|
|
151
153
|
|
|
152
154
|
## Models
|
|
153
155
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
const { Connection }
|
|
2
|
-
const
|
|
3
|
-
const
|
|
4
|
-
const {
|
|
1
|
+
const { Connection } = require('./connection');
|
|
2
|
+
const { MigrationError } = require('./migrationErrors');
|
|
3
|
+
const utils = require('../utils');
|
|
4
|
+
const { Sequelize, Op } = require('sequelize');
|
|
5
|
+
const { underscore } = require('inflection');
|
|
5
6
|
|
|
6
7
|
const ID_TYPES = ['bigint', 'int', 'uuid'];
|
|
7
8
|
const DEFAULT_ID_TYPE = 'bigint';
|
|
@@ -24,7 +25,7 @@ class Migrator {
|
|
|
24
25
|
|
|
25
26
|
|
|
26
27
|
async run() {
|
|
27
|
-
if (!this.migrationModule.up) throw new
|
|
28
|
+
if (!this.migrationModule.up) throw new MigrationError('Missing migration `up`');
|
|
28
29
|
|
|
29
30
|
await this.perform(() => this.migrationModule.up.call(this));
|
|
30
31
|
}
|
|
@@ -122,8 +123,8 @@ class Migrator {
|
|
|
122
123
|
if (idType === false) return {};
|
|
123
124
|
|
|
124
125
|
if (!ID_TYPES.includes(idType)) {
|
|
125
|
-
throw new
|
|
126
|
-
|
|
126
|
+
throw new MigrationError(`Unknown id type "${idType}". Use one of: ${ID_TYPES.join(', ')}, `
|
|
127
|
+
+ `or false for a table that declares its own primary key.`);
|
|
127
128
|
}
|
|
128
129
|
|
|
129
130
|
return this[`${idType}IdColumn`];
|
|
@@ -213,8 +214,10 @@ class Migrator {
|
|
|
213
214
|
|
|
214
215
|
|
|
215
216
|
async addIndex(tableName, fields, { concurrently, unique, name, using, where }={}) {
|
|
217
|
+
if (concurrently) this.assertConcurrentIndexAllowed('addIndex');
|
|
218
|
+
|
|
216
219
|
const options = {fields: fields, ...this.queryOptions};
|
|
217
|
-
options.name
|
|
220
|
+
options.name = name || this.defaultIndexName(tableName, fields);
|
|
218
221
|
if (concurrently) options.concurrently = true;
|
|
219
222
|
if (unique) options.unique = true;
|
|
220
223
|
if (using) options.using = using;
|
|
@@ -224,8 +227,10 @@ class Migrator {
|
|
|
224
227
|
}
|
|
225
228
|
|
|
226
229
|
|
|
227
|
-
async removeIndex(tableName, indexNameOrAttributes, concurrently=
|
|
228
|
-
|
|
230
|
+
async removeIndex(tableName, indexNameOrAttributes, { concurrently }={}) {
|
|
231
|
+
if (concurrently) this.assertConcurrentIndexAllowed('removeIndex');
|
|
232
|
+
|
|
233
|
+
const options = this.queryOptions;
|
|
229
234
|
if (concurrently) options.concurrently = true;
|
|
230
235
|
|
|
231
236
|
const indexName = this.indexNameFor(tableName, indexNameOrAttributes);
|
|
@@ -233,6 +238,18 @@ class Migrator {
|
|
|
233
238
|
}
|
|
234
239
|
|
|
235
240
|
|
|
241
|
+
/***********************************************************************************************
|
|
242
|
+
* INDEX GUARDS
|
|
243
|
+
***********************************************************************************************/
|
|
244
|
+
assertConcurrentIndexAllowed(methodName) {
|
|
245
|
+
if (!this.#transaction) return;
|
|
246
|
+
|
|
247
|
+
throw new MigrationError(`${methodName} cannot use \`concurrently\` inside a transaction. `
|
|
248
|
+
+ 'Add `transactional: false` to the migration module, and keep that '
|
|
249
|
+
+ 'migration to the statements that need it.');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
|
|
236
253
|
/***********************************************************************************************
|
|
237
254
|
* INDEX NAMING
|
|
238
255
|
***********************************************************************************************/
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
class
|
|
1
|
+
class DependencyRegistry {
|
|
2
2
|
#dependencies = {};
|
|
3
3
|
#controllerClass = null;
|
|
4
4
|
|
|
@@ -56,5 +56,6 @@ class AppDependencies {
|
|
|
56
56
|
|
|
57
57
|
|
|
58
58
|
module.exports = {
|
|
59
|
-
AppDependencies: new
|
|
59
|
+
AppDependencies: new DependencyRegistry(),
|
|
60
|
+
DependencyRegistry
|
|
60
61
|
};
|
package/lib/http/README.md
CHANGED
|
@@ -49,14 +49,28 @@ const response = await client.getTickets({ page: 1, pageSize: 25 });
|
|
|
49
49
|
|
|
50
50
|
| Method | Options |
|
|
51
51
|
|---|---|
|
|
52
|
-
| `get(endpoint, opts)` | `requestParams`, `headers`, `timeout`, `signal` |
|
|
53
|
-
| `post(endpoint, opts)` | `body`, `headers`, `timeout`, `signal` |
|
|
54
|
-
| `put(endpoint, opts)` | `body`, `headers`, `timeout`, `signal` |
|
|
55
|
-
| `delete(endpoint, opts)` | `requestParams`, `headers`, `timeout`, `signal` |
|
|
52
|
+
| `get(endpoint, opts)` | `requestParams`, `headers`, `timeout`, `signal`, `binaryResponse` |
|
|
53
|
+
| `post(endpoint, opts)` | `body`, `headers`, `timeout`, `signal`, `binaryResponse` |
|
|
54
|
+
| `put(endpoint, opts)` | `body`, `headers`, `timeout`, `signal`, `binaryResponse` |
|
|
55
|
+
| `delete(endpoint, opts)` | `requestParams`, `headers`, `timeout`, `signal`, `binaryResponse` |
|
|
56
56
|
|
|
57
57
|
For `GET`/`DELETE`, `requestParams` becomes the query string. For `POST`/`PUT`, `body` becomes the request body. Object bodies are serialized as JSON; string bodies are sent unchanged.
|
|
58
58
|
|
|
59
59
|
|
|
60
|
+
### Binary responses
|
|
61
|
+
|
|
62
|
+
By default a response body is read as text and parsed as JSON when the `Content-Type` says so. That is wrong for media: decoding bytes as UTF-8 replaces every sequence that is not valid UTF-8, so the content arrives corrupted.
|
|
63
|
+
|
|
64
|
+
Pass `binaryResponse: true` and `data` is a `Buffer` of the exact bytes instead. It is per call, so one client can fetch JSON metadata and binary media.
|
|
65
|
+
|
|
66
|
+
```js
|
|
67
|
+
async fetchRecording(path) {
|
|
68
|
+
const response = await this.get(path, { binaryResponse: true, headers: { Accept: 'audio/*' } });
|
|
69
|
+
return response.data; // Buffer
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
|
|
60
74
|
### Query string encoding
|
|
61
75
|
|
|
62
76
|
For `GET`/`DELETE`, the way `requestParams` is appended to the URL depends on the `Content-Type` header:
|
package/lib/http/client.js
CHANGED
|
@@ -7,22 +7,22 @@ logger.addScope('http');
|
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class HttpClient {
|
|
10
|
-
async get(endpoint, { requestParams, headers, timeout, signal }={}) {
|
|
10
|
+
async get(endpoint, { requestParams, headers, timeout, signal, binaryResponse }={}) {
|
|
11
11
|
return await this.#makeRequest(endpoint, "GET", arguments[1]);
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
async post(endpoint, { body, headers, timeout, signal }={}) {
|
|
15
|
+
async post(endpoint, { body, headers, timeout, signal, binaryResponse }={}) {
|
|
16
16
|
return await this.#makeRequest(endpoint, "POST", arguments[1]);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
async put(endpoint, { body, headers, timeout, signal }={}) {
|
|
20
|
+
async put(endpoint, { body, headers, timeout, signal, binaryResponse }={}) {
|
|
21
21
|
return await this.#makeRequest(endpoint, "PUT", arguments[1]);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
|
|
25
|
-
async delete(endpoint, { requestParams, headers, timeout, signal }={}) {
|
|
25
|
+
async delete(endpoint, { requestParams, headers, timeout, signal, binaryResponse }={}) {
|
|
26
26
|
return await this.#makeRequest(endpoint, "DELETE", arguments[1]);
|
|
27
27
|
}
|
|
28
28
|
|
|
@@ -32,7 +32,7 @@ class HttpClient {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
|
|
35
|
-
async #_makeRequest(endpoint, method, { requestParams, body, headers, timeout, signal }={}) {
|
|
35
|
+
async #_makeRequest(endpoint, method, { requestParams, body, headers, timeout, signal, binaryResponse }={}) {
|
|
36
36
|
this.#logRequest(method, endpoint);
|
|
37
37
|
|
|
38
38
|
endpoint = `${this.urlRoot}${endpoint}`;
|
|
@@ -42,7 +42,7 @@ class HttpClient {
|
|
|
42
42
|
|
|
43
43
|
try {
|
|
44
44
|
const res = await fetch(url.toString(), payload);
|
|
45
|
-
return await this.#handleRequestResponse(res, payload);
|
|
45
|
+
return await this.#handleRequestResponse(res, payload, binaryResponse);
|
|
46
46
|
} catch (err) {
|
|
47
47
|
this.#handleRequestError(err, payload, timeout, signal);
|
|
48
48
|
}
|
|
@@ -70,9 +70,9 @@ class HttpClient {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
|
|
73
|
-
async #handleRequestResponse(res, payload) {
|
|
73
|
+
async #handleRequestResponse(res, payload, binaryResponse) {
|
|
74
74
|
if (res.status && res.status < 300) {
|
|
75
|
-
const data = await this.#parseResponseData(res);
|
|
75
|
+
const data = await this.#parseResponseData(res, binaryResponse);
|
|
76
76
|
return {data, status: res.status};
|
|
77
77
|
} else {
|
|
78
78
|
throw new HttpError(res, payload);
|
|
@@ -80,7 +80,11 @@ class HttpClient {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
|
|
83
|
-
async #parseResponseData(res) {
|
|
83
|
+
async #parseResponseData(res, binaryResponse) {
|
|
84
|
+
// Reading bytes as text decodes them as UTF-8, which replaces every sequence that is not
|
|
85
|
+
// valid UTF-8. Media has to be taken as bytes or it arrives corrupted.
|
|
86
|
+
if (binaryResponse) return Buffer.from(await res.arrayBuffer());
|
|
87
|
+
|
|
84
88
|
const body = await res.text();
|
|
85
89
|
if (!this.#isJsonResponse(res)) return body;
|
|
86
90
|
|
package/lib/jobQueue/README.md
CHANGED
|
@@ -179,6 +179,7 @@ await queue.getJob(id); // one job, or null
|
|
|
179
179
|
|
|
180
180
|
await queue.clean(grace, limit, type); // remove finished jobs older than `grace` ms
|
|
181
181
|
await queue.clearQueuedJobs(); // drain all queued jobs
|
|
182
|
+
await queue.obliterate(); // delete the queue and every key it owns in Redis
|
|
182
183
|
|
|
183
184
|
await queue.setMaximumConcurrency(8); // usually left to the Worker option above
|
|
184
185
|
await queue.maximumConcurrency();
|
|
@@ -188,6 +189,9 @@ await queue.removeMaximumConcurrency();
|
|
|
188
189
|
`clean` takes BullMQ's argument order, which is `(grace, limit, type)`. Bull's was
|
|
189
190
|
`(grace, type, limit)` — worth checking when porting a call.
|
|
190
191
|
|
|
192
|
+
`obliterate` forces, so it ends jobs that are still running rather than refusing. It leaves nothing
|
|
193
|
+
behind for the queue to be rebuilt from — use it to retire a queue for good, not to empty one.
|
|
194
|
+
|
|
191
195
|
Each `get*Jobs` and `getJob` result has this shape:
|
|
192
196
|
|
|
193
197
|
```js
|
package/lib/jobQueue/queue.js
CHANGED
|
@@ -64,6 +64,13 @@ class Queue extends AbstractJobComponent {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
|
|
67
|
+
// Forced because bullmq refuses to obliterate a queue that still has active jobs. A caller
|
|
68
|
+
// tearing a queue down wants those gone too.
|
|
69
|
+
async obliterate() {
|
|
70
|
+
await this.#queue.obliterate({ force: true });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
67
74
|
/***********************************************************************************************
|
|
68
75
|
* LIMITS
|
|
69
76
|
***********************************************************************************************/
|
|
@@ -38,6 +38,7 @@ const InstanceMethods = {
|
|
|
38
38
|
const errors = [];
|
|
39
39
|
|
|
40
40
|
if (parameterValidations?.optional && value === undefined) return errors;
|
|
41
|
+
if (parameterValidations?.nullable === true && value === null) return errors;
|
|
41
42
|
|
|
42
43
|
for (const [validationType, validationOptions] of Object.entries(parameterValidations)) {
|
|
43
44
|
const validationMethod = `validate${camelize(validationType)}`;
|
package/package.json
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
const
|
|
1
|
+
const Helpers = require('../helpers/migrator');
|
|
2
|
+
const { MigrationError } = require('../../../lib/activeRecord/db/migrationErrors');
|
|
2
3
|
|
|
3
4
|
|
|
4
5
|
afterEach(() => {
|
|
@@ -422,13 +423,13 @@ describe('Migrator', () => {
|
|
|
422
423
|
|
|
423
424
|
describe('Migrator#removeIndex', () => {
|
|
424
425
|
it.each([
|
|
425
|
-
['default options',
|
|
426
|
-
['concurrently', true, { concurrently: true, transaction: null }],
|
|
427
|
-
])('passes removeIndex options for %s', async (_label,
|
|
426
|
+
['default options', {}, { transaction: null }],
|
|
427
|
+
['concurrently', { concurrently: true }, { concurrently: true, transaction: null }],
|
|
428
|
+
])('passes removeIndex options for %s', async (_label, indexOptions, expectedOptions) => {
|
|
428
429
|
const tableName = Helpers.randomString();
|
|
429
430
|
const fields = [Helpers.randomString(), Helpers.randomString()];
|
|
430
431
|
|
|
431
|
-
await Helpers.migrator.removeIndex(tableName, fields,
|
|
432
|
+
await Helpers.migrator.removeIndex(tableName, fields, indexOptions);
|
|
432
433
|
|
|
433
434
|
expect(Helpers.mockQueryInterface.removeIndex).toHaveBeenCalledTimes(1);
|
|
434
435
|
expect(Helpers.mockQueryInterface.removeIndex).toHaveBeenCalledWith(
|
|
@@ -453,6 +454,17 @@ describe('Migrator', () => {
|
|
|
453
454
|
});
|
|
454
455
|
|
|
455
456
|
|
|
457
|
+
describe('Migrator concurrent indexes inside a transaction', () => {
|
|
458
|
+
it.each([
|
|
459
|
+
['addIndex', async function() { await this.addIndex('users', ['team_id'], { concurrently: true }); }],
|
|
460
|
+
['removeIndex', async function() { await this.removeIndex('users', ['team_id'], { concurrently: true }); }],
|
|
461
|
+
])('throws when %s asks for concurrently', async (methodName, operation) => {
|
|
462
|
+
await expect(Helpers.runInMigration(operation)).rejects.toThrow(MigrationError);
|
|
463
|
+
expect(Helpers.mockQueryInterface[methodName]).not.toHaveBeenCalled();
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
|
|
456
468
|
describe('Migrator#defaultIndexName', () => {
|
|
457
469
|
it ('builds idx_<table>_<columns> from a single column', () => {
|
|
458
470
|
expect(Helpers.migrator.defaultIndexName('users', ['team_id'])).toBe('idx_users_team_id');
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const { AppDependencies } = require('../../lib/appDependencies');
|
|
1
|
+
const { AppDependencies, DependencyRegistry } = require('../../lib/appDependencies');
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
class FooController {}
|
|
@@ -129,3 +129,40 @@ describe('AppDependencies', () => {
|
|
|
129
129
|
});
|
|
130
130
|
});
|
|
131
131
|
});
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
describe('DependencyRegistry', () => {
|
|
135
|
+
it ('builds registries that are independent of the AppDependencies singleton', () => {
|
|
136
|
+
const registry = new DependencyRegistry();
|
|
137
|
+
|
|
138
|
+
registry.setDependency('scopedHelper', 'scoped');
|
|
139
|
+
|
|
140
|
+
expect(registry.get('scopedHelper')).toBe('scoped');
|
|
141
|
+
expect(AppDependencies.get('scopedHelper')).toBeNull();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
it ('builds registries that are independent of each other', () => {
|
|
146
|
+
const first = new DependencyRegistry();
|
|
147
|
+
const second = new DependencyRegistry();
|
|
148
|
+
|
|
149
|
+
first.setDependency('myHelper', 'one');
|
|
150
|
+
second.setDependency('myHelper', 'two');
|
|
151
|
+
|
|
152
|
+
expect(first.myHelper).toBe('one');
|
|
153
|
+
expect(second.myHelper).toBe('two');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
it ('resets one registry without touching another', () => {
|
|
158
|
+
const first = new DependencyRegistry();
|
|
159
|
+
const second = new DependencyRegistry();
|
|
160
|
+
first.setDependency('myHelper', 'one');
|
|
161
|
+
second.setDependency('myHelper', 'two');
|
|
162
|
+
|
|
163
|
+
first.reset();
|
|
164
|
+
|
|
165
|
+
expect(first.get('myHelper')).toBeNull();
|
|
166
|
+
expect(second.get('myHelper')).toBe('two');
|
|
167
|
+
});
|
|
168
|
+
});
|
package/test/http/client.test.js
CHANGED
|
@@ -181,6 +181,48 @@ describe('HttpClient', () => {
|
|
|
181
181
|
});
|
|
182
182
|
|
|
183
183
|
|
|
184
|
+
describe('binary responses', () => {
|
|
185
|
+
const client = new HttpClient();
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
beforeEach(() => Helpers.mockFetchBinary());
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
it ('returns the exact bytes as a Buffer', async () => {
|
|
192
|
+
const result = await client.get(Helpers.apiUrl, { binaryResponse: true });
|
|
193
|
+
|
|
194
|
+
expect(result.status).toBe(200);
|
|
195
|
+
expect(Buffer.isBuffer(result.data)).toBe(true);
|
|
196
|
+
expect(result.data).toEqual(Helpers.binaryPayload);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
// The point of the option: a text read replaces every byte that is not valid UTF-8.
|
|
201
|
+
it ('never reads the body as text', async () => {
|
|
202
|
+
await client.get(Helpers.apiUrl, { binaryResponse: true });
|
|
203
|
+
|
|
204
|
+
const response = await fetch.mock.results[0].value;
|
|
205
|
+
expect(response.text).not.toHaveBeenCalled();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
it ('reads the body as text without the option, losing the bytes', async () => {
|
|
210
|
+
const result = await client.get(Helpers.apiUrl);
|
|
211
|
+
|
|
212
|
+
expect(typeof result.data).toBe('string');
|
|
213
|
+
expect(Buffer.from(result.data)).not.toEqual(Helpers.binaryPayload);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
const verbs = [['post'], ['put'], ['delete']];
|
|
218
|
+
it.each(verbs)('applies to %s as well', async (methodName) => {
|
|
219
|
+
const result = await client[methodName](Helpers.apiUrl, { binaryResponse: true });
|
|
220
|
+
|
|
221
|
+
expect(result.data).toEqual(Helpers.binaryPayload);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
|
|
184
226
|
describe('abort/timeout support', () => {
|
|
185
227
|
const client = new HttpClient();
|
|
186
228
|
|
|
@@ -18,6 +18,10 @@ const apiUrl = `${baseUrl}${endpoint}`;
|
|
|
18
18
|
const responsePayload = randomPayload();
|
|
19
19
|
const responseBody = JSON.stringify(responsePayload);
|
|
20
20
|
|
|
21
|
+
// A RIFF header followed by byte sequences that are not valid UTF-8, so a text read cannot
|
|
22
|
+
// reproduce them.
|
|
23
|
+
const binaryPayload = Buffer.from([0x52, 0x49, 0x46, 0x46, 0xff, 0xfe, 0x00, 0x80]);
|
|
24
|
+
|
|
21
25
|
|
|
22
26
|
class ClientWithUrlRoot extends HttpClient {
|
|
23
27
|
get urlRoot() {
|
|
@@ -67,11 +71,19 @@ const mockFetchResponse = ({ body, contentType, status = 200 }) => {
|
|
|
67
71
|
get: jest.fn((headerName) => headerName == 'content-type' ? contentType : undefined),
|
|
68
72
|
},
|
|
69
73
|
status,
|
|
70
|
-
|
|
74
|
+
// Interpolated rather than passed through, so a Buffer body decodes the way a real text
|
|
75
|
+
// read would.
|
|
76
|
+
text: jest.fn().mockResolvedValue(`${body ?? ''}`),
|
|
77
|
+
arrayBuffer: jest.fn().mockResolvedValue(Buffer.from(body ?? '')),
|
|
71
78
|
});
|
|
72
79
|
}
|
|
73
80
|
|
|
74
81
|
|
|
82
|
+
const mockFetchBinary = () => {
|
|
83
|
+
mockFetchResponse({ body: binaryPayload, contentType: 'audio/wav' });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
75
87
|
/***************************************************************************************************
|
|
76
88
|
* ASSERTIONS
|
|
77
89
|
***************************************************************************************************/
|
|
@@ -109,11 +121,13 @@ module.exports = {
|
|
|
109
121
|
apiUrl,
|
|
110
122
|
basicAuthCredentials,
|
|
111
123
|
bearerToken,
|
|
124
|
+
binaryPayload,
|
|
112
125
|
ClientWithBasicAuth,
|
|
113
126
|
ClientWithBearerToken,
|
|
114
127
|
ClientWithUrlRoot,
|
|
115
128
|
endpoint,
|
|
116
129
|
expectRequest,
|
|
130
|
+
mockFetchBinary,
|
|
117
131
|
mockFetchResponse,
|
|
118
132
|
mockFetchSuccess,
|
|
119
133
|
randomPayload,
|
|
@@ -218,6 +218,17 @@ describe('Queue', () => {
|
|
|
218
218
|
});
|
|
219
219
|
|
|
220
220
|
|
|
221
|
+
describe('#obliterate', () => {
|
|
222
|
+
it ('calls bullmq obliterate() with force so active jobs do not block it', async () => {
|
|
223
|
+
await queue.obliterate();
|
|
224
|
+
|
|
225
|
+
const instance = bullmqMock.instances.queues[0];
|
|
226
|
+
expect(instance.obliterate).toHaveBeenCalledTimes(1);
|
|
227
|
+
expect(instance.obliterate).toHaveBeenCalledWith({ force: true });
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
|
|
221
232
|
describe('maximum concurrency', () => {
|
|
222
233
|
it ('sets the queue maximum', async () => {
|
|
223
234
|
const maximum = TestHelpers.Faker.Math.randomNumber(20);
|