@onify/fake-amqplib 0.8.2 → 0.9.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 (3) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/index.js +125 -47
  3. package/package.json +5 -8
package/CHANGELOG.md ADDED
@@ -0,0 +1,61 @@
1
+ Changelog
2
+ =========
3
+
4
+ # 0.9.0
5
+
6
+ - support url object
7
+ - smqp@6
8
+
9
+ # 0.8.5
10
+
11
+ - ack/nack all only cares about messages consumed by channel, previously everything was gone
12
+
13
+ # 0.8.4
14
+
15
+ - ack/nack all fix
16
+
17
+ # 0.8.3
18
+
19
+ - Call confirm channel callback when the message is queued, not when it is consumed!
20
+ - implement publish with empty string special case
21
+ - hide some internal props from message
22
+
23
+ # 0.8.2
24
+
25
+ - share behind the scenes broker if connection hosts and vhost are the same
26
+ - add new `connectSync` helper method to be able to get a connection synchronously to facilitate testing
27
+
28
+ # 0.8.1
29
+
30
+ - be a better mimic of amqplib, some stuff didn't work at all prior to this version
31
+
32
+ ## Additions
33
+
34
+ - Handle different behaviours between RabbitMQ versions
35
+
36
+ # 0.8.0
37
+
38
+ - bump `smqp@5`
39
+ - stop building for node 10 (mocha's fault)
40
+
41
+ # 0.7.0
42
+
43
+ - bump `smqp@4`
44
+
45
+ # 0.6.0
46
+
47
+ - bump `smqp@3.2`
48
+
49
+ # 0.5.0
50
+
51
+ - support exclusive queue and its behaviour
52
+ - emit return on channel if mandatory message was not routed
53
+
54
+ # 0.4.0
55
+
56
+ - apparently connection is killed as well when trying to consume exclusive consumed queue
57
+ - try to mimic real behaviour and throw some errors with code
58
+
59
+ # 0.3.0
60
+
61
+ - kill channel if trying to consume exclusive consumed queue
package/index.js CHANGED
@@ -2,7 +2,9 @@
2
2
 
3
3
  const {Broker} = require('smqp');
4
4
  const {EventEmitter} = require('events');
5
- const {URL} = require('url');
5
+ const {URL, format: urlFormat} = require('url');
6
+
7
+ const smqpSymbol = Symbol.for('smqp');
6
8
 
7
9
  class FakeAmqpError extends Error {
8
10
  constructor(message, code, killChannel, killConnection) {
@@ -14,8 +16,8 @@ class FakeAmqpError extends Error {
14
16
  }
15
17
 
16
18
  class FakeAmqpNotFoundError extends FakeAmqpError {
17
- constructor(type, name, killConnection = false) {
18
- super(`Channel closed by server: 404 (NOT-FOUND) with message "NOT_FOUND - no ${type} '${name}' in vhost '/'`, 404, true, killConnection);
19
+ constructor(type, name, vhost, killConnection = false) {
20
+ super(`Channel closed by server: 404 (NOT-FOUND) with message "NOT_FOUND - no ${type} '${name}' in vhost '${vhost || '/'}'`, 404, true, killConnection);
19
21
  }
20
22
  }
21
23
 
@@ -42,7 +44,7 @@ function Fake(minorVersion) {
42
44
  }
43
45
 
44
46
  function connectSync(amqpUrl, ...args) {
45
- const {_broker} = connections.find((conn) => compareConnectionString(conn.options[0], amqpUrl)) || {};
47
+ const {_broker} = connections.find((conn) => compareConnectionString(conn._url, amqpUrl)) || {};
46
48
  const broker = _broker || Broker();
47
49
  const connection = Connection(broker, defaultVersion, amqpUrl, ...args);
48
50
  connections.push(connection);
@@ -55,16 +57,18 @@ function Fake(minorVersion) {
55
57
  }
56
58
  }
57
59
 
58
- function Connection(broker, version, ...connArgs) {
60
+ function Connection(broker, version, amqpUrl, ...connArgs) {
59
61
  const emitter = new EventEmitter();
60
62
  const options = connArgs.filter((a) => typeof a !== 'function');
61
63
  let closed = false;
62
64
  const channels = [];
65
+ const url = normalizeAmqpUrl(amqpUrl);
63
66
 
64
67
  return {
65
68
  _id: generateId(),
66
69
  _broker: broker,
67
70
  _version: version,
71
+ _url: url,
68
72
  get _closed() {
69
73
  return closed;
70
74
  },
@@ -87,6 +91,7 @@ function Fake(minorVersion) {
87
91
  return resolveOrCallback(args.slice(-1)[0], null, channel);
88
92
  },
89
93
  async close(...args) {
94
+ if (closed) return resolveOrCallback(args.slice(-1)[0]);
90
95
  closed = true;
91
96
 
92
97
  const idx = connections.indexOf(this);
@@ -165,7 +170,7 @@ function Fake(minorVersion) {
165
170
  return callBroker(check, ...args);
166
171
 
167
172
  function check() {
168
- if (!broker.getExchange(name)) throw new FakeAmqpError(`Channel closed by server: 404 (NOT-FOUND) with message "NOT_FOUND - no exchange '${name}' in vhost '/'`, 404, true);
173
+ if (!broker.getExchange(name)) throw new FakeAmqpNotFoundError('exchange', name, connection._url.pathname);
169
174
  return true;
170
175
  }
171
176
  },
@@ -175,7 +180,7 @@ function Fake(minorVersion) {
175
180
  function check() {
176
181
  let queue;
177
182
  if (!(queue = broker.getQueue(name))) {
178
- throw new FakeAmqpNotFoundError('queue', name);
183
+ throw new FakeAmqpNotFoundError('queue', name, connection._url.pathname);
179
184
  }
180
185
 
181
186
  return {
@@ -189,8 +194,10 @@ function Fake(minorVersion) {
189
194
 
190
195
  function getMessage(...getargs) {
191
196
  const q = broker.getQueue(queue);
192
- if (!q) throw new FakeAmqpNotFoundError('queue');
193
- return q.get(...getargs) || false;
197
+ if (!q) throw new FakeAmqpNotFoundError('queue', queue, connection._url.pathname);
198
+ const msg = q.get(...getargs) || false;
199
+ if (!msg) return msg;
200
+ return new Message(msg);
194
201
  }
195
202
  },
196
203
  deleteExchange(exchange, ...args) {
@@ -198,7 +205,7 @@ function Fake(minorVersion) {
198
205
 
199
206
  function check() {
200
207
  const result = broker.deleteExchange(exchange, ...args);
201
- if (!result && version < 3.2) throw new FakeAmqpNotFoundError('exchange', exchange);
208
+ if (!result && version < 3.2) throw new FakeAmqpNotFoundError('exchange', exchange, connection._url.pathname);
202
209
  return result;
203
210
  }
204
211
  },
@@ -207,18 +214,21 @@ function Fake(minorVersion) {
207
214
 
208
215
  function check() {
209
216
  const result = broker.deleteQueue(queue, ...args);
210
- if (!result && version < 3.2) throw new FakeAmqpNotFoundError('queue', queue);
217
+ if (!result && version < 3.2) throw new FakeAmqpNotFoundError('queue', queue, connection._url.pathname);
211
218
  return result;
212
219
  }
213
220
  },
214
221
  publish(exchange, routingKey, content, options, callback) {
215
222
  if (!Buffer.isBuffer(content)) throw new TypeError('content is not a buffer');
216
- if (confirmChannel) {
217
- options = {...options, confirm: makeConfirmCallback(callback)};
218
- }
223
+ if (exchange === '') return this.sendToQueue(routingKey, content, options, callback);
224
+
225
+ const args = [broker.publish, exchange, routingKey, content];
226
+
227
+ if (confirmChannel) args.push(...addConfirmCallback(options, callback));
228
+ else args.push(options);
219
229
 
220
230
  this.checkExchange(exchange).then(() => {
221
- return callBroker(broker.publish, exchange, routingKey, content, options);
231
+ return callBroker(...args);
222
232
  }).catch((err) => {
223
233
  emitter.emit('error', err);
224
234
  });
@@ -230,18 +240,20 @@ function Fake(minorVersion) {
230
240
 
231
241
  function check() {
232
242
  const result = broker.purgeQueue(queue);
233
- if (!result && version < 3.2) throw new FakeAmqpNotFoundError('queue', queue);
243
+ if (!result && version < 3.2) throw new FakeAmqpNotFoundError('queue', queue, connection._url.pathname);
234
244
  return result === undefined ? undefined : {messageCount: result};
235
245
  }
236
246
  },
237
247
  sendToQueue(queue, content, options, callback) {
238
248
  if (!Buffer.isBuffer(content)) throw new TypeError('content is not a buffer');
239
- if (confirmChannel) {
240
- options = {...options, confirm: makeConfirmCallback(callback)};
241
- }
249
+
250
+ const args = [broker.sendToQueue, queue, content];
251
+
252
+ if (confirmChannel) args.push(...addConfirmCallback(options, callback));
253
+ else args.push(options);
242
254
 
243
255
  this.checkQueue(queue).then(() => {
244
- return callBroker(broker.sendToQueue, queue, content, options);
256
+ return callBroker(...args);
245
257
  }).catch((err) => {
246
258
  emitter.emit('error', err);
247
259
  });
@@ -259,7 +271,7 @@ function Fake(minorVersion) {
259
271
  if (!exchange) throw new FakeAmqpNotFoundError('exchange', source);
260
272
 
261
273
  const result = broker.unbindExchange(source, destination, pattern);
262
- if (!result && version <= 3.2) throw new FakeAmqpNotFoundError('binding', pattern);
274
+ if (!result && version <= 3.2) throw new FakeAmqpNotFoundError('binding', pattern, connection._url.pathname);
263
275
 
264
276
  return true;
265
277
  }
@@ -275,7 +287,7 @@ function Fake(minorVersion) {
275
287
  if (!exchange) throw new FakeAmqpNotFoundError('exchange', source);
276
288
 
277
289
  const binding = exchange.getBinding(queue, pattern);
278
- if (!binding && version <= 3.2) throw new FakeAmqpNotFoundError('binding', pattern, version < 3.2);
290
+ if (!binding && version <= 3.2) throw new FakeAmqpNotFoundError('binding', pattern, connection._url.pathname, version < 3.2);
279
291
 
280
292
  broker.unbindQueue(queue, source, pattern);
281
293
  return true;
@@ -286,14 +298,12 @@ function Fake(minorVersion) {
286
298
 
287
299
  function check() {
288
300
  const q = queue && broker.getQueue(queue);
289
- if (queue && !q) {
290
- throw new FakeAmqpNotFoundError('queue', queue);
301
+ if (!q) {
302
+ throw new FakeAmqpNotFoundError('queue', queue, connection._url.pathname);
291
303
  }
292
304
 
293
- if (q) {
294
- if (q.exclusive || (q.options.exclusive && q.options._connectionId !== connection._id)) {
295
- throw new FakeAmqpError(`Channel closed by server: 403 (ACCESS-REFUSED) with message "ACCESS_REFUSED - queue '${queue}' in vhost '/' in exclusive use"`, 403, true, true);
296
- }
305
+ if (q.exclusive || (q.options.exclusive && q.options._connectionId !== connection._id)) {
306
+ throw new FakeAmqpError(`Channel closed by server: 403 (ACCESS-REFUSED) with message "ACCESS_REFUSED - queue '${queue}' in vhost '${connection._url.pathname}' in exclusive use"`, 403, true, true);
297
307
  }
298
308
 
299
309
  const {consumerTag} = broker.consume(queue, onMessage && handler, {...options, channelName, prefetch});
@@ -301,7 +311,7 @@ function Fake(minorVersion) {
301
311
  }
302
312
 
303
313
  function handler(_, msg) {
304
- onMessage(msg);
314
+ onMessage(new Message(msg));
305
315
  }
306
316
  },
307
317
  cancel(consumerTag, ...args) {
@@ -316,15 +326,25 @@ function Fake(minorVersion) {
316
326
  emitter.emit('close');
317
327
  return resolveOrCallback(callback);
318
328
  },
319
- ack: broker.ack,
320
- ackAll: broker.ackAll,
329
+ ack(message, ...args) {
330
+ broker.ack(message[smqpSymbol], ...args);
331
+ },
332
+ ackAll() {
333
+ const consumers = broker.getConsumers().filter(({options}) => options.channelName === channelName);
334
+ consumers.forEach((c) => broker.getConsumer(c.consumerTag).ackAll());
335
+ },
321
336
  ...(version >= 2.3 ? {
322
337
  nack(message, ...args) {
323
- return broker.nack(message, ...args);
338
+ return broker.nack(message[smqpSymbol], ...args);
324
339
  }
325
340
  } : undefined),
326
- reject: broker.reject,
327
- nackAll: broker.nackAll,
341
+ reject(message, ...args) {
342
+ broker.reject(message[smqpSymbol], ...args);
343
+ },
344
+ nackAll(requeue = false) {
345
+ const consumers = broker.getConsumers().filter(({options}) => options.channelName === channelName);
346
+ consumers.forEach((c) => broker.getConsumer(c.consumerTag).nackAll(requeue));
347
+ },
328
348
  prefetch(val) {
329
349
  prefetch = val;
330
350
  },
@@ -336,30 +356,37 @@ function Fake(minorVersion) {
336
356
  },
337
357
  };
338
358
 
339
- function makeConfirmCallback(callback) {
359
+ function addConfirmCallback(options, callback) {
340
360
  const confirm = 'msg.' + generateId();
341
361
  const consumerTag = 'ct-' + confirm;
362
+ options = {...options, confirm};
363
+
342
364
  broker.on('message.*', onConsumeMessage, {consumerTag});
343
365
 
366
+ let undelivered;
344
367
  function onConsumeMessage(event) {
345
- if (event.properties.confirm !== confirm) return;
346
-
368
+ if (event.properties && event.properties.confirm !== confirm) return;
347
369
  switch(event.name) {
348
370
  case 'message.nack':
349
- return confirmCallback(new Error('message nacked'));
350
371
  case 'message.undelivered':
351
- return confirmCallback(new Error('message undelivered'));
352
- case 'message.ack':
353
- return confirmCallback(null, true);
372
+ undelivered = event.name;
373
+ break;
354
374
  }
375
+ }
355
376
 
356
- function confirmCallback(err, ok) {
357
- broker.off('message.*', {consumerTag});
358
- callback(err, ok);
377
+ function confirmCallback() {
378
+ broker.off('message.*', consumerTag);
379
+ switch (undelivered) {
380
+ case 'message.nack':
381
+ return callback(new Error('message nacked'));
382
+ case 'message.undelivered':
383
+ throw callback(new Error('message undelivered'));
384
+ default:
385
+ return callback(null, true);
359
386
  }
360
387
  }
361
388
 
362
- return confirm;
389
+ return [options, confirmCallback];
363
390
  }
364
391
 
365
392
  function callBroker(fn, ...args) {
@@ -404,7 +431,58 @@ function generateId() {
404
431
  }
405
432
 
406
433
  function compareConnectionString(url1, url2) {
407
- const parsedUrl1 = new URL(url1);
408
- const parsedUrl2 = new URL(url2);
434
+ const parsedUrl1 = normalizeAmqpUrl(url1);
435
+ const parsedUrl2 = normalizeAmqpUrl(url2);
436
+
409
437
  return parsedUrl1.host === parsedUrl2.host && parsedUrl1.pathname === parsedUrl2.pathname;
410
438
  }
439
+
440
+ function Message(smqpMessage) {
441
+ this[smqpSymbol] = smqpMessage;
442
+ this.content = smqpMessage.content;
443
+ this.fields = smqpMessage.fields;
444
+ this.properties = smqpMessage.properties;
445
+ }
446
+
447
+ function normalizeAmqpUrl(url) {
448
+ if (!url) return url = new URL('amqp://localhost:5672/');
449
+ if (typeof url === 'string') url = new URL(url);
450
+
451
+ if (!(url instanceof URL)) {
452
+ const {
453
+ protocol = 'amqp',
454
+ hostname = 'localhost',
455
+ port = 5672,
456
+ vhost = '/',
457
+ username,
458
+ password,
459
+ ...rest
460
+ } = url;
461
+ let auth = username;
462
+ if (auth && password) {
463
+ auth += ':' + password;
464
+ }
465
+ url = new URL(urlFormat({
466
+ protocol,
467
+ hostname,
468
+ port,
469
+ pathname: vhost,
470
+ slashes: true,
471
+ auth,
472
+ }));
473
+
474
+ for (const k in rest) {
475
+ switch (k) {
476
+ case 'locale':
477
+ case 'frameMax':
478
+ case 'heartbeat':
479
+ url.searchParams.set(k, rest[k]);
480
+ break;
481
+ }
482
+ }
483
+ }
484
+
485
+ if (!url.port) url.port = 5672;
486
+ if (!url.pathname) url.pathname = '/';
487
+ return url;
488
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onify/fake-amqplib",
3
- "version": "0.8.2",
3
+ "version": "0.9.0",
4
4
  "description": "Fake amqplib",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -21,7 +21,7 @@
21
21
  },
22
22
  "license": "MIT",
23
23
  "dependencies": {
24
- "smqp": "^5.0.0"
24
+ "smqp": "^6.0.0"
25
25
  },
26
26
  "keywords": [
27
27
  "fake",
@@ -31,15 +31,12 @@
31
31
  "rabbitmq"
32
32
  ],
33
33
  "devDependencies": {
34
- "chai": "^4.2.0",
34
+ "chai": "^4.3.6",
35
35
  "eslint": "^7.32.0",
36
- "mocha": "^9.1.1",
36
+ "mocha": "^9.2.0",
37
37
  "nyc": "^15.1.0"
38
38
  },
39
39
  "files": [
40
40
  "index.js"
41
- ],
42
- "directories": {
43
- "test": "test"
44
- }
41
+ ]
45
42
  }