snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
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
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s

This commit is contained in:
upstream-mirror
2026-09-16 01:21:02 +05:00
commit 30810eecd7
10161 changed files with 987110 additions and 0 deletions

View File

@@ -0,0 +1,66 @@
import { expect } from 'chai';
import { beforeEach, describe, it } from 'mocha';
import p from 'proxyquire';
import sinon from 'sinon';
import { createFakeMessage, createFakeRoom } from '../../../../mocks/data';
const modelsMock = { Messages: { findOneById: sinon.stub() } };
const getMentionsMock = sinon.stub();
const replyMock = sinon.stub();
const callbacksRunAsyncMock = sinon.stub();
const { processThreads } = p.noCallThru().load('../../../../../server/hooks/messages/processThreads.ts', {
'@rocket.chat/models': modelsMock,
'meteor/meteor': { Meteor: { startup: sinon.stub() } },
'./notifyUsersOnMessage': { updateThreadUsersSubscriptions: sinon.stub(), getMentions: getMentionsMock },
'./sendNotificationsOnMessage': { sendMessageNotifications: sinon.stub() },
'../../lib/callbacks': {
callbacks: { runAsync: callbacksRunAsyncMock, add: sinon.stub(), remove: sinon.stub(), priority: { LOW: 'low' } },
},
'../../lib/messaging/threads/functions': { reply: replyMock },
'../../lib/notifyListener': { notifyOnMessageChange: sinon.stub() },
'../../settings': { settings: { watch: sinon.stub() } },
});
describe('processThreads', () => {
const room = createFakeRoom();
const parentMessage = createFakeMessage({ rid: room._id, tcount: 1, replies: [] });
const author = { _id: 'authorId', username: 'author' };
const threadReply = (overrides: Record<string, unknown> = {}) =>
createFakeMessage({ rid: room._id, tmid: parentMessage._id, u: author, ...overrides });
beforeEach(() => {
callbacksRunAsyncMock.reset();
replyMock.reset();
modelsMock.Messages.findOneById.reset();
modelsMock.Messages.findOneById.resolves(parentMessage);
getMentionsMock.reset();
getMentionsMock.resolves({ mentionIds: [] });
});
it('should mark the thread as read for the author of the reply', async () => {
await processThreads(threadReply(), room);
expect(callbacksRunAsyncMock.calledWith('afterReadMessages', room, { uid: author._id, tmid: parentMessage._id })).to.be.true;
});
it('should mark the thread as read only after the thread metadata is updated', async () => {
await processThreads(threadReply(), room);
expect(replyMock.calledBefore(callbacksRunAsyncMock)).to.be.true;
});
it('should not mark anything as read when the message is not a thread reply', async () => {
await processThreads(createFakeMessage({ rid: room._id, u: author }), room);
expect(callbacksRunAsyncMock.called).to.be.false;
});
it('should not mark anything as read when the message was edited', async () => {
await processThreads(threadReply({ editedAt: new Date(), editedBy: author }), room);
expect(callbacksRunAsyncMock.called).to.be.false;
});
});

View File

@@ -0,0 +1,53 @@
import { expect } from 'chai';
import { describe, it, beforeEach } from 'mocha';
import proxyquire from 'proxyquire';
import sinon from 'sinon';
const findStub = sinon.stub();
const { beforeNewRoomPatched } = proxyquire.noCallThru().load('../../../../../ee/server/hooks/omnichannel/beforeNewRoom.ts', {
'meteor/meteor': {
Meteor: {
Error,
},
},
'@rocket.chat/models': {
OmnichannelServiceLevelAgreements: {
findOneByIdOrName: findStub,
},
},
'../../../../server/lib/omnichannel/hooks': {
beforeNewRoom: { patch: sinon.stub() },
},
});
describe('beforeRoom', () => {
beforeEach(() => findStub.withArgs('high').resolves({ _id: 'high' }).withArgs('invalid').resolves(null));
it('should return roomInfo with customFields when provided', async () => {
const roomInfo = { name: 'test' };
const extraData = { customFields: { test: 'test' } };
const result = await beforeNewRoomPatched(undefined, roomInfo, extraData);
expect(result).to.deep.equal({ ...roomInfo, customFields: extraData.customFields });
});
it('should throw an error when provided with an invalid sla', async () => {
const roomInfo = { name: 'test' };
const extraData = { customFields: { test: 'test' }, sla: 'invalid' };
await expect(beforeNewRoomPatched(undefined, roomInfo, extraData)).to.be.rejectedWith(Error, 'error-invalid-sla');
});
it('should not include field in roomInfo when extraData has field other than customFields, sla', async () => {
const roomInfo = { name: 'test' };
const extraData = { customFields: { test: 'test' }, sla: 'high' };
const result = await beforeNewRoomPatched(undefined, roomInfo, extraData);
expect(result).to.deep.equal({ ...roomInfo, customFields: extraData.customFields, slaId: 'high' });
});
it('should return roomInfo with no customFields when customFields is not an object', async () => {
const roomInfo = { name: 'test' };
const extraData = { customFields: 'not an object' };
const result = await beforeNewRoomPatched(undefined, roomInfo, extraData);
expect(result).to.deep.equal({ ...roomInfo });
});
});

View File

@@ -0,0 +1,201 @@
import { expect } from 'chai';
import { describe, it, beforeEach } from 'mocha';
import moment from 'moment';
import proxyquire from 'proxyquire';
import Sinon from 'sinon';
import { createFakeMessage, createFakeUser } from '../../../../mocks/data';
const models = {
LivechatContacts: { isContactActiveOnPeriod: Sinon.stub(), markContactActiveForPeriod: Sinon.stub() },
LivechatInquiry: { markInquiryActiveForPeriod: Sinon.stub() },
LivechatRooms: {
getVisitorActiveForPeriodUpdateQuery: Sinon.stub(),
getAgentLastMessageTsUpdateQuery: Sinon.stub(),
getResponseByRoomIdUpdateQuery: Sinon.stub(),
},
};
const settingsGetMock = {
get: Sinon.stub(),
};
const isMessageFromBotMock = { isMessageFromBot: Sinon.stub() };
const { markRoomResponded } = proxyquire.noCallThru().load('../../../../../server/hooks/omnichannel/markRoomResponded.ts', {
'../../lib/callbacks': { callbacks: { add: Sinon.stub(), priority: { HIGH: 'high' } } },
'../../lib/notifyListener': { notifyOnLivechatInquiryChanged: Sinon.stub() },
'@rocket.chat/models': models,
'../../settings': { settings: settingsGetMock },
'../../lib/omnichannel/isMessageFromBot': isMessageFromBotMock,
});
describe('markRoomResponded', () => {
beforeEach(() => {
models.LivechatContacts.isContactActiveOnPeriod.reset();
models.LivechatContacts.markContactActiveForPeriod.reset();
models.LivechatInquiry.markInquiryActiveForPeriod.reset();
models.LivechatRooms.getVisitorActiveForPeriodUpdateQuery.reset();
models.LivechatRooms.getAgentLastMessageTsUpdateQuery.reset();
models.LivechatRooms.getResponseByRoomIdUpdateQuery.reset();
settingsGetMock.get.reset();
});
it('should return void if message is system message', async () => {
const message = {
t: 'livechat-started',
};
const room = {};
const res = await markRoomResponded(message, room, {});
expect(res).to.be.undefined;
});
it('should return void if message is edited message', async () => {
const message = {
editedAt: new Date(),
editedBy: { _id: '123' },
};
const room = {};
const res = await markRoomResponded(message, room, {});
expect(res).to.be.undefined;
});
it('should return void if message is from visitor', async () => {
const message = {
token: 'token',
};
const room = {};
const res = await markRoomResponded(message, room, {});
expect(res).to.be.undefined;
});
it('should return void if message is from bot and setting is enabled', async () => {
settingsGetMock.get.withArgs('Omnichannel_Metrics_Ignore_Automatic_Messages').resolves(true);
const user = createFakeUser({ roles: ['bot'] });
isMessageFromBotMock.isMessageFromBot.resolves(user);
const message = createFakeMessage();
const room = {};
const res = await markRoomResponded(message, room, user);
expect(res).to.be.undefined;
});
it('should try to mark visitor as active for current period', async () => {
const message = {};
const room = { v: { _id: '1234' } };
await markRoomResponded(message, room, {});
expect(models.LivechatContacts.markContactActiveForPeriod.calledOnce).to.be.true;
});
it('should try to mark inquiry as active for current period when room.v.activity doesnt include current period', async () => {
const message = {};
const room = { v: { activity: [] } };
models.LivechatInquiry.markInquiryActiveForPeriod.resolves({});
await markRoomResponded(message, room, {});
expect(models.LivechatInquiry.markInquiryActiveForPeriod.calledOnce).to.be.true;
});
it('should pass the current activity to getVisitorActiveForPeriodUpdateQuery so a null value can be healed', async () => {
const message = {};
const room = { v: { _id: '1234', activity: null } };
const roomUpdater = {};
models.LivechatInquiry.markInquiryActiveForPeriod.resolves({});
await markRoomResponded(message, room, roomUpdater);
expect(models.LivechatRooms.getVisitorActiveForPeriodUpdateQuery.calledOnce).to.be.true;
expect(models.LivechatRooms.getVisitorActiveForPeriodUpdateQuery.getCall(0).args).to.be.deep.equal([
moment().format('YYYY-MM'),
roomUpdater,
null,
]);
});
it('should not coerce an absent activity to null so it still gets the $addToSet treatment', async () => {
const message = {};
const room = { v: { _id: '1234' } };
const roomUpdater = {};
models.LivechatInquiry.markInquiryActiveForPeriod.resolves({});
await markRoomResponded(message, room, roomUpdater);
expect(models.LivechatRooms.getVisitorActiveForPeriodUpdateQuery.calledOnce).to.be.true;
expect(models.LivechatRooms.getVisitorActiveForPeriodUpdateQuery.getCall(0).args[2]).to.be.undefined;
});
it('should return room.responseBy when room is not waiting for response', async () => {
const message = {};
const room = { v: { _id: '1234' }, waitingResponse: false, responseBy: { _id: '1234' } };
const res = await markRoomResponded(message, room, {});
expect(res).to.be.equal(room.responseBy);
expect(models.LivechatRooms.getAgentLastMessageTsUpdateQuery.calledOnce).to.be.true;
});
it('should try to update the lastMessageTs property when a room was already answered by an agent', async () => {
const message = { u: { _id: '1234', username: 'username' }, ts: new Date() };
const room = { responseBy: { _id: '1234' }, v: { _id: '1234' } };
const res = await markRoomResponded(message, room, {});
expect(res).to.be.deep.equal(room.responseBy);
expect(models.LivechatRooms.getAgentLastMessageTsUpdateQuery.calledOnce).to.be.true;
});
it('should add a new responseBy object when room is waiting for response', async () => {
const message = { u: { _id: '1234', username: 'username' }, ts: new Date() };
const room = { waitingResponse: true, v: { _id: '1234' } };
const res = await markRoomResponded(message, room, {});
expect(res).to.be.deep.equal({ _id: '1234', username: 'username', firstResponseTs: message.ts, lastMessageTs: message.ts });
expect(models.LivechatRooms.getResponseByRoomIdUpdateQuery.calledOnce).to.be.true;
expect(models.LivechatRooms.getResponseByRoomIdUpdateQuery.getCall(0).args[0]).to.be.deep.equal({
_id: '1234',
lastMessageTs: message.ts,
firstResponseTs: message.ts,
username: 'username',
});
});
// This should never happpen on the wild, checking because of a data inconsistency bug found
it('should update only the lastMessageTs property when a room has both waitingResponse and responseBy properties', async () => {
const message = { u: { _id: '1234', username: 'username' }, ts: new Date() };
const room = {
waitingResponse: true,
responseBy: { _id: '1234', username: 'username', firstResponseTs: new Date() },
v: { _id: '1234' },
};
const res = await markRoomResponded(message, room, {});
expect(res).to.be.deep.equal({
_id: '1234',
username: 'username',
firstResponseTs: room.responseBy.firstResponseTs,
lastMessageTs: message.ts,
});
});
});

View File

@@ -0,0 +1,623 @@
import { expect } from 'chai';
import { it, describe } from 'mocha';
import p from 'proxyquire';
import sinon from 'sinon';
const settingsStub = sinon.stub();
const models = {
LivechatDepartment: {
findOneById: sinon.stub(),
},
LivechatBusinessHours: {
findOneById: sinon.stub(),
},
Messages: {
findAgentLastMessageByVisitorLastMessageTs: sinon.stub(),
},
LivechatRooms: {
setVisitorInactivityInSecondsById: sinon.stub(),
},
};
const businessHourManagerMock = {
getBusinessHour: sinon.stub(),
};
const { getSecondsWhenOfficeHoursIsDisabled, parseDays, getSecondsSinceLastAgentResponse, onCloseRoom } = p
.noCallThru()
.load('../../../../../server/hooks/omnichannel/processRoomAbandonment.ts', {
'@rocket.chat/models': models,
'../../lib/callbacks': {
callbacks: { add: sinon.stub(), priority: { HIGH: 'high' } },
},
'../../settings': {
settings: { get: settingsStub },
},
'../../lib/omnichannel/business-hour': { businessHourManager: businessHourManagerMock },
});
describe('processRoomAbandonment', () => {
describe('getSecondsWhenOfficeHoursIsDisabled', () => {
it('should return the seconds since the agents last message till room was closed', () => {
const room = {
closedAt: new Date('2024-01-01T12:00:10Z'),
};
const agentLastMessage = {
ts: new Date('2024-01-01T12:00:00Z'),
};
const result = getSecondsWhenOfficeHoursIsDisabled(room, agentLastMessage);
expect(result).to.be.equal(10);
});
it('should return the seconds since agents last message till now when room.closedAt is undefined', () => {
const room = {
closedAt: undefined,
};
const agentLastMessage = {
ts: new Date(new Date().getTime() - 10000),
};
const result = getSecondsWhenOfficeHoursIsDisabled(room, agentLastMessage);
expect(result).to.be.equal(10);
});
});
describe('parseDays', () => {
it('should properly return the days in the expected format', () => {
const days = [
{
day: 'Monday',
start: { utc: { dayOfWeek: 'Monday', time: '10:00' } },
finish: { utc: { dayOfWeek: 'Monday', time: '11:00' } },
open: true,
},
{
day: 'Tuesday',
start: { utc: { dayOfWeek: 'Tuesday', time: '10:00' } },
finish: { utc: { dayOfWeek: 'Tuesday', time: '11:00' } },
open: true,
},
{
day: 'Wednesday',
start: { utc: { dayOfWeek: 'Wednesday', time: '10:00' } },
finish: { utc: { dayOfWeek: 'Wednesday', time: '11:00' } },
open: true,
},
];
const result = days.reduce(parseDays, {});
expect(result).to.be.deep.equal({
Monday: {
start: { day: 'Monday', time: '10:00' },
finish: { day: 'Monday', time: '11:00' },
open: true,
},
Tuesday: {
start: { day: 'Tuesday', time: '10:00' },
finish: { day: 'Tuesday', time: '11:00' },
open: true,
},
Wednesday: {
start: { day: 'Wednesday', time: '10:00' },
finish: { day: 'Wednesday', time: '11:00' },
open: true,
},
});
});
it('should properly parse open/close days', () => {
const days = [
{
day: 'Monday',
start: { utc: { dayOfWeek: 'Monday', time: '10:00' } },
finish: { utc: { dayOfWeek: 'Monday', time: '11:00' } },
open: true,
},
{
day: 'Tuesday',
start: { utc: { dayOfWeek: 'Tuesday', time: '10:00' } },
finish: { utc: { dayOfWeek: 'Tuesday', time: '11:00' } },
open: false,
},
{
day: 'Wednesday',
start: { utc: { dayOfWeek: 'Wednesday', time: '10:00' } },
finish: { utc: { dayOfWeek: 'Wednesday', time: '11:00' } },
open: true,
},
];
const result = days.reduce(parseDays, {});
expect(result).to.be.deep.equal({
Monday: {
start: { day: 'Monday', time: '10:00' },
finish: { day: 'Monday', time: '11:00' },
open: true,
},
Tuesday: {
start: { day: 'Tuesday', time: '10:00' },
finish: { day: 'Tuesday', time: '11:00' },
open: false,
},
Wednesday: {
start: { day: 'Wednesday', time: '10:00' },
finish: { day: 'Wednesday', time: '11:00' },
open: true,
},
});
});
});
describe('getSecondsSinceLastAgentResponse', () => {
beforeEach(() => {
settingsStub.reset();
models.LivechatDepartment.findOneById.reset();
models.LivechatBusinessHours.findOneById.reset();
businessHourManagerMock.getBusinessHour.reset();
});
it('should return the seconds since agent last message when Livechat_enable_business_hours is false', async () => {
settingsStub.withArgs('Livechat_enable_business_hours').returns(false);
const room = {
closedAt: undefined,
};
const agentLastMessage = {
ts: new Date(new Date().getTime() - 10000),
};
const result = await getSecondsSinceLastAgentResponse(room, agentLastMessage);
expect(result).to.be.equal(10);
});
it('should return the seconds since last agent message when room has a department but department has an invalid business hour attached', async () => {
settingsStub.withArgs('Livechat_enable_business_hours').returns(true);
models.LivechatDepartment.findOneById.withArgs('departmentId').resolves({
businessHourId: 'businessHourId',
});
models.LivechatBusinessHours.findOneById.withArgs('businessHourId').resolves(null);
const room = {
closedAt: undefined,
departmentId: 'departmentId',
};
const agentLastMessage = {
ts: new Date(new Date().getTime() - 10000),
};
const result = await getSecondsSinceLastAgentResponse(room, agentLastMessage);
expect(models.LivechatDepartment.findOneById.calledWith(room.departmentId)).to.be.true;
expect(result).to.be.equal(10);
});
it('should return the seconds since last agent message when department has a valid business hour but business hour doest have work hours', async () => {
settingsStub.withArgs('Livechat_enable_business_hours').returns(true);
models.LivechatDepartment.findOneById.withArgs('departmentId').resolves({
businessHourId: 'businessHourId',
});
models.LivechatBusinessHours.findOneById.withArgs('businessHourId').resolves({
workHours: [],
});
businessHourManagerMock.getBusinessHour.withArgs('businessHourId').resolves(null);
const room = {
closedAt: undefined,
departmentId: 'departmentId',
};
const agentLastMessage = {
ts: new Date(new Date().getTime() - 10000),
};
const result = await getSecondsSinceLastAgentResponse(room, agentLastMessage);
expect(result).to.be.equal(10);
});
it('should return the seconds since last agent message when department has a valid business hour but business hour workhours is empty', async () => {
settingsStub.withArgs('Livechat_enable_business_hours').returns(true);
models.LivechatDepartment.findOneById.withArgs('departmentId').resolves({
businessHourId: 'businessHourId',
});
models.LivechatBusinessHours.findOneById.withArgs('businessHourId').resolves({
workHours: [],
});
businessHourManagerMock.getBusinessHour.withArgs('businessHourId').resolves({
workHours: [],
});
const room = {
closedAt: undefined,
departmentId: 'departmentId',
};
const agentLastMessage = {
ts: new Date(new Date().getTime() - 10000),
};
const result = await getSecondsSinceLastAgentResponse(room, agentLastMessage);
expect(result).to.be.equal(10);
});
it('should get the data from the default business hour when room has no department attached and return the seconds since last agent message when default bh has no workhours', async () => {
settingsStub.withArgs('Livechat_enable_business_hours').returns(true);
businessHourManagerMock.getBusinessHour.resolves({
workHours: [],
});
const room = {
closedAt: undefined,
};
const agentLastMessage = {
ts: new Date(new Date().getTime() - 10000),
};
const result = await getSecondsSinceLastAgentResponse(room, agentLastMessage);
expect(models.LivechatDepartment.findOneById.called).to.be.false;
expect(models.LivechatBusinessHours.findOneById.called).to.be.false;
expect(businessHourManagerMock.getBusinessHour.called).to.be.true;
expect(businessHourManagerMock.getBusinessHour.getCall(0).args.length).to.be.equal(0);
expect(result).to.be.equal(10);
});
it('should return the proper number of seconds the room was inactive considering business hours (inactive same day)', async () => {
settingsStub.withArgs('Livechat_enable_business_hours').returns(true);
const room = {
closedAt: new Date('2024-01-01T12:00:00Z'),
};
const agentLastMessage = {
ts: new Date('2024-01-01T00:00:00Z'),
};
businessHourManagerMock.getBusinessHour.resolves({
workHours: [
{
day: 'Monday',
start: { utc: { dayOfWeek: 'Monday', time: '10:00' } },
finish: { utc: { dayOfWeek: 'Monday', time: '11:00' } },
open: true,
},
{
day: 'Tuesday',
start: { utc: { dayOfWeek: 'Tuesday', time: '10:00' } },
finish: { utc: { dayOfWeek: 'Tuesday', time: '11:00' } },
open: true,
},
{
day: 'Wednesday',
start: { utc: { dayOfWeek: 'Wednesday', time: '10:00' } },
finish: { utc: { dayOfWeek: 'Wednesday', time: '11:00' } },
open: true,
},
],
});
const result = await getSecondsSinceLastAgentResponse(room, agentLastMessage);
expect(result).to.be.equal(3600);
});
it('should return the proper number of seconds the room was inactive considering business hours (inactive same day)', async () => {
settingsStub.withArgs('Livechat_enable_business_hours').returns(true);
const room = {
closedAt: new Date('2024-01-01T12:00:00Z'),
};
const agentLastMessage = {
ts: new Date('2024-01-01T00:00:00Z'),
};
businessHourManagerMock.getBusinessHour.resolves({
workHours: [
{
day: 'Monday',
start: { utc: { dayOfWeek: 'Monday', time: '10:00' } },
finish: { utc: { dayOfWeek: 'Monday', time: '23:00' } },
open: true,
},
{
day: 'Tuesday',
start: { utc: { dayOfWeek: 'Tuesday', time: '10:00' } },
finish: { utc: { dayOfWeek: 'Tuesday', time: '11:00' } },
open: true,
},
{
day: 'Wednesday',
start: { utc: { dayOfWeek: 'Wednesday', time: '10:00' } },
finish: { utc: { dayOfWeek: 'Wednesday', time: '11:00' } },
open: true,
},
],
});
const result = await getSecondsSinceLastAgentResponse(room, agentLastMessage);
expect(result).to.be.equal(7200);
});
it('should return 0 if a room happened to be inactive on a day outside of business hours', async () => {
settingsStub.withArgs('Livechat_enable_business_hours').returns(true);
const room = {
closedAt: new Date('2024-01-03T12:00:00Z'),
};
const agentLastMessage = {
ts: new Date('2024-01-03T00:00:00Z'),
};
businessHourManagerMock.getBusinessHour.resolves({
workHours: [
{
day: 'Monday',
start: { utc: { dayOfWeek: 'Monday', time: '10:00' } },
finish: { utc: { dayOfWeek: 'Monday', time: '11:00' } },
open: true,
},
{
day: 'Tuesday',
start: { utc: { dayOfWeek: 'Tuesday', time: '10:00' } },
finish: { utc: { dayOfWeek: 'Tuesday', time: '11:00' } },
open: true,
},
],
});
const result = await getSecondsSinceLastAgentResponse(room, agentLastMessage);
expect(result).to.be.equal(0);
});
it('should return the proper number of seconds when a room was inactive for more than 1 day', async () => {
settingsStub.withArgs('Livechat_enable_business_hours').returns(true);
const room = {
closedAt: new Date('2024-01-03T12:00:00Z'),
};
const agentLastMessage = {
ts: new Date('2024-01-01T00:00:00Z'),
};
businessHourManagerMock.getBusinessHour.resolves({
workHours: [
{
day: 'Monday',
start: { utc: { dayOfWeek: 'Monday', time: '10:00' } },
finish: { utc: { dayOfWeek: 'Monday', time: '11:00' } },
open: true,
},
{
day: 'Tuesday',
start: { utc: { dayOfWeek: 'Tuesday', time: '10:00' } },
finish: { utc: { dayOfWeek: 'Tuesday', time: '11:00' } },
open: true,
},
],
});
const result = await getSecondsSinceLastAgentResponse(room, agentLastMessage);
expect(result).to.be.equal(7200);
});
it('should return the proper number of seconds when a room was inactive for more than 1 day, and one of those days was a closed day', async () => {
settingsStub.withArgs('Livechat_enable_business_hours').returns(true);
const room = {
closedAt: new Date('2024-01-03T12:00:00Z'),
};
const agentLastMessage = {
ts: new Date('2024-01-01T00:00:00Z'),
};
businessHourManagerMock.getBusinessHour.resolves({
workHours: [
{
day: 'Monday',
start: { utc: { dayOfWeek: 'Monday', time: '10:00' } },
finish: { utc: { dayOfWeek: 'Monday', time: '11:00' } },
open: true,
},
{
day: 'Tuesday',
start: { utc: { dayOfWeek: 'Tuesday', time: '10:00' } },
finish: { utc: { dayOfWeek: 'Tuesday', time: '11:00' } },
open: false,
},
{
day: 'Wednesday',
start: { utc: { dayOfWeek: 'Wednesday', time: '10:00' } },
finish: { utc: { dayOfWeek: 'Wednesday', time: '11:00' } },
open: true,
},
],
});
const result = await getSecondsSinceLastAgentResponse(room, agentLastMessage);
expect(result).to.be.equal(7200);
});
it('should return the proper number of seconds when a room was inactive for more than 1 day and one of those days is not in configuration', async () => {
settingsStub.withArgs('Livechat_enable_business_hours').returns(true);
const room = {
closedAt: new Date('2024-01-03T12:00:00Z'),
};
const agentLastMessage = {
ts: new Date('2024-01-01T00:00:00Z'),
};
businessHourManagerMock.getBusinessHour.resolves({
workHours: [
{
day: 'Monday',
start: { utc: { dayOfWeek: 'Monday', time: '10:00' } },
finish: { utc: { dayOfWeek: 'Monday', time: '11:00' } },
open: true,
},
{
day: 'Wednesday',
start: { utc: { dayOfWeek: 'Tuesday', time: '10:00' } },
finish: { utc: { dayOfWeek: 'Tuesday', time: '11:00' } },
open: true,
},
],
});
const result = await getSecondsSinceLastAgentResponse(room, agentLastMessage);
expect(result).to.be.equal(7200);
});
it('should return the proper number of seconds when a room has been inactive for more than a week', async () => {
settingsStub.withArgs('Livechat_enable_business_hours').returns(true);
const room = {
closedAt: new Date('2024-01-10T12:00:00Z'),
};
const agentLastMessage = {
ts: new Date('2024-01-01T00:00:00Z'),
};
businessHourManagerMock.getBusinessHour.resolves({
workHours: [
{
day: 'Monday',
start: { utc: { dayOfWeek: 'Monday', time: '10:00' } },
finish: { utc: { dayOfWeek: 'Monday', time: '11:00' } },
open: true,
},
{
day: 'Tuesday',
start: { utc: { dayOfWeek: 'Tuesday', time: '10:00' } },
finish: { utc: { dayOfWeek: 'Tuesday', time: '11:00' } },
open: true,
},
{
day: 'Wednesday',
start: { utc: { dayOfWeek: 'Wednesday', time: '10:00' } },
finish: { utc: { dayOfWeek: 'Wednesday', time: '11:00' } },
open: true,
},
{
day: 'Thursday',
start: { utc: { dayOfWeek: 'Thursday', time: '10:00' } },
finish: { utc: { dayOfWeek: 'Thursday', time: '11:00' } },
open: false,
},
{
day: 'Saturday',
start: { utc: { dayOfWeek: 'Friday', time: '10:00' } },
finish: { utc: { dayOfWeek: 'Friday', time: '11:00' } },
open: true,
},
{
day: 'Sunday',
start: { utc: { dayOfWeek: 'Saturday', time: '10:00' } },
finish: { utc: { dayOfWeek: 'Saturday', time: '11:00' } },
open: true,
},
],
});
const result = await getSecondsSinceLastAgentResponse(room, agentLastMessage);
expect(result).to.be.equal(28800);
});
it('should return 0 when room was inactive in the same day but the configuration for bh on that day is invalid', async () => {
settingsStub.withArgs('Livechat_enable_business_hours').returns(true);
const room = {
closedAt: new Date('2024-01-01T12:00:00Z'),
};
const agentLastMessage = {
ts: new Date('2024-01-01T00:00:00Z'),
};
businessHourManagerMock.getBusinessHour.resolves({
workHours: [
{
day: 'Monday',
start: { utc: { dayOfWeek: 'Monday', time: undefined } },
finish: { utc: { dayOfWeek: 'Monday', time: undefined } },
open: true,
},
{
day: 'Wednesday',
start: { utc: { dayOfWeek: 'Tuesday', time: '10:00' } },
finish: { utc: { dayOfWeek: 'Tuesday', time: '11:00' } },
open: false,
},
],
});
const result = await getSecondsSinceLastAgentResponse(room, agentLastMessage);
expect(result).to.be.equal(0);
});
it('should return the proper number of seconds when a room has been inactive for more than a day but the inactivity started after BH started', async () => {
settingsStub.withArgs('Livechat_enable_business_hours').returns(true);
const room = {
closedAt: new Date('2024-01-02T12:00:00Z'),
};
const agentLastMessage = {
ts: new Date('2024-01-01T10:15:00Z'),
};
businessHourManagerMock.getBusinessHour.resolves({
workHours: [
{
day: 'Monday',
start: { utc: { dayOfWeek: 'Monday', time: '10:00' } },
finish: { utc: { dayOfWeek: 'Monday', time: '11:00' } },
open: true,
},
{
day: 'Tuesday',
start: { utc: { dayOfWeek: 'Tuesday', time: '10:00' } },
finish: { utc: { dayOfWeek: 'Tuesday', time: '11:00' } },
open: true,
},
],
});
const result = await getSecondsSinceLastAgentResponse(room, agentLastMessage);
expect(result).to.be.equal(6300);
});
it('should return the proper number of seconds when a room was inactive between a BH start and end', async () => {
settingsStub.withArgs('Livechat_enable_business_hours').returns(true);
const room = {
closedAt: new Date('2024-01-01T10:50:00Z'),
};
const agentLastMessage = {
ts: new Date('2024-01-01T10:15:00Z'),
};
businessHourManagerMock.getBusinessHour.resolves({
workHours: [
{
day: 'Monday',
start: { utc: { dayOfWeek: 'Monday', time: '10:00' } },
finish: { utc: { dayOfWeek: 'Monday', time: '11:00' } },
open: true,
},
{
day: 'Tuesday',
start: { utc: { dayOfWeek: 'Tuesday', time: '10:00' } },
finish: { utc: { dayOfWeek: 'Tuesday', time: '11:00' } },
open: true,
},
],
});
const result = await getSecondsSinceLastAgentResponse(room, agentLastMessage);
expect(result).to.be.equal(2100);
});
});
describe('onCloseRoom', () => {
beforeEach(() => {
models.Messages.findAgentLastMessageByVisitorLastMessageTs.reset();
});
it('should skip the hook if room is not an omnichannel room', async () => {
const param = { room: { t: 'd' } };
const r = await onCloseRoom(param);
expect(models.Messages.findAgentLastMessageByVisitorLastMessageTs.called).to.be.false;
expect(r).to.be.equal(param);
});
it('should skip if room was not closed by agent', async () => {
const param = { room: { t: 'l' }, closer: 'visitor' };
const r = await onCloseRoom(param);
expect(models.Messages.findAgentLastMessageByVisitorLastMessageTs.called).to.be.false;
expect(r).to.be.equal(param);
});
it('should skip if the last message on room was not from an agent', async () => {
const param = { room: { t: 'l' }, closer: 'user', lastMessage: { token: 'xxxx' } };
const r = await onCloseRoom(param);
expect(models.Messages.findAgentLastMessageByVisitorLastMessageTs.called).to.be.false;
expect(r).to.be.equal(param);
});
it('should skip if the last message is not on db', async () => {
models.Messages.findAgentLastMessageByVisitorLastMessageTs.resolves(null);
const param = { room: { _id: 'xyz', t: 'l', v: { lastMessageTs: new Date() }, closer: 'user', lastMessage: { msg: 'test' } } };
const r = await onCloseRoom(param);
expect(models.Messages.findAgentLastMessageByVisitorLastMessageTs.calledWith('xyz', param.room.v.lastMessageTs)).to.be.true;
expect(r).to.be.equal(param);
});
it('should skip if the visitor has not send any messages', async () => {
models.Messages.findAgentLastMessageByVisitorLastMessageTs.resolves({ ts: undefined });
const param = { room: { _id: 'xyz', t: 'l', v: { token: 'xfasfdsa' }, closer: 'user', lastMessage: { msg: 'test' } } };
const r = await onCloseRoom(param);
expect(models.Messages.findAgentLastMessageByVisitorLastMessageTs.called).to.be.false;
expect(r).to.be.equal(param);
});
it('should set the visitor inactivity in seconds when all params are valid', async () => {
models.Messages.findAgentLastMessageByVisitorLastMessageTs.resolves({ ts: new Date('2024-01-01T10:15:00Z') });
settingsStub.withArgs('Livechat_enable_business_hours').returns(false);
const param = {
room: {
_id: 'xyz',
t: 'l',
v: { lastMessageTs: new Date() },
closedAt: new Date('2024-01-01T10:50:00Z'),
closer: 'user',
lastMessage: { msg: 'test' },
},
};
const r = await onCloseRoom(param);
expect(models.Messages.findAgentLastMessageByVisitorLastMessageTs.calledWith('xyz', param.room.v.lastMessageTs)).to.be.true;
expect(models.LivechatRooms.setVisitorInactivityInSecondsById.calledWith('xyz', 2100)).to.be.true;
expect(r).to.be.equal(param);
});
});
});

View File

@@ -0,0 +1,123 @@
import { expect } from 'chai';
import p from 'proxyquire';
import sinon from 'sinon';
const resultObj = {
result: true,
};
const { sendMessageType, isOmnichannelNavigationMessage, isOmnichannelClosingMessage, getAdditionalFieldsByType } = p
.noCallThru()
.load('../../../../../server/hooks/omnichannel/sendToCRM', {
'../../settings': {
settings: {
get() {
return resultObj.result;
},
},
},
'../../lib/utils/functions/normalizeMessageFileUpload': {
normalizeMessageFileUpload: sinon.stub().returnsArg(0),
},
'../../lib/omnichannel/webhooks': {},
'../../lib/omnichannel/guests': { getLivechatRoomGuestInfo: sinon.stub() },
});
describe('[OC] Send TO CRM', () => {
describe('isOmnichannelNavigationMessage', () => {
it('should return true if the message is a navigation message', () => {
const message = { t: 'livechat_navigation_history' } as any;
expect(isOmnichannelNavigationMessage(message)).to.be.true;
});
it('should return false if the message is not a navigation message', () => {
const message = { t: 'livechat-close' } as any;
expect(isOmnichannelNavigationMessage(message)).to.be.false;
});
});
describe('isOmnichannelClosingMessage', () => {
it('should return true if the message is a closing message', () => {
const message = { t: 'livechat-close' } as any;
expect(isOmnichannelClosingMessage(message)).to.be.true;
});
it('should return false if the message is not a closing message', () => {
const message = { t: 'livechat_navigation_history' } as any;
expect(isOmnichannelClosingMessage(message)).to.be.false;
});
});
describe('sendMessageType', () => {
it('should return true if the message type is a closing message', () => {
expect(sendMessageType('livechat-close')).to.be.true;
});
it('should return true if the message type is a navigation message and the settings are enabled', () => {
expect(sendMessageType('livechat_navigation_history')).to.be.true;
});
it('should return false if the message type is a navigation message and the settings are disabled', () => {
resultObj.result = false;
expect(sendMessageType('livechat_navigation_history')).to.be.false;
});
it('should return false if the message type is not a closing or navigation message', () => {
expect(sendMessageType('message')).to.be.false;
});
});
describe('getAdditionalFieldsByType', () => {
it('should return the correct fields for the LivechatSessionStart type', () => {
const room = { departmentId: 'departmentId' } as any;
expect(getAdditionalFieldsByType('LivechatSessionStart', room)).to.deep.equal({ departmentId: 'departmentId' });
});
it('should return the correct fields for the LivechatSessionQueued type', () => {
const room = { departmentId: 'departmentId' } as any;
expect(getAdditionalFieldsByType('LivechatSessionQueued', room)).to.deep.equal({ departmentId: 'departmentId' });
});
it('should return the correct fields for the LivechatSession type', () => {
const room = {
departmentId: 'departmentId',
servedBy: 'servedBy',
closedAt: 'closedAt',
closedBy: 'closedBy',
closer: 'closer',
} as any;
expect(getAdditionalFieldsByType('LivechatSession', room)).to.deep.equal({
departmentId: 'departmentId',
servedBy: 'servedBy',
closedAt: 'closedAt',
closedBy: 'closedBy',
closer: 'closer',
});
});
it('should return the correct fields for the LivechatSessionTaken type', () => {
const room = { departmentId: 'departmentId', servedBy: 'servedBy' } as any;
expect(getAdditionalFieldsByType('LivechatSessionTaken', room)).to.deep.equal({ departmentId: 'departmentId', servedBy: 'servedBy' });
});
it('should return the correct fields for the LivechatSessionForwarded type', () => {
const room = {
departmentId: 'departmentId',
servedBy: 'servedBy',
oldDepartmentId: 'oldDepartmentId',
oldServedBy: 'oldServedBy',
} as any;
expect(getAdditionalFieldsByType('LivechatSessionForwarded', room)).to.deep.equal({
departmentId: 'departmentId',
servedBy: 'servedBy',
oldDepartmentId: 'oldDepartmentId',
oldServedBy: 'oldServedBy',
});
});
it('should return an empty object for an unknown type', () => {
const room = {} as any;
expect(getAdditionalFieldsByType('unknownType' as any, room)).to.deep.equal({});
});
});
});