peanar
Version:
A job queue for Node.js based on RabbitMQ
164 lines (163 loc) • 7.38 kB
JavaScript
;
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;
});
});
});