UNPKG

amqplib

Version:

An AMQP 0-9-1 (e.g., RabbitMQ) library and client.

598 lines (535 loc) 17.3 kB
'use strict'; var assert = require('assert'); var api = require('../channel_api'); var util = require('./util'); var succeed = util.succeed, fail = util.fail; var schedule = util.schedule; var randomString = util.randomString; var Promise = require('bluebird'); var Buffer = require('safe-buffer').Buffer; var URL = process.env.URL || 'amqp://localhost'; function connect() { return api.connect(URL); } // Expect this promise to fail, and flip the results accordingly. function expectFail(promise) { return new Promise(function(resolve, reject) { return promise.then(reject).catch(resolve); }); } // I'll rely on operations being rejected, rather than the channel // close error, to detect failure. function ignore () {} function ignoreErrors(c) { c.on('error', ignore); return c; } function logErrors(c) { c.on('error', console.warn); return c; } // Run a test with `name`, given a function that takes an open // channel, and returns a promise that is resolved on test success or // rejected on test failure. function channel_test(chmethod, name, chfun) { test(name, function(done) { connect(URL).then(logErrors).then(function(c) { c[chmethod]().then(ignoreErrors).then(chfun) .then(succeed(done), fail(done)) // close the connection regardless of what happens with the test .finally(function() {c.close();}); }); }); } var chtest = channel_test.bind(null, 'createChannel'); suite("connect", function() { test("at all", function(done) { connect(URL).then(function(c) { return c.close() ;}).then(succeed(done), fail(done)); }); chtest("create channel", ignore); // i.e., just don't bork }); var QUEUE_OPTS = {durable: false}; var EX_OPTS = {durable: false}; suite("assert, check, delete", function() { chtest("assert and check queue", function(ch) { return ch.assertQueue('test.check-queue', QUEUE_OPTS) .then(function(qok) { return ch.checkQueue('test.check-queue'); }); }); chtest("assert and check exchange", function(ch) { return ch.assertExchange('test.check-exchange', 'direct', EX_OPTS) .then(function(eok) { assert.equal('test.check-exchange', eok.exchange); return ch.checkExchange('test.check-exchange'); }); }); chtest("fail on reasserting queue with different options", function(ch) { var q = 'test.reassert-queue'; return ch.assertQueue( q, {durable: false, autoDelete: true}) .then(function() { return expectFail( ch.assertQueue(q, {durable: false, autoDelete: false})); }); }); chtest("fail on checking a queue that's not there", function(ch) { return expectFail(ch.checkQueue('test.random-' + randomString())); }); chtest("fail on checking an exchange that's not there", function(ch) { return expectFail(ch.checkExchange('test.random-' + randomString())); }); chtest("fail on reasserting exchange with different type", function(ch) { var ex = 'test.reassert-ex'; return ch.assertExchange(ex, 'fanout', EX_OPTS) .then(function() { return expectFail( ch.assertExchange(ex, 'direct', EX_OPTS)); }); }); chtest("channel break on publishing to non-exchange", function(ch) { return new Promise(function(resolve) { ch.on('error', resolve); ch.publish(randomString(), '', Buffer.from('foobar')); }); }); chtest("delete queue", function(ch) { var q = 'test.delete-queue'; return Promise.join( ch.assertQueue(q, QUEUE_OPTS), ch.checkQueue(q)) .then(function() { return ch.deleteQueue(q);}) .then(function() { return expectFail(ch.checkQueue(q));}); }); chtest("delete exchange", function(ch) { var ex = 'test.delete-exchange'; return Promise.join( ch.assertExchange(ex, 'fanout', EX_OPTS), ch.checkExchange(ex)) .then(function() { return ch.deleteExchange(ex);}) .then(function() { return expectFail(ch.checkExchange(ex));}); }); }); // Wait for the queue to meet the condition; useful for waiting for // messages to arrive, for example. function waitForQueue(q, condition) { return connect(URL).then(function(c) { return c.createChannel() .then(function(ch) { return ch.checkQueue(q).then(function(qok) { function check() { return ch.checkQueue(q).then(function(qok) { if (condition(qok)) { c.close(); return qok; } else schedule(check); }); } return check(); }); }); }); } // Return a promise that resolves when the queue has at least `num` // messages. If num is not supplied its assumed to be 1. function waitForMessages(q, num) { var min = (num === undefined) ? 1 : num; return waitForQueue(q, function(qok) { return qok.messageCount >= min; }); } suite("sendMessage", function() { // publish different size messages chtest("send to queue and get from queue", function(ch) { var q = 'test.send-to-q'; var msg = randomString(); return Promise.join(ch.assertQueue(q, QUEUE_OPTS), ch.purgeQueue(q)) .then(function() { ch.sendToQueue(q, Buffer.from(msg)); return waitForMessages(q); }) .then(function() { return ch.get(q, {noAck: true}); }) .then(function(m) { assert(m); assert.equal(msg, m.content.toString()); }); }); chtest("send (and get) zero content to queue", function(ch) { var q = 'test.send-to-q'; var msg = Buffer.alloc(0); return Promise.join( ch.assertQueue(q, QUEUE_OPTS), ch.purgeQueue(q)) .then(function() { ch.sendToQueue(q, msg); return waitForMessages(q);}) .then(function() { return ch.get(q, {noAck: true});}) .then(function(m) { assert(m); assert.deepEqual(msg, m.content); }); }); }); suite("binding, consuming", function() { // bind, publish, get chtest("route message", function(ch) { var ex = 'test.route-message'; var q = 'test.route-message-q'; var msg = randomString(); return Promise.join( ch.assertExchange(ex, 'fanout', EX_OPTS), ch.assertQueue(q, QUEUE_OPTS), ch.purgeQueue(q), ch.bindQueue(q, ex, '', {})) .then(function() { ch.publish(ex, '', Buffer.from(msg)); return waitForMessages(q);}) .then(function() { return ch.get(q, {noAck: true});}) .then(function(m) { assert(m); assert.equal(msg, m.content.toString()); }); }); // send to queue, purge, get-empty chtest("purge queue", function(ch) { var q = 'test.purge-queue'; return ch.assertQueue(q, {durable: false}) .then(function() { ch.sendToQueue(q, Buffer.from('foobar')); return waitForMessages(q);}) .then(function() { ch.purgeQueue(q); return ch.get(q, {noAck: true});}) .then(function(m) { assert(!m); // get-empty }); }); // bind again, unbind, publish, get-empty chtest("unbind queue", function(ch) { var ex = 'test.unbind-queue-ex'; var q = 'test.unbind-queue'; var viabinding = randomString(); var direct = randomString(); return Promise.join( ch.assertExchange(ex, 'fanout', EX_OPTS), ch.assertQueue(q, QUEUE_OPTS), ch.purgeQueue(q), ch.bindQueue(q, ex, '', {})) .then(function() { ch.publish(ex, '', Buffer.from('foobar')); return waitForMessages(q);}) .then(function() { // message got through! return ch.get(q, {noAck:true}) .then(function(m) {assert(m);});}) .then(function() { return ch.unbindQueue(q, ex, '', {});}) .then(function() { // via the no-longer-existing binding ch.publish(ex, '', Buffer.from(viabinding)); // direct to the queue ch.sendToQueue(q, Buffer.from(direct)); return waitForMessages(q);}) .then(function() {return ch.get(q)}) .then(function(m) { // the direct to queue message got through, the via-binding // message (sent first) did not assert.equal(direct, m.content.toString()); }); }); // To some extent this is now just testing semantics of the server, // but we can at least try out a few settings, and consume. chtest("consume via exchange-exchange binding", function(ch) { var ex1 = 'test.ex-ex-binding1', ex2 = 'test.ex-ex-binding2'; var q = 'test.ex-ex-binding-q'; var rk = 'test.routing.key', msg = randomString(); return Promise.join( ch.assertExchange(ex1, 'direct', EX_OPTS), ch.assertExchange(ex2, 'fanout', {durable: false, internal: true}), ch.assertQueue(q, QUEUE_OPTS), ch.purgeQueue(q), ch.bindExchange(ex2, ex1, rk, {}), ch.bindQueue(q, ex2, '', {})) .then(function() { return new Promise(function(resolve, reject) { function delivery(m) { if (m.content.toString() === msg) resolve(); else reject(new Error("Wrong message")); } ch.consume(q, delivery, {noAck: true}) .then(function() { ch.publish(ex1, rk, Buffer.from(msg)); }); }); }); }); // bind again, unbind, publish, get-empty chtest("unbind exchange", function(ch) { var source = 'test.unbind-ex-source'; var dest = 'test.unbind-ex-dest'; var q = 'test.unbind-ex-queue'; var viabinding = randomString(); var direct = randomString(); return Promise.join( ch.assertExchange(source, 'fanout', EX_OPTS), ch.assertExchange(dest, 'fanout', EX_OPTS), ch.assertQueue(q, QUEUE_OPTS), ch.purgeQueue(q), ch.bindExchange(dest, source, '', {}), ch.bindQueue(q, dest, '', {})) .then(function() { ch.publish(source, '', Buffer.from('foobar')); return waitForMessages(q);}) .then(function() { // message got through! return ch.get(q, {noAck:true}) .then(function(m) {assert(m);});}) .then(function() { return ch.unbindExchange(dest, source, '', {});}) .then(function() { // via the no-longer-existing binding ch.publish(source, '', Buffer.from(viabinding)); // direct to the queue ch.sendToQueue(q, Buffer.from(direct)); return waitForMessages(q);}) .then(function() {return ch.get(q)}) .then(function(m) { // the direct to queue message got through, the via-binding // message (sent first) did not assert.equal(direct, m.content.toString()); }); }); // This is a bit convoluted. Sorry. chtest("cancel consumer", function(ch) { var q = 'test.consumer-cancel'; var ctag; var recv1 = new Promise(function (resolve, reject) { Promise.join( ch.assertQueue(q, QUEUE_OPTS), ch.purgeQueue(q), // My callback is 'resolve the promise in `arrived`' ch.consume(q, resolve, {noAck:true}) .then(function(ok) { ctag = ok.consumerTag; ch.sendToQueue(q, Buffer.from('foo')); })); }); // A message should arrive because of the consume return recv1.then(function() { var recv2 = Promise.join( ch.cancel(ctag).then(function() { return ch.sendToQueue(q, Buffer.from('bar')); }), // but check a message did arrive in the queue waitForMessages(q)) .then(function() { return ch.get(q, {noAck:true}); }) .then(function(m) { // I'm going to reject it, because I flip succeed/fail // just below if (m.content.toString() === 'bar') { throw new Error(); } }); return expectFail(recv2); }); }); chtest("cancelled consumer", function(ch) { var q = 'test.cancelled-consumer'; return new Promise(function(resolve, reject) { return Promise.join( ch.assertQueue(q), ch.purgeQueue(q), ch.consume(q, function(msg) { if (msg === null) resolve(); else reject(new Error('Message not expected')); })) .then(function() { return ch.deleteQueue(q); }); }); }); // ack, by default, removes a single message from the queue chtest("ack", function(ch) { var q = 'test.ack'; var msg1 = randomString(), msg2 = randomString(); return Promise.join( ch.assertQueue(q, QUEUE_OPTS), ch.purgeQueue(q)) .then(function() { ch.sendToQueue(q, Buffer.from(msg1)); ch.sendToQueue(q, Buffer.from(msg2)); return waitForMessages(q, 2); }) .then(function() { return ch.get(q, {noAck: false}) }) .then(function(m) { assert.equal(msg1, m.content.toString()); ch.ack(m); // %%% is there a race here? may depend on // rabbitmq-sepcific semantics return ch.get(q); }) .then(function(m) { assert(m); assert.equal(msg2, m.content.toString()); }); }); // Nack, by default, puts a message back on the queue (where in the // queue is up to the server) chtest("nack", function(ch) { var q = 'test.nack'; var msg1 = randomString(); return Promise.join( ch.assertQueue(q, QUEUE_OPTS), ch.purgeQueue(q)) .then(function() { ch.sendToQueue(q, Buffer.from(msg1)); return waitForMessages(q);}) .then(function() { return ch.get(q, {noAck: false})}) .then(function(m) { assert.equal(msg1, m.content.toString()); ch.nack(m); return waitForMessages(q);}) .then(function() { return ch.get(q);}) .then(function(m) { assert(m); assert.equal(msg1, m.content.toString()); }); }); // reject is a near-synonym for nack, the latter of which is not // available in earlier RabbitMQ (or in AMQP proper). chtest("reject", function(ch) { var q = 'test.reject'; var msg1 = randomString(); return Promise.join( ch.assertQueue(q, QUEUE_OPTS), ch.purgeQueue(q)) .then(function() { ch.sendToQueue(q, Buffer.from(msg1)); return waitForMessages(q);}) .then(function() { return ch.get(q, {noAck: false})}) .then(function(m) { assert.equal(msg1, m.content.toString()); ch.reject(m); return waitForMessages(q);}) .then(function() { return ch.get(q);}) .then(function(m) { assert(m); assert.equal(msg1, m.content.toString()); }); }); chtest("prefetch", function(ch) { var q = 'test.prefetch'; return Promise.join( ch.assertQueue(q, QUEUE_OPTS), ch.purgeQueue(q), ch.prefetch(1)) .then(function() { ch.sendToQueue(q, Buffer.from('foobar')); ch.sendToQueue(q, Buffer.from('foobar')); return waitForMessages(q, 2); }) .then(function() { return new Promise(function(resolve) { var messageCount = 0; function receive(msg) { ch.ack(msg); if (++messageCount > 1) { resolve(messageCount); } } return ch.consume(q, receive, {noAck: false}) }); }) .then(function(c) { return assert.equal(2, c); }); }); chtest('close', function(ch) { // Resolving promise guarantees // channel is closed return ch.close(); }); }); var confirmtest = channel_test.bind(null, 'createConfirmChannel'); suite("confirms", function() { confirmtest('message is confirmed', function(ch) { var q = 'test.confirm-message'; return Promise.join( ch.assertQueue(q, QUEUE_OPTS), ch.purgeQueue(q)) .then(function() { return ch.sendToQueue(q, Buffer.from('bleep')); }); }); // Usually one can provoke the server into confirming more than one // message in an ack by simply sending a few messages in quick // succession; a bit unscientific I know. Luckily we can eavesdrop on // the acknowledgements coming through to see if we really did get a // multi-ack. confirmtest('multiple confirms', function(ch) { var q = 'test.multiple-confirms'; return Promise.join( ch.assertQueue(q, QUEUE_OPTS), ch.purgeQueue(q)) .then(function() { var multipleRainbows = false; ch.on('ack', function(a) { if (a.multiple) multipleRainbows = true; }); function prod(num) { var cs = []; function sendAndPushPromise() { var conf = Promise.fromCallback(function(cb) { return ch.sendToQueue(q, Buffer.from('bleep'), {}, cb); }); cs.push(conf); } for (var i=0; i < num; i++) sendAndPushPromise(); return Promise.all(cs).then(function() { if (multipleRainbows) return true; else if (num > 500) throw new Error( "Couldn't provoke the server" + " into multi-acking with " + num + " messages; giving up"); else { //console.warn("Failed with " + num + "; trying " + num * 2); return prod(num * 2); } }); } return prod(5); }); }); confirmtest('wait for confirms', function(ch) { for (var i=0; i < 1000; i++) { ch.publish('', '', Buffer.from('foobar'), {}); } return ch.waitForConfirms(); }) confirmtest('works when channel is closed', function(ch) { for (var i=0; i < 1000; i++) { ch.publish('', '', Buffer.from('foobar'), {}); } return ch.close().then(function () { return ch.waitForConfirms() }).then(function () { assert.strictEqual(true, false, 'Wait should have failed.') }, function (e) { assert.strictEqual(e.message, 'channel closed') }); }); });