@pixellot/pxlt-rabbit-handler
Version:
A generic class that handles RabbitMQ connection, consume and produce functionality.
755 lines (663 loc) • 26.7 kB
JavaScript
const amqp = require('amqplib');
jest.mock('amqplib');
const utils = require('../../../lib/utils');
const ClassUnderTest = require('../../../lib/rabbitHandler');
describe('Testing basic functionality of Rabbit handler class', () => {
it('positive connect', () => {
const registeredEvents = {};
const rabbitHandler = new ClassUnderTest('connectionString', {});
const createConfirmChannelMock = jest.fn();
createConfirmChannelMock.mockResolvedValue({
consume: jest.fn(),
prefetch: jest.fn(),
on: jest.fn(),
checkQueue: jest.fn().mockResolvedValue({
queue: 'q'
})
});
amqp.connect = jest.fn().mockResolvedValue({
on: (messagetype, callback) => {
expect(callback).not.toBe(null);
registeredEvents[messagetype] = true;
},
createConfirmChannel: createConfirmChannelMock
});
return rabbitHandler.connect().then(() => {
expect(rabbitHandler.connection).not.toBe(null);
expect(registeredEvents.close).toBe(true);
expect(registeredEvents.error).toBe(true);
});
});
it('Negative connect - no retry', (done) => {
amqp.connect = jest.fn().mockImplementation(() => Promise.reject('error'));
utils.exponentialBackoff = jest.fn();
utils.exponentialBackoff.mockReturnValue(0);
const rabbitHandler = new ClassUnderTest('connectionString', {
maxBackoffCount: 0
});
rabbitHandler.connect().then(() => {
done.fail(new Error('Expected connect to fail but succeeded')); // fail test if this then is reached
}).catch(() => {
expect(utils.exponentialBackoff.mock.calls.length).toBe(0);
done();
});
});
it('Negative connect - default retry == 5', (done) => {
amqp.connect = jest.fn().mockImplementation(() => Promise.reject('error'));
utils.exponentialBackoff = jest.fn();
utils.exponentialBackoff.mockReturnValue(0);
const rabbitHandler = new ClassUnderTest('connectionString');
rabbitHandler.connect().then(() => {
done.fail(new Error('Expected connect to fail but succeeded')); // fail test if this then is reached
}).catch(() => {
expect(utils.exponentialBackoff.mock.calls.length).toBe(5);
done();
});
});
it('Negative connect - retry is set from constructor', (done) => {
amqp.connect = jest.fn().mockImplementation(() => Promise.reject('error'));
utils.exponentialBackoff = jest.fn();
utils.exponentialBackoff.mockReturnValue(0);
const maxBackoffCount = 3;
const rabbitHandler = new ClassUnderTest('connectionString', {
maxBackoffCount
});
rabbitHandler.connect()
.then(() => {
done.fail(new Error('Expected connect to fail but succeeded')); // fail test if this then is reached
}).catch(() => {
expect(utils.exponentialBackoff.mock.calls.length).toBe(maxBackoffCount);
done();
});
});
it('Publish - Positive', () => {
const createConfirmChannelMock = jest.fn();
const publishMock = jest.fn();
createConfirmChannelMock.mockResolvedValue({
on: jest.fn(),
checkExchange: jest.fn().mockResolvedValue(true),
publish: publishMock.mockImplementation((_, __, ___, ____, callback) => {
callback();
})
});
amqp.connect = jest.fn().mockResolvedValue({
on: jest.fn(),
createConfirmChannel: createConfirmChannelMock
});
const rabbitHandler = new ClassUnderTest('connectionString', {
maxBackoffCount: 0
});
return rabbitHandler.connect()
.then(() => rabbitHandler.publish('exchangeName', 'routingKey', 'message'))
.then(() => {
const { calls } = publishMock.mock;
expect(calls.length).toBe(1);
const [param1, param2, param3] = calls[0];
expect(param1).toBe('exchangeName');
expect(param2).toBe('routingKey');
expect(param3).toBe('message');
});
});
it('Publish - Negative', (done) => {
const createConfirmChannelMock = jest.fn();
const publishMock = jest.fn();
createConfirmChannelMock.mockResolvedValue({
on: jest.fn(),
checkExchange: jest.fn().mockResolvedValue(true),
publish: publishMock.mockImplementation(() => {
throw 'error';
})
});
amqp.connect = jest.fn().mockResolvedValue({
on: jest.fn(),
createConfirmChannel: createConfirmChannelMock
});
const rabbitHandler = new ClassUnderTest('connectionString', {
maxBackoffCount: 0
});
rabbitHandler.connect()
.then(() => rabbitHandler.publish('exchangeName', 'routingKey', 'message'))
.then(() => {
done.fail(new Error('Expected publish to fail but succeeded')); // fail test if this then is reached
})
.catch(() => {
expect(publishMock.mock.calls.length).toBe(1);
done();
});
});
it('Publish - Negative, with channel error/close events validation', (done) => {
const createConfirmChannelMock = jest.fn();
const channelOnCloseMock = jest.fn();
const amqplPublishMock = jest.fn();
createConfirmChannelMock.mockResolvedValue({
on: channelOnCloseMock,
checkExchange: jest.fn().mockImplementationOnce(() => {
throw 'Bad Exchange';
}),
publish: amqplPublishMock
});
amqp.connect = jest.fn().mockResolvedValue({
on: jest.fn(),
createConfirmChannel: createConfirmChannelMock
});
const rabbitHandler = new ClassUnderTest('connectionString', {
maxBackoffCount: 0
});
rabbitHandler.connect()
.then(() => rabbitHandler.checkExchange('exchangeName'))
.then(() => {
done.fail(new Error('Expected checkExchange to fail, but succeeded')); // fail test if this then is reached
})
.catch(() => {
expect(amqplPublishMock.mock.calls.length).toBe(0);
expect(channelOnCloseMock.mock.calls.length).toBe(2); // both on channel error and on channel close are supposed to be triggered
done();
});
});
it('Consume - Negative queue', (done) => {
const createConfirmChannelMock = jest.fn();
createConfirmChannelMock.mockResolvedValue({
prefetch: jest.fn(),
on: jest.fn(),
checkQueue: jest.fn().mockImplementation(() => {
throw 'Queue Error';
})
});
const amqplConsumeMock = jest.fn();
amqp.connect = jest.fn().mockResolvedValue({
on: jest.fn(),
createConfirmChannel: createConfirmChannelMock,
consume: amqplConsumeMock
});
const rabbitHandler = new ClassUnderTest('connectionString', {
maxBackoffCount: 5
});
rabbitHandler.connect()
.then(() => rabbitHandler.consume('queueName'))
.then(() => {
done.fail(new Error('Expected consume to fail but succeeded')); // fail test if this then is reached
})
.catch(() => {
expect(amqplConsumeMock.mock.calls.length).toBe(0);
done();
});
});
it('Consume from multiple queues', (done) => {
const createConfirmChannelMock = jest.fn();
const amqplConsumeMock = jest.fn();
createConfirmChannelMock.mockResolvedValue({
consume: amqplConsumeMock.mockResolvedValue({
consumerTag: 'Tag'
}),
prefetch: jest.fn(),
on: jest.fn(),
checkQueue: jest.fn().mockResolvedValue({
queue: 'q'
}),
ch: 1,
cancel: jest.fn().mockResolvedValue(),
close: jest.fn()
});
amqp.connect = jest.fn().mockResolvedValue({
on: jest.fn(),
createConfirmChannel: createConfirmChannelMock,
close: jest.fn()
});
const rabbitHandler = new ClassUnderTest('connectionString', {
maxBackoffCount: 0,
logLevel: 'info'
});
rabbitHandler.connect()
.then(() => {
const queues = ['queue1', 'queue2'];
return Promise.all(queues.map((q) => rabbitHandler.consume(q, () => {
})));
})
.then(() => {
expect(amqplConsumeMock.mock.calls.length).toBe(2);
done();
});
});
it('Consume from same queue twice', (done) => {
const createConfirmChannelMock = jest.fn();
const amqplConsumeMock = jest.fn();
createConfirmChannelMock.mockResolvedValue({
consume: amqplConsumeMock.mockResolvedValue({
consumerTag: 'Tag'
}),
prefetch: jest.fn(),
on: jest.fn(),
checkQueue: jest.fn().mockResolvedValue({
queue: 'q'
}),
ch: 1,
cancel: jest.fn().mockResolvedValue(),
close: jest.fn()
});
amqp.connect = jest.fn().mockResolvedValue({
on: jest.fn(),
createConfirmChannel: createConfirmChannelMock,
close: jest.fn()
});
const rabbitHandler = new ClassUnderTest('connectionString', {
maxBackoffCount: 0,
logLevel: 'info'
});
rabbitHandler.connect()
.then(() => {
const queues = ['queue1', 'queue1'];
return Promise.all(queues.map((q) => rabbitHandler.consume(q, () => {
})));
})
.then(() => {
done.fail(new Error('Expected consume to fail but succeeded')); // fail test if this then is reached
})
.catch(() => {
expect(amqplConsumeMock.mock.calls.length).toBe(0);
done();
});
});
it('Consume and cancel consumer', (done) => {
const createConfirmChannelMock = jest.fn();
const amqplConsumeMock = jest.fn();
createConfirmChannelMock.mockResolvedValue({
consume: amqplConsumeMock.mockResolvedValue({
consumerTag: 'Tag'
}),
prefetch: jest.fn(),
on: jest.fn(),
checkQueue: jest.fn().mockResolvedValue({
queue: 'q'
}),
ch: 1,
cancel: jest.fn().mockResolvedValue(),
close: jest.fn()
});
amqp.connect = jest.fn().mockResolvedValue({
on: jest.fn(),
createConfirmChannel: createConfirmChannelMock,
close: jest.fn()
});
const rabbitHandler = new ClassUnderTest('connectionString', {
maxBackoffCount: 0,
logLevel: 'info'
});
rabbitHandler.connect()
.then(() => {
const queues = ['queue1', 'queue2'];
return Promise.all(queues.map((q) => rabbitHandler.consume(q, () => {
})));
})
.then(() => {
expect(amqplConsumeMock.mock.calls.length).toBe(2);
})
.then(() => rabbitHandler.cancelConsumer('queue1'))
.then(() => {
expect(rabbitHandler.consumers.queue1).toBeUndefined();
expect(rabbitHandler.consumers.queue2).toBeInstanceOf(Object);
done();
});
});
it('Consume and gracefully disconnect with failed attempt to reconnect', (done) => {
const createConfirmChannelMock = jest.fn();
const amqplConsumeMock = jest.fn();
createConfirmChannelMock.mockResolvedValue({
consume: amqplConsumeMock.mockResolvedValue({
consumerTag: 'Tag'
}),
prefetch: jest.fn(),
on: jest.fn(),
checkQueue: jest.fn().mockResolvedValue({
queue: 'q'
}),
ch: 1,
cancel: jest.fn().mockResolvedValue(),
close: jest.fn()
});
amqp.connect = jest.fn().mockResolvedValue({
on: jest.fn(),
createConfirmChannel: createConfirmChannelMock,
close: jest.fn()
});
const rabbitHandler = new ClassUnderTest('connectionString', {
maxBackoffCount: 0,
logLevel: 'info'
});
rabbitHandler.connect()
.then(() => {
const queues = ['queue1', 'queue2'];
return Promise.all(queues.map((q) => rabbitHandler.consume(q, () => {
})));
})
.then(() => {
expect(amqplConsumeMock.mock.calls.length).toBe(2);
})
.then(() => rabbitHandler.gracefullyDisconnect())
.then(() => {
expect(rabbitHandler.consumers).toEqual({});
})
.catch((err) => {
done.fail(err);
})
.then(() => rabbitHandler.connect())
.then(() => {
done.fail(new Error('Expected connect after disconnect to fail but succeeded')); // fail test if this then is reached
})
.catch(() => {
expect(amqp.connect.mock.calls.length).toEqual(1);
done();
});
});
it('getMessage and ack', () => {
const createConfirmChannelMock = jest.fn();
const ackMock = jest.fn();
createConfirmChannelMock.mockResolvedValue({
on: jest.fn(),
get: jest.fn().mockResolvedValue({
fields: { deliveryTag: 1 },
content: 'message'
}),
ack: ackMock
});
amqp.connect = jest.fn().mockResolvedValue({
on: jest.fn(),
createConfirmChannel: createConfirmChannelMock
});
const rabbitHandler = new ClassUnderTest('connectionString', {
maxBackoffCount: 0
});
return rabbitHandler.connect()
.then(() => rabbitHandler.getMessage('bla'))
.then((msg) => {
expect(msg.message.content).toBe('message');
return rabbitHandler.ack(msg);
})
.then(() => {
expect(ackMock.mock.calls.length).toBe(1);
});
});
it('Consume with callback', (done) => {
const createConfirmChannelMock = jest.fn();
const ackMock = jest.fn();
let consumeCb;
createConfirmChannelMock.mockResolvedValue({
on: jest.fn(),
ack: ackMock,
prefetch: jest.fn(),
checkQueue: jest.fn().mockResolvedValue({
queue: 'q'
}),
consume: jest.fn().mockImplementation((q, cb) => {
consumeCb = cb;
return 'tag';
}),
ch: 1
});
amqp.connect = jest.fn().mockResolvedValue({
on: jest.fn(),
createConfirmChannel: createConfirmChannelMock
});
const rabbitHandler = new ClassUnderTest('connectionString', {
maxBackoffCount: 0
});
function process(msg) {
return Promise.resolve()
.then(() => rabbitHandler.ack(msg));
}
rabbitHandler.connect()
.then(() => rabbitHandler.consume('bla', process))
.then(() => {
const msg = {
fields: {
deliveryTag: 1
}
};
return Promise.resolve(consumeCb(msg));
})
.then(() => {
expect(ackMock.mock.calls.length).toBe(1);
done();
});
});
it('Connection state test', () => {
const rabbitHandler = new ClassUnderTest('connectionString', {});
const registeredEvents = {};
amqp.connect = jest.fn().mockResolvedValue({
on: (messagetype, callback) => {
expect(callback).not.toBe(null);
registeredEvents[messagetype] = callback;
}
});
return rabbitHandler.connect()
.then(() => {
expect(rabbitHandler.getConnectionState()).toBe('active');
})
.then(() => {
registeredEvents.close.call();
})
.then(() => {
expect(rabbitHandler.getConnectionState()).toBe('closed');
registeredEvents.error.call();
})
.then(() => {
expect(rabbitHandler.getConnectionState()).toBe('error');
});
});
it('Reconnect and Reconsume after connection closed event', () => {
const rabbitHandler = new ClassUnderTest('connectionString', {});
const registeredEvents = {};
utils.exponentialBackoff = jest.fn().mockReturnValue(0);
const createConfirmChannelMock = jest.fn();
const amqplConsumeMock = jest.fn();
amqp.connect = jest.fn().mockResolvedValue({
on: (messagetype, callback) => {
expect(typeof callback).toBe('function');
registeredEvents[messagetype] = callback;
},
createConfirmChannel: createConfirmChannelMock
});
createConfirmChannelMock.mockResolvedValue({
consume: amqplConsumeMock.mockResolvedValue({
consumerTag: 'Tag'
}),
prefetch: jest.fn(),
on: jest.fn(),
checkQueue: jest.fn().mockResolvedValue({
queue: 'q'
}),
ch: 1
});
return rabbitHandler.connect()
.then(() => rabbitHandler.consume('queueName', () => {
}))
.then(() => {
expect(rabbitHandler.getConnectionState()).toBe('active');
registeredEvents.close.call();
expect(rabbitHandler.getConnectionState()).toBe('closed');
return new Promise((resolve) => {
setTimeout(() => {
expect(rabbitHandler.getConnectionState()).toBe('active');
expect(amqplConsumeMock.mock.calls.length).toBe(2);
resolve();
}, 1000);
});
});
});
it('Reconnect and Negative Reconsume after connection closed event', () => {
const rabbitHandler = new ClassUnderTest('connectionString', {});
const registeredEvents = {};
utils.exponentialBackoff = jest.fn().mockReturnValue(0);
const createConfirmChannelMock = jest.fn();
const amqplConsumeMock = jest.fn();
const connectionClosedCb = jest.fn();
rabbitHandler.on('connectionClosed', (err) => {
connectionClosedCb.call(err);
});
amqp.connect = jest.fn().mockResolvedValue({
on: (messagetype, callback) => {
expect(callback).not.toBe(null);
registeredEvents[messagetype] = callback;
},
createConfirmChannel: createConfirmChannelMock,
close: jest.fn()
});
createConfirmChannelMock.mockResolvedValue({
consume: amqplConsumeMock.mockResolvedValueOnce({
consumerTag: 'Tag'
}).mockRejectedValueOnce('Consume Error'),
prefetch: jest.fn(),
on: jest.fn(),
checkQueue: jest.fn().mockResolvedValue({
queue: 'q'
})
});
return rabbitHandler.connect()
.then(() => rabbitHandler.consume('queueName', () => {
}))
.then(() => {
expect(rabbitHandler.getConnectionState()).toBe('active');
registeredEvents.close.call();
expect(rabbitHandler.getConnectionState()).toBe('closed');
return new Promise((resolve) => {
setTimeout(() => {
expect(rabbitHandler.getConnectionState()).toBe('closed');
expect(amqplConsumeMock.mock.calls.length).toBe(2);
expect(connectionClosedCb.mock.calls.length).toBe(1);
resolve();
}, 1000);
});
});
});
});
describe('Testing persistan publish', () => {
it('persistent publish without options', () => {
expect.assertions(2);
const createConfirmChannelMock = jest.fn();
const publishMock = jest.fn();
createConfirmChannelMock.mockResolvedValue({
on: jest.fn(),
checkExchange: jest.fn().mockResolvedValue(true),
publish: publishMock.mockImplementation((_, __, ___, ____, callback) => {
callback();
})
});
amqp.connect = jest.fn().mockResolvedValue({
on: jest.fn(),
createConfirmChannel: createConfirmChannelMock
});
const rabbitHandler = new ClassUnderTest('connectionString', {
maxBackoffCount: 0
});
return rabbitHandler.connect()
.then(() => rabbitHandler.publish('exchangeName', 'routingKey', 'message'))
.then(() => {
const { calls } = publishMock.mock;
expect(calls.length).toBe(1);
expect(calls[0][3]).toStrictEqual({ persistent: true });
});
});
it('persistent publish with options that includes persistent', () => {
expect.assertions(2);
const createConfirmChannelMock = jest.fn();
const publishMock = jest.fn();
createConfirmChannelMock.mockResolvedValue({
on: jest.fn(),
checkExchange: jest.fn().mockResolvedValue(true),
publish: publishMock.mockImplementation((_, __, ___, ____, callback) => {
callback();
})
});
amqp.connect = jest.fn().mockResolvedValue({
on: jest.fn(),
createConfirmChannel: createConfirmChannelMock
});
const rabbitHandler = new ClassUnderTest('connectionString', {
maxBackoffCount: 0
});
return rabbitHandler.connect()
.then(() => rabbitHandler.publish('exchangeName', 'routingKey', 'message', { persistent: true }))
.then(() => {
const { calls } = publishMock.mock;
expect(calls.length).toBe(1);
expect(calls[0][3]).toStrictEqual({ persistent: true });
});
});
it('persistent publish with options that does not include persistent', () => {
expect.assertions(2);
const createConfirmChannelMock = jest.fn();
const publishMock = jest.fn();
createConfirmChannelMock.mockResolvedValue({
on: jest.fn(),
checkExchange: jest.fn().mockResolvedValue(true),
publish: publishMock.mockImplementation((_, __, ___, ____, callback) => {
callback();
})
});
amqp.connect = jest.fn().mockResolvedValue({
on: jest.fn(),
createConfirmChannel: createConfirmChannelMock
});
const rabbitHandler = new ClassUnderTest('connectionString', {
maxBackoffCount: 0
});
return rabbitHandler.connect()
.then(() => rabbitHandler.publish('exchangeName', 'routingKey', 'message', { priority: 1 }))
.then(() => {
const { calls } = publishMock.mock;
expect(calls.length).toBe(1);
expect(calls[0][3]).toStrictEqual({ priority: 1, persistent: true });
});
});
it('publish persistent true when no options where provided', () => {
expect.assertions(2);
const createConfirmChannelMock = jest.fn();
const publishMock = jest.fn();
createConfirmChannelMock.mockResolvedValue({
on: jest.fn(),
checkExchange: jest.fn().mockResolvedValue(true),
publish: publishMock.mockImplementation((_, __, ___, ____, callback) => {
callback();
})
});
amqp.connect = jest.fn().mockResolvedValue({
on: jest.fn(),
createConfirmChannel: createConfirmChannelMock
});
const rabbitHandler = new ClassUnderTest('connectionString', {
maxBackoffCount: 0
});
return rabbitHandler.connect()
.then(() => rabbitHandler.publish('exchangeName', 'routingKey', 'message'))
.then(() => {
const { calls } = publishMock.mock;
expect(calls.length).toBe(1);
expect(calls[0][3]).toStrictEqual({ persistent: true });
});
});
it('publish persistent false when explicitly provided that option', () => {
expect.assertions(2);
const createConfirmChannelMock = jest.fn();
const publishMock = jest.fn();
createConfirmChannelMock.mockResolvedValue({
on: jest.fn(),
checkExchange: jest.fn().mockResolvedValue(true),
publish: publishMock.mockImplementation((_, __, ___, ____, callback) => {
callback();
})
});
amqp.connect = jest.fn().mockResolvedValue({
on: jest.fn(),
createConfirmChannel: createConfirmChannelMock
});
const rabbitHandler = new ClassUnderTest('connectionString', {
maxBackoffCount: 0
});
return rabbitHandler.connect()
.then(() => rabbitHandler.publish('exchangeName', 'routingKey', 'message', { persistent: false }))
.then(() => {
const { calls } = publishMock.mock;
expect(calls.length).toBe(1);
expect(calls[0][3]).toStrictEqual({ persistent: false });
});
});
});