@natlibfi/melinda-rest-api-commons 6.0.1 → 6.1.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.
@@ -0,0 +1,535 @@
1
+ import assert from 'node:assert';
2
+ import {test} from 'node:test';
3
+ import {execFile} from 'node:child_process';
4
+ import {fileURLToPath} from 'node:url';
5
+ import {Error as ApiError} from '@natlibfi/melinda-commons';
6
+ import createAmqp from '../src/amqp.js';
7
+
8
+ const AMQP_MODULE_PATH = fileURLToPath(new URL('../src/amqp.js', import.meta.url));
9
+
10
+ const DUMMY_URL = 'amqp://stub:5672/';
11
+ const QUEUE = 'TESTQUEUE';
12
+
13
+ function makeMessage(correlationId, deliveryTag) {
14
+ const data = {leader: '00000nam a22000000i 4500', fields: [{tag: '001', value: '123'}]};
15
+ const content = JSON.stringify({data});
16
+ return {
17
+ content: Buffer.from(content),
18
+ properties: {correlationId, headers: {from: 'TEST'}},
19
+ fields: {deliveryTag}
20
+ };
21
+ }
22
+
23
+ function makeChannel(overrides = {}) {
24
+ return {
25
+ assertQueue: async () => ({messageCount: 0}),
26
+ checkQueue: async () => ({messageCount: 0}),
27
+ get: async () => false,
28
+ ack: () => {},
29
+ nack: () => {},
30
+ purgeQueue: async () => {},
31
+ close: async () => {},
32
+ sendToQueue: async () => {},
33
+ ...overrides
34
+ };
35
+ }
36
+
37
+ async function makeOperator(channelOverrides) {
38
+ const channel = makeChannel(channelOverrides);
39
+ const connection = {createChannel: async () => channel, close: async () => {}};
40
+ return createAmqp(DUMMY_URL, false, {connection, channel});
41
+ }
42
+
43
+ test('consumeChunk returns false when queue drains mid-pump', async () => {
44
+ // (src/amqp.js, getData/pump):
45
+ // FIXED: pump now bails out (returns results) when channel.get -> null,
46
+ // and consumeChunk maps an empty pump result to false, matching the
47
+ // empty-queue contract of checkQueue. Previously this crashed with
48
+ // TypeError: Cannot read properties of null (reading 'properties').
49
+ const amqpOperator = await makeOperator({
50
+ checkQueue: async () => ({messageCount: 1}),
51
+ get: async () => null
52
+ });
53
+ const result = await amqpOperator.consumeChunk(QUEUE, false);
54
+ assert.strictEqual(result, false);
55
+ });
56
+
57
+ test('consumeChunk returns partial chunk when queue drains after first message', async () => {
58
+ // Drains after one of two messages: pump must stop gracefully and
59
+ // consumeChunk must return the one message it got.
60
+ const first = makeMessage('c1', 1);
61
+ let drained = false;
62
+ const amqpOperator = await makeOperator({
63
+ checkQueue: async () => ({messageCount: 2}),
64
+ get: async () => {
65
+ if (drained) return null;
66
+ drained = true;
67
+ return first;
68
+ }
69
+ });
70
+ const {records, messages} = await amqpOperator.consumeChunk(QUEUE, true);
71
+ assert.equal(messages.length, 1);
72
+ assert.equal(records.length, 1);
73
+ });
74
+
75
+
76
+ test('consumeChunk deduplicates messages with same correlationId + deliveryTag', async () => {
77
+ // (src/amqp.js, getData/pump):
78
+ // FIXED: dedup now compares correlationId + deliveryTag field-by-field,
79
+ // so duplicate messages are filtered out. (Previously the includes()
80
+ // check compared object references and never triggered.)
81
+ const duplicate = makeMessage('c1', 1);
82
+ const amqpOperator = await makeOperator({
83
+ checkQueue: async () => ({messageCount: 2}),
84
+ get: async () => duplicate
85
+ });
86
+ const {records, messages} = await amqpOperator.consumeChunk(QUEUE, true);
87
+ assert.equal(messages.length, 1);
88
+ assert.equal(records.length, 1);
89
+ });
90
+
91
+ test('same deliveryTag with different correlationIds is NOT deduplicated', async () => {
92
+ // Dedup key is correlationId + deliveryTag; a redelivery of a message
93
+ // with a new correlationId must still come through.
94
+ const messages = [makeMessage('c1', 1), makeMessage('c2', 1)];
95
+ const amqpOperator = await makeOperator({
96
+ checkQueue: async () => ({messageCount: 2}),
97
+ get: async () => messages.shift()
98
+ });
99
+ const {messages: got} = await amqpOperator.consumeChunk(QUEUE, true);
100
+ assert.equal(got.length, 2);
101
+ });
102
+
103
+ test('removeQueue closes the temp channel even when deleteQueue throws', async () => {
104
+ // (src/amqp.js, removeQueue):
105
+ // FIXED: the throwaway tempChannel is now closed in a finally block, so a
106
+ // missing queue (deleteQueue throws) no longer leaks a channel.
107
+ const closedChannels = [];
108
+ const tempChannel = {
109
+ deleteQueue: async () => {
110
+ throw new Error('NOT_FOUND - queue missing');
111
+ },
112
+ close: async () => {
113
+ // eslint-disable-next-line functional/immutable-data
114
+ closedChannels.push('temp');
115
+ }
116
+ };
117
+ const amqpOperator = await createAmqp(DUMMY_URL, false, {
118
+ connection: {createChannel: async () => tempChannel, close: async () => {}},
119
+ channel: makeChannel()
120
+ });
121
+ await assert.rejects(() => amqpOperator.removeQueue(QUEUE));
122
+ assert.deepStrictEqual(closedChannels, ['temp']);
123
+ });
124
+
125
+ test('removeQueue closes the temp channel on success too', async () => {
126
+ const closedChannels = [];
127
+ const amqpOperator = await createAmqp(DUMMY_URL, false, {
128
+ connection: {
129
+ createChannel: async () => ({
130
+ deleteQueue: async () => {},
131
+ close: async () => {
132
+ // eslint-disable-next-line functional/immutable-data
133
+ closedChannels.push('temp');
134
+ }
135
+ }),
136
+ close: async () => {}
137
+ },
138
+ channel: makeChannel()
139
+ });
140
+ await amqpOperator.removeQueue(QUEUE);
141
+ assert.deepStrictEqual(closedChannels, ['temp']);
142
+ });
143
+
144
+ test('healthCheck loop stops after closeChannel and does not keep firing or throw', async () => {
145
+ // (src/amqp.js, healthCheck):
146
+ // FIXED: closeChannel/closeConnection cancel the recursive healthCheck loop,
147
+ // so the ~200 ms chain short-circuits instead of running until the closed
148
+ // channel throws (which used to make graceful shutdown flaky).
149
+ // The stub only starts throwing for the HEALTHCHECK queue once it is
150
+ // closed, so the un-cancelled iterations before the close succeed.
151
+ let closed = false;
152
+ let healthCheckCalls = 0;
153
+ const channel = {
154
+ assertQueue: async (name) => {
155
+ if (name === 'HEALTHCHECK') {
156
+ // eslint-disable-next-line functional/immutable-data
157
+ healthCheckCalls += 1;
158
+ if (closed) {
159
+ throw new Error('channel closed');
160
+ }
161
+ }
162
+ return {messageCount: 0};
163
+ },
164
+ checkQueue: async () => ({messageCount: 0}),
165
+ get: async () => false,
166
+ ack: () => {},
167
+ nack: () => {},
168
+ purgeQueue: async () => {},
169
+ close: async () => {
170
+ // eslint-disable-next-line functional/immutable-data
171
+ closed = true;
172
+ }
173
+ };
174
+ const amqpOperator = await createAmqp(DUMMY_URL, true, {
175
+ connection: {createChannel: async () => channel, close: async () => {}},
176
+ channel
177
+ });
178
+ // let the health check run at least once, then cancel it
179
+ await new Promise((resolve) => setTimeout(resolve, 350));
180
+ await amqpOperator.closeChannel();
181
+ const callsAfterClose = healthCheckCalls;
182
+ // wait longer than the 200 ms loop interval - no further health checks may fire
183
+ await new Promise((resolve) => setTimeout(resolve, 500));
184
+ assert.ok(healthCheckCalls > 0, 'health check should have run before close');
185
+ assert.equal(healthCheckCalls, callsAfterClose, 'no health checks after closeChannel');
186
+ });
187
+
188
+ test('happy path: consumeChunk returns two distinct messages', async () => {
189
+ const messages = [
190
+ makeMessage('c1', 1),
191
+ makeMessage('c2', 2)
192
+ ];
193
+ const amqpOperator = await makeOperator({
194
+ checkQueue: async () => ({messageCount: messages.length}),
195
+ get: async () => messages.shift()
196
+ });
197
+ const {headers, records, messages: got} = await amqpOperator.consumeChunk(QUEUE, true);
198
+ assert.equal(got.length, 2);
199
+ assert.equal(records.length, 2);
200
+ assert.deepStrictEqual(headers, {from: 'TEST'});
201
+ });
202
+
203
+ // ---------------------------------------------------------------------------
204
+ // checkQueue styles
205
+ // ---------------------------------------------------------------------------
206
+
207
+ test('checkQueue style messages returns the message count', async () => {
208
+ for (const count of [0, 3, 42]) {
209
+ const amqpOperator = await makeOperator({assertQueue: async () => ({messageCount: count})});
210
+ const result = await amqpOperator.checkQueue({queue: QUEUE, style: 'messages'});
211
+ assert.strictEqual(result, count);
212
+ }
213
+ });
214
+
215
+ test('checkQueue style one returns the {headers, records, messages} form when toRecord is true', async () => {
216
+ const message = makeMessage('c1', 1);
217
+ const amqpOperator = await makeOperator({
218
+ assertQueue: async () => ({messageCount: 1}),
219
+ get: async () => message
220
+ });
221
+ const {headers, records, messages} = await amqpOperator.checkQueue({queue: QUEUE, style: 'one', toRecord: true});
222
+ assert.deepStrictEqual(headers, {from: 'TEST'});
223
+ assert.equal(records.length, 1);
224
+ assert.strictEqual(messages.length, 1);
225
+ assert.strictEqual(messages[0], message);
226
+ });
227
+
228
+ test('checkQueue style one returns the plain message when toRecord is false', async () => {
229
+ const message = makeMessage('c1', 1);
230
+ const amqpOperator = await makeOperator({
231
+ assertQueue: async () => ({messageCount: 1}),
232
+ get: async () => message
233
+ });
234
+ const result = await amqpOperator.checkQueue({queue: QUEUE, style: 'one', toRecord: false});
235
+ assert.strictEqual(result, message);
236
+ });
237
+
238
+ test('checkQueue style one returns false for an empty queue', async () => {
239
+ const amqpOperator = await makeOperator({
240
+ assertQueue: async () => ({messageCount: 0}),
241
+ get: async () => false
242
+ });
243
+ const result = await amqpOperator.checkQueue({queue: QUEUE, style: 'one'});
244
+ assert.strictEqual(result, false);
245
+ });
246
+
247
+ test('checkQueue style basic returns false for an empty queue', async () => {
248
+ const amqpOperator = await makeOperator({assertQueue: async () => ({messageCount: 0})});
249
+ const result = await amqpOperator.checkQueue({queue: QUEUE, style: 'basic'});
250
+ assert.strictEqual(result, false);
251
+ });
252
+
253
+ test('checkQueue style basic returns a chunk of records and messages when the queue is not empty', async () => {
254
+ const messages = [makeMessage('c1', 1), makeMessage('c2', 2)];
255
+ const amqpOperator = await makeOperator({
256
+ assertQueue: async () => ({messageCount: messages.length}),
257
+ checkQueue: async () => ({messageCount: messages.length}),
258
+ get: async () => messages.shift()
259
+ });
260
+ const {headers, records, messages: got} = await amqpOperator.checkQueue({queue: QUEUE, style: 'basic', toRecord: true});
261
+ assert.deepStrictEqual(headers, {from: 'TEST'});
262
+ assert.equal(records.length, 2);
263
+ assert.equal(got.length, 2);
264
+ });
265
+
266
+ test('checkQueue rejects with a 500 ApiError for an unknown style', async () => {
267
+ const amqpOperator = await makeOperator({assertQueue: async () => ({messageCount: 0})});
268
+ await assert.rejects(
269
+ async () => amqpOperator.checkQueue({queue: QUEUE, style: 'unknown-style'}),
270
+ (err) => {
271
+ assert.ok(err instanceof ApiError);
272
+ assert.equal(err.status, 500);
273
+ return true;
274
+ }
275
+ );
276
+ });
277
+
278
+ test('checkQueue purges the queue before returning the style result', async () => {
279
+ const purgeCalls = [];
280
+ let assertCalls = 0;
281
+ const amqpOperator = await makeOperator({
282
+ assertQueue: async () => {
283
+ // eslint-disable-next-line functional/immutable-data
284
+ assertCalls += 1;
285
+ // first call (before the purge) reports 3 pending, after the purge 0
286
+ return assertCalls === 1 ? {messageCount: 3} : {messageCount: 0};
287
+ },
288
+ purgeQueue: async (name) => {
289
+ // eslint-disable-next-line functional/immutable-data
290
+ purgeCalls.push(name);
291
+ }
292
+ });
293
+ const result = await amqpOperator.checkQueue({queue: QUEUE, style: 'messages', purge: true});
294
+ assert.deepStrictEqual(purgeCalls, [QUEUE]);
295
+ assert.strictEqual(result, 0);
296
+ });
297
+
298
+ // ---------------------------------------------------------------------------
299
+ // consumeOne
300
+ // ---------------------------------------------------------------------------
301
+
302
+ test('consumeOne returns the plain message when toRecord is false', async () => {
303
+ const message = makeMessage('c1', 1);
304
+ const amqpOperator = await makeOperator({get: async () => message});
305
+ const result = await amqpOperator.consumeOne(QUEUE, false);
306
+ assert.strictEqual(result, message);
307
+ });
308
+
309
+ test('consumeOne returns the {headers, records, messages} form when toRecord is true', async () => {
310
+ const message = makeMessage('c1', 1);
311
+ const amqpOperator = await makeOperator({get: async () => message});
312
+ const {headers, records, messages} = await amqpOperator.consumeOne(QUEUE, true);
313
+ assert.deepStrictEqual(headers, {from: 'TEST'});
314
+ assert.equal(records.length, 1);
315
+ assert.strictEqual(messages.length, 1);
316
+ assert.strictEqual(messages[0], message);
317
+ });
318
+
319
+ test('consumeOne returns false when the queue is empty', async () => {
320
+ const amqpOperator = await makeOperator({get: async () => false});
321
+ const result = await amqpOperator.consumeOne(QUEUE, true);
322
+ assert.strictEqual(result, false);
323
+ });
324
+
325
+ test('consumeOne rejects when channel.get throws a plain Error', async () => {
326
+ const amqpOperator = await makeOperator({
327
+ get: async () => {
328
+ throw new Error('get exploded');
329
+ }
330
+ });
331
+ await assert.rejects(async () => amqpOperator.consumeOne(QUEUE, true), /get exploded/);
332
+ });
333
+
334
+ // ---------------------------------------------------------------------------
335
+ // consumeChunk extra branches
336
+ // ---------------------------------------------------------------------------
337
+
338
+ test('consumeChunk returns {headers, messages} without records when toRecord is false', async () => {
339
+ const messages = [makeMessage('c1', 1), makeMessage('c2', 2)];
340
+ const amqpOperator = await makeOperator({
341
+ checkQueue: async () => ({messageCount: messages.length}),
342
+ get: async () => messages.shift()
343
+ });
344
+ const {headers, messages: got} = await amqpOperator.consumeChunk(QUEUE, false);
345
+ assert.deepStrictEqual(headers, {from: 'TEST'});
346
+ assert.equal(got.length, 2);
347
+ });
348
+
349
+ test('consumeChunk rejects when the amqplib checkQueue throws a plain Error', async () => {
350
+ const amqpOperator = await makeOperator({
351
+ checkQueue: async () => {
352
+ throw new Error('count exploded');
353
+ }
354
+ });
355
+ await assert.rejects(async () => amqpOperator.consumeChunk(QUEUE, true), /count exploded/);
356
+ });
357
+
358
+ // ---------------------------------------------------------------------------
359
+ // ackMessages / nackMessages
360
+ // ---------------------------------------------------------------------------
361
+
362
+ test('ackMessages acks every message on the channel', async () => {
363
+ const acked = [];
364
+ const messages = [makeMessage('c1', 1), makeMessage('c2', 2)];
365
+ const amqpOperator = await makeOperator({
366
+ ack: (message) => {
367
+ // eslint-disable-next-line functional/immutable-data
368
+ acked.push(message);
369
+ }
370
+ });
371
+ amqpOperator.ackMessages(messages);
372
+ assert.deepStrictEqual(acked, messages);
373
+ });
374
+
375
+ test('nackMessages nacks every message on the channel', async () => {
376
+ const nacked = [];
377
+ const messages = [makeMessage('c1', 1), makeMessage('c2', 2)];
378
+ const amqpOperator = await makeOperator({
379
+ nack: (message) => {
380
+ // eslint-disable-next-line functional/immutable-data
381
+ nacked.push(message);
382
+ }
383
+ });
384
+ amqpOperator.nackMessages(messages);
385
+ assert.deepStrictEqual(nacked, messages);
386
+ });
387
+
388
+ // ---------------------------------------------------------------------------
389
+ // sendToQueue
390
+ // ---------------------------------------------------------------------------
391
+
392
+ test('sendToQueue sends the JSON body with correlationId, persistent and headers options', async () => {
393
+ const sent = [];
394
+ const amqpOperator = await makeOperator({
395
+ sendToQueue: async (queue, content, options) => {
396
+ // eslint-disable-next-line functional/immutable-data
397
+ sent.push({queue, content, options});
398
+ }
399
+ });
400
+ const data = {title: 'Test record', tags: ['a', 'b']};
401
+ await amqpOperator.sendToQueue({
402
+ queue: QUEUE,
403
+ correlationId: 'cid-1',
404
+ headers: {h: 1},
405
+ data
406
+ });
407
+ assert.equal(sent.length, 1);
408
+ assert.equal(sent[0].queue, QUEUE);
409
+ assert.ok(Buffer.isBuffer(sent[0].content));
410
+ assert.deepStrictEqual(JSON.parse(sent[0].content.toString()), {data});
411
+ assert.deepStrictEqual(sent[0].options, {correlationId: 'cid-1', persistent: true, headers: {h: 1}});
412
+ });
413
+
414
+ test('sendToQueue rejects with a 500 ApiError when the queue is undefined', async () => {
415
+ const amqpOperator = await makeOperator();
416
+ await assert.rejects(
417
+ async () => amqpOperator.sendToQueue({queue: undefined, correlationId: 'cid', data: {}}),
418
+ (err) => {
419
+ assert.ok(err instanceof ApiError);
420
+ assert.equal(err.status, 500);
421
+ assert.equal(err.payload, 'Undefined queue!');
422
+ return true;
423
+ }
424
+ );
425
+ });
426
+
427
+ test('checkQueue rejects with a 500 ApiError when the queue is an empty string', async () => {
428
+ const amqpOperator = await makeOperator();
429
+ await assert.rejects(
430
+ async () => amqpOperator.checkQueue({queue: '', style: 'basic'}),
431
+ (err) => {
432
+ assert.ok(err instanceof ApiError);
433
+ assert.equal(err.status, 500);
434
+ assert.equal(err.payload, 'Undefined queue!');
435
+ return true;
436
+ }
437
+ );
438
+ });
439
+
440
+ // ---------------------------------------------------------------------------
441
+ // closeConnection / healthCheck error handling
442
+ // ---------------------------------------------------------------------------
443
+
444
+ test('closeConnection closes the connection and the operator resolves', async () => {
445
+ const closed = [];
446
+ const channel = makeChannel();
447
+ const amqpOperator = await createAmqp(DUMMY_URL, false, {
448
+ connection: {
449
+ createChannel: async () => channel,
450
+ close: async () => {
451
+ // eslint-disable-next-line functional/immutable-data
452
+ closed.push('connection');
453
+ }
454
+ },
455
+ channel
456
+ });
457
+ await amqpOperator.closeConnection();
458
+ assert.deepStrictEqual(closed, ['connection']);
459
+ });
460
+
461
+ test('healthCheck error arriving after cancel does not rethrow and stops the loop', async () => {
462
+ let rejectAssert;
463
+ const pending = new Promise((_, reject) => {
464
+ // eslint-disable-next-line functional/immutable-data
465
+ rejectAssert = reject;
466
+ });
467
+ let healthCheckCalls = 0;
468
+ const channel = makeChannel({
469
+ assertQueue: async (name) => {
470
+ if (name === 'HEALTHCHECK') {
471
+ // eslint-disable-next-line functional/immutable-data
472
+ healthCheckCalls += 1;
473
+ return pending;
474
+ }
475
+ return {messageCount: 0};
476
+ }
477
+ });
478
+ const amqpOperator = await createAmqp(DUMMY_URL, true, {
479
+ connection: {createChannel: async () => channel, close: async () => {}},
480
+ channel
481
+ });
482
+ // by now the loop is suspended on the pending assertQueue call;
483
+ // cancel first, then let the error arrive so the catch sees the flag already cleared
484
+ await amqpOperator.closeChannel();
485
+ rejectAssert(new Error('amqp down after close'));
486
+ await new Promise((resolve) => setTimeout(resolve, 50));
487
+ assert.equal(healthCheckCalls, 1, 'no further health checks after cancel');
488
+ });
489
+
490
+ test('healthCheck error while running rethrows into the un-awaited loop promise', async () => {
491
+ // When assertQueue rejects while healthCheckRunning is still true, the catch
492
+ // rethrows via handleAmqpErrors into the loop promise, which is never
493
+ // awaited -> the rejection escapes as an unhandled rejection.
494
+ // The node test runner fails a test on any in-process unhandled rejection,
495
+ // so the escape is verified in a child node process that installs its own
496
+ // unhandledRejection handler before triggering the error.
497
+ const script = `
498
+ import createAmqp from '${AMQP_MODULE_PATH}';
499
+ const channel = {
500
+ assertQueue: async (name) => {
501
+ if (name === 'HEALTHCHECK') {
502
+ throw new Error('amqp down');
503
+ }
504
+ return {messageCount: 0};
505
+ },
506
+ checkQueue: async () => ({messageCount: 0}),
507
+ get: async () => false,
508
+ ack: () => {},
509
+ nack: () => {},
510
+ purgeQueue: async () => {},
511
+ close: async () => {},
512
+ sendToQueue: async () => {}
513
+ };
514
+ const captured = [];
515
+ process.on('unhandledRejection', (err) => captured.push(err));
516
+ await createAmqp('amqp://stub:5672/', true, {
517
+ connection: {createChannel: async () => channel, close: async () => {}},
518
+ channel
519
+ });
520
+ // the first loop iteration rejects and the error escapes the un-awaited loop promise
521
+ await new Promise((resolve) => setTimeout(resolve, 50));
522
+ const ok = captured.length === 1 && /amqp down/.test(String(captured[0] && captured[0].message));
523
+ process.stdout.write(ok ? 'OK' : 'FAIL:' + JSON.stringify(captured.map((e) => String(e && e.message))));
524
+ process.exit(ok ? 0 : 1);
525
+ `;
526
+ const {code, stdout} = await new Promise((resolve) => {
527
+ const child = execFile(process.execPath, ['--input-type=module', '-e', script], (err, out) => {
528
+ resolve({code: err ? (err.code ?? 1) : 0, stdout: out ?? ''});
529
+ });
530
+ void child;
531
+ });
532
+ assert.equal(code, 0, `child process output: ${stdout}`);
533
+ // handleAmqpErrors logs the error to stdout before the escape, so only the tail matters
534
+ assert.ok(stdout.endsWith('OK'), `expected child output to end with OK, got: ${stdout}`);
535
+ });
@@ -1,4 +1,5 @@
1
- //import assert from 'node:assert';
1
+ import {describe, it} from 'node:test';
2
+ import assert from 'node:assert';
2
3
  import createDebugLogger from 'debug';
3
4
  import {amqpFactory} from '../src/index.js';
4
5
 
@@ -25,17 +26,24 @@ describe('HealthCheck: amqpOperator should error if channel or connection fails'
25
26
  debug(`Amqp Operator should have been alive.`);
26
27
 
27
28
  debug(`Testing healthCheckLoop with closed channel`);
28
- debug(`Waiting 0.5s and expecting healthCheckLoop to throw error`);
29
+ debug(`Waiting 0.5s and expecting the healthCheckLoop to stop silently (no error)`);
29
30
  await awaitTime(amqpOperator, true);
30
- debug(`Amqp Operator should have been errored.`);
31
+ // closeChannel() sets the run flag so the loop short-circuits gracefully:
32
+ // it must stop silently without throwing or firing further health checks
33
+ assert.ok(true, 'healthCheck loop stopped without error');
31
34
 
32
35
  debug(`Closing connection`);
33
36
  await amqpOperator.closeConnection();
34
37
 
35
38
  async function awaitTime(amqpOperator, close) {
36
39
  if (close) {
40
+ // wrap the wait so any error thrown by the (cancelled) loop fails the test
37
41
  await amqpOperator.closeChannel();
38
- await setTimeoutPromise(500);
42
+ try {
43
+ await setTimeoutPromise(500);
44
+ } catch (error) {
45
+ assert.fail(`healthCheck loop threw after closeChannel: ${error}`);
46
+ }
39
47
  return;
40
48
  }
41
49
 
@@ -82,7 +82,10 @@ async function callback({
82
82
  const {headers, records} = await queueCheckNextMessage();
83
83
  const nextMessage = {headers, records};
84
84
  const expectedNextMessage = getFixture('expectedMessage.json');
85
- assert.equal(nextMessage, expectedNextMessage);
85
+ assert.deepStrictEqual(
86
+ JSON.parse(JSON.stringify(nextMessage)),
87
+ expectedNextMessage,
88
+ );
86
89
  return;
87
90
  }
88
91
 
@@ -137,7 +140,7 @@ async function callback({
137
140
  const result = await amqpOperator.checkQueue({queue, style: 'one', toRecord: true, purge: false});
138
141
 
139
142
  if (expectFalseCheckResult) {
140
- expect(result).to.eql(false);
143
+ assert.strictEqual(result, false);
141
144
  return;
142
145
  }
143
146
 
@@ -155,15 +158,10 @@ async function callback({
155
158
  const {fields} = result;
156
159
 
157
160
  if (expectFalseCheckResult) {
158
- expect(result).to.eql(false);
161
+ assert.strictEqual(result, false);
159
162
  return;
160
163
  }
161
164
 
162
- assert.equal(Object.hasOwn(result, 'deliveryTag'), true);
163
- assert.equal(Object.hasOwn(result, 'exchange'), true);
164
- assert.equal(Object.hasOwn(result, 'messageCount'), true);
165
- assert.equal(Object.hasOwn(result, 'redelivered'), true);
166
- assert.equal(Object.hasOwn(result, 'routingKey'), true);
167
165
  assert.equal(typeof fields.deliveryTag, 'number');
168
166
  assert.equal(typeof fields.messageCount, 'number');
169
167
  assert.equal(typeof fields.redelivered, 'boolean');
@@ -176,7 +174,7 @@ async function callback({
176
174
  const result = await amqpOperator.checkQueue({queue, style: 'basic', toRecord: true, purge: false});
177
175
 
178
176
  if (expectFalseCheckResult) {
179
- expect(result).to.eql(false);
177
+ assert.strictEqual(result, false);
180
178
  return;
181
179
  }
182
180
 
@@ -186,7 +184,10 @@ async function callback({
186
184
  assert.equal(result.records.length, 3);
187
185
  assert.equal(result.messages.length, 3);
188
186
  const expectedResult = getFixture('expectedResult.json');
189
- assert.deepStrictEqual(result.records, expectedResult);
187
+ assert.deepStrictEqual(
188
+ JSON.parse(JSON.stringify(result.records)),
189
+ expectedResult,
190
+ );
190
191
 
191
192
  return;
192
193
  }
@@ -195,7 +196,7 @@ async function callback({
195
196
  const result = await amqpOperator.checkQueue({queue, style: 'basic', toRecord: false, purge: false});
196
197
 
197
198
  if (expectFalseCheckResult) {
198
- expect(result).to.eql(false);
199
+ assert.strictEqual(result, false);
199
200
  return;
200
201
  }
201
202
 
@@ -205,12 +206,6 @@ async function callback({
205
206
 
206
207
  const [{fields}] = result.messages;
207
208
 
208
- assert.equal(Object.hasOwn(result, 'deliveryTag'), true);
209
- assert.equal(Object.hasOwn(result, 'exchange'), true);
210
- assert.equal(Object.hasOwn(result, 'messageCount'), true);
211
- assert.equal(Object.hasOwn(result, 'redelivered'), true);
212
- assert.equal(Object.hasOwn(result, 'routingKey'), true);
213
-
214
209
  assert.equal(typeof fields.deliveryTag, 'number');
215
210
  assert.equal(typeof fields.messageCount, 'number');
216
211
  assert.equal(typeof fields.redelivered, 'boolean');
@@ -277,7 +272,7 @@ function testHeathCheck() {
277
272
  await testHealthCheckLoopErroring(amqpOperator2);
278
273
  } catch (error) {
279
274
  debug(error);
280
- expect(error).to.eql('');
275
+ assert.strictEqual(error, '');
281
276
  } finally {
282
277
  amqpOperator2.closeConnection();
283
278
  }
@@ -65,6 +65,22 @@ describe('services/conversion', () => {
65
65
  assert.equal(data, alephSeq);
66
66
  });
67
67
 
68
+ it('Should rewrap a plain (non-ConversionError) serializer error into a ConversionError with 400', () => {
69
+ // a plain {} makes MARCXML.to throw a TypeError, which serialize must
70
+ // rewrap into a ConversionError 400 (not propagate the raw TypeError)
71
+ // eslint-disable-next-line functional/no-let
72
+ let threw = false;
73
+ try {
74
+ conversionService.serialize({}, CONVERSION_FORMATS.MARCXML);
75
+ } catch (error) {
76
+ threw = true;
77
+ assert(error instanceof ConversionError);
78
+ assert.equal(error.status, 400);
79
+ assert.ok(error.payload.includes('Error while serializing record.'));
80
+ }
81
+ assert.equal(threw, true);
82
+ });
83
+
68
84
  });
69
85
 
70
86
  describe('#unserialize', () => {