UNPKG

peanar

Version:

A job queue for Node.js based on RabbitMQ

164 lines (163 loc) 7.38 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const events_1 = __importDefault(require("events")); const chai_1 = require("chai"); const assert_1 = require("assert"); const sinon_1 = __importDefault(require("sinon")); const amqplib_1 = __importDefault(require("amqplib")); const broker_1 = __importDefault(require("./broker")); const pool_1 = require("./pool"); const consumer_1 = __importDefault(require("./consumer")); // Mock amqplib Channel const createMockChannel = (id) => ({ channelNumber: id, // Add an identifier for easier debugging isDead: false, assertQueue: sinon_1.default.stub().resolves(), assertExchange: sinon_1.default.stub().resolves(), bindQueue: sinon_1.default.stub().resolves(), consume: sinon_1.default.stub().resolves({ consumerTag: `test-consumer-tag-${id}` }), publish: sinon_1.default.stub().returns(true), close: sinon_1.default.stub().resolves(), on: sinon_1.default.stub(), once: sinon_1.default.stub(), prefetch: sinon_1.default.stub().resolves(), ack: sinon_1.default.stub(), nack: sinon_1.default.stub(), cancel: sinon_1.default.stub().resolves(), // Add cancel stub removeAllListeners: sinon_1.default.stub(), // Add removeAllListeners stub }); let mockChannel; // Mock amqplib Connection const connectionEmitter = new events_1.default(); const mockConnection = { channelCount: 1, createChannel: sinon_1.default.stub().callsFake(() => Promise.resolve(createMockChannel(++mockConnection.channelCount))), close: sinon_1.default.stub().resolves(), on: connectionEmitter.on.bind(connectionEmitter), off: connectionEmitter.off.bind(connectionEmitter), once: connectionEmitter.once.bind(connectionEmitter), }; // Stub amqplib.connect before importing the broker const connectStub = sinon_1.default.stub(amqplib_1.default, 'connect'); describe('NodeAmqpBroker', () => { let broker; let sandbox; const config = { poolSize: 5, prefetch: 1 }; beforeEach(() => { sandbox = sinon_1.default.createSandbox(); // Create a fresh mock channel for each test mockChannel = createMockChannel(1); // Restore stubs and mocks connectStub.resetHistory(); connectStub.resetBehavior(); connectStub.resolves(mockConnection); broker = new broker_1.default(config); // Reset mockConnection stubs Object.values(mockConnection).forEach(stub => stub.resetHistory?.()); connectionEmitter.removeAllListeners(); }); afterEach(() => { sandbox.restore(); }); after(() => { // Explicitly restore the original amqplib.connect if necessary connectStub.restore(); }); describe('connect', () => { it('should connect to the broker and create channels', async () => { await broker.connect(); sinon_1.default.assert.calledOnce(connectStub); sinon_1.default.assert.called(mockConnection.createChannel); (0, chai_1.expect)(broker.pool?.size).to.equal(config.poolSize); }); it('should throw on non-retryable errors', async () => { const error = new Error('Connection error'); connectStub.rejects(error); await (0, assert_1.rejects)(broker.connect(), error); }); it('should retry connection on ECONNREFUSED', async () => { class ConnectionRefusedError extends Error { code = 'ECONNREFUSED'; name = 'ConnectionRefusedError'; constructor() { super('Connection refused'); } } const error = new ConnectionRefusedError(); connectStub.onCall(0).rejects(error); connectStub.onCall(1).resolves(mockConnection); sandbox.stub(global, 'setTimeout').callsFake((cb) => cb()); await broker.connect(); sinon_1.default.assert.calledTwice(connectStub); sinon_1.default.assert.called(mockConnection.createChannel); (0, chai_1.expect)(broker.pool?.size).to.equal(config.poolSize); }); }); describe('shutdown', () => { let poolCloseSpy; beforeEach(() => { poolCloseSpy = sinon_1.default.spy(pool_1.ChannelPool.prototype, 'close'); poolCloseSpy.resetHistory(); }); afterEach(() => { poolCloseSpy.restore(); }); it('should close the connection and channels', async () => { await broker.connect(); await broker.shutdown(); sinon_1.default.assert.calledOnce(mockConnection.close); sinon_1.default.assert.calledOnce(poolCloseSpy); (0, chai_1.expect)(() => broker.ready()).to.throw('Not connected!'); (0, chai_1.expect)(broker.pool).to.be.undefined; }); it('should consider shutdown a noop if not connected', async () => { await broker.shutdown(); sinon_1.default.assert.notCalled(mockConnection.close); sinon_1.default.assert.notCalled(poolCloseSpy); }); }); describe('connection error handling', () => { class ConnectionError extends Error { code; name = 'ConnectionError'; constructor(code) { super('Connection refused'); this.code = code; } } it('should reconnect on connection error', async () => { await broker.connect(); connectStub.resetHistory(); connectionEmitter.emit('close', new ConnectionError(500)); await broker.ready(); sinon_1.default.assert.calledOnce(connectStub); sinon_1.default.assert.called(mockConnection.createChannel); (0, chai_1.expect)(broker.pool?.size).to.equal(config.poolSize); }); for (const error of [undefined, new ConnectionError(200)]) { it(`should not reconnect on ${error ? 'graceful server shutdown' : 'no'} error`, async () => { await broker.connect(); connectStub.resetHistory(); connectionEmitter.emit('close', error); (0, chai_1.expect)(() => broker.ready()).to.throw('Not connected!'); sinon_1.default.assert.notCalled(connectStub); (0, chai_1.expect)(broker.pool).to.be.undefined; (0, chai_1.expect)(connectionEmitter.listenerCount('close')).to.equal(0); }); } }); describe('rewireConsumersOnChannel', () => { it('should rewire consumers to a new channel', async () => { const newChannel = createMockChannel(2); const consumer = new consumer_1.default(mockChannel, 'test-queue'); broker._channelConsumers.set(mockChannel, new Set([consumer])); await broker.rewireConsumersOnChannel(mockChannel, newChannel); sinon_1.default.assert.calledOnce(newChannel.consume); (0, chai_1.expect)(broker._channelConsumers.get(newChannel)).to.include(consumer); (0, chai_1.expect)(broker._channelConsumers.get(mockChannel)).to.be.undefined; }); }); });