amqplib
Version:
An AMQP 0-9-1 (e.g., RabbitMQ) library and client.
593 lines (549 loc) • 18.2 kB
JavaScript
const { describe, it } = require('node:test');
const assert = require('node:assert');
const promisify = require('node:util').promisify;
const Channel = require('../lib/channel').Channel;
const Connection = require('../lib/connection').Connection;
const util = require('./lib/util');
const latch = util.latch;
const completes = util.completes;
const handshake = util.handshake;
const defs = require('../lib/defs');
const OPEN_OPTS = require('./lib/data').OPEN_OPTS;
const LOG_ERRORS = process.env.LOG_ERRORS;
function baseChannelTest(client, server) {
return (_t, done) => {
const decrementLatch = latch(2, done);
const pair = util.socketPair();
const c = new Connection(pair.client);
if (LOG_ERRORS) c.on('error', console.warn);
c.open(OPEN_OPTS, (err, _ok) => {
if (err === null) client(c, decrementLatch);
else decrementLatch(err);
});
pair.server.read(8); // discard the protocol header
util.runServer(pair.server, (send, wait) => {
handshake(send, wait).then(() => {
server(send, wait, decrementLatch);
}, decrementLatch);
});
};
}
function channelTest(client, server) {
return baseChannelTest(
(conn, done) => {
const ch = new Channel(conn);
if (LOG_ERRORS) ch.on('error', console.warn);
client(ch, done, conn);
},
(send, wait, done) => {
channel_handshake(send, wait)
.then((ch) => server(send, wait, done, ch))
.then(null, done); // so you can return a promise to let
// errors bubble out
},
);
}
function channel_handshake(send, wait) {
return wait(defs.ChannelOpen)().then((open) => {
assert.notEqual(0, open.channel);
send(defs.ChannelOpenOk, { channelId: Buffer.from('') }, open.channel);
return open.channel;
});
}
// fields for deliver and publish and get-ok
const DELIVER_FIELDS = {
consumerTag: 'fake',
deliveryTag: 1,
redelivered: false,
exchange: 'foo',
routingKey: 'bar',
replyCode: defs.constants.NO_ROUTE,
replyText: 'derp',
};
function open(ch) {
ch.allocate();
return promisify((cb) => {
ch._rpc(defs.ChannelOpen, { outOfBand: '' }, defs.ChannelOpenOk, cb);
})();
}
describe('Channel', () => {
describe('channel open and close', () => {
it('open', channelTest(
(ch, cb) => open(ch).then((ok) => {
assert.equal(ok.id, defs.ChannelOpenOk);
cb();
}),
(_send, _wait, cb) => cb()
));
it('bad server', baseChannelTest(
(c, cb) => {
const ch = new Channel(c);
assert.rejects(() => open(ch), (err) => {
assert.match(err.message, /Expected ChannelOpenOk; got <ChannelCloseOk channel:1>/);
return true;
}).then(() => cb());
}, (send, wait, cb) =>
wait(defs.ChannelOpen)()
.then((open) => send(defs.ChannelCloseOk, {}, open.channel))
.then(cb)
)
);
it('open, close', channelTest(
(ch, cb) => {
open(ch)
.then(() => new Promise((resolve) => {
ch.closeBecause('Bye', defs.constants.REPLY_SUCCESS, resolve);
}))
.then(cb);
}, (send, wait, cb, ch) =>
wait(defs.ChannelClose)()
.then((_close) => send(defs.ChannelCloseOk, {}, ch))
.then(cb)
)
);
it('server close', channelTest(
(ch, cb) => {
ch.once('error', (err) => {
assert.match(err.message, /Channel closed by server: 504 \(CHANNEL-ERROR\)/);
assert.strictEqual(504, err.code);
assert.strictEqual(0, err.classId);
assert.strictEqual(0, err.methodId);
cb();
});
open(ch);
}, (send, wait, cb, ch) => {
send(defs.ChannelClose, {
replyText: 'Forced close',
replyCode: defs.constants.CHANNEL_ERROR,
classId: 0,
methodId: 0,
}, ch);
wait(defs.ChannelCloseOk)().then(cb);
}));
it('overlapping channel/server close', channelTest(
(ch, cb, conn) => {
const decrementLatch = latch(2, cb);
conn.once('error', (err) => {
assert.match(err.message, /Connection closed: 541 \(INTERNAL-ERROR\)/);
decrementLatch();
});
ch.on('close', (err) => {
assert.ifError(err);
decrementLatch();
});
open(ch)
.then(() => ch.closeBecause('Bye', defs.constants.REPLY_SUCCESS));
}, (send, wait, cb, _ch) => {
wait(defs.ChannelClose)()
.then(() => send(defs.ConnectionClose, {
replyText: 'Got there first',
replyCode: defs.constants.INTERNAL_ERROR,
classId: 0,
methodId: 0,
}, 0)
)
.then(wait(defs.ConnectionCloseOk))
.then(cb);
}));
it('double close', channelTest((ch, cb) => {
open(ch)
.then(() => {
ch.closeBecause('First close', defs.constants.REPLY_SUCCESS);
// NB no synchronisation, we do this straight away
assert.throws(() => {
ch.closeBecause('Second close', defs.constants.REPLY_SUCCESS);
}, (err) => {
assert.match(err.message, /Channel closing/);
return true;
});
})
.then(cb);
}, (send, wait, cb, ch) => {
wait(defs.ChannelClose)()
.then(() => send(defs.ChannelCloseOk, {}, ch))
.then(cb);
}));
});
describe('channel machinery', () => {
it('RPC', channelTest((ch, cb) => {
const decrementLatch = latch(3, cb);
open(ch)
.then(() => {
const fields = { prefetchCount: 10, prefetchSize: 0, global: false };
ch._rpc(defs.BasicQos, fields, defs.BasicQosOk, decrementLatch);
ch._rpc(defs.BasicQos, fields, defs.BasicQosOk, decrementLatch);
ch._rpc(defs.BasicQos, fields, defs.BasicQosOk, decrementLatch);
})
}, (send, wait, cb, ch) => {
function sendOk() {
send(defs.BasicQosOk, {}, ch);
}
return wait(defs.BasicQos)()
.then(sendOk)
.then(wait(defs.BasicQos))
.then(sendOk)
.then(wait(defs.BasicQos))
.then(sendOk)
.then(cb);
}));
it('Bad RPC', channelTest(
(ch, cb) => {
// We want to see the RPC rejected and the channel closed (with an error)
const decrementLatch = latch(2, cb);
ch.once('error', (err) => {
assert.match(err.message, /Expected BasicRecoverOk; got <BasicGetEmpty channel:1>/);
assert.strictEqual(505, err.code);
assert.strictEqual(60, err.classId);
assert.strictEqual(72, err.methodId);
decrementLatch();
});
open(ch).then(() => {
ch._rpc(defs.BasicRecover, { requeue: true }, defs.BasicRecoverOk, (err) => {
assert.match(err.message, /Expected BasicRecoverOk; got <BasicGetEmpty channel:1>/);
decrementLatch();
});
}, decrementLatch);
},
(send, wait, cb, ch) =>
wait()()
.then(() => send(defs.BasicGetEmpty, { clusterId: '' }, ch))
// oh wait! that was wrong! expect a channel close
.then(wait(defs.ChannelClose))
.then(() => send(defs.ChannelCloseOk, {}, ch))
.then(cb),
));
it('RPC on closed channel', channelTest(
(ch, cb) => {
open(ch);
const close = new Promise((resolve) => {
ch.once('error', (err) => {
assert.match(err.message, /Channel closed by server: 504 \(CHANNEL-ERROR\)/);
assert.strictEqual(504, err.code);
assert.strictEqual(0, err.classId);
assert.strictEqual(0, err.methodId);
resolve();
});
});
function failureCb(resolve, reject) {
return (err) => {
if (err !== null) resolve();
else reject();
};
}
const fail1 = new Promise((resolve, reject) =>
ch._rpc(defs.BasicRecover, { requeue: true }, defs.BasicRecoverOk, failureCb(resolve, reject)),
);
const fail2 = new Promise((resolve, reject) =>
ch._rpc(defs.BasicRecover, { requeue: true }, defs.BasicRecoverOk, failureCb(resolve, reject)),
);
Promise.all([close, fail1, fail2]).then(cb);
},
(send, wait, cb, ch) => {
wait(defs.BasicRecover)()
.then(() => {
send(defs.ChannelClose, {
replyText: 'Nuh-uh!',
replyCode: defs.constants.CHANNEL_ERROR,
methodId: 0,
classId: 0,
}, ch);
return wait(defs.ChannelCloseOk);
})
.then(cb);
},
));
it('publish all < single chunk threshold', channelTest(
(ch, cb) => {
open(ch)
.then(() => {
ch.sendMessage({
exchange: 'foo',
routingKey: 'bar',
mandatory: false,
immediate: false,
ticket: 0,
}, {}, Buffer.from('foobar'));
})
.then(cb);
},
(_send, wait, cb, _ch) => {
wait(defs.BasicPublish)()
.then(wait(defs.BasicProperties))
.then(wait()) // content frame
.then((f) => {
assert.equal('foobar', f.content.toString());
})
.then(cb);
},
));
it('publish content > single chunk threshold', channelTest(
(ch, cb) => {
open(ch);
completes(() => {
ch.sendMessage(
{
exchange: 'foo',
routingKey: 'bar',
mandatory: false,
immediate: false,
ticket: 0,
},
{},
Buffer.alloc(3000),
);
}, cb);
}, (_send, wait, cb, _ch) => {
wait(defs.BasicPublish)()
.then(wait(defs.BasicProperties))
.then(wait()) // content frame
.then((f) => {
assert.equal(3000, f.content.length);
})
.then(cb);
}
));
it('publish method & headers > threshold', channelTest(
(ch, cb) => {
open(ch);
completes(() => {
ch.sendMessage({
exchange: 'foo',
routingKey: 'bar',
mandatory: false,
immediate: false,
ticket: 0,
}, {
headers: { foo: Buffer.alloc(3000) },
}, Buffer.from('foobar'));
}, cb);
}, (_send, wait, cb, _ch) => {
wait(defs.BasicPublish)()
.then(wait(defs.BasicProperties))
.then(wait()) // content frame
.then((f) => {
assert.equal('foobar', f.content.toString());
})
.then(cb);
}
));
it('publish zero-length message', channelTest((ch, cb) => {
open(ch);
completes(() => {
ch.sendMessage({
exchange: 'foo',
routingKey: 'bar',
mandatory: false,
immediate: false,
ticket: 0,
}, {}, Buffer.alloc(0));
ch.sendMessage({
exchange: 'foo',
routingKey: 'bar',
mandatory: false,
immediate: false,
ticket: 0,
}, {}, Buffer.alloc(0));
}, cb);
}, (_send, wait, cb, _ch) => {
wait(defs.BasicPublish)()
.then(wait(defs.BasicProperties))
// no content frame for a zero-length message
.then(wait(defs.BasicPublish))
.then(cb);
}));
it('delivery', channelTest((ch, cb) => {
open(ch);
ch.on('delivery', (m) => {
completes(() => {
assert.equal('barfoo', m.content.toString());
}, cb);
});
}, (send, _wait, cb, ch) => {
completes(() => {
send(defs.BasicDeliver, DELIVER_FIELDS, ch, Buffer.from('barfoo'));
}, cb);
}));
it('zero byte msg', channelTest(
(ch, cb) => {
open(ch);
ch.on('delivery', (m) => {
completes(() => {
assert.deepEqual(Buffer.alloc(0), m.content);
}, cb);
});
},
(send, _wait, cb, ch) => {
completes(() => {
send(defs.BasicDeliver, DELIVER_FIELDS, ch, Buffer.from(''));
}, cb);
}
));
it('bad delivery', channelTest((ch, cb) => {
const decrementLatch = latch(2, cb);
ch.on('error', (err) => {
assert.match(err.message, /Expected headers frame after delivery/);
assert.strictEqual(505, err.code);
assert.strictEqual(60, err.classId);
assert.strictEqual(60, err.methodId);
decrementLatch();
});
ch.on('close', decrementLatch);
open(ch);
}, (send, wait, cb, ch) => {
send(defs.BasicDeliver, DELIVER_FIELDS, ch);
// now send another deliver without having sent the content
send(defs.BasicDeliver, DELIVER_FIELDS, ch);
return wait(defs.ChannelClose)()
.then(() => send(defs.ChannelCloseOk, {}, ch))
.then(cb);
}));
it('bad content send', channelTest((ch, cb) => {
completes(() => {
open(ch);
assert.throws(() => {
ch.sendMessage({ routingKey: 'foo', exchange: 'amq.direct' }, {}, null);
}, (err) => {
assert.match(err.message, /content is not a buffer/);
return true;
});
}, cb);
}, (_send, _wait, cb, _ch) => cb()));
it('bad properties send', channelTest((ch, cb) => {
completes(() => {
open(ch);
assert.throws(() => {
ch.sendMessage({ routingKey: 'foo', exchange: 'amq.direct' }, { contentEncoding: 7 }, Buffer.from('foobar'));
}, (err) => {
assert.match(err.message, /Field 'contentEncoding' is the wrong type/);
return true;
});
}, cb);
}, (_send, _wait, cb, _ch) => cb()));
it('bad consumer', channelTest((ch, cb) => {
const decrementLatch = latch(2, cb);
ch.on('delivery', () => {
throw new Error('I am a bad consumer');
});
ch.on('error', (err) => {
assert.match(err.message, /I am a bad consumer/);
assert.strictEqual(541, err.code);
assert.strictEqual(undefined, err.classId);
assert.strictEqual(undefined, err.methodId);
decrementLatch();
});
ch.on('close', decrementLatch);
open(ch);
}, (send, wait, cb, ch) => {
send(defs.BasicDeliver, DELIVER_FIELDS, ch, Buffer.from('barfoo'));
return wait(defs.ChannelClose)()
.then(() => send(defs.ChannelCloseOk, {}, ch))
.then(cb);
}));
it('bad send in consumer', channelTest((ch, cb) => {
const decrementLatch = latch(2, cb);
ch.on('delivery', () => {
ch.sendMessage({ routingKey: 'foo', exchange: 'amq.direct' }, {}, null); // can't send null
});
ch.on('error', (err) => {
assert.match(err.message, /content is not a buffer/);
assert.strictEqual(541, err.code);
assert.strictEqual(undefined, err.classId);
assert.strictEqual(undefined, err.methodId);
decrementLatch();
});
ch.on('close', decrementLatch);
open(ch);
}, (send, wait, cb, ch) => {
const decrementLatch = latch(2, cb);
completes(() => {
send(defs.BasicDeliver, DELIVER_FIELDS, ch, Buffer.from('barfoo'));
}, decrementLatch);
return wait(defs.ChannelClose)()
.then(() => send(defs.ChannelCloseOk, {}, ch))
.then(decrementLatch);
}));
it('return', channelTest((ch, cb) => {
ch.on('return', (m) => {
completes(() => {
assert.equal('barfoo', m.content.toString());
}, cb);
});
open(ch);
}, (send, _wait, cb, ch) => {
completes(() => {
send(defs.BasicReturn, DELIVER_FIELDS, ch, Buffer.from('barfoo'));
}, cb);
}));
it('cancel', channelTest((ch, cb) => {
ch.on('cancel', (f) => {
completes(() => {
assert.equal('product of society', f.consumerTag);
}, cb);
});
open(ch);
}, (send, _wait, cb, ch) => {
completes(() => {
send(defs.BasicCancel, {
consumerTag: 'product of society',
nowait: false,
}, ch);
}, cb);
}));
function confirmTest(variety, method) {
return it(`confirm ${variety}`, channelTest((ch, done) => {
ch.on(variety, (f) => {
completes(() => {
assert.equal(1, f.deliveryTag);
}, done);
});
open(ch);
}, (send, _wait, done, ch) => {
completes(() => {
send(method, {
deliveryTag: 1,
multiple: false,
}, ch);
}, done);
}));
}
confirmTest('ack', defs.BasicAck);
confirmTest('nack', defs.BasicNack);
it('out-of-order acks', channelTest((ch, cb) => {
const decrementLatch = latch(3, () => {
completes(() => {
assert.equal(0, ch.unconfirmed.length);
assert.equal(4, ch.lwm);
}, cb);
});
ch.pushConfirmCallback(decrementLatch);
ch.pushConfirmCallback(decrementLatch);
ch.pushConfirmCallback(decrementLatch);
open(ch);
}, (send, _wait, cb, ch) => {
completes(() => {
send(defs.BasicAck, { deliveryTag: 2, multiple: false }, ch);
send(defs.BasicAck, { deliveryTag: 3, multiple: false }, ch);
send(defs.BasicAck, { deliveryTag: 1, multiple: false }, ch);
}, cb);
}));
it('not all out-of-order acks', channelTest((ch, cb) => {
const decrementLatch = latch(2, () => {
completes(() => {
assert.equal(1, ch.unconfirmed.length);
assert.equal(3, ch.lwm);
}, cb);
});
ch.pushConfirmCallback(decrementLatch); // tag = 1
ch.pushConfirmCallback(decrementLatch); // tag = 2
ch.pushConfirmCallback(() => {
assert.fail('Confirm callback should not be called');
});
open(ch);
}, (send, _wait, cb, ch) => {
completes(() => {
send(defs.BasicAck, { deliveryTag: 2, multiple: false }, ch);
send(defs.BasicAck, { deliveryTag: 1, multiple: false }, ch);
}, cb);
}));
});
});