From a4f2102af1c2e875c60cafebd0163105bdaca678 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Fri, 31 Jan 2025 09:47:23 -0600 Subject: [PATCH] test: OmnichannelQueueInactivityMonitor (#35073) --- .../server/lib/QueueInactivityMonitor.ts | 4 +- .../server/lib/QueueInactivityMonitor.spec.ts | 198 ++++++++++++++++++ 2 files changed, 201 insertions(+), 1 deletion(-) create mode 100644 apps/meteor/ee/tests/unit/apps/livechat-enterprise/server/lib/QueueInactivityMonitor.spec.ts diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/QueueInactivityMonitor.ts b/apps/meteor/ee/app/livechat-enterprise/server/lib/QueueInactivityMonitor.ts index 31ac11f0c2b1..83dd905e715d 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/QueueInactivityMonitor.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/QueueInactivityMonitor.ts @@ -13,7 +13,7 @@ import { i18n } from '../../../../../server/lib/i18n'; const SCHEDULER_NAME = 'omnichannel_queue_inactivity_monitor'; -class OmnichannelQueueInactivityMonitorClass { +export class OmnichannelQueueInactivityMonitorClass { scheduler: Agenda; running: boolean; @@ -91,6 +91,7 @@ class OmnichannelQueueInactivityMonitorClass { return; } await this.scheduler.cancel({}); + this.running = false; } async stopInquiry(inquiryId: string): Promise { @@ -109,6 +110,7 @@ class OmnichannelQueueInactivityMonitorClass { async closeRoom({ attrs: { data } }: any = {}): Promise { const { inquiryId } = data; + // TODO: add projection and maybe use findOneQueued to avoid fetching the whole inquiry const inquiry = await LivechatInquiryRaw.findOneById(inquiryId); if (!inquiry || inquiry.status !== 'queued') { return; diff --git a/apps/meteor/ee/tests/unit/apps/livechat-enterprise/server/lib/QueueInactivityMonitor.spec.ts b/apps/meteor/ee/tests/unit/apps/livechat-enterprise/server/lib/QueueInactivityMonitor.spec.ts new file mode 100644 index 000000000000..383c78586387 --- /dev/null +++ b/apps/meteor/ee/tests/unit/apps/livechat-enterprise/server/lib/QueueInactivityMonitor.spec.ts @@ -0,0 +1,198 @@ +import { expect } from 'chai'; +import { describe, afterEach, before } from 'mocha'; +import proxyquire from 'proxyquire'; +import sinon from 'sinon'; + +const AgendaJobStub = { + schedule: sinon.stub(), + unique: sinon.stub(), + save: sinon.stub(), +}; +const AgendaStub = { + start: sinon.stub(), + define: sinon.stub(), + cancel: sinon.stub(), + create: sinon.stub().returns(AgendaJobStub), +}; + +const modelsMock = { + LivechatRooms: { findOneById: sinon.stub() }, + LivechatInquiry: { findOneById: sinon.stub() }, + Users: { findOneById: sinon.stub() }, +}; +const meteorMock = { Meteor: { startup: sinon.stub() } }; +const createIndexStub = sinon.stub(); +const mongoMock = { + MongoInternals: { + defaultRemoteCollectionDriver: sinon.stub().returns({ + mongo: { db: { collection: sinon.stub().returns({ createIndex: createIndexStub }) }, client: { db: sinon.stub() } }, + }), + }, +}; +const livechatMock = { Livechat: { closeRoom: sinon.stub() } }; +const settingsMock = { settings: { get: sinon.stub() } }; + +const { OmnichannelQueueInactivityMonitorClass } = proxyquire + .noCallThru() + .load('../../../../../../app/livechat-enterprise/server/lib/QueueInactivityMonitor', { + '@rocket.chat/agenda': { + Agenda: sinon.stub().returns(AgendaStub), + }, + '@rocket.chat/models': modelsMock, + 'meteor/meteor': meteorMock, + 'meteor/mongo': mongoMock, + '../../../../../app/livechat/server/lib/LivechatTyped': livechatMock, + '../../../../../app/settings/server': settingsMock, + '../../../../../server/lib/i18n': { i18n: { t: sinon.stub().returns('Closed automatically') } }, + }); + +describe('OmnichannelQueueInactivityMonitorClass', () => { + afterEach(() => { + modelsMock.Users.findOneById.reset(); + }); + describe('getRocketChatUser', () => { + it('should return rocket.cat user', async () => { + const qclass = new OmnichannelQueueInactivityMonitorClass(); + await qclass.getRocketCatUser(); + + expect(modelsMock.Users.findOneById.calledWith('rocket.cat')).to.be.true; + }); + }); + + describe('getName', () => { + it('should return valid name', () => { + const qclass = new OmnichannelQueueInactivityMonitorClass(); + const result = qclass.getName('inquiryId'); + expect(result).to.be.equal('Omnichannel-Queue-Inactivity-Monitor-inquiryId'); + }); + }); + + describe('createIndex', () => { + before(() => { + createIndexStub.reset(); + }); + it('should create index', () => { + const qclass = new OmnichannelQueueInactivityMonitorClass(); + qclass.createIndex(); + expect(createIndexStub.calledWith(sinon.match({ 'data.inquiryId': 1 }), sinon.match({ unique: true }))).to.be.true; + }); + }); + + describe('start', () => { + before(() => { + AgendaStub.start.reset(); + }); + it('should do nothing if its already running', async () => { + const qclass = new OmnichannelQueueInactivityMonitorClass(); + qclass.running = true; + await qclass.start(); + expect(AgendaStub.start.calledOnce).to.be.false; + }); + it('should start scheduler', async () => { + const qclass = new OmnichannelQueueInactivityMonitorClass(); + await qclass.start(); + expect(AgendaStub.start.calledOnce).to.be.true; + expect(qclass.running).to.be.true; + }); + }); + + describe('scheduleInquiry', () => { + beforeEach(() => { + AgendaStub.define.reset(); + }); + it('should schedule inquiry', async () => { + const qclass = new OmnichannelQueueInactivityMonitorClass(); + const now = new Date(); + await qclass.scheduleInquiry('inquiryId', now); + + expect(AgendaStub.cancel.calledOnce).to.be.true; + expect(AgendaStub.cancel.calledBefore(AgendaStub.define)).to.be.true; + expect(AgendaStub.define.calledOnce).to.be.true; + expect(AgendaJobStub.schedule.calledOnceWith(now)).to.be.true; + expect(AgendaJobStub.unique.calledOnceWith(sinon.match({ 'data.inquiryId': 'inquiryId' }))).to.be.true; + }); + }); + + describe('stop', () => { + beforeEach(() => { + AgendaStub.cancel.reset(); + }); + it('should do nothing if process is already stopped', async () => { + const qclass = new OmnichannelQueueInactivityMonitorClass(); + qclass.running = false; + await qclass.stop(); + expect(AgendaStub.cancel.calledOnce).to.be.false; + }); + it('should not call cancel twice if stop is called twice', async () => { + const qclass = new OmnichannelQueueInactivityMonitorClass(); + qclass.running = true; + await qclass.stop(); + await qclass.stop(); + expect(AgendaStub.cancel.calledOnce).to.be.true; + }); + it('should cancel all inquiries and flag service as not running', async () => { + const qclass = new OmnichannelQueueInactivityMonitorClass(); + qclass.running = true; + await qclass.stop(); + expect(AgendaStub.cancel.calledOnce).to.be.true; + expect(qclass.running).to.be.false; + }); + }); + + describe('stopInquiry', () => { + beforeEach(() => { + AgendaStub.cancel.reset(); + }); + it('should cancel inquiry', async () => { + const qclass = new OmnichannelQueueInactivityMonitorClass(); + await qclass.stopInquiry('inquiryId'); + expect(AgendaStub.cancel.calledOnce).to.be.true; + }); + }); + + describe('closeRoom', () => { + beforeEach(() => { + modelsMock.LivechatInquiry.findOneById.reset(); + modelsMock.LivechatRooms.findOneById.reset(); + livechatMock.Livechat.closeRoom.reset(); + }); + it('should ignore the inquiry if its not in queue anymore', async () => { + const qclass = new OmnichannelQueueInactivityMonitorClass(); + modelsMock.LivechatInquiry.findOneById.resolves({ status: 'taken' }); + + await qclass.closeRoom({ attrs: { data: { inquiryId: 'inquiryId' } } }); + expect(modelsMock.LivechatInquiry.findOneById.calledWith('inquiryId')).to.be.true; + expect(livechatMock.Livechat.closeRoom.notCalled).to.be.true; + }); + it('should ignore an inquiry with no room', async () => { + const qclass = new OmnichannelQueueInactivityMonitorClass(); + modelsMock.LivechatInquiry.findOneById.resolves({ status: 'queued', rid: 'roomId' }); + modelsMock.LivechatRooms.findOneById.resolves(undefined); + await qclass.closeRoom({ attrs: { data: { inquiryId: 'inquiryId' } } }); + + expect(modelsMock.LivechatInquiry.findOneById.calledWith('inquiryId')).to.be.true; + expect(modelsMock.LivechatRooms.findOneById.calledWith('roomId')).to.be.true; + expect(livechatMock.Livechat.closeRoom.notCalled).to.be.true; + }); + it('should close a room', async () => { + const qclass = new OmnichannelQueueInactivityMonitorClass(); + modelsMock.LivechatInquiry.findOneById.resolves({ status: 'queued', rid: 'roomId' }); + modelsMock.LivechatRooms.findOneById.resolves({ _id: 'roomId' }); + modelsMock.Users.findOneById.resolves({ _id: 'rocket.cat' }); + + await qclass.closeRoom({ attrs: { data: { inquiryId: 'inquiryId' } } }); + + expect(modelsMock.LivechatInquiry.findOneById.calledWith('inquiryId')).to.be.true; + expect(modelsMock.LivechatRooms.findOneById.calledWith('roomId')).to.be.true; + expect( + livechatMock.Livechat.closeRoom.calledWith( + sinon.match({ + comment: 'Closed automatically', + room: { _id: 'roomId' }, + user: { _id: 'rocket.cat' }, + }), + ), + ); + }); + }); +});