amqp-node
Version:
An AMQP 0-9-1 (e.g., RabbitMQ) library and client.
702 lines (636 loc) • 19.3 kB
JavaScript
;
var assert = require('assert');
var api = require('../lib');
var util = require('./util');
var succeed = util.succeed,
fail = util.fail;
var schedule = util.schedule;
var randomString = util.randomString;
var Promise = require('bluebird');
var defer = Promise.defer;
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 promise.bind(this).then(Promise.reject, Promise.resolve);
// return new Promise(function (resolve, reject) {
// return promise.bind(this).then(reject, resolve);
// })
}
// Often, even interdependent operations don't need to be explicitly
// chained together with `.then`, since the channel implicitly
// serialises RPCs. Synchronising on the last operation is sufficient,
// provided all the operations are successful. This procedure removes
// some of the `then` noise, while still failing if any of its
// arguments fail.
function doAll() {
return Promise.all(Array.prototype.slice.call(arguments, 0))
.then(function (results) {
return results[results.length - 1];
});
}
// 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) {
it(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
.then(function () {
c.close();
});
});
});
}
var chtest = channel_test.bind(null, 'createChannel');
describe('connect', function () {
it('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
};
describe('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) {
var bork = defer();
ch.on('error', bork.resolve.bind(bork));
ch.publish(randomString(), '', new Buffer('foobar'));
return bork.promise;
});
chtest('delete queue', function (ch) {
var q = 'test.delete-queue';
return doAll(
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 doAll(
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 new Promise(function (resolve, reject) {
connect(URL).then(function (c) {
return c.createChannel()
.then(function (ch) {
function check() {
ch.checkQueue(q).then(function (qok) {
if (condition(qok)) {
c.close();
resolve(qok);
} else {
schedule(check);
}
});
}
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;
});
}
describe('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 doAll(
ch.assertQueue(q, QUEUE_OPTS),
ch.purgeQueue(q))
.then(function () {
ch.sendToQueue(q, new Buffer(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 = new Buffer(0);
return doAll(
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);
});
});
});
describe('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 doAll(
ch.assertExchange(ex, 'fanout', EX_OPTS),
ch.assertQueue(q, QUEUE_OPTS),
ch.purgeQueue(q),
ch.bindQueue(q, ex, '', {}))
.then(function () {
ch.publish(ex, '', new Buffer(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, new Buffer('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 doAll(
ch.assertExchange(ex, 'fanout', EX_OPTS),
ch.assertQueue(q, QUEUE_OPTS),
ch.purgeQueue(q),
ch.bindQueue(q, ex, '', {}))
.then(function () {
ch.publish(ex, '', new Buffer('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, '', new Buffer(viabinding));
// direct to the queue
ch.sendToQueue(q, new Buffer(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 doAll(
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 () {
var arrived = defer();
function delivery(m) {
if (m.content.toString() === msg) {
arrived.resolve();
} else {
arrived.reject(new Error('Wrong message'));
}
}
ch.consume(q, delivery, {
noAck: true
})
.then(function () {
ch.publish(ex1, rk, new Buffer(msg));
});
return arrived.promise;
});
});
// 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 doAll(
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, '', new Buffer('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, '', new Buffer(viabinding));
// direct to the queue
ch.sendToQueue(q, new Buffer(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 recv1 = defer();
var ctag;
doAll(
ch.assertQueue(q, QUEUE_OPTS),
ch.purgeQueue(q),
// My callback is 'resolve the promise in `arrived`'
ch.consume(q, function () {
recv1.resolve();
}, {
noAck: true
})
.then(function (ok) {
ctag = ok.consumerTag;
ch.sendToQueue(q, new Buffer('foo'));
}));
// A message should arrive because of the consume
return recv1.promise.then(function () {
// replace the promise resolved by the consume callback
recv1 = defer();
return doAll(
ch.cancel(ctag).then(function () {
ch.sendToQueue(q, new Buffer('bar'));
}),
// but check a message did arrive in the queue
waitForMessages(q))
.then(function () {
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') {
recv1.reject();
}
});
return expectFail(recv1.promise);
// i.e., fail on delivery, succeed on get-ok
});
});
});
chtest('cancelled consumer', function (ch) {
var q = 'test.cancelled-consumer';
var nullRecv = defer();
doAll(
ch.assertQueue(q),
ch.purgeQueue(q),
ch.consume(q, function (msg) {
if (msg === null) {
nullRecv.resolve();
} else {
nullRecv.reject(new Error('Message not expected'));
}
}))
.then(function () {
ch.deleteQueue(q);
});
return nullRecv.promise;
});
// ack, by default, removes a single message from the queue
chtest('ack', function (ch) {
var q = 'test.ack';
var msg1 = randomString(),
msg2 = randomString();
return doAll(
ch.assertQueue(q, QUEUE_OPTS),
ch.purgeQueue(q))
.then(function () {
ch.sendToQueue(q, new Buffer(msg1));
ch.sendToQueue(q, new Buffer(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 doAll(
ch.assertQueue(q, QUEUE_OPTS), ch.purgeQueue(q))
.then(function () {
ch.sendToQueue(q, new Buffer(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 doAll(
ch.assertQueue(q, QUEUE_OPTS), ch.purgeQueue(q))
.then(function () {
ch.sendToQueue(q, new Buffer(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 doAll(
ch.assertQueue(q, QUEUE_OPTS), ch.purgeQueue(q),
ch.prefetch(1))
.then(function () {
ch.sendToQueue(q, new Buffer('foobar'));
ch.sendToQueue(q, new Buffer('foobar'));
return waitForMessages(q, 2);
})
.then(function () {
var first = defer();
return doAll(
ch.consume(q, function (m) {
first.resolve(m);
}, {
noAck: false
}),
first.promise.then(function (m) {
first = defer();
ch.ack(m);
return first.promise.then(function (m) {
ch.ack(m);
});
}));
});
});
});
var confirmtest = channel_test.bind(null, 'createConfirmChannel');
describe('confirms', function () {
confirmtest('message is confirmed', function (ch) {
var q = 'test.confirm-message';
return doAll(
ch.assertQueue(q, QUEUE_OPTS), ch.purgeQueue(q))
.then(function () {
return ch.sendToQueue(q, new Buffer('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 doAll(
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 = defer();
ch.sendToQueue(q, new Buffer('bleep'), {},
function (err) {
if (err) {
conf.reject();
} else {
conf.resolve();
}
});
cs.push(conf.promise);
}
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('', '', new Buffer('foobar'), {});
}
return ch.waitForConfirms();
});
});