@natlibfi/melinda-rest-api-commons 6.1.0-alpha.0 → 6.2.0-alpha.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/.github/workflows/melinda-node-tests-and-publish.yml +4 -2
- package/README.md +53 -5
- package/dist/amqp.d.ts +41 -0
- package/dist/amqp.d.ts.map +1 -0
- package/dist/amqp.js +237 -235
- package/dist/amqp.js.map +1 -7
- package/dist/constants.d.ts +49 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +38 -38
- package/dist/constants.js.map +1 -7
- package/dist/helpers/conversion.d.ts +8 -0
- package/dist/helpers/conversion.d.ts.map +1 -0
- package/dist/helpers/conversion.js +70 -64
- package/dist/helpers/conversion.js.map +1 -7
- package/dist/helpers/fix-constants.d.ts +76 -0
- package/dist/helpers/fix-constants.d.ts.map +1 -0
- package/dist/helpers/fix-constants.js +41 -41
- package/dist/helpers/fix-constants.js.map +1 -7
- package/dist/helpers/fix-handle-tempurns.d.ts +3 -0
- package/dist/helpers/fix-handle-tempurns.d.ts.map +1 -0
- package/dist/helpers/fix-handle-tempurns.js +73 -71
- package/dist/helpers/fix-handle-tempurns.js.map +1 -7
- package/dist/helpers/fix-strip-f884s.d.ts +10 -0
- package/dist/helpers/fix-strip-f884s.d.ts.map +1 -0
- package/dist/helpers/fix-strip-f884s.js +161 -163
- package/dist/helpers/fix-strip-f884s.js.map +1 -7
- package/dist/helpers/fix.d.ts +6 -0
- package/dist/helpers/fix.d.ts.map +1 -0
- package/dist/helpers/fix.js +133 -121
- package/dist/helpers/fix.js.map +1 -7
- package/dist/helpers/validation.d.ts +9 -0
- package/dist/helpers/validation.d.ts.map +1 -0
- package/dist/helpers/validation.js +14 -16
- package/dist/helpers/validation.js.map +1 -7
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -7
- package/dist/mongo.d.ts +165 -0
- package/dist/mongo.d.ts.map +1 -0
- package/dist/mongo.js +595 -547
- package/dist/mongo.js.map +1 -7
- package/dist/mongoError.d.ts +2 -0
- package/dist/mongoError.d.ts.map +1 -0
- package/dist/mongoError.js +19 -0
- package/dist/mongoError.js.map +1 -0
- package/dist/mongoLog.d.ts +50 -0
- package/dist/mongoLog.d.ts.map +1 -0
- package/dist/mongoLog.js +274 -242
- package/dist/mongoLog.js.map +1 -7
- package/dist/utils.d.ts +36 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +112 -110
- package/dist/utils.js.map +1 -7
- package/eslint.config.ts +66 -0
- package/package.json +15 -11
- package/{tests/amqp-error-branches.test.js → test/amqp-error-branches.test.ts} +68 -52
- package/{tests/amqp.healthtest.js → test/amqp.healthtest.ts} +7 -3
- package/{tests/amqp.tests.localtest.js → test/amqp.tests.localtest.ts} +54 -35
- package/{tests/helpers/conversion.test.js → test/helpers/conversion.test.ts} +26 -14
- package/{tests/helpers/fix-edge.test.js → test/helpers/fix-edge.test.ts} +16 -12
- package/{tests/helpers/fix.test.js → test/helpers/fix.test.ts} +14 -8
- package/{tests/helpers/validation.test.js → test/helpers/validation.test.ts} +7 -6
- package/{tests/mongo-add-blob-size.test.js → test/mongo-add-blob-size.test.ts} +14 -15
- package/{tests/mongo-check-and-check-state.test.js → test/mongo-check-and-check-state.test.ts} +6 -8
- package/{tests/mongo-check-and-set-import-job-state.test.js → test/mongo-check-and-set-import-job-state.test.ts} +6 -9
- package/{tests/mongo-check-timeout.test.js → test/mongo-check-timeout.test.ts} +6 -9
- package/{tests/mongo-create-bulk.test.js → test/mongo-create-bulk.test.ts} +6 -7
- package/{tests/mongo-create-prio.test.js → test/mongo-create-prio.test.ts} +6 -6
- package/{tests/mongo-error-branches.test.js → test/mongo-error-branches.test.ts} +147 -51
- package/{tests/mongo-get-one.test.js → test/mongo-get-one.test.ts} +6 -6
- package/{tests/mongo-get-stream.test.js → test/mongo-get-stream.test.ts} +6 -8
- package/{tests/mongo-push-ids.test.js → test/mongo-push-ids.test.ts} +6 -6
- package/{tests/mongo-push-messages.test.js → test/mongo-push-messages.test.ts} +6 -6
- package/{tests/mongo-query-by-id.test.js → test/mongo-query-by-id.test.ts} +6 -9
- package/{tests/mongo-query.test.js → test/mongo-query.test.ts} +6 -7
- package/{tests/mongo-read-content.test.js → test/mongo-read-content.test.ts} +6 -9
- package/{tests/mongo-remove.test.js → test/mongo-remove.test.ts} +6 -8
- package/{tests/mongo-set-blob-size.test.js → test/mongo-set-blob-size.test.ts} +6 -6
- package/{tests/mongo-set-import-job-state.test.js → test/mongo-set-import-job-state.test.ts} +6 -6
- package/{tests/mongo-set-operation.test.js → test/mongo-set-operation.test.ts} +6 -6
- package/{tests/mongo-set-operations.test.js → test/mongo-set-operations.test.ts} +6 -6
- package/{tests/mongo-set-state.test.js → test/mongo-set-state.test.ts} +6 -6
- package/{tests/mongoLog-protect.test.js → test/mongoLog-protect.test.ts} +18 -17
- package/{tests/mongoLog.test.js → test/mongoLog.test.ts} +8 -8
- package/{tests/testUtils.js → test/testUtils.ts} +49 -15
- package/{tests/utils.test.js → test/utils.test.ts} +5 -5
- package/tsconfig.json +62 -0
- package/tsconfig.test.json +17 -0
- package/dist/helpers/fix-utils.js +0 -39
- package/dist/helpers/fix-utils.js.map +0 -7
- package/eslint.config.js +0 -53
|
@@ -3,24 +3,47 @@ import {test} from 'node:test';
|
|
|
3
3
|
import {execFile} from 'node:child_process';
|
|
4
4
|
import {fileURLToPath} from 'node:url';
|
|
5
5
|
import {Error as ApiError} from '@natlibfi/melinda-commons';
|
|
6
|
-
import
|
|
6
|
+
import type {Message} from 'amqplib';
|
|
7
|
+
import createAmqp, {type QueueItemMessagePayload, type QueueItemRecordPayload} from '../src/amqp.ts';
|
|
7
8
|
|
|
8
|
-
const AMQP_MODULE_PATH = fileURLToPath(new URL('../src/amqp.
|
|
9
|
+
const AMQP_MODULE_PATH = fileURLToPath(new URL('../src/amqp.ts', import.meta.url));
|
|
9
10
|
|
|
10
11
|
const DUMMY_URL = 'amqp://stub:5672/';
|
|
11
12
|
const QUEUE = 'TESTQUEUE';
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
/**
|
|
15
|
+
* A minimal stub for an amqplib message. The fields beyond deliveryTag
|
|
16
|
+
* match the shape of a real amqplib message (the tests only exercise
|
|
17
|
+
* deliveryTag, correlationId and the message body).
|
|
18
|
+
*/
|
|
19
|
+
function makeMessage(correlationId: string, deliveryTag: number): Message {
|
|
14
20
|
const data = {leader: '00000nam a22000000i 4500', fields: [{tag: '001', value: '123'}]};
|
|
15
21
|
const content = JSON.stringify({data});
|
|
16
22
|
return {
|
|
17
23
|
content: Buffer.from(content),
|
|
18
|
-
|
|
19
|
-
|
|
24
|
+
// only correlationId and headers are exercised by these tests; the
|
|
25
|
+
// remaining message properties stay undefined
|
|
26
|
+
properties: {
|
|
27
|
+
contentType: undefined,
|
|
28
|
+
contentEncoding: undefined,
|
|
29
|
+
headers: {from: 'TEST'},
|
|
30
|
+
deliveryMode: undefined,
|
|
31
|
+
priority: undefined,
|
|
32
|
+
correlationId,
|
|
33
|
+
replyTo: undefined,
|
|
34
|
+
expiration: undefined,
|
|
35
|
+
messageId: undefined,
|
|
36
|
+
timestamp: undefined,
|
|
37
|
+
type: undefined,
|
|
38
|
+
userId: undefined,
|
|
39
|
+
appId: undefined,
|
|
40
|
+
clusterId: undefined
|
|
41
|
+
},
|
|
42
|
+
fields: {deliveryTag, redelivered: false, exchange: '', routingKey: ''}
|
|
20
43
|
};
|
|
21
44
|
}
|
|
22
45
|
|
|
23
|
-
function makeChannel(overrides = {}) {
|
|
46
|
+
function makeChannel(overrides: Record<string, unknown> = {}) {
|
|
24
47
|
return {
|
|
25
48
|
assertQueue: async () => ({messageCount: 0}),
|
|
26
49
|
checkQueue: async () => ({messageCount: 0}),
|
|
@@ -34,14 +57,17 @@ function makeChannel(overrides = {}) {
|
|
|
34
57
|
};
|
|
35
58
|
}
|
|
36
59
|
|
|
37
|
-
|
|
60
|
+
/**
|
|
61
|
+
* Create an operator with the given channel stub overrides injected via deps
|
|
62
|
+
*/
|
|
63
|
+
async function makeOperator(channelOverrides?: Record<string, unknown>) {
|
|
38
64
|
const channel = makeChannel(channelOverrides);
|
|
39
65
|
const connection = {createChannel: async () => channel, close: async () => {}};
|
|
40
66
|
return createAmqp(DUMMY_URL, false, {connection, channel});
|
|
41
67
|
}
|
|
42
68
|
|
|
43
69
|
test('consumeChunk returns false when queue drains mid-pump', async () => {
|
|
44
|
-
// (src/amqp.
|
|
70
|
+
// (src/amqp.ts, getData/pump):
|
|
45
71
|
// FIXED: pump now bails out (returns results) when channel.get -> null,
|
|
46
72
|
// and consumeChunk maps an empty pump result to false, matching the
|
|
47
73
|
// empty-queue contract of checkQueue. Previously this crashed with
|
|
@@ -67,14 +93,14 @@ test('consumeChunk returns partial chunk when queue drains after first message',
|
|
|
67
93
|
return first;
|
|
68
94
|
}
|
|
69
95
|
});
|
|
70
|
-
const {records, messages} = await amqpOperator.consumeChunk(QUEUE, true);
|
|
96
|
+
const {records, messages} = (await amqpOperator.consumeChunk(QUEUE, true)) as QueueItemRecordPayload;
|
|
71
97
|
assert.equal(messages.length, 1);
|
|
72
98
|
assert.equal(records.length, 1);
|
|
73
99
|
});
|
|
74
100
|
|
|
75
101
|
|
|
76
102
|
test('consumeChunk deduplicates messages with same correlationId + deliveryTag', async () => {
|
|
77
|
-
// (src/amqp.
|
|
103
|
+
// (src/amqp.ts, getData/pump):
|
|
78
104
|
// FIXED: dedup now compares correlationId + deliveryTag field-by-field,
|
|
79
105
|
// so duplicate messages are filtered out. (Previously the includes()
|
|
80
106
|
// check compared object references and never triggered.)
|
|
@@ -83,7 +109,7 @@ test('consumeChunk deduplicates messages with same correlationId + deliveryTag',
|
|
|
83
109
|
checkQueue: async () => ({messageCount: 2}),
|
|
84
110
|
get: async () => duplicate
|
|
85
111
|
});
|
|
86
|
-
const {records, messages} = await amqpOperator.consumeChunk(QUEUE, true);
|
|
112
|
+
const {records, messages} = (await amqpOperator.consumeChunk(QUEUE, true)) as QueueItemRecordPayload;
|
|
87
113
|
assert.equal(messages.length, 1);
|
|
88
114
|
assert.equal(records.length, 1);
|
|
89
115
|
});
|
|
@@ -96,21 +122,20 @@ test('same deliveryTag with different correlationIds is NOT deduplicated', async
|
|
|
96
122
|
checkQueue: async () => ({messageCount: 2}),
|
|
97
123
|
get: async () => messages.shift()
|
|
98
124
|
});
|
|
99
|
-
const {messages: got} = await amqpOperator.consumeChunk(QUEUE, true);
|
|
125
|
+
const {messages: got} = (await amqpOperator.consumeChunk(QUEUE, true)) as QueueItemRecordPayload;
|
|
100
126
|
assert.equal(got.length, 2);
|
|
101
127
|
});
|
|
102
128
|
|
|
103
129
|
test('removeQueue closes the temp channel even when deleteQueue throws', async () => {
|
|
104
|
-
// (src/amqp.
|
|
130
|
+
// (src/amqp.ts, removeQueue):
|
|
105
131
|
// FIXED: the throwaway tempChannel is now closed in a finally block, so a
|
|
106
132
|
// missing queue (deleteQueue throws) no longer leaks a channel.
|
|
107
|
-
const closedChannels = [];
|
|
133
|
+
const closedChannels: any[] = [];
|
|
108
134
|
const tempChannel = {
|
|
109
135
|
deleteQueue: async () => {
|
|
110
136
|
throw new Error('NOT_FOUND - queue missing');
|
|
111
137
|
},
|
|
112
138
|
close: async () => {
|
|
113
|
-
// eslint-disable-next-line functional/immutable-data
|
|
114
139
|
closedChannels.push('temp');
|
|
115
140
|
}
|
|
116
141
|
};
|
|
@@ -123,13 +148,12 @@ test('removeQueue closes the temp channel even when deleteQueue throws', async (
|
|
|
123
148
|
});
|
|
124
149
|
|
|
125
150
|
test('removeQueue closes the temp channel on success too', async () => {
|
|
126
|
-
const closedChannels = [];
|
|
151
|
+
const closedChannels: any[] = [];
|
|
127
152
|
const amqpOperator = await createAmqp(DUMMY_URL, false, {
|
|
128
153
|
connection: {
|
|
129
154
|
createChannel: async () => ({
|
|
130
155
|
deleteQueue: async () => {},
|
|
131
156
|
close: async () => {
|
|
132
|
-
// eslint-disable-next-line functional/immutable-data
|
|
133
157
|
closedChannels.push('temp');
|
|
134
158
|
}
|
|
135
159
|
}),
|
|
@@ -142,7 +166,7 @@ test('removeQueue closes the temp channel on success too', async () => {
|
|
|
142
166
|
});
|
|
143
167
|
|
|
144
168
|
test('healthCheck loop stops after closeChannel and does not keep firing or throw', async () => {
|
|
145
|
-
// (src/amqp.
|
|
169
|
+
// (src/amqp.ts, healthCheck):
|
|
146
170
|
// FIXED: closeChannel/closeConnection cancel the recursive healthCheck loop,
|
|
147
171
|
// so the ~200 ms chain short-circuits instead of running until the closed
|
|
148
172
|
// channel throws (which used to make graceful shutdown flaky).
|
|
@@ -151,9 +175,8 @@ test('healthCheck loop stops after closeChannel and does not keep firing or thro
|
|
|
151
175
|
let closed = false;
|
|
152
176
|
let healthCheckCalls = 0;
|
|
153
177
|
const channel = {
|
|
154
|
-
assertQueue: async (name) => {
|
|
178
|
+
assertQueue: async (name: string) => {
|
|
155
179
|
if (name === 'HEALTHCHECK') {
|
|
156
|
-
// eslint-disable-next-line functional/immutable-data
|
|
157
180
|
healthCheckCalls += 1;
|
|
158
181
|
if (closed) {
|
|
159
182
|
throw new Error('channel closed');
|
|
@@ -167,7 +190,6 @@ test('healthCheck loop stops after closeChannel and does not keep firing or thro
|
|
|
167
190
|
nack: () => {},
|
|
168
191
|
purgeQueue: async () => {},
|
|
169
192
|
close: async () => {
|
|
170
|
-
// eslint-disable-next-line functional/immutable-data
|
|
171
193
|
closed = true;
|
|
172
194
|
}
|
|
173
195
|
};
|
|
@@ -194,7 +216,7 @@ test('happy path: consumeChunk returns two distinct messages', async () => {
|
|
|
194
216
|
checkQueue: async () => ({messageCount: messages.length}),
|
|
195
217
|
get: async () => messages.shift()
|
|
196
218
|
});
|
|
197
|
-
const {headers, records, messages: got} = await amqpOperator.consumeChunk(QUEUE, true);
|
|
219
|
+
const {headers, records, messages: got} = (await amqpOperator.consumeChunk(QUEUE, true)) as QueueItemRecordPayload;
|
|
198
220
|
assert.equal(got.length, 2);
|
|
199
221
|
assert.equal(records.length, 2);
|
|
200
222
|
assert.deepStrictEqual(headers, {from: 'TEST'});
|
|
@@ -218,7 +240,7 @@ test('checkQueue style one returns the {headers, records, messages} form when to
|
|
|
218
240
|
assertQueue: async () => ({messageCount: 1}),
|
|
219
241
|
get: async () => message
|
|
220
242
|
});
|
|
221
|
-
const {headers, records, messages} = await amqpOperator.checkQueue({queue: QUEUE, style: 'one', toRecord: true});
|
|
243
|
+
const {headers, records, messages} = (await amqpOperator.checkQueue({queue: QUEUE, style: 'one', toRecord: true})) as QueueItemRecordPayload;
|
|
222
244
|
assert.deepStrictEqual(headers, {from: 'TEST'});
|
|
223
245
|
assert.equal(records.length, 1);
|
|
224
246
|
assert.strictEqual(messages.length, 1);
|
|
@@ -257,7 +279,7 @@ test('checkQueue style basic returns a chunk of records and messages when the qu
|
|
|
257
279
|
checkQueue: async () => ({messageCount: messages.length}),
|
|
258
280
|
get: async () => messages.shift()
|
|
259
281
|
});
|
|
260
|
-
const {headers, records, messages: got} = await amqpOperator.checkQueue({queue: QUEUE, style: 'basic', toRecord: true});
|
|
282
|
+
const {headers, records, messages: got} = (await amqpOperator.checkQueue({queue: QUEUE, style: 'basic', toRecord: true})) as QueueItemRecordPayload;
|
|
261
283
|
assert.deepStrictEqual(headers, {from: 'TEST'});
|
|
262
284
|
assert.equal(records.length, 2);
|
|
263
285
|
assert.equal(got.length, 2);
|
|
@@ -276,17 +298,15 @@ test('checkQueue rejects with a 500 ApiError for an unknown style', async () =>
|
|
|
276
298
|
});
|
|
277
299
|
|
|
278
300
|
test('checkQueue purges the queue before returning the style result', async () => {
|
|
279
|
-
const purgeCalls = [];
|
|
301
|
+
const purgeCalls: any[] = [];
|
|
280
302
|
let assertCalls = 0;
|
|
281
303
|
const amqpOperator = await makeOperator({
|
|
282
304
|
assertQueue: async () => {
|
|
283
|
-
// eslint-disable-next-line functional/immutable-data
|
|
284
305
|
assertCalls += 1;
|
|
285
306
|
// first call (before the purge) reports 3 pending, after the purge 0
|
|
286
307
|
return assertCalls === 1 ? {messageCount: 3} : {messageCount: 0};
|
|
287
308
|
},
|
|
288
|
-
purgeQueue: async (name) => {
|
|
289
|
-
// eslint-disable-next-line functional/immutable-data
|
|
309
|
+
purgeQueue: async (name: string) => {
|
|
290
310
|
purgeCalls.push(name);
|
|
291
311
|
}
|
|
292
312
|
});
|
|
@@ -309,7 +329,7 @@ test('consumeOne returns the plain message when toRecord is false', async () =>
|
|
|
309
329
|
test('consumeOne returns the {headers, records, messages} form when toRecord is true', async () => {
|
|
310
330
|
const message = makeMessage('c1', 1);
|
|
311
331
|
const amqpOperator = await makeOperator({get: async () => message});
|
|
312
|
-
const {headers, records, messages} = await amqpOperator.consumeOne(QUEUE, true);
|
|
332
|
+
const {headers, records, messages} = (await amqpOperator.consumeOne(QUEUE, true)) as QueueItemRecordPayload;
|
|
313
333
|
assert.deepStrictEqual(headers, {from: 'TEST'});
|
|
314
334
|
assert.equal(records.length, 1);
|
|
315
335
|
assert.strictEqual(messages.length, 1);
|
|
@@ -341,7 +361,7 @@ test('consumeChunk returns {headers, messages} without records when toRecord is
|
|
|
341
361
|
checkQueue: async () => ({messageCount: messages.length}),
|
|
342
362
|
get: async () => messages.shift()
|
|
343
363
|
});
|
|
344
|
-
const {headers, messages: got} = await amqpOperator.consumeChunk(QUEUE, false);
|
|
364
|
+
const {headers, messages: got} = (await amqpOperator.consumeChunk(QUEUE, false)) as QueueItemMessagePayload;
|
|
345
365
|
assert.deepStrictEqual(headers, {from: 'TEST'});
|
|
346
366
|
assert.equal(got.length, 2);
|
|
347
367
|
});
|
|
@@ -360,11 +380,10 @@ test('consumeChunk rejects when the amqplib checkQueue throws a plain Error', as
|
|
|
360
380
|
// ---------------------------------------------------------------------------
|
|
361
381
|
|
|
362
382
|
test('ackMessages acks every message on the channel', async () => {
|
|
363
|
-
const acked = [];
|
|
383
|
+
const acked: Message[] = [];
|
|
364
384
|
const messages = [makeMessage('c1', 1), makeMessage('c2', 2)];
|
|
365
385
|
const amqpOperator = await makeOperator({
|
|
366
|
-
ack: (message) => {
|
|
367
|
-
// eslint-disable-next-line functional/immutable-data
|
|
386
|
+
ack: (message: Message) => {
|
|
368
387
|
acked.push(message);
|
|
369
388
|
}
|
|
370
389
|
});
|
|
@@ -373,11 +392,10 @@ test('ackMessages acks every message on the channel', async () => {
|
|
|
373
392
|
});
|
|
374
393
|
|
|
375
394
|
test('nackMessages nacks every message on the channel', async () => {
|
|
376
|
-
const nacked = [];
|
|
395
|
+
const nacked: Message[] = [];
|
|
377
396
|
const messages = [makeMessage('c1', 1), makeMessage('c2', 2)];
|
|
378
397
|
const amqpOperator = await makeOperator({
|
|
379
|
-
nack: (message) => {
|
|
380
|
-
// eslint-disable-next-line functional/immutable-data
|
|
398
|
+
nack: (message: Message) => {
|
|
381
399
|
nacked.push(message);
|
|
382
400
|
}
|
|
383
401
|
});
|
|
@@ -390,10 +408,9 @@ test('nackMessages nacks every message on the channel', async () => {
|
|
|
390
408
|
// ---------------------------------------------------------------------------
|
|
391
409
|
|
|
392
410
|
test('sendToQueue sends the JSON body with correlationId, persistent and headers options', async () => {
|
|
393
|
-
const sent = [];
|
|
411
|
+
const sent: {queue: string, content: Buffer, options: Record<string, unknown>}[] = [];
|
|
394
412
|
const amqpOperator = await makeOperator({
|
|
395
|
-
sendToQueue: async (queue, content, options) => {
|
|
396
|
-
// eslint-disable-next-line functional/immutable-data
|
|
413
|
+
sendToQueue: async (queue: string, content: Buffer, options: Record<string, unknown>) => {
|
|
397
414
|
sent.push({queue, content, options});
|
|
398
415
|
}
|
|
399
416
|
});
|
|
@@ -405,10 +422,10 @@ test('sendToQueue sends the JSON body with correlationId, persistent and headers
|
|
|
405
422
|
data
|
|
406
423
|
});
|
|
407
424
|
assert.equal(sent.length, 1);
|
|
408
|
-
assert.equal(sent[0]
|
|
409
|
-
assert.ok(Buffer.isBuffer(sent[0].content));
|
|
410
|
-
assert.deepStrictEqual(JSON.parse(sent[0]
|
|
411
|
-
assert.deepStrictEqual(sent[0]
|
|
425
|
+
assert.equal(sent[0]?.queue, QUEUE);
|
|
426
|
+
assert.ok(sent[0] && Buffer.isBuffer(sent[0].content));
|
|
427
|
+
assert.deepStrictEqual(JSON.parse(sent[0]?.content.toString() ?? ''), {data});
|
|
428
|
+
assert.deepStrictEqual(sent[0]?.options, {correlationId: 'cid-1', persistent: true, headers: {h: 1}});
|
|
412
429
|
});
|
|
413
430
|
|
|
414
431
|
test('sendToQueue rejects with a 500 ApiError when the queue is undefined', async () => {
|
|
@@ -442,13 +459,12 @@ test('checkQueue rejects with a 500 ApiError when the queue is an empty string',
|
|
|
442
459
|
// ---------------------------------------------------------------------------
|
|
443
460
|
|
|
444
461
|
test('closeConnection closes the connection and the operator resolves', async () => {
|
|
445
|
-
const closed = [];
|
|
462
|
+
const closed: any[] = [];
|
|
446
463
|
const channel = makeChannel();
|
|
447
464
|
const amqpOperator = await createAmqp(DUMMY_URL, false, {
|
|
448
465
|
connection: {
|
|
449
466
|
createChannel: async () => channel,
|
|
450
467
|
close: async () => {
|
|
451
|
-
// eslint-disable-next-line functional/immutable-data
|
|
452
468
|
closed.push('connection');
|
|
453
469
|
}
|
|
454
470
|
},
|
|
@@ -459,16 +475,14 @@ test('closeConnection closes the connection and the operator resolves', async ()
|
|
|
459
475
|
});
|
|
460
476
|
|
|
461
477
|
test('healthCheck error arriving after cancel does not rethrow and stops the loop', async () => {
|
|
462
|
-
let rejectAssert;
|
|
478
|
+
let rejectAssert: ((reason?: unknown) => void) | undefined;
|
|
463
479
|
const pending = new Promise((_, reject) => {
|
|
464
|
-
// eslint-disable-next-line functional/immutable-data
|
|
465
480
|
rejectAssert = reject;
|
|
466
481
|
});
|
|
467
482
|
let healthCheckCalls = 0;
|
|
468
483
|
const channel = makeChannel({
|
|
469
|
-
assertQueue: async (name) => {
|
|
484
|
+
assertQueue: async (name: string) => {
|
|
470
485
|
if (name === 'HEALTHCHECK') {
|
|
471
|
-
// eslint-disable-next-line functional/immutable-data
|
|
472
486
|
healthCheckCalls += 1;
|
|
473
487
|
return pending;
|
|
474
488
|
}
|
|
@@ -482,7 +496,9 @@ test('healthCheck error arriving after cancel does not rethrow and stops the loo
|
|
|
482
496
|
// by now the loop is suspended on the pending assertQueue call;
|
|
483
497
|
// cancel first, then let the error arrive so the catch sees the flag already cleared
|
|
484
498
|
await amqpOperator.closeChannel();
|
|
485
|
-
|
|
499
|
+
if (rejectAssert) {
|
|
500
|
+
rejectAssert(new Error('amqp down after close'));
|
|
501
|
+
}
|
|
486
502
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
487
503
|
assert.equal(healthCheckCalls, 1, 'no further health checks after cancel');
|
|
488
504
|
});
|
|
@@ -497,7 +513,7 @@ test('healthCheck error while running rethrows into the un-awaited loop promise'
|
|
|
497
513
|
const script = `
|
|
498
514
|
import createAmqp from '${AMQP_MODULE_PATH}';
|
|
499
515
|
const channel = {
|
|
500
|
-
assertQueue: async (name) => {
|
|
516
|
+
assertQueue: /** @param {*} name */ async (name) => {
|
|
501
517
|
if (name === 'HEALTHCHECK') {
|
|
502
518
|
throw new Error('amqp down');
|
|
503
519
|
}
|
|
@@ -523,7 +539,7 @@ test('healthCheck error while running rethrows into the un-awaited loop promise'
|
|
|
523
539
|
process.stdout.write(ok ? 'OK' : 'FAIL:' + JSON.stringify(captured.map((e) => String(e && e.message))));
|
|
524
540
|
process.exit(ok ? 0 : 1);
|
|
525
541
|
`;
|
|
526
|
-
const {code, stdout} = await new Promise((resolve) => {
|
|
542
|
+
const {code, stdout} = await new Promise<{code: number | string, stdout: string}>((resolve) => {
|
|
527
543
|
const child = execFile(process.execPath, ['--input-type=module', '-e', script], (err, out) => {
|
|
528
544
|
resolve({code: err ? (err.code ?? 1) : 0, stdout: out ?? ''});
|
|
529
545
|
});
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import {describe, it} from 'node:test';
|
|
2
2
|
import assert from 'node:assert';
|
|
3
3
|
import createDebugLogger from 'debug';
|
|
4
|
-
import {amqpFactory} from '../src/index.
|
|
4
|
+
import {amqpFactory} from '../src/index.ts';
|
|
5
|
+
import type {AmqpOperator} from '../src/amqp.ts';
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
const debug = createDebugLogger('@natlibfi/melinda-rest-api-commons:amqp:test');
|
|
@@ -35,7 +36,11 @@ describe('HealthCheck: amqpOperator should error if channel or connection fails'
|
|
|
35
36
|
debug(`Closing connection`);
|
|
36
37
|
await amqpOperator.closeConnection();
|
|
37
38
|
|
|
38
|
-
|
|
39
|
+
/**
|
|
40
|
+
* @param amqpOperator
|
|
41
|
+
* @param close
|
|
42
|
+
*/
|
|
43
|
+
async function awaitTime(amqpOperator: AmqpOperator, close: boolean): Promise<void> {
|
|
39
44
|
if (close) {
|
|
40
45
|
// wrap the wait so any error thrown by the (cancelled) loop fails the test
|
|
41
46
|
await amqpOperator.closeChannel();
|
|
@@ -53,4 +58,3 @@ describe('HealthCheck: amqpOperator should error if channel or connection fails'
|
|
|
53
58
|
|
|
54
59
|
});
|
|
55
60
|
});
|
|
56
|
-
|
|
@@ -3,8 +3,11 @@ import createDebugLogger from 'debug';
|
|
|
3
3
|
import {promisify} from 'util';
|
|
4
4
|
import {READERS} from '@natlibfi/fixura';
|
|
5
5
|
import generateTests from '@natlibfi/fixugen';
|
|
6
|
+
import type {CallbackArgs} from '@natlibfi/fixugen';
|
|
6
7
|
import {Error as ApiError} from '@natlibfi/melinda-commons';
|
|
7
|
-
import {
|
|
8
|
+
import type {GetMessage, Message} from 'amqplib';
|
|
9
|
+
import {amqpFactory} from '../src/index.ts';
|
|
10
|
+
import type {AmqpOperator, QueueItemMessagePayload, QueueItemRecordPayload, SendToQueueParams} from '../src/amqp.ts';
|
|
8
11
|
|
|
9
12
|
const setTimeoutPromise = promisify(setTimeout);
|
|
10
13
|
const debug = createDebugLogger('@natlibfi/melinda-rest-api-commons:amqp:test');
|
|
@@ -18,8 +21,7 @@ const testData = {
|
|
|
18
21
|
|
|
19
22
|
debug(`Testing amqp against a local AMQP instance - we'll need some kind of mockup for this`);
|
|
20
23
|
const amqpUrl = 'amqp://127.0.0.1:5672/';
|
|
21
|
-
|
|
22
|
-
let amqpOperator;
|
|
24
|
+
let amqpOperator: AmqpOperator;
|
|
23
25
|
|
|
24
26
|
generateTests({
|
|
25
27
|
callback,
|
|
@@ -48,8 +50,15 @@ generateTests({
|
|
|
48
50
|
}
|
|
49
51
|
});
|
|
50
52
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
+
/**
|
|
54
|
+
* One fixture case. See the fixture metadata files in test-fixtures/amqp
|
|
55
|
+
* for the possible keys (getFixture, expectToFail, expectedErrorStatus,
|
|
56
|
+
* ackMessages, nackMessages, checkNextMessage, checkQueueStyle, queue,
|
|
57
|
+
* sentToQueue, expectedMessageCount, removeQueue, expectedErrorMessage,
|
|
58
|
+
* expectApiError, expectFalseCheckResult).
|
|
59
|
+
*/
|
|
60
|
+
async function callback(callbackParams: CallbackArgs) {
|
|
61
|
+
const {
|
|
53
62
|
getFixture,
|
|
54
63
|
expectToFail = false,
|
|
55
64
|
expectedErrorStatus = '',
|
|
@@ -57,14 +66,16 @@ async function callback({
|
|
|
57
66
|
nackMessages = false,
|
|
58
67
|
checkNextMessage = false,
|
|
59
68
|
checkQueueStyle = false,
|
|
60
|
-
queue = testData.queue,
|
|
69
|
+
queue: queueFromFixture = testData.queue,
|
|
61
70
|
sentToQueue,
|
|
62
71
|
expectedMessageCount,
|
|
63
72
|
removeQueue = false,
|
|
64
73
|
expectedErrorMessage = undefined,
|
|
65
74
|
expectApiError = false,
|
|
66
75
|
expectFalseCheckResult = false
|
|
67
|
-
}
|
|
76
|
+
} = callbackParams;
|
|
77
|
+
// the fixture contract passes the queue name as a string
|
|
78
|
+
const queue = queueFromFixture as string;
|
|
68
79
|
try {
|
|
69
80
|
await sendMessagesToQueue();
|
|
70
81
|
|
|
@@ -79,7 +90,7 @@ async function callback({
|
|
|
79
90
|
assert.equal(messageCount, expectedMessageCount);
|
|
80
91
|
|
|
81
92
|
if (checkNextMessage) {
|
|
82
|
-
const {headers, records} = await queueCheckNextMessage();
|
|
93
|
+
const {headers, records} = (await queueCheckNextMessage()) as QueueItemRecordPayload;
|
|
83
94
|
const nextMessage = {headers, records};
|
|
84
95
|
const expectedNextMessage = getFixture('expectedMessage.json');
|
|
85
96
|
assert.deepStrictEqual(
|
|
@@ -97,15 +108,19 @@ async function callback({
|
|
|
97
108
|
testErrors(error);
|
|
98
109
|
}
|
|
99
110
|
|
|
100
|
-
|
|
111
|
+
/**
|
|
112
|
+
* @param error
|
|
113
|
+
*/
|
|
114
|
+
function testErrors(error: unknown) {
|
|
101
115
|
const isApiError = error instanceof ApiError;
|
|
102
116
|
if (expectApiError) {
|
|
103
117
|
assert.equal(isApiError, true);
|
|
104
118
|
debug(`We expected and got an ApiError`);
|
|
105
|
-
|
|
119
|
+
const apiError = error as ApiError;
|
|
120
|
+
assert.equal(apiError.status, expectedErrorStatus);
|
|
106
121
|
|
|
107
122
|
if (expectedErrorMessage) {
|
|
108
|
-
assert.equal(
|
|
123
|
+
assert.equal(apiError.payload, expectedErrorMessage);
|
|
109
124
|
return;
|
|
110
125
|
}
|
|
111
126
|
|
|
@@ -113,26 +128,26 @@ async function callback({
|
|
|
113
128
|
}
|
|
114
129
|
assert.equal(isApiError, false);
|
|
115
130
|
debug(`We expected and got an non-ApiError`);
|
|
116
|
-
|
|
131
|
+
const maybeError = error as Error;
|
|
132
|
+
debug(`Error is a ${maybeError.constructor.name}`);
|
|
117
133
|
}
|
|
118
134
|
|
|
119
135
|
function queueRemoval() {
|
|
120
136
|
if (removeQueue) {
|
|
121
|
-
return amqpOperator.removeQueue(removeQueue);
|
|
137
|
+
return amqpOperator.removeQueue(removeQueue as string);
|
|
122
138
|
}
|
|
123
139
|
|
|
124
140
|
return;
|
|
125
141
|
}
|
|
126
142
|
|
|
127
|
-
function queueCheckNextMessage() {
|
|
143
|
+
function queueCheckNextMessage(): Promise<QueueItemRecordPayload> | false {
|
|
128
144
|
if (checkNextMessage) {
|
|
129
|
-
return amqpOperator.consumeOne(testData.queue, true)
|
|
145
|
+
return amqpOperator.consumeOne(testData.queue, true) as Promise<QueueItemRecordPayload>;
|
|
130
146
|
}
|
|
131
147
|
|
|
132
148
|
return false;
|
|
133
149
|
}
|
|
134
150
|
|
|
135
|
-
// eslint-disable-next-line max-statements
|
|
136
151
|
async function checkQueue() {
|
|
137
152
|
debug(`checkQueueStyle: ${checkQueueStyle}`);
|
|
138
153
|
|
|
@@ -144,18 +159,19 @@ async function callback({
|
|
|
144
159
|
return;
|
|
145
160
|
}
|
|
146
161
|
|
|
147
|
-
|
|
148
|
-
assert.equal(Object.hasOwn(
|
|
149
|
-
assert.equal(Object.hasOwn(
|
|
150
|
-
assert.equal(
|
|
151
|
-
assert.equal(
|
|
162
|
+
const recordResult = result as QueueItemRecordPayload;
|
|
163
|
+
assert.equal(Object.hasOwn(recordResult, 'headers'), true);
|
|
164
|
+
assert.equal(Object.hasOwn(recordResult, 'records'), true);
|
|
165
|
+
assert.equal(Object.hasOwn(recordResult, 'messages'), true);
|
|
166
|
+
assert.equal(recordResult.records.length, 1);
|
|
167
|
+
assert.equal(recordResult.messages.length, 1);
|
|
152
168
|
|
|
153
169
|
return;
|
|
154
170
|
}
|
|
155
171
|
|
|
156
172
|
if (checkQueueStyle === 'oneNotRecord') {
|
|
157
173
|
const result = await amqpOperator.checkQueue({queue, style: 'one', toRecord: false, purge: false});
|
|
158
|
-
const {fields} = result;
|
|
174
|
+
const {fields} = result as GetMessage;
|
|
159
175
|
|
|
160
176
|
if (expectFalseCheckResult) {
|
|
161
177
|
assert.strictEqual(result, false);
|
|
@@ -178,14 +194,15 @@ async function callback({
|
|
|
178
194
|
return;
|
|
179
195
|
}
|
|
180
196
|
|
|
181
|
-
|
|
182
|
-
assert.equal(Object.hasOwn(
|
|
183
|
-
assert.equal(Object.hasOwn(
|
|
184
|
-
assert.equal(
|
|
185
|
-
assert.equal(
|
|
197
|
+
const recordResult = result as QueueItemRecordPayload;
|
|
198
|
+
assert.equal(Object.hasOwn(recordResult, 'headers'), true);
|
|
199
|
+
assert.equal(Object.hasOwn(recordResult, 'records'), true);
|
|
200
|
+
assert.equal(Object.hasOwn(recordResult, 'messages'), true);
|
|
201
|
+
assert.equal(recordResult.records.length, 3);
|
|
202
|
+
assert.equal(recordResult.messages.length, 3);
|
|
186
203
|
const expectedResult = getFixture('expectedResult.json');
|
|
187
204
|
assert.deepStrictEqual(
|
|
188
|
-
JSON.parse(JSON.stringify(
|
|
205
|
+
JSON.parse(JSON.stringify(recordResult.records)),
|
|
189
206
|
expectedResult,
|
|
190
207
|
);
|
|
191
208
|
|
|
@@ -200,11 +217,13 @@ async function callback({
|
|
|
200
217
|
return;
|
|
201
218
|
}
|
|
202
219
|
|
|
203
|
-
|
|
204
|
-
assert.equal(Object.hasOwn(
|
|
205
|
-
assert.equal(
|
|
220
|
+
const messageResult = result as QueueItemMessagePayload;
|
|
221
|
+
assert.equal(Object.hasOwn(messageResult, 'headers'), true);
|
|
222
|
+
assert.equal(Object.hasOwn(messageResult, 'messages'), true);
|
|
223
|
+
assert.equal(messageResult.messages.length, 3);
|
|
206
224
|
|
|
207
|
-
const [
|
|
225
|
+
const [firstMessage] = messageResult.messages as [Message, ...Message[]];
|
|
226
|
+
const {fields} = firstMessage;
|
|
208
227
|
|
|
209
228
|
assert.equal(typeof fields.deliveryTag, 'number');
|
|
210
229
|
assert.equal(typeof fields.messageCount, 'number');
|
|
@@ -228,14 +247,14 @@ async function callback({
|
|
|
228
247
|
async function ackOrNackMessages() {
|
|
229
248
|
if (ackMessages) {
|
|
230
249
|
const message = await amqpOperator.consumeOne(testData.queue, false);
|
|
231
|
-
await amqpOperator.ackMessages([message]);
|
|
250
|
+
await amqpOperator.ackMessages([message as GetMessage]);
|
|
232
251
|
|
|
233
252
|
return setTimeoutPromise(10);
|
|
234
253
|
}
|
|
235
254
|
|
|
236
255
|
if (nackMessages) {
|
|
237
256
|
const message = await amqpOperator.consumeOne(testData.queue, false);
|
|
238
|
-
await amqpOperator.nackMessages([message]);
|
|
257
|
+
await amqpOperator.nackMessages([message as GetMessage]);
|
|
239
258
|
|
|
240
259
|
return setTimeoutPromise(10);
|
|
241
260
|
}
|
|
@@ -245,7 +264,7 @@ async function callback({
|
|
|
245
264
|
|
|
246
265
|
async function sendMessagesToQueue() {
|
|
247
266
|
if (sentToQueue) {
|
|
248
|
-
const messages = getFixture('input.json');
|
|
267
|
+
const messages = getFixture('input.json') as SendToQueueParams[];
|
|
249
268
|
// {queue, correlationId, headers, data}
|
|
250
269
|
|
|
251
270
|
const promises = messages.map(message => amqpOperator.sendToQueue(message));
|