Files
upstream-rocketchat/apps/meteor/tests/unit/server/services/omnichannel/queue.tests.ts
upstream-mirror 30810eecd7
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Code scanning - action / CodeQL-Build (push) Failing after 2s
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
2026-09-16 01:21:02 +05:00

442 lines
15 KiB
TypeScript

import { expect } from 'chai';
import { beforeEach, describe, after, it } from 'mocha';
import p from 'proxyquire';
import Sinon from 'sinon';
const dispatchAgentDelegated = Sinon.stub();
const getConfig = Sinon.stub();
const delegateInquiry = Sinon.stub();
const libSettings = { getInquirySortMechanismSetting: Sinon.stub().returns('timestamp') };
const settings = {
get: Sinon.stub(),
};
const queueLogger = {
info: Sinon.stub(),
debug: Sinon.stub(),
error: Sinon.stub(),
};
const mockedInquiry = {
_id: 'inquiryId',
rid: 'rid',
department: 'department1',
ts: new Date(),
};
const models = {
LivechatInquiry: {
unlockAll: Sinon.stub(),
findNextAndLock: Sinon.stub(),
getDistinctQueuedDepartments: Sinon.stub(),
unlock: Sinon.stub(),
removeByRoomId: Sinon.stub(),
takeInquiry: Sinon.stub(),
},
LivechatRooms: {
findOneById: Sinon.stub(),
},
};
const license = {
shouldPreventAction: Sinon.stub(),
};
const { OmnichannelQueue } = p.noCallThru().load('../../../../../server/services/omnichannel/queue', {
'../../lib/omnichannel/Helper': {
dispatchAgentDelegated,
},
'../../lib/omnichannel/RoutingManager': {
RoutingManager: {
getConfig,
delegateInquiry,
},
},
'../../lib/omnichannel/settings': libSettings,
'../../settings': { settings },
'./logger': { queueLogger },
'@rocket.chat/models': models,
'@rocket.chat/license': { License: license },
'../../lib/metrics': {
metrics: {
timeToQueueProcessingByQueue: { observe: Sinon.stub() },
timeToQueueProcessingByQueueHistogram: { observe: Sinon.stub() },
totalItemsProcessedByQueue: { inc: Sinon.stub() },
totalItemsProcessedByReconciliationQueue: { inc: Sinon.stub() },
totalItemsFailedByQueue: { inc: Sinon.stub() },
},
},
});
describe('Omnichannel Queue processor', () => {
describe('isRunning', () => {
it('should return the running status', () => {
const queue = new OmnichannelQueue();
expect(queue.isRunning()).to.be.false;
});
it('should return the running status', () => {
const queue = new OmnichannelQueue();
queue.running = true;
expect(queue.isRunning()).to.be.true;
});
});
describe('delay', () => {
after(() => {
settings.get.reset();
});
it('should return 5000 if setting is not set', () => {
settings.get.returns(undefined);
const queue = new OmnichannelQueue();
expect(queue.delay()).to.be.equal(5000);
});
it('should return the right value if setting has a value above 1', () => {
settings.get.returns(10);
const queue = new OmnichannelQueue();
expect(queue.delay()).to.be.equal(10000);
});
});
describe('getActiveQueues', () => {
after(() => {
models.LivechatInquiry.getDistinctQueuedDepartments.reset();
});
it('should return empty array when there are no active queues', async () => {
models.LivechatInquiry.getDistinctQueuedDepartments.resolves([]);
const queue = new OmnichannelQueue();
expect(await queue.getActiveQueues()).to.be.eql([]);
});
it('should return [department1] when department1 is an active queue', async () => {
models.LivechatInquiry.getDistinctQueuedDepartments.resolves([{ _id: 'department1' }]);
const queue = new OmnichannelQueue();
expect(await queue.getActiveQueues()).to.be.eql(['department1']);
});
it('should return [null, department1] when department1 is an active queue and there are elements on public queue', async () => {
models.LivechatInquiry.getDistinctQueuedDepartments.resolves([{ _id: 'department1' }, { _id: null }]);
const queue = new OmnichannelQueue();
expect(await queue.getActiveQueues()).to.be.eql(['department1', null]);
});
});
describe('checkQueue', () => {
let clock: any;
beforeEach(() => {
models.LivechatInquiry.findNextAndLock.resetHistory();
models.LivechatInquiry.takeInquiry.resetHistory();
models.LivechatInquiry.unlock.resetHistory();
queueLogger.error.resetHistory();
queueLogger.info.resetHistory();
clock = Sinon.useFakeTimers();
});
afterEach(() => {
clock.restore();
});
after(() => {
models.LivechatInquiry.findNextAndLock.reset();
models.LivechatInquiry.takeInquiry.reset();
models.LivechatInquiry.unlock.reset();
queueLogger.error.reset();
queueLogger.info.reset();
clock.reset();
});
it('should return undefined when the queue is empty', async () => {
models.LivechatInquiry.findNextAndLock.returns(null);
const queue = new OmnichannelQueue();
queue.execute = Sinon.stub();
expect(await queue.checkQueue()).to.be.undefined;
});
it('should try to process the inquiry when there is one', async () => {
models.LivechatInquiry.findNextAndLock.returns(mockedInquiry);
const queue = new OmnichannelQueue();
queue.processWaitingQueue = Sinon.stub().throws('error');
queue.execute = Sinon.stub();
await queue.checkQueue();
expect(models.LivechatInquiry.findNextAndLock.calledOnce).to.be.true;
expect(queue.processWaitingQueue.calledOnce).to.be.true;
});
it('should call unlock when the inquiry could not be processed', async () => {
models.LivechatInquiry.findNextAndLock.returns(mockedInquiry);
const queue = new OmnichannelQueue();
queue.processWaitingQueue = Sinon.stub().returns(false);
queue.execute = Sinon.stub();
await queue.checkQueue();
expect(queue.processWaitingQueue.calledOnce).to.be.true;
expect(models.LivechatInquiry.unlock.calledOnce).to.be.true;
});
it('should unlock the inquiry when it was processed succesfully', async () => {
models.LivechatInquiry.findNextAndLock.returns(mockedInquiry);
const queue = new OmnichannelQueue();
queue.processWaitingQueue = Sinon.stub().returns(true);
queue.execute = Sinon.stub();
await queue.checkQueue();
expect(queue.processWaitingQueue.calledOnce).to.be.true;
expect(models.LivechatInquiry.unlock.calledOnce).to.be.true;
});
it('should print a log when there was an error processing inquiry', async () => {
models.LivechatInquiry.findNextAndLock.throws('error');
const queue = new OmnichannelQueue();
queue.execute = Sinon.stub();
await queue.checkQueue();
expect(queueLogger.error.calledOnce).to.be.true;
});
});
describe('shouldStart', () => {
beforeEach(() => {
settings.get.resetHistory();
getConfig.resetHistory();
});
after(() => {
settings.get.reset();
getConfig.reset();
});
it('should call stop if Livechat is not enabled', async () => {
settings.get.returns(false);
const queue = new OmnichannelQueue();
queue.stop = Sinon.stub();
await queue.shouldStart();
expect(queue.stop.calledOnce).to.be.true;
});
it('should call start if routing algorithm supports auto assignment', async () => {
settings.get.returns(true);
getConfig.returns({ autoAssignAgent: true });
const queue = new OmnichannelQueue();
queue.start = Sinon.stub();
await queue.shouldStart();
expect(queue.start.calledOnce).to.be.true;
expect(queue.start.calledAfter(getConfig)).to.be.true;
});
it('should call stop if routing algorithm does not support auto assignment', async () => {
settings.get.returns(true);
getConfig.returns({ autoAssignAgent: false });
const queue = new OmnichannelQueue();
queue.stop = Sinon.stub();
await queue.shouldStart();
expect(queue.stop.calledOnce).to.be.true;
expect(queue.stop.calledAfter(getConfig)).to.be.true;
});
});
describe('reconciliation', () => {
beforeEach(() => {
models.LivechatInquiry.removeByRoomId.resetHistory();
models.LivechatInquiry.takeInquiry.resetHistory();
});
it('should remove inquiries from rooms that do not exist', async () => {
const queue = new OmnichannelQueue();
await queue.reconciliation('missing', { roomId: 'rid', inquiryId: 'inquiryId' });
expect(models.LivechatInquiry.removeByRoomId.calledOnce).to.be.true;
});
it('should take an inquiry if the room was taken', async () => {
const queue = new OmnichannelQueue();
await queue.reconciliation('taken', { roomId: 'rid', inquiryId: 'inquiryId' });
expect(models.LivechatInquiry.takeInquiry.calledOnce).to.be.true;
});
it('should remove inquiries from rooms that were closed', async () => {
const queue = new OmnichannelQueue();
await queue.reconciliation('closed', { roomId: 'rid', inquiryId: 'inquiryId' });
expect(models.LivechatInquiry.removeByRoomId.calledOnce).to.be.true;
});
it('should return true for any other case', async () => {
const queue = new OmnichannelQueue();
expect(await queue.reconciliation('random', { roomId: 'rid', inquiryId: 'inquiryId' })).to.be.true;
expect(models.LivechatInquiry.removeByRoomId.notCalled).to.be.true;
expect(models.LivechatInquiry.takeInquiry.notCalled).to.be.true;
});
});
describe('processWaitingQueue', () => {
let clock: any;
beforeEach(() => {
models.LivechatRooms.findOneById.reset();
models.LivechatInquiry.takeInquiry.resetHistory();
models.LivechatInquiry.removeByRoomId.resetHistory();
delegateInquiry.resetHistory();
queueLogger.debug.resetHistory();
clock = Sinon.useFakeTimers();
});
afterEach(() => {
clock.restore();
});
after(() => {
models.LivechatRooms.findOneById.reset();
models.LivechatInquiry.takeInquiry.reset();
delegateInquiry.reset();
queueLogger.debug.reset();
clock.reset();
});
it('should process the public queue when department is undefined', async () => {
const queue = new OmnichannelQueue();
expect(await queue.processWaitingQueue(undefined, mockedInquiry)).to.be.true;
expect(queueLogger.debug.calledWith('Processing inquiry inquiryId from queue Public'));
expect(models.LivechatRooms.findOneById.calledOnce).to.be.true;
});
it('should call removeInquiry when findOneById returns null', async () => {
models.LivechatRooms.findOneById.returns(null);
const queue = new OmnichannelQueue();
expect(await queue.processWaitingQueue('department1', mockedInquiry)).to.be.true;
expect(
queueLogger.debug.calledWith({
msg: 'Room from inquiry missing. Removing inquiry',
roomId: 'rid',
inquiryId: 'inquiryId',
step: 'reconciliation',
}),
).to.be.true;
expect(models.LivechatInquiry.removeByRoomId.calledOnce).to.be.true;
});
it('should call takeInquiry when findOneById returns a room thats already being served', async () => {
models.LivechatRooms.findOneById.returns({ _id: 'rid', servedBy: { some: 'thing' } });
const queue = new OmnichannelQueue();
queue.reconciliation = Sinon.stub().returns(true);
expect(await queue.processWaitingQueue('department1', mockedInquiry)).to.be.true;
expect(queue.reconciliation.calledOnce).to.be.true;
});
it('should call removeInquiry when findOneById returns a room that was closed', async () => {
models.LivechatRooms.findOneById.returns({ _id: 'rid', closedAt: new Date() });
const queue = new OmnichannelQueue();
queue.reconciliation = Sinon.stub().returns(true);
expect(await queue.processWaitingQueue('department1', mockedInquiry)).to.be.true;
expect(queue.reconciliation.calledOnce).to.be.true;
});
it('should call delegateInquiry when prechecks are met and return false if inquiry was not served', async () => {
models.LivechatRooms.findOneById.returns({ _id: 'rid' });
delegateInquiry.returns({});
const queue = new OmnichannelQueue();
expect(await queue.processWaitingQueue('department1', mockedInquiry)).to.be.false;
expect(delegateInquiry.calledOnce).to.be.true;
});
it('should call delegateInquiry and return true if inquiry was served', async () => {
models.LivechatRooms.findOneById.returns({ _id: 'rid' });
delegateInquiry.returns({ _id: 'rid', servedBy: { _id: 'agentId' } });
const queue = new OmnichannelQueue();
expect(await queue.processWaitingQueue('department1', mockedInquiry)).to.be.true;
expect(delegateInquiry.calledOnce).to.be.true;
});
it('should call dispatchAgentDelegated if inquiry was served (after 1s)', async () => {
models.LivechatRooms.findOneById.returns({ _id: 'rid' });
delegateInquiry.returns({ _id: 'rid', servedBy: { _id: 'agentId' } });
const queue = new OmnichannelQueue();
expect(await queue.processWaitingQueue('department1', mockedInquiry)).to.be.true;
expect(delegateInquiry.calledOnce).to.be.true;
clock.tick(1000);
expect(dispatchAgentDelegated.calledOnce).to.be.true;
});
});
describe('execute', () => {
beforeEach(() => {
license.shouldPreventAction.reset();
queueLogger.debug.reset();
});
after(() => {
license.shouldPreventAction.reset();
queueLogger.debug.reset();
});
it('should return undefined if service is not running', async () => {
const queue = new OmnichannelQueue();
queue.running = false;
expect(await queue.execute()).to.be.undefined;
});
it('should return undefined if license is over mac limits', async () => {
license.shouldPreventAction.returns(true);
const queue = new OmnichannelQueue();
queue.running = true;
expect(await queue.execute()).to.be.undefined;
expect(license.shouldPreventAction.calledOnce).to.be.true;
expect(queue.running).to.be.false;
});
it('should try to process a queue if license is not over mac limits', async () => {
license.shouldPreventAction.returns(false);
const queue = new OmnichannelQueue();
queue.running = true;
queue.getActiveQueues = Sinon.stub().resolves([null]);
await queue.execute();
expect(queue.getActiveQueues.calledOnce).to.be.true;
});
});
describe('start', () => {
beforeEach(() => {
queueLogger.info.resetHistory();
queueLogger.debug.resetHistory();
});
after(() => {
queueLogger.info.reset();
queueLogger.debug.reset();
});
it('should do nothing if queue is already running', async () => {
const queue = new OmnichannelQueue();
queue.running = true;
queue.execute = Sinon.stub();
await queue.start();
expect(queue.execute.notCalled).to.be.true;
});
it('should fetch active queues and set running to true', async () => {
const queue = new OmnichannelQueue();
queue.running = false;
queue.getActiveQueues = Sinon.stub().returns(['department1']);
queue.execute = Sinon.stub();
await queue.start();
expect(queue.running).to.be.true;
expect(queue.getActiveQueues.calledOnce).to.be.true;
expect(queueLogger.info.calledOnce).to.be.true;
expect(queueLogger.info.calledWith('Service started')).to.be.true;
expect(queue.execute.calledOnce).to.be.true;
});
});
describe('stop', () => {
beforeEach(() => {
models.LivechatInquiry.unlockAll.reset();
queueLogger.info.resetHistory();
});
after(() => {
models.LivechatInquiry.unlockAll.reset();
queueLogger.info.reset();
});
it('should unlock all inquiries and set running to false', async () => {
const queue = new OmnichannelQueue();
queue.running = true;
await queue.stop();
expect(queue.running).to.be.false;
expect(models.LivechatInquiry.unlockAll.calledOnce).to.be.true;
expect(queueLogger.info.calledOnce).to.be.true;
expect(queueLogger.info.calledWith('Service stopped')).to.be.true;
});
});
});