amqplib
Version:
An AMQP 0-9-1 (e.g., RabbitMQ) library and client.
538 lines (499 loc) • 19.1 kB
JavaScript
const { describe, it } = require('node:test');
const assert = require('node:assert');
const api = require('../channel_api');
const util = require('./lib/util');
const schedule = util.schedule;
const randomString = util.randomString;
const promisify = require('node:util').promisify;
const URL = process.env.URL || 'amqp://localhost';
const QUEUE_OPTS = { durable: false };
const EX_OPTS = { durable: false };
function ignore() {}
describe('connect', () => {
it('at all', () => connect().then((c) => c.close()));
it('create channel', () => withChannel(ignore));
});
describe('updateSecret', () => {
it('updateSecret', () => {
return connect().then((c) =>
c.updateSecret(Buffer.from('new secret'), 'no reason')
.finally(() => c.close())
);
});
});
describe('assert, check, delete', () => {
it('assert and check queue', () => {
return withChannel((ch) => {
return ch.assertQueue('test.check-queue', QUEUE_OPTS)
.then((qok) => assert.strictEqual(qok.queue, 'test.check-queue'))
.then(() => ch.checkQueue('test.check-queue'))
.then((qok) => assert.strictEqual(qok.queue, 'test.check-queue'))
});
});
it('assert and check exchange', () => {
return withChannel((ch) => {
return ch.assertExchange('test.check-exchange', 'direct', EX_OPTS)
.then((eok) => assert.strictEqual(eok.exchange, 'test.check-exchange'))
.then(() => ch.checkExchange('test.check-exchange'))
.then((eok) => assert.ok(eok));
});
});
it('fail on reasserting queue with different options', () => {
return withChannel((ch) => {
ch.once('error', ignore);
return ch.assertQueue('test.reassert-queue', { durable: false, autoDelete: true })
.then(() => assert.rejects(() => ch.assertQueue('test.reassert-queue', { durable: false, autoDelete: false }), (err) => {
assert.match(err.message, /PRECONDITION_FAILED/);
assert.strictEqual(406, err.code);
assert.strictEqual(50, err.classId);
assert.strictEqual(10, err.methodId);
return true;
}));
});
});
it("fail on checking a queue that's not there", () => {
return withChannel((ch) => {
ch.once('error', ignore);
return assert.rejects(() => ch.checkQueue(`test.random-${randomString()}`), (err) => {
assert.match(err.message, /NOT_FOUND/);
assert.strictEqual(404, err.code);
assert.strictEqual(50, err.classId);
assert.strictEqual(10, err.methodId);
return true;
});
});
});
it("fail on checking an exchange that's not there", () => {
return withChannel((ch) => {
ch.once('error', ignore);
return assert.rejects(() => ch.checkExchange(`test.random-${randomString()}`), (err) => {
assert.match(err.message, /NOT_FOUND/);
assert.strictEqual(404, err.code);
return true;
});
});
});
it('fail on reasserting exchange with different type', () => {
return withChannel((ch) => {
ch.once('error', ignore);
const ex = 'test.reassert-ex';
return ch.assertExchange(ex, 'fanout', EX_OPTS)
.then(() => assert.rejects(() => ch.assertExchange(ex, 'direct', EX_OPTS), (err) => {
assert.match(err.message, /PRECONDITION_FAILED/);
assert.strictEqual(406, err.code);
return true;
}));
});
});
it('channel break on publishing to non-exchange', () => {
return withChannel((ch) =>
new Promise((resolve) => {
ch.once('error', (err) => {
assert.match(err.message, /NOT_FOUND/);
assert.strictEqual(404, err.code);
resolve();
});
ch.publish(randomString(), '', Buffer.from('foobar'));
})
);
});
it('delete queue', () => {
return withChannel((ch) => {
return ch.assertQueue('test.delete-queue', QUEUE_OPTS)
.then(() => ch.deleteQueue('test.delete-queue'))
.then(() => {
ch.once('error', ignore);
return assert.rejects(() => ch.checkQueue('test.delete-queue'), (err) => {
assert.match(err.message, /NOT_FOUND/);
assert.strictEqual(404, err.code);
return true;
});
});
});
});
it('delete exchange', () => {
return withChannel((ch) => {
return ch.assertExchange('test.delete-exchange', 'fanout', EX_OPTS)
.then(() => ch.deleteExchange('test.delete-exchange'))
.then(() => {
ch.once('error', ignore);
return assert.rejects(() => ch.checkExchange('test.delete-exchange'), (err) => {
assert.match(err.message, /NOT_FOUND/);
assert.strictEqual(404, err.code);
return true;
});
});
});
});
});
describe('sendMessage', () => {
it('send to queue and get from queue', () => {
const msg = randomString();
return withChannel((ch) =>
ch.assertQueue('test.send-to-q', QUEUE_OPTS)
.then(() => ch.purgeQueue('test.send-to-q'))
.then(() => {
ch.sendToQueue('test.send-to-q', Buffer.from(msg));
return waitForMessages('test.send-to-q');
})
.then(() => ch.get('test.send-to-q', { noAck: true }))
.then((m) => {
assert(m);
assert.equal(msg, m.content.toString());
})
);
});
it('send (and get) zero content to queue', () => {
return withChannel((ch) =>
ch.assertQueue('test.send-to-q', QUEUE_OPTS)
.then(() => ch.purgeQueue('test.send-to-q'))
.then(() => {
ch.sendToQueue('test.send-to-q', Buffer.alloc(0));
return waitForMessages('test.send-to-q');
})
.then(() => ch.get('test.send-to-q', { noAck: true }))
.then((m) => {
assert(m);
assert.deepEqual(Buffer.alloc(0), m.content);
})
);
});
});
describe('binding, consuming', () => {
// bind, publish, get
it('route message', () => {
const msg = randomString();
return withChannel((ch) =>
ch.assertExchange('test.route-message', 'fanout', EX_OPTS)
.then(() => ch.assertQueue('test.route-message-q', QUEUE_OPTS))
.then(() => ch.purgeQueue('test.route-message-q'))
.then(() => ch.bindQueue('test.route-message-q', 'test.route-message', '', {}))
.then(() => {
ch.publish('test.route-message', '', Buffer.from(msg));
return waitForMessages('test.route-message-q');
})
.then(() => ch.get('test.route-message-q', { noAck: true }))
.then((m) => {
assert(m);
assert.equal(msg, m.content.toString());
})
);
});
// send to queue, purge, get-empty
it('purge queue', () => {
return withChannel((ch) =>
ch.assertQueue('test.purge-queue', QUEUE_OPTS)
.then(() => {
ch.sendToQueue('test.purge-queue', Buffer.from('foobar'));
return waitForMessages('test.purge-queue');
})
.then(() => ch.purgeQueue('test.purge-queue'))
.then(() => ch.get('test.purge-queue', { noAck: true }))
.then((m) => {
assert(!m); // get-empty
})
);
});
// bind again, unbind, publish, get-empty
it('unbind queue', () => {
const viabinding = randomString();
const direct = randomString();
return withChannel((ch) =>
ch.assertExchange('test.unbind-queue-ex', 'fanout', EX_OPTS)
.then(() => ch.assertQueue('test.unbind-queue', QUEUE_OPTS))
.then(() => ch.purgeQueue('test.unbind-queue'))
.then(() => ch.bindQueue('test.unbind-queue', 'test.unbind-queue-ex', '', {}))
.then(() => {
ch.publish('test.unbind-queue-ex', '', Buffer.from('foobar'));
return waitForMessages('test.unbind-queue');
})
.then(() => ch.get('test.unbind-queue', { noAck: true }))
.then((m) => assert(m)) // message got through!
.then(() => ch.unbindQueue('test.unbind-queue', 'test.unbind-queue-ex', '', {}))
.then(() => {
// via the no-longer-existing binding
ch.publish('test.unbind-queue-ex', '', Buffer.from(viabinding));
// direct to the queue
ch.sendToQueue('test.unbind-queue', Buffer.from(direct));
return waitForMessages('test.unbind-queue');
})
.then(() => ch.get('test.unbind-queue'))
.then((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.
it('consume via exchange-exchange binding', () => {
const msg = randomString();
return withChannel((ch) =>
ch.assertExchange('test.ex-ex-binding1', 'direct', EX_OPTS)
.then(() => ch.assertExchange('test.ex-ex-binding2', 'fanout', { durable: false, internal: true }))
.then(() => ch.assertQueue('test.ex-ex-binding-q', QUEUE_OPTS))
.then(() => ch.purgeQueue('test.ex-ex-binding-q'))
.then(() => ch.bindExchange('test.ex-ex-binding2', 'test.ex-ex-binding1', 'test.routing.key', {}))
.then(() => ch.bindQueue('test.ex-ex-binding-q', 'test.ex-ex-binding2', '', {}))
.then(() => new Promise((resolve, reject) => {
ch.consume('test.ex-ex-binding-q', (m) => {
if (m.content.toString() === msg) resolve();
else reject(new Error('Wrong message'));
}, { noAck: true }).then(() => {
ch.publish('test.ex-ex-binding1', 'test.routing.key', Buffer.from(msg));
});
}))
);
});
// bind again, unbind, publish, get-empty
it('unbind exchange', () => {
const viabinding = randomString();
const direct = randomString();
return withChannel((ch) =>
ch.assertExchange('test.unbind-ex-source', 'fanout', EX_OPTS)
.then(() => ch.assertExchange('test.unbind-ex-dest', 'fanout', EX_OPTS))
.then(() => ch.assertQueue('test.unbind-ex-queue', QUEUE_OPTS))
.then(() => ch.purgeQueue('test.unbind-ex-queue'))
.then(() => ch.bindExchange('test.unbind-ex-dest', 'test.unbind-ex-source', '', {}))
.then(() => ch.bindQueue('test.unbind-ex-queue', 'test.unbind-ex-dest', '', {}))
.then(() => {
ch.publish('test.unbind-ex-source', '', Buffer.from('foobar'));
return waitForMessages('test.unbind-ex-queue');
})
.then(() => ch.get('test.unbind-ex-queue', { noAck: true }))
.then((m) => assert(m)) // message got through!
.then(() => ch.unbindExchange('test.unbind-ex-dest', 'test.unbind-ex-source', '', {}))
.then(() => {
// via the no-longer-existing binding
ch.publish('test.unbind-ex-source', '', Buffer.from(viabinding));
// direct to the queue
ch.sendToQueue('test.unbind-ex-queue', Buffer.from(direct));
return waitForMessages('test.unbind-ex-queue');
})
.then(() => ch.get('test.unbind-ex-queue'))
.then((m) => {
// the direct to queue message got through, the via-binding message (sent first) did not
assert.equal(direct, m.content.toString());
})
);
});
it('cancel consumer', () => {
let ctag;
return withChannel((ch) =>
ch.assertQueue('test.consumer-cancel', QUEUE_OPTS)
.then(() => ch.purgeQueue('test.consumer-cancel'))
.then(() => new Promise((resolve) => {
ch.consume('test.consumer-cancel', resolve, { noAck: true })
.then((ok) => {
ctag = ok.consumerTag;
ch.sendToQueue('test.consumer-cancel', Buffer.from('foo'));
});
}))
// first message arrived via consumer
.then(() => ch.cancel(ctag))
.then(() => {
ch.sendToQueue('test.consumer-cancel', Buffer.from('bar'));
return waitForMessages('test.consumer-cancel');
})
.then(() => ch.get('test.consumer-cancel', { noAck: true }))
.then((m) => {
// message arrived in queue but NOT via the cancelled consumer
assert.equal('bar', m.content.toString());
})
);
});
it('cancelled consumer', () => {
return withChannel((ch) =>
ch.assertQueue('test.cancelled-consumer')
.then(() => ch.purgeQueue('test.cancelled-consumer'))
.then(() => new Promise((resolve, reject) => {
ch.consume('test.cancelled-consumer', (msg) => {
if (msg === null) resolve();
else reject(new Error('Message not expected'));
}).then(() => ch.deleteQueue('test.cancelled-consumer'));
}))
);
});
// ack, by default, removes a single message from the queue
it('ack', () => {
const msg1 = randomString();
const msg2 = randomString();
return withChannel((ch) =>
ch.assertQueue('test.ack', QUEUE_OPTS)
.then(() => ch.purgeQueue('test.ack'))
.then(() => {
ch.sendToQueue('test.ack', Buffer.from(msg1));
ch.sendToQueue('test.ack', Buffer.from(msg2));
return waitForMessages('test.ack', 2);
})
.then(() => ch.get('test.ack', { noAck: false }))
.then((m) => {
assert.equal(msg1, m.content.toString());
ch.ack(m);
// %%% is there a race here? may depend on rabbitmq-specific semantics
return ch.get('test.ack');
})
.then((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)
it('nack', () => {
const msg1 = randomString();
return withChannel((ch) =>
ch.assertQueue('test.nack', QUEUE_OPTS)
.then(() => ch.purgeQueue('test.nack'))
.then(() => {
ch.sendToQueue('test.nack', Buffer.from(msg1));
return waitForMessages('test.nack');
})
.then(() => ch.get('test.nack', { noAck: false }))
.then((m) => {
assert.equal(msg1, m.content.toString());
ch.nack(m);
return waitForMessages('test.nack');
})
.then(() => ch.get('test.nack'))
.then((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).
it('reject', () => {
const msg1 = randomString();
return withChannel((ch) =>
ch.assertQueue('test.reject', QUEUE_OPTS)
.then(() => ch.purgeQueue('test.reject'))
.then(() => {
ch.sendToQueue('test.reject', Buffer.from(msg1));
return waitForMessages('test.reject');
})
.then(() => ch.get('test.reject', { noAck: false }))
.then((m) => {
assert.equal(msg1, m.content.toString());
ch.reject(m);
return waitForMessages('test.reject');
})
.then(() => ch.get('test.reject'))
.then((m) => {
assert(m);
assert.equal(msg1, m.content.toString());
})
);
});
it('prefetch', () => {
return withChannel((ch) =>
ch.assertQueue('test.prefetch', QUEUE_OPTS)
.then(() => ch.purgeQueue('test.prefetch'))
.then(() => ch.prefetch(1))
.then(() => {
ch.sendToQueue('test.prefetch', Buffer.from('foobar'));
ch.sendToQueue('test.prefetch', Buffer.from('foobar'));
return waitForMessages('test.prefetch', 2);
})
.then(() => new Promise((resolve) => {
let messageCount = 0;
ch.consume('test.prefetch', (msg) => {
ch.ack(msg);
if (++messageCount > 1) resolve(messageCount);
}, { noAck: false });
}))
.then((c) => assert.equal(2, c))
);
});
it('close', () => {
return withChannel((ch) => ch.close());
});
});
describe('confirms', () => {
it('message is confirmed', () => {
return withConfirmChannel((ch) =>
ch.assertQueue('test.confirm-message', QUEUE_OPTS)
.then(() => ch.purgeQueue('test.confirm-message'))
.then(() => ch.sendToQueue('test.confirm-message', 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.
it('multiple confirms', () => {
return withConfirmChannel((ch) =>
ch.assertQueue('test.multiple-confirms', QUEUE_OPTS)
.then(() => ch.purgeQueue('test.multiple-confirms'))
.then(() => {
let multipleRainbows = false;
ch.on('ack', (a) => {
if (a.multiple) multipleRainbows = true;
});
function prod(num) {
const cs = [];
for (let i = 0; i < num; i++) {
cs.push(promisify((cb) => ch.sendToQueue('test.multiple-confirms', Buffer.from('bleep'), {}, cb))());
}
return Promise.all(cs).then(() => {
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 return prod(num * 2);
});
}
return prod(5);
})
);
});
it('wait for confirms', () => {
return withConfirmChannel((ch) => {
for (let i = 0; i < 1000; i++) {
ch.publish('', '', Buffer.from('foobar'), {});
}
return ch.waitForConfirms();
});
});
it('works when channel is closed', () => {
return withConfirmChannel((ch) => {
for (let i = 0; i < 1000; i++) {
ch.publish('', '', Buffer.from('foobar'), {});
}
return ch.close()
.then(() => assert.rejects(() => ch.waitForConfirms(), (e) => {
assert.strictEqual(e.message, 'channel closed');
return true;
}));
});
});
});
function connect() {
return api.connect(URL);
}
function withChannel(cb) {
return connect().then((c) => c.createChannel().then(cb).finally(() => c.close()));
}
function withConfirmChannel(cb) {
return connect().then((c) => c.createConfirmChannel().then(cb).finally(() => c.close()));
}
function waitForQueue(q, condition) {
return connect(URL).then((c) =>
c.createChannel().then((ch) =>
ch.checkQueue(q).then((_qok) => {
function check() {
return ch.checkQueue(q).then((qok) => {
if (condition(qok)) {
c.close();
return qok;
} else schedule(check);
});
}
return check();
}),
),
);
}
function waitForMessages(q, num) {
const min = num === undefined ? 1 : num;
return waitForQueue(q, (qok) => qok.messageCount >= min);
}