UNPKG

@pixellot/pxlt-rabbit-handler

Version:

A generic class that handles RabbitMQ connection, consume and produce functionality.

755 lines (663 loc) 26.7 kB
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 }); }); }); });