mock-amqplib
Version:
stub rabbit mq in integration tests
679 lines (574 loc) • 24.3 kB
JavaScript
const amqp = require('./main');
const generateQueueName = () => `test-queue-${Math.random()}`;
const generateExchangeName = () => `test-exchange-${Math.random()}`;
const sleep = timeMs => new Promise(resolve => {
setTimeout(resolve, timeMs);
});
test('getting a single message from queue', async () => {
const connection = await amqp.connect('some-random-uri');
const channel = await connection.createChannel();
const queueName = generateQueueName();
await channel.assertQueue(queueName, { durable: true });
channel.sendToQueue(queueName, 'test-content', {
headers: { groupBy: 'groupness' }
});
const message = await channel.get(queueName, { noAck: true });
const emptyQueueResponse = await channel.get(queueName, { noAck: true });
expect(message).toMatchObject({
content: 'test-content',
properties: {
headers: { groupBy: 'groupness' }
}
});
expect(emptyQueueResponse).toEqual(false);
});
test('consuming messages', async () => {
const connection = await amqp.connect('some-random-uri');
const channel = await connection.createChannel();
const queueName = generateQueueName();
await channel.assertQueue(queueName, { durable: true });
const consumer = jest.fn();
channel.sendToQueue(queueName, 'test-message-1');
await channel.prefetch(10);
const { consumerTag } = await channel.consume(queueName, consumer);
channel.sendToQueue(queueName, 'test-message-2');
await channel.cancel(consumerTag);
channel.sendToQueue(queueName, 'test-message-3');
expect(consumer.mock.calls).toMatchObject([
[{ content: 'test-message-1', properties: { headers: expect.anything() } }],
[{ content: 'test-message-2' }]
]);
});
test('nackinkg a message puts it back to queue', async () => {
const connection = await amqp.connect('some-random-uri');
const channel = await connection.createChannel();
const queueName = generateQueueName();
await channel.assertQueue(queueName, { durable: true });
channel.sendToQueue(queueName, 'test-content');
const message = await channel.get(queueName);
const afterRead = await channel.get(queueName);
channel.nack(message);
const reRead = await channel.get(queueName);
expect(afterRead).toEqual(false);
expect(reRead.content).toEqual('test-content');
});
test('checkQueue return status for the queue', async () => {
const connection = await amqp.connect('some-random-uri');
const channel = await connection.createChannel();
const queueName = generateQueueName();
await channel.assertQueue(queueName, { durable: true });
channel.sendToQueue(queueName, 'test-content-1');
channel.sendToQueue(queueName, 'test-content-2');
const status = await channel.checkQueue(queueName);
expect(status).toEqual({
consumerCount: 0,
queue: queueName,
messageCount: 2
});
});
test('assertExchange and checkExchange return exchange name', async () => {
const connection = await amqp.connect('some-random-uri');
const channel = await connection.createChannel();
const exchangeName = generateExchangeName();
const assertResult = await channel.assertExchange(exchangeName, 'direct');
const checkResult = await channel.checkExchange(exchangeName);
expect(assertResult).toEqual({ exchange: exchangeName });
expect(checkResult).toEqual(assertResult);
});
test('purgeQueue deletes messages from queue', async () => {
const connection = await amqp.connect('some-random-uri');
const channel = await connection.createChannel();
const queueName = generateQueueName();
await channel.assertQueue(queueName, { durable: true });
channel.sendToQueue(queueName, 'test-content-1');
channel.sendToQueue(queueName, 'test-content-2');
await channel.purgeQueue(queueName);
const message = await channel.get(queueName);
expect(message).toEqual(false);
});
test('default exchange', async () => {
const connection = await amqp.connect('some-random-uri');
const channel = await connection.createChannel();
const targetQueueName = generateQueueName();
const anotherQueueName = generateQueueName();
await channel.assertQueue(targetQueueName);
await channel.assertQueue(anotherQueueName);
await channel.publish('', targetQueueName, 'content-1');
expect(await channel.get(targetQueueName)).toMatchObject({
content: 'content-1',
fields: {
exchange: '',
routingKey: targetQueueName
}
});
expect(await channel.get(anotherQueueName)).toEqual(false);
});
test('direct exchange', async () => {
const connection = await amqp.connect('some-random-uri');
const channel = await connection.createChannel();
await channel.assertExchange('retry-exchange', 'direct');
await channel.assertQueue('retry-queue-10s');
await channel.assertQueue('retry-queue-20s');
await channel.bindQueue('retry-queue-10s', 'retry-exchange', 'some-target-queue');
await channel.bindQueue('retry-queue-20s', 'retry-exchange', 'some-other-queue');
await channel.publish('retry-exchange', 'some-target-queue', 'content-1');
expect(await channel.get('retry-queue-10s')).toMatchObject({
content: 'content-1',
fields: {
exchange: 'retry-exchange',
routingKey: 'some-target-queue'
}
});
expect(await channel.get('retry-queue-20s')).toEqual(false);
});
test('x-delayed-message exchange', async () => {
const connection = await amqp.connect('some-random-uri');
const channel = await connection.createChannel();
await channel.assertExchange('retry-exchange', 'x-delayed-message');
await channel.assertQueue('retry-queue-10s');
await channel.assertQueue('retry-queue-20s');
await channel.bindQueue('retry-queue-10s', 'retry-exchange', 'some-target-queue');
await channel.bindQueue('retry-queue-20s', 'retry-exchange', 'some-other-queue');
await channel.publish('retry-exchange', 'some-target-queue', 'content-1');
expect(await channel.get('retry-queue-10s')).toMatchObject({
content: 'content-1',
fields: {
exchange: 'retry-exchange',
routingKey: 'some-target-queue'
}
});
expect(await channel.get('retry-queue-20s')).toEqual(false);
await channel.assertExchange('retry-exchange-with-options', 'x-delayed-message', {
durable: true,
arguments: { 'x-delayed-type': 'direct' }
});
await channel.assertQueue('retry-queue-10s-options');
await channel.assertQueue('retry-queue-20s-options');
await channel.bindQueue(
'retry-queue-10s-options',
'retry-exchange-with-options',
'some-target-queue-options'
);
await channel.bindQueue(
'retry-queue-20s-options',
'retry-exchange-with-options',
'some-other-queue-options'
);
await channel.publish('retry-exchange-with-options', 'some-target-queue-options', 'content-2');
expect(await channel.get('retry-queue-10s-options')).toMatchObject({
content: 'content-2',
fields: {
exchange: 'retry-exchange-with-options',
routingKey: 'some-target-queue-options'
}
});
expect(await channel.get('retry-queue-20s-options')).toEqual(false);
});
const routingKeys = [
'a1', '2b', '3c4', 'a1.2b', 'a1.3c4', 'a1.d', '2b.3c4', '2b.d', '3c4.d',
'a1.2b.3c4', 'a1.3c4.d', 'a1.2b.d', '2b.3c4.d', 'a1.2b.3c4.d'
];
const topicCases = [
{ pattern: 'some.pattern', result: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] },
{ pattern: 'a1.2b', result: [0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] },
{ pattern: 'a1.*.*', result: [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0] },
{ pattern: 'a1.*.#', result: [0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 1] },
{ pattern: '*.2b.*', result: [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0] },
{ pattern: '#.3c4', result: [0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0] },
{ pattern: '*.#.d', result: [0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1] },
{ pattern: '*', result: [1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] },
{ pattern: '*.#.*', result: [0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] },
{ pattern: '#.*.*.#', result: [0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] },
{ pattern: '#.*', result: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] },
{ pattern: '#.#.#', result: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] },
{ pattern: '#', result: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] },
];
test.each(topicCases)('topic exchange: $pattern', async (test) => {
const connection = await amqp.connect('some-random-uri');
const channel = await connection.createChannel();
const queueName = generateQueueName();
const exchangeName = generateExchangeName();
await channel.assertExchange(exchangeName, 'topic');
await channel.assertQueue(queueName);
await channel.bindQueue(queueName, exchangeName, test.pattern);
for (const key of routingKeys) {
const i = routingKeys.indexOf(key);
await channel.publish(exchangeName, key, 'content-1');
const message = await channel.get(queueName);
if (test.result[i]) {
expect(message).toMatchObject({
content: 'content-1',
fields: {
exchange: exchangeName,
routingKey: key
},
properties: {}
});
} else {
expect(message).toEqual(false);
}
}
});
test('headers exchange', async () => {
const connection = await amqp.connect('some-random-uri');
const channel = await connection.createChannel();
await channel.assertExchange('retry-exchange', 'headers');
await channel.assertQueue('retry-queue-10s');
await channel.assertQueue('retry-queue-20s');
await channel.bindQueue('retry-queue-10s', 'retry-exchange', '', {
retryCount: 1
});
await channel.bindQueue('retry-queue-20s', 'retry-exchange', '', {
retryCount: 2
});
await channel.publish('retry-exchange', 'some-target-queue', 'content-1', {
headers: { retryCount: 1 }
});
await channel.publish('retry-exchange', 'some-other-queue', 'content-2', {
headers: { retryCount: 2 }
});
expect(await channel.get('retry-queue-10s')).toMatchObject({
content: 'content-1',
fields: {
exchange: 'retry-exchange',
routingKey: 'some-target-queue'
}
});
expect(await channel.get('retry-queue-20s')).toMatchObject({
content: 'content-2'
});
});
test('should send and get message via ConfirmChannel', async () => {
const connection = await amqp.connect('some-random-uri');
const channel = await connection.createConfirmChannel();
const targetQueueName = generateQueueName();
const anotherQueueName = generateQueueName();
await channel.assertQueue(targetQueueName);
await channel.assertQueue(anotherQueueName);
await new Promise((resolve, reject) => {
channel.sendToQueue(targetQueueName, 'content-1', {}, (err, result) => {
if (err) {
reject(err);
return;
}
resolve(result);
});
});
expect(await channel.get(targetQueueName)).toMatchObject({
content: 'content-1',
fields: {
exchange: '',
routingKey: targetQueueName
}
});
expect(await channel.get(anotherQueueName)).toEqual(false);
});
test('direct exchange via ConfirmChannel', async () => {
const connection = await amqp.connect('some-random-uri');
const channel = await connection.createConfirmChannel();
const targetQueueName = generateQueueName();
const anotherQueueName = generateQueueName();
const exchangeName = generateExchangeName();
await channel.assertExchange(exchangeName, 'direct');
await channel.assertQueue(targetQueueName);
await channel.assertQueue(anotherQueueName);
await channel.bindQueue(targetQueueName, exchangeName, 'some-target-queue');
await channel.bindQueue(anotherQueueName, exchangeName, 'some-other-queue');
await new Promise((resolve, reject) => {
channel.publish(exchangeName, 'some-target-queue', 'content-1', {}, (err, result) => {
if (err) {
reject(err);
return;
}
resolve(result);
});
});
expect(await channel.get(targetQueueName)).toMatchObject({
content: 'content-1',
fields: {
exchange: exchangeName,
routingKey: 'some-target-queue'
}
});
expect(await channel.get(anotherQueueName)).toEqual(false);
});
test('emitting on a channel triggers on callbacks', async () => {
const connection = await amqp.connect('some-random-uri');
const channel = await connection.createChannel();
const listener = jest.fn();
channel.on('close', listener);
channel.emit('close');
expect(listener).toBeCalled();
});
test('it should always set header property of messages even if not set', async () => {
const connection = await amqp.connect('some-random-uri');
const channel = await connection.createChannel();
const queueName = generateQueueName();
await channel.assertQueue(queueName);
channel.sendToQueue(queueName, 'test-content');
const message = await channel.get(queueName);
expect(message).toMatchObject({
content: 'test-content',
properties: {
headers: expect.anything()
}
});
});
it('should not put nack-ed messages back to queue if requeue is set to false', async () => {
const connection = await amqp.connect('some-random-uri');
const channel = await connection.createChannel();
const queueName = generateQueueName();
await channel.assertQueue(queueName);
channel.sendToQueue(queueName, 'test-content');
const message = await channel.get(queueName);
channel.nack(message, false, false);
const reRead = await channel.get(queueName);
expect(reRead).toEqual(false);
});
test('assert queue should return object with property "queue"', async () => {
const connection = await amqp.connect('some-random-uri');
const channel = await connection.createChannel();
const queueName = generateQueueName();
const queue = await channel.assertQueue(queueName);
expect(queue).toMatchObject({
queue: queueName
});
});
test('assert empty queue should create new queue with random name with prefix "amq.gen-"', async () => {
const connection = await amqp.connect('some-random-uri');
const channel = await connection.createChannel();
const queue1 = await channel.assertQueue('');
const queue2 = await channel.assertQueue('');
expect(queue1.queue).toMatch(/^amq.gen-\w{22}/);
expect(queue2.queue).toMatch(/^amq.gen-\w{22}/);
expect(queue1.queue).not.toBe(queue2.queue);
});
test('assert "credentials" to have been defined with required methods', () => {
expect(amqp.credentials).toBeDefined();
expect(amqp.credentials.plain).toBeDefined();
expect(amqp.credentials.amqplain).toBeDefined();
expect(amqp.credentials.external).toBeDefined();
});
test('assert required methods of "credentials"', () => {
expect(amqp.credentials.plain('user', 'pass')).toEqual({
mechanism: 'PLAIN',
response: expect.any(Function),
username: 'user',
password: 'pass'
});
expect(amqp.credentials.amqplain('user', 'pass')).toEqual({
mechanism: 'AMQPLAIN',
response: expect.any(Function),
username: 'user',
password: 'pass'
});
expect(amqp.credentials.external()).toEqual({
mechanism: 'EXTERNAL',
response: expect.any(Function)
});
});
test('ensure consuming messages works even is queues is asserted multiple times', async () => {
const connection = await amqp.connect('some-random-uri');
const channel = await connection.createChannel();
const queueName = generateQueueName();
await channel.assertQueue(queueName);
const receivedMessages = [];
await channel.consume(queueName, ({ content }) => {
receivedMessages.push(content);
});
await channel.assertQueue(queueName);
channel.sendToQueue(queueName, 1);
await channel.assertQueue(queueName);
channel.sendToQueue(queueName, 2);
expect(receivedMessages).toEqual([1, 2]);
});
test('promise race condition for publishing to another queue from consumer', async () => {
const connection = await amqp.connect('some-random-uri');
const channel = await connection.createChannel();
const queueName = generateQueueName();
const errorQueueName = generateQueueName();
const listener = jest.fn().mockResolvedValue(true);
const cb = async () => {
await listener();
channel.sendToQueue(errorQueueName);
}
await channel.assertQueue(queueName);
await channel.assertQueue(errorQueueName);
await channel.consume(queueName, cb);
await channel.consume(errorQueueName, listener);
channel.sendToQueue(queueName, 2);
await sleep(0);
expect(listener).toBeCalledTimes(2);
});
test('emitting on a connection triggers on callbacks', async () => {
const connection = await amqp.connect('some-random-uri');
const listener = jest.fn();
connection.on('error', listener);
connection.emit('error');
expect(listener).toBeCalled();
});
const ttlCases = [
{ wait: 3, queueTtl: 5, msgTtl: undefined, result: true },
{ wait: 7, queueTtl: 5, msgTtl: undefined, result: false },
{ wait: 3, queueTtl: undefined, msgTtl: 5, result: true },
{ wait: 7, queueTtl: undefined, msgTtl: 5, result: false },
{ wait: 3, queueTtl: 7, msgTtl: 5, result: true },
{ wait: 6, queueTtl: 7, msgTtl: 5, result: false },
{ wait: 8, queueTtl: 7, msgTtl: 5, result: false },
]
test.each(ttlCases)(`should receive=$result message when message TTL=$msgTtl and queue TTL=$queueTtl after waiting $wait`, async (test) => {
const connection = await amqp.connect('some-random-uri');
const channel = await connection.createChannel();
const queueName = generateQueueName();
const queueArgs = test.queueTtl >= 0 ? {
'x-message-ttl': test.queueTtl,
} : undefined;
await channel.assertQueue(queueName, { durable: true, arguments: queueArgs });
const publishOptions = test.msgTtl >= 0 ? {
expiration: test.msgTtl,
} : undefined;
channel.sendToQueue(queueName, 'test-content', publishOptions);
await sleep(test.wait);
const message = await channel.get(queueName);
expect(!!message).toEqual(test.result);
});
const dlxCases = [
{ wait: 2, qTtl: [4, 4, 4], msgTtl: undefined, result: 0 },
{ wait: 6, qTtl: [4, 4, 4], msgTtl: undefined, result: 1 },
{ wait: 10, qTtl: [4, 4, 4], msgTtl: undefined, result: 2 },
{ wait: 14, qTtl: [4, 4, 4], msgTtl: undefined, result: undefined },
{ wait: 1, qTtl: [4, 4, 4], msgTtl: 2, result: 0 },
{ wait: 3, qTtl: [4, 4, 4], msgTtl: 2, result: 1 },
{ wait: 5, qTtl: [4, 4, 4], msgTtl: 2, result: 1 },
{ wait: 8, qTtl: [4, 4, 4], msgTtl: 2, result: 2 },
{ wait: 12, qTtl: [4, 4, 4], msgTtl: 2, result: undefined },
]
test.each(dlxCases)('should route message with TTL=$msgTtl to queue ' +
'#$result when DLX defined on expiration after waiting $wait', async (test) => {
const connection = await amqp.connect('some-random-uri');
const channel = await connection.createChannel();
const queues = test.qTtl.map(() => generateQueueName());
const getArgs = i => test.qTtl[i] >= 0 ? {
'x-message-ttl': test.qTtl[i],
'x-dead-letter-exchange': i + 1 < queues.length ? '' : undefined,
'x-dead-letter-routing-key': i + 1 < queues.length ? queues[i + 1] : undefined,
} : undefined;
await Promise.all(queues.map((q, i) => channel.assertQueue(q, {
arguments: getArgs(i),
})));
const publishOptions = test.msgTtl >= 0 ? {
expiration: test.msgTtl,
} : undefined;
channel.sendToQueue(queues[0], 'test-content', publishOptions);
await sleep(test.wait);
for (let i = 0; i < queues.length; i++) {
const queue = queues[i];
const message = await channel.get(queue);
expect(!!message).toEqual(test.result === i);
}
});
test('should route to DLX after nack (requeue=false)', async () => {
const connection = await amqp.connect('some-random-uri');
const channel = await connection.createChannel();
const exchange = generateExchangeName();
const routingKey = 'key';
const nackQueue = generateQueueName();
const targetQueue = generateQueueName();
await channel.assertExchange(exchange, 'direct');
await channel.assertQueue(nackQueue, {
arguments: {
'x-dead-letter-exchange': exchange,
'x-dead-letter-routing-key': routingKey,
},
});
await channel.assertQueue(targetQueue);
await channel.bindQueue(targetQueue, exchange, routingKey);
channel.sendToQueue(nackQueue, 'test-content');
const message = await channel.get(nackQueue);
expect(message).toBeTruthy();
await channel.nack(message, false, false);
const msg = await channel.get(targetQueue);
expect(msg).toBeTruthy();
expect(msg).toMatchObject({
content: 'test-content',
fields: { exchange, routingKey },
properties: {
headers: {
'x-first-death-exchange': '', // first send was to queue directly
'x-first-death-queue': nackQueue,
'x-first-death-reason': 'rejected',
'x-death': [
{
count: 1,
exchange: '',
queue: nackQueue,
reason: 'rejected',
'routing-keys': [nackQueue],
},
],
},
},
});
});
test('should emit return event when exchange:mandatory=true', async () => {
const connection = await amqp.connect('some-random-uri');
const channel = await connection.createConfirmChannel();
const queue = generateQueueName();
const exchange = generateExchangeName();
await channel.assertExchange(exchange, 'direct');
await channel.assertQueue(queue);
await channel.bindQueue(queue, exchange, 'right-pattern');
const returnCallback = jest.fn();
channel.on('return', msg => {
returnCallback(msg);
});
await new Promise((resolve, reject) => {
channel.publish(exchange, 'wrong-key', 'content-1', { mandatory: true }, (err, result) =>
err ? reject(err) : resolve(result)
);
});
expect(await channel.get(queue)).toEqual(false);
expect(returnCallback).toBeCalledWith(expect.objectContaining({
content: 'content-1',
fields: { exchange, routingKey: 'wrong-key' },
}));
});
test('should send to alternate exchange if specified', async () => {
const connection = await amqp.connect('some-random-uri');
const channel = await connection.createConfirmChannel();
const emptyQueue = generateQueueName();
const targetQueue = generateQueueName();
const emptyExchange = generateExchangeName();
const targetExchange = generateExchangeName();
await channel.assertExchange(emptyExchange, 'topic', { alternateExchange: targetExchange });
await channel.assertExchange(targetExchange, 'topic');
await channel.assertQueue(emptyQueue);
await channel.assertQueue(targetQueue);
await channel.bindQueue(emptyQueue, emptyExchange, 'wrong.*');
await channel.bindQueue(targetQueue, targetExchange, 'right.*');
await new Promise((resolve, reject) => {
channel.publish(emptyExchange, 'right.key', 'content-1', {}, (err, result) =>
err ? reject(err) : resolve(result)
);
});
expect(await channel.get(emptyQueue)).toEqual(false);
expect(await channel.get(targetQueue)).toMatchObject({
content: 'content-1',
fields: { exchange: targetExchange, routingKey: 'right.key' },
});
});
test('should not fail when callback is not provided', async () => {
const connection = await amqp.connect('some-random-uri');
const channel = await connection.createConfirmChannel();
const targetQueueName = generateQueueName();
await channel.assertQueue(targetQueueName);
channel.sendToQueue(targetQueueName, 'content-1', {});
await new Promise((resolve) => setTimeout(resolve, 500))
expect(await channel.get(targetQueueName)).toMatchObject({
content: 'content-1',
fields: {
exchange: '',
routingKey: targetQueueName
}
});
});