@onify/fake-amqplib 3.4.0 → 3.6.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 (5) hide show
  1. package/README.md +76 -44
  2. package/index.d.ts +70 -25
  3. package/index.js +94 -3
  4. package/main.cjs +96 -3
  5. package/package.json +22 -11
package/README.md CHANGED
@@ -4,6 +4,18 @@
4
4
 
5
5
  Mocked version of https://www.npmjs.com/package/amqplib.
6
6
 
7
+ <!-- toc -->
8
+
9
+ - [Fake api](#fake-api)
10
+ - [RabbitMQ versions](#rabbitmq-versions)
11
+ - [Mocking amqplib](#mocking-amqplib)
12
+ - [ESM](#esm)
13
+ - [Node 20+ — `node:test` `mock.module` (recommended)](#node-20-nodetest-mockmodule-recommended)
14
+ - [Alternative — Quibble](#alternative-quibble)
15
+ - [CommonJS](#commonjs)
16
+
17
+ <!-- /toc -->
18
+
7
19
  ## Fake api
8
20
 
9
21
  - `async connect(amqpurl[, ...otherOptions, callback])`: wait for a fake connection or expect one in the callback
@@ -38,46 +50,62 @@ var fakeAmqp = require('@onify/fake-amqplib');
38
50
 
39
51
  You might want to override `amqplib` with `@onify/fake-amqplib` in tests. This can be done in a number of ways.
40
52
 
41
- ### CommonJS
53
+ ### ESM
42
54
 
43
- Example on how to mock amqplib when working with commonjs.
55
+ Example on how to mock the `amqplib` import when working with modules.
44
56
 
45
- ```js
46
- const amqplib = require('amqplib');
47
- const fakeAmqp = require('@onify/fake-amqplib');
57
+ #### Node 20+ — `node:test` `mock.module` (recommended)
48
58
 
49
- amqplib.connect = fakeAmqp.connect;
59
+ Node's built-in test runner ships an experimental module-mocking API that needs no extra dependency. Set it up at the top of the test file (or in a setup file), then dynamically import `amqplib`.
60
+
61
+ _.mocharc.json_
62
+
63
+ ```json
64
+ {
65
+ "recursive": true,
66
+ "require": ["chai/register-expect.js"],
67
+ "node-option": ["experimental-test-module-mocks", "no-warnings"]
68
+ }
50
69
  ```
51
70
 
52
- or:
71
+ The `experimental-test-module-mocks` flag enables `mock.module`; `no-warnings` silences the "experimental feature" notice. Drop it if you'd rather see the warning.
53
72
 
54
- ```js
55
- const mock = require('mock-require');
56
- const fakeAmqp = require('@onify/fake-amqplib');
73
+ _test/amqplib-connection-test.js_
57
74
 
58
- mock('amqplib/callback_api', fakeAmqp);
59
- ```
75
+ ```javascript
76
+ import { mock } from 'node:test';
77
+ import { connect as fakeConnect, resetMock } from '@onify/fake-amqplib';
60
78
 
61
- or just mock the entire amqplib with:
79
+ describe('connection', () => {
80
+ let connect;
81
+ let ctx;
62
82
 
63
- ```js
64
- const mock = require('mock-require');
65
- const fakeAmqp = require('@onify/fake-amqplib');
83
+ before(async () => {
84
+ ctx = mock.module('amqplib', { namedExports: { connect: fakeConnect } });
85
+ ({ connect } = await import('amqplib'));
86
+ });
66
87
 
67
- mock('amqplib', fakeAmqp);
68
- ```
88
+ after(() => {
89
+ ctx.restore();
90
+ resetMock();
91
+ });
69
92
 
70
- ### ESM
93
+ it('connects to the fake', async () => {
94
+ const connection = await connect('amqp://host');
95
+ expect(connection.connection.serverProperties).to.have.property('product', 'RabbitMQ');
96
+ });
97
+ });
98
+ ```
71
99
 
72
- Example on how to mock amqplib import when working with modules.
100
+ If you also use `mocha --parallel` or run tests via `node --test`, the same setup works — `mock.module` is process-global, so register it before the first dynamic import in each test file.
73
101
 
74
- **[Quibble](https://www.npmjs.com/package/quibble) mocha example**
102
+ #### Alternative — Quibble
75
103
 
76
- Both amqplib and fake-amqplib have to be quibbled if reset mock is used during testing.
104
+ [Quibble](https://www.npmjs.com/package/quibble) is useful if you're on a Node version older than 20, or prefer not to rely on an experimental flag.
77
105
 
78
106
  _test/setup.js_
79
107
 
80
- ```javascript
108
+ ```js
81
109
  import * as fakeAmqpLib from '@onify/fake-amqplib';
82
110
  import { connect as fakeConnect } from '@onify/fake-amqplib';
83
111
  import quibble from 'quibble';
@@ -88,7 +116,7 @@ import quibble from 'quibble';
88
116
  })();
89
117
  ```
90
118
 
91
- _.mocharc.json_ (true for node version < 20)
119
+ _.mocharc.json_ (the `loader=quibble` option is only needed on Node < 20)
92
120
 
93
121
  ```json
94
122
  {
@@ -98,29 +126,33 @@ _.mocharc.json_ (true for node version < 20)
98
126
  }
99
127
  ```
100
128
 
101
- _test/amqplib-connection-test.js_
129
+ Then import `amqplib` normally in your tests; quibble rewires the import.
102
130
 
103
- ```javascript
104
- import assert from 'node:assert';
105
- import { connect } from 'amqplib';
106
- import { connect as connectCb } from 'amqplib';
131
+ ### CommonJS
107
132
 
108
- import { resetMock } from '@onify/fake-amqplib';
133
+ Example on how to mock amqplib when working with commonjs.
109
134
 
110
- describe('connection', () => {
111
- afterEach(resetMock);
135
+ ```js
136
+ const amqplib = require('amqplib');
137
+ const fakeAmqp = require('@onify/fake-amqplib');
112
138
 
113
- it('connect promise', async () => {
114
- const connection = await connect('amqp://host');
115
- assert.equal(connection.connection.serverProperties.version, '3.5.0');
116
- });
139
+ amqplib.connect = fakeAmqp.connect;
140
+ ```
117
141
 
118
- it('connect callback', (done) => {
119
- connectCb('amqp://host', (err, connection) => {
120
- if (err) return done(err);
121
- assert.equal(connection.connection.serverProperties.version, '3.5.0');
122
- done();
123
- });
124
- });
125
- });
142
+ or:
143
+
144
+ ```js
145
+ const mock = require('mock-require');
146
+ const fakeAmqp = require('@onify/fake-amqplib');
147
+
148
+ mock('amqplib/callback_api', fakeAmqp);
149
+ ```
150
+
151
+ or just mock the entire amqplib with:
152
+
153
+ ```js
154
+ const mock = require('mock-require');
155
+ const fakeAmqp = require('@onify/fake-amqplib');
156
+
157
+ mock('amqplib', fakeAmqp);
126
158
  ```
package/index.d.ts CHANGED
@@ -1,59 +1,104 @@
1
- /// <reference types="amqplib" />
2
1
  /// <reference types="node" />
3
2
 
4
3
  declare module '@onify/fake-amqplib' {
5
- import { Options, Connection, Channel, ChannelModel } from 'amqplib';
4
+ import { Options, Channel, ChannelModel, Replies, RecoveryOptions, SocketOptions } from 'amqplib';
6
5
  import { Broker } from 'smqp';
7
6
 
7
+ export { RecoveryOptions, SocketOptions };
8
+
8
9
  export interface FakeAmqplibChannel extends Channel {
9
10
  /** Channel name and identifier, for faking purposes */
10
11
  _channelName: string;
11
12
  _broker: Broker;
12
13
  _version: number;
14
+ readonly _closed: boolean;
15
+ }
16
+
17
+ export const FakeAmqplibChannel: {
13
18
  new (broker: Broker, connection: FakeAmqplibConnection): FakeAmqplibChannel;
14
- get _closed(): boolean;
19
+ };
20
+
21
+ export interface FakeAmqplibConfirmChannel extends FakeAmqplibChannel {
22
+ publish(
23
+ exchange: string,
24
+ routingKey: string,
25
+ content: Buffer,
26
+ options?: Options.Publish,
27
+ callback?: (err: Error | null, ok: Replies.Empty) => void
28
+ ): boolean;
29
+ sendToQueue(
30
+ queue: string,
31
+ content: Buffer,
32
+ options?: Options.Publish,
33
+ callback?: (err: Error | null, ok: Replies.Empty) => void
34
+ ): boolean;
35
+ waitForConfirms(callback?: (err: Error | null) => void): Promise<void>;
15
36
  }
16
37
 
17
- export interface FakeAmqplibConnection extends Connection, ChannelModel {
38
+ export const FakeAmqplibConfirmChannel: {
39
+ new (broker: Broker, connection: FakeAmqplibConnection): FakeAmqplibConfirmChannel;
40
+ };
41
+
42
+ export interface FakeAmqplibConnection extends ChannelModel {
18
43
  _channels: FakeAmqplibChannel[];
19
44
  _url: URL;
20
45
  /** Connection identifier, for faking purposes */
21
46
  _id: string;
22
47
  _broker: Broker;
23
48
  _version: number;
24
- new (broker: Broker, version: number, amqpUrl: string, options?: any): FakeAmqplibConnection;
25
- get _closed(): boolean;
49
+ readonly _closed: boolean;
26
50
  }
27
51
 
28
- interface SocketOptions {
29
- host?: string;
30
- keepAlive?: boolean;
31
- keepAliveDelay?: number;
32
- noDelay?: boolean;
33
- port?: number;
34
- serverName?: string;
35
- timeout?: number;
36
- [x: string]: any;
37
- }
52
+ export const FakeAmqplibConnection: {
53
+ new (broker: Broker, version: number, amqpUrl: string, options?: SocketOptions): FakeAmqplibConnection;
54
+ };
38
55
 
39
- type connectCallback = (err: Error, connection: FakeAmqplibConnection) => void;
56
+ export type ConnectCallback = (err: Error | null, connection: FakeAmqplibConnection) => void;
40
57
 
41
- export class FakeAmqplib {
58
+ export interface FakeAmqplib {
42
59
  version: number;
43
60
  connections: FakeAmqplibConnection[];
44
- constructor(version?: number);
45
61
  connect(url: string | Options.Connect, socketOptions?: SocketOptions): Promise<FakeAmqplibConnection>;
46
- connect(url: string | Options.Connect, socketOptions: SocketOptions, callback: connectCallback): void;
47
- connect(url: string | Options.Connect, callback: (err: Error, connection: FakeAmqplibConnection) => void): void;
62
+ connect(url: string | Options.Connect, socketOptions: SocketOptions, callback: ConnectCallback): void;
63
+ connect(url: string | Options.Connect, callback: ConnectCallback): void;
48
64
  connectSync(url: string | Options.Connect, socketOptions?: SocketOptions): FakeAmqplibConnection;
65
+ connectWithRecoveryPromise(
66
+ url: string | Options.Connect,
67
+ recoveryOptions?: RecoveryOptions,
68
+ socketOptions?: SocketOptions
69
+ ): Promise<FakeAmqplibConnection>;
70
+ connectWithRecoveryCallback(
71
+ url: string | Options.Connect,
72
+ recoveryOptions: RecoveryOptions,
73
+ socketOptions: SocketOptions,
74
+ callback: ConnectCallback
75
+ ): void;
49
76
  resetMock(): void;
50
- setVersion(minorVersion: number): void;
77
+ setVersion(minorVersion: number | string): void;
51
78
  }
52
79
 
80
+ export const FakeAmqplib: {
81
+ new (minorVersion?: number | string): FakeAmqplib;
82
+ (minorVersion?: number | string): FakeAmqplib;
83
+ };
84
+
85
+ export const connections: FakeAmqplibConnection[];
86
+
53
87
  export function connect(url: string | Options.Connect, socketOptions?: SocketOptions): Promise<FakeAmqplibConnection>;
54
- export function connect(url: string | Options.Connect, socketOptions: SocketOptions, callback: connectCallback): void;
55
- export function connect(url: string | Options.Connect, callback: connectCallback): void;
88
+ export function connect(url: string | Options.Connect, socketOptions: SocketOptions, callback: ConnectCallback): void;
89
+ export function connect(url: string | Options.Connect, callback: ConnectCallback): void;
56
90
  export function connectSync(url: string | Options.Connect, socketOptions?: SocketOptions): FakeAmqplibConnection;
91
+ export function connectWithRecoveryPromise(
92
+ url: string | Options.Connect,
93
+ recoveryOptions?: RecoveryOptions,
94
+ socketOptions?: SocketOptions
95
+ ): Promise<FakeAmqplibConnection>;
96
+ export function connectWithRecoveryCallback(
97
+ url: string | Options.Connect,
98
+ recoveryOptions: RecoveryOptions,
99
+ socketOptions: SocketOptions,
100
+ callback: ConnectCallback
101
+ ): void;
57
102
  export function resetMock(): void;
58
- export function setVersion(minorVersion: number): void;
103
+ export function setVersion(minorVersion: number | string): void;
59
104
  }
package/index.js CHANGED
@@ -7,6 +7,9 @@ const kClosed = Symbol.for('closed');
7
7
  const kDeliveryTag = Symbol.for('channel delivery tag');
8
8
  const kPrefetch = Symbol.for('prefetch');
9
9
  const kChannelPrefetch = Symbol.for('channel prefetch');
10
+ const kPendingConfirms = Symbol.for('pending confirms');
11
+
12
+ const CHANNEL_CLOSED_ERROR = 'Channel is closed';
10
13
 
11
14
  class AmqplibBroker extends Broker {
12
15
  constructor(...args) {
@@ -429,6 +432,21 @@ export class FakeAmqplibChannel extends EventEmitter {
429
432
  brokerMessage.reject(requeue);
430
433
  }
431
434
  }
435
+ recover(...args) {
436
+ const channelQ = this._channelQueue;
437
+ return this._callBroker(recoverChannel, ...args);
438
+
439
+ function recoverChannel() {
440
+ const snapshot = [];
441
+ let msg;
442
+ while ((msg = channelQ.get())) snapshot.push(msg);
443
+ for (const m of snapshot) {
444
+ m.content[kSmqp].reject(true);
445
+ m.reject(false);
446
+ }
447
+ return {};
448
+ }
449
+ }
432
450
  prefetch(val, isChannelPrefetch) {
433
451
  if (this.connection._version < 3.3) {
434
452
  if (isChannelPrefetch !== undefined) {
@@ -449,7 +467,7 @@ export class FakeAmqplibChannel extends EventEmitter {
449
467
  else poppedCb = null;
450
468
 
451
469
  if (this.connection._closed) throw new FakeAmqpError('Connection is closed', 504);
452
- if (this[kClosed]) throw new Error('Channel is closed');
470
+ if (this[kClosed]) throw new Error(CHANNEL_CLOSED_ERROR);
453
471
 
454
472
  return new Promise((resolve, reject) => {
455
473
  try {
@@ -509,19 +527,25 @@ export class FakeAmqplibChannel extends EventEmitter {
509
527
  }
510
528
 
511
529
  export class FakeAmqplibConfirmChannel extends FakeAmqplibChannel {
530
+ constructor(broker, connection) {
531
+ super(broker, connection);
532
+ this[kPendingConfirms] = new Set();
533
+ }
512
534
  publish(exchange, routingKey, content, options, callback) {
513
535
  if (!Buffer.isBuffer(content)) throw new TypeError('content is not a buffer');
514
536
  if (exchange === '') return this.sendToQueue(routingKey, content, options, callback);
537
+ if (this[kClosed]) throw new Error(CHANNEL_CLOSED_ERROR);
515
538
 
516
539
  const args = [this._broker.publish, exchange, routingKey, content];
517
540
 
518
- args.push(...addConfirmCallback(this._broker, options, callback));
541
+ args.push(...addConfirmCallback(this._broker, options, this._trackConfirm(callback)));
519
542
 
520
543
  this.checkExchange(exchange)
521
544
  .then(() => {
522
545
  return this._callBroker(...args);
523
546
  })
524
547
  .catch((err) => {
548
+ if (err.message === CHANNEL_CLOSED_ERROR) return;
525
549
  this.emit('error', err);
526
550
  });
527
551
 
@@ -529,21 +553,61 @@ export class FakeAmqplibConfirmChannel extends FakeAmqplibChannel {
529
553
  }
530
554
  sendToQueue(queue, content, options, callback) {
531
555
  if (!Buffer.isBuffer(content)) throw new TypeError('content is not a buffer');
556
+ if (this[kClosed]) throw new Error(CHANNEL_CLOSED_ERROR);
532
557
 
533
558
  const args = [this._broker.sendToQueue, queue, content];
534
559
 
535
- args.push(...addConfirmCallback(this._broker, options, callback));
560
+ args.push(...addConfirmCallback(this._broker, options, this._trackConfirm(callback)));
536
561
 
537
562
  this.checkQueue(queue)
538
563
  .then(() => {
539
564
  return this._callBroker(...args);
540
565
  })
541
566
  .catch((err) => {
567
+ if (err.message === CHANNEL_CLOSED_ERROR) return;
542
568
  this.emit('error', err);
543
569
  });
544
570
 
545
571
  return true;
546
572
  }
573
+ _trackConfirm(userCallback) {
574
+ let resolveSettled;
575
+ const settled = new Promise((resolve) => {
576
+ resolveSettled = resolve;
577
+ });
578
+ const entry = { settled, resolveSettled, userCallback };
579
+ this[kPendingConfirms].add(entry);
580
+
581
+ return (err, ok) => {
582
+ this[kPendingConfirms].delete(entry);
583
+ resolveSettled(err || null);
584
+ if (typeof userCallback === 'function') userCallback(err, ok);
585
+ };
586
+ }
587
+ waitForConfirms(callback) {
588
+ const snapshot = [...this[kPendingConfirms]].map((entry) => entry.settled);
589
+ const promise = Promise.all(snapshot).then((errs) => {
590
+ const firstErr = errs.find((e) => e !== null);
591
+ if (firstErr) throw firstErr;
592
+ });
593
+ if (typeof callback === 'function') {
594
+ promise.then(
595
+ () => callback(null),
596
+ (err) => callback(err)
597
+ );
598
+ }
599
+ return promise;
600
+ }
601
+ _teardown() {
602
+ super._teardown();
603
+ if (!this[kPendingConfirms] || this[kPendingConfirms].size === 0) return;
604
+ const err = new Error(CHANNEL_CLOSED_ERROR);
605
+ for (const entry of [...this[kPendingConfirms]]) {
606
+ this[kPendingConfirms].delete(entry);
607
+ entry.resolveSettled(err);
608
+ if (typeof entry.userCallback === 'function') entry.userCallback(err);
609
+ }
610
+ }
547
611
  }
548
612
 
549
613
  export class FakeAmqplibConnection extends EventEmitter {
@@ -586,6 +650,10 @@ export class FakeAmqplibConnection extends EventEmitter {
586
650
  this._channels.push(channel);
587
651
  return resolveOrCallback(args.slice(-1)[0], null, channel);
588
652
  }
653
+ updateSecret(...args) {
654
+ process.nextTick(() => this.emit('update-secret-ok'));
655
+ return resolveOrCallback(args.slice(-1)[0]);
656
+ }
589
657
  close(...args) {
590
658
  if (this[kClosed]) return resolveOrCallback(args.slice(-1)[0]);
591
659
  this[kClosed] = true;
@@ -608,6 +676,8 @@ export function FakeAmqplib(minorVersion = '3.5') {
608
676
 
609
677
  this.connect = this.connect.bind(this);
610
678
  this.connectSync = this.connectSync.bind(this);
679
+ this.connectWithRecoveryPromise = this.connectWithRecoveryPromise.bind(this);
680
+ this.connectWithRecoveryCallback = this.connectWithRecoveryCallback.bind(this);
611
681
  this.resetMock = this.resetMock.bind(this);
612
682
  this.setVersion = this.setVersion.bind(this);
613
683
  }
@@ -617,6 +687,19 @@ FakeAmqplib.prototype.connect = function fakeConnect(amqpUrl, ...args) {
617
687
  return resolveOrCallback(args.slice(-1)[0], null, connection);
618
688
  };
619
689
 
690
+ FakeAmqplib.prototype.connectWithRecoveryPromise = function fakeConnectWithRecoveryPromise(amqpUrl, recoveryOptions, socketOptions) {
691
+ return this.connect(amqpUrl, socketOptions);
692
+ };
693
+
694
+ FakeAmqplib.prototype.connectWithRecoveryCallback = function fakeConnectWithRecoveryCallback(
695
+ amqpUrl,
696
+ recoveryOptions,
697
+ socketOptions,
698
+ callback
699
+ ) {
700
+ return this.connect(amqpUrl, socketOptions, callback);
701
+ };
702
+
620
703
  FakeAmqplib.prototype.connectSync = function fakeConnectSync(amqpUrl, ...args) {
621
704
  const { _broker } = this.connections.find((conn) => compareConnectionString(conn._url, amqpUrl)) || {};
622
705
  const broker = _broker || new AmqplibBroker(this);
@@ -762,6 +845,14 @@ export function connectSync(amqpUrl, ...args) {
762
845
  return defaultFake.connectSync(amqpUrl, ...args);
763
846
  }
764
847
 
848
+ export function connectWithRecoveryPromise(amqpUrl, recoveryOptions, socketOptions) {
849
+ return defaultFake.connectWithRecoveryPromise(amqpUrl, recoveryOptions, socketOptions);
850
+ }
851
+
852
+ export function connectWithRecoveryCallback(amqpUrl, recoveryOptions, socketOptions, callback) {
853
+ return defaultFake.connectWithRecoveryCallback(amqpUrl, recoveryOptions, socketOptions, callback);
854
+ }
855
+
765
856
  export function resetMock() {
766
857
  return defaultFake.resetMock();
767
858
  }
package/main.cjs CHANGED
@@ -9,6 +9,9 @@ const kClosed = Symbol.for('closed');
9
9
  const kDeliveryTag = Symbol.for('channel delivery tag');
10
10
  const kPrefetch = Symbol.for('prefetch');
11
11
  const kChannelPrefetch = Symbol.for('channel prefetch');
12
+ const kPendingConfirms = Symbol.for('pending confirms');
13
+
14
+ const CHANNEL_CLOSED_ERROR = 'Channel is closed';
12
15
 
13
16
  class AmqplibBroker extends smqp.Broker {
14
17
  constructor(...args) {
@@ -431,6 +434,21 @@ class FakeAmqplibChannel extends events.EventEmitter {
431
434
  brokerMessage.reject(requeue);
432
435
  }
433
436
  }
437
+ recover(...args) {
438
+ const channelQ = this._channelQueue;
439
+ return this._callBroker(recoverChannel, ...args);
440
+
441
+ function recoverChannel() {
442
+ const snapshot = [];
443
+ let msg;
444
+ while ((msg = channelQ.get())) snapshot.push(msg);
445
+ for (const m of snapshot) {
446
+ m.content[kSmqp].reject(true);
447
+ m.reject(false);
448
+ }
449
+ return {};
450
+ }
451
+ }
434
452
  prefetch(val, isChannelPrefetch) {
435
453
  if (this.connection._version < 3.3) {
436
454
  if (isChannelPrefetch !== undefined) {
@@ -451,7 +469,7 @@ class FakeAmqplibChannel extends events.EventEmitter {
451
469
  else poppedCb = null;
452
470
 
453
471
  if (this.connection._closed) throw new FakeAmqpError('Connection is closed', 504);
454
- if (this[kClosed]) throw new Error('Channel is closed');
472
+ if (this[kClosed]) throw new Error(CHANNEL_CLOSED_ERROR);
455
473
 
456
474
  return new Promise((resolve, reject) => {
457
475
  try {
@@ -511,19 +529,25 @@ class FakeAmqplibChannel extends events.EventEmitter {
511
529
  }
512
530
 
513
531
  class FakeAmqplibConfirmChannel extends FakeAmqplibChannel {
532
+ constructor(broker, connection) {
533
+ super(broker, connection);
534
+ this[kPendingConfirms] = new Set();
535
+ }
514
536
  publish(exchange, routingKey, content, options, callback) {
515
537
  if (!Buffer.isBuffer(content)) throw new TypeError('content is not a buffer');
516
538
  if (exchange === '') return this.sendToQueue(routingKey, content, options, callback);
539
+ if (this[kClosed]) throw new Error(CHANNEL_CLOSED_ERROR);
517
540
 
518
541
  const args = [this._broker.publish, exchange, routingKey, content];
519
542
 
520
- args.push(...addConfirmCallback(this._broker, options, callback));
543
+ args.push(...addConfirmCallback(this._broker, options, this._trackConfirm(callback)));
521
544
 
522
545
  this.checkExchange(exchange)
523
546
  .then(() => {
524
547
  return this._callBroker(...args);
525
548
  })
526
549
  .catch((err) => {
550
+ if (err.message === CHANNEL_CLOSED_ERROR) return;
527
551
  this.emit('error', err);
528
552
  });
529
553
 
@@ -531,21 +555,61 @@ class FakeAmqplibConfirmChannel extends FakeAmqplibChannel {
531
555
  }
532
556
  sendToQueue(queue, content, options, callback) {
533
557
  if (!Buffer.isBuffer(content)) throw new TypeError('content is not a buffer');
558
+ if (this[kClosed]) throw new Error(CHANNEL_CLOSED_ERROR);
534
559
 
535
560
  const args = [this._broker.sendToQueue, queue, content];
536
561
 
537
- args.push(...addConfirmCallback(this._broker, options, callback));
562
+ args.push(...addConfirmCallback(this._broker, options, this._trackConfirm(callback)));
538
563
 
539
564
  this.checkQueue(queue)
540
565
  .then(() => {
541
566
  return this._callBroker(...args);
542
567
  })
543
568
  .catch((err) => {
569
+ if (err.message === CHANNEL_CLOSED_ERROR) return;
544
570
  this.emit('error', err);
545
571
  });
546
572
 
547
573
  return true;
548
574
  }
575
+ _trackConfirm(userCallback) {
576
+ let resolveSettled;
577
+ const settled = new Promise((resolve) => {
578
+ resolveSettled = resolve;
579
+ });
580
+ const entry = { settled, resolveSettled, userCallback };
581
+ this[kPendingConfirms].add(entry);
582
+
583
+ return (err, ok) => {
584
+ this[kPendingConfirms].delete(entry);
585
+ resolveSettled(err || null);
586
+ if (typeof userCallback === 'function') userCallback(err, ok);
587
+ };
588
+ }
589
+ waitForConfirms(callback) {
590
+ const snapshot = [...this[kPendingConfirms]].map((entry) => entry.settled);
591
+ const promise = Promise.all(snapshot).then((errs) => {
592
+ const firstErr = errs.find((e) => e !== null);
593
+ if (firstErr) throw firstErr;
594
+ });
595
+ if (typeof callback === 'function') {
596
+ promise.then(
597
+ () => callback(null),
598
+ (err) => callback(err)
599
+ );
600
+ }
601
+ return promise;
602
+ }
603
+ _teardown() {
604
+ super._teardown();
605
+ if (!this[kPendingConfirms] || this[kPendingConfirms].size === 0) return;
606
+ const err = new Error(CHANNEL_CLOSED_ERROR);
607
+ for (const entry of [...this[kPendingConfirms]]) {
608
+ this[kPendingConfirms].delete(entry);
609
+ entry.resolveSettled(err);
610
+ if (typeof entry.userCallback === 'function') entry.userCallback(err);
611
+ }
612
+ }
549
613
  }
550
614
 
551
615
  class FakeAmqplibConnection extends events.EventEmitter {
@@ -588,6 +652,10 @@ class FakeAmqplibConnection extends events.EventEmitter {
588
652
  this._channels.push(channel);
589
653
  return resolveOrCallback(args.slice(-1)[0], null, channel);
590
654
  }
655
+ updateSecret(...args) {
656
+ process.nextTick(() => this.emit('update-secret-ok'));
657
+ return resolveOrCallback(args.slice(-1)[0]);
658
+ }
591
659
  close(...args) {
592
660
  if (this[kClosed]) return resolveOrCallback(args.slice(-1)[0]);
593
661
  this[kClosed] = true;
@@ -610,6 +678,8 @@ function FakeAmqplib(minorVersion = '3.5') {
610
678
 
611
679
  this.connect = this.connect.bind(this);
612
680
  this.connectSync = this.connectSync.bind(this);
681
+ this.connectWithRecoveryPromise = this.connectWithRecoveryPromise.bind(this);
682
+ this.connectWithRecoveryCallback = this.connectWithRecoveryCallback.bind(this);
613
683
  this.resetMock = this.resetMock.bind(this);
614
684
  this.setVersion = this.setVersion.bind(this);
615
685
  }
@@ -619,6 +689,19 @@ FakeAmqplib.prototype.connect = function fakeConnect(amqpUrl, ...args) {
619
689
  return resolveOrCallback(args.slice(-1)[0], null, connection);
620
690
  };
621
691
 
692
+ FakeAmqplib.prototype.connectWithRecoveryPromise = function fakeConnectWithRecoveryPromise(amqpUrl, recoveryOptions, socketOptions) {
693
+ return this.connect(amqpUrl, socketOptions);
694
+ };
695
+
696
+ FakeAmqplib.prototype.connectWithRecoveryCallback = function fakeConnectWithRecoveryCallback(
697
+ amqpUrl,
698
+ recoveryOptions,
699
+ socketOptions,
700
+ callback
701
+ ) {
702
+ return this.connect(amqpUrl, socketOptions, callback);
703
+ };
704
+
622
705
  FakeAmqplib.prototype.connectSync = function fakeConnectSync(amqpUrl, ...args) {
623
706
  const { _broker } = this.connections.find((conn) => compareConnectionString(conn._url, amqpUrl)) || {};
624
707
  const broker = _broker || new AmqplibBroker(this);
@@ -764,6 +847,14 @@ function connectSync(amqpUrl, ...args) {
764
847
  return defaultFake.connectSync(amqpUrl, ...args);
765
848
  }
766
849
 
850
+ function connectWithRecoveryPromise(amqpUrl, recoveryOptions, socketOptions) {
851
+ return defaultFake.connectWithRecoveryPromise(amqpUrl, recoveryOptions, socketOptions);
852
+ }
853
+
854
+ function connectWithRecoveryCallback(amqpUrl, recoveryOptions, socketOptions, callback) {
855
+ return defaultFake.connectWithRecoveryCallback(amqpUrl, recoveryOptions, socketOptions, callback);
856
+ }
857
+
767
858
  function resetMock() {
768
859
  return defaultFake.resetMock();
769
860
  }
@@ -778,6 +869,8 @@ exports.FakeAmqplibConfirmChannel = FakeAmqplibConfirmChannel;
778
869
  exports.FakeAmqplibConnection = FakeAmqplibConnection;
779
870
  exports.connect = connect;
780
871
  exports.connectSync = connectSync;
872
+ exports.connectWithRecoveryCallback = connectWithRecoveryCallback;
873
+ exports.connectWithRecoveryPromise = connectWithRecoveryPromise;
781
874
  exports.connections = connections;
782
875
  exports.resetMock = resetMock;
783
876
  exports.setVersion = setVersion;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onify/fake-amqplib",
3
- "version": "3.4.0",
3
+ "version": "3.6.0",
4
4
  "description": "Fake amqplib",
5
5
  "type": "module",
6
6
  "exports": {
@@ -13,19 +13,21 @@
13
13
  "main": "./main.cjs",
14
14
  "scripts": {
15
15
  "test": "mocha",
16
- "posttest": "npm run lint && npm run dist",
16
+ "posttest": "npm run lint && npm run dist && npm run test:md",
17
17
  "lint": "eslint . --cache && prettier . -c --cache",
18
18
  "dist": "rollup -c",
19
19
  "prepack": "npm run dist",
20
20
  "cov:html": "c8 -r html -r text mocha",
21
- "test:lcov": "c8 -r lcov mocha && npm run lint"
21
+ "test:lcov": "c8 -r lcov mocha && npm run lint",
22
+ "test:md": "texample ./README.md -c ./.mocharc.json -r scripts/texample-mocha.js",
23
+ "toc": "node scripts/toc.js"
22
24
  },
23
25
  "author": {
24
26
  "name": "Onify",
25
27
  "url": "https://github.com/onify"
26
28
  },
27
29
  "engines": {
28
- "node": ">=14"
30
+ "node": ">=18"
29
31
  },
30
32
  "repository": {
31
33
  "type": "git",
@@ -33,21 +35,30 @@
33
35
  },
34
36
  "license": "MIT",
35
37
  "dependencies": {
36
- "smqp": "^11.0.0"
38
+ "smqp": "^12.0.0"
37
39
  },
38
40
  "devDependencies": {
41
+ "@eslint/js": "^10.0.1",
39
42
  "@rollup/plugin-commonjs": "^29.0.0",
40
- "@types/amqplib": "^0.10.2",
41
- "amqplib": "^0.10.4",
42
- "c8": "^10.1.2",
43
+ "@types/node": "^20.19.40",
44
+ "amqplib": "^1.2.0",
45
+ "c8": "^11.0.0",
43
46
  "chai": "^6.2.1",
44
- "eslint": "^9.0.0",
45
- "globals": "^16.0.0",
47
+ "eslint": "^10.3.0",
48
+ "globals": "^17.0.0",
46
49
  "mocha": "^11.0.1",
47
50
  "prettier": "^3.2.5",
48
51
  "quibble": "^0.9.2",
49
52
  "rollup": "^4.0.2",
50
- "texample": "^0.1.0"
53
+ "texample": "^1.0.2"
54
+ },
55
+ "overrides": {
56
+ "c8": {
57
+ "yargs": "^18.0.0"
58
+ },
59
+ "mocha": {
60
+ "yargs": "^18.0.0"
61
+ }
51
62
  },
52
63
  "keywords": [
53
64
  "fake",