import type { Credentials } from '@rocket.chat/api-client'; import type { ILivechatAgent, ILivechatBusinessHour, ILivechatDepartment } from '@rocket.chat/core-typings'; import { LivechatBusinessHourBehaviors, LivechatBusinessHourTypes, ILivechatAgentStatus } from '@rocket.chat/core-typings'; import { expect } from 'chai'; import { after, before, describe, it } from 'mocha'; import { sleep } from '../../../../lib/utils/sleep'; import { getCredentials, api, request, credentials } from '../../../data/api-data'; import { getDefaultBusinessHour, removeAllCustomBusinessHours, saveBusinessHour, openOrCloseBusinessHour, createCustomBusinessHour, getCustomBusinessHourById, getWorkHours, } from '../../../data/livechat/businessHours'; import { addOrRemoveAgentFromDepartment, archiveDepartment, createDepartmentWithAnOnlineAgent, createDepartmentWithAgent, disableDepartment, getDepartmentById, deleteDepartment, } from '../../../data/livechat/department'; import { createAgent, createManager, makeAgentAvailable } from '../../../data/livechat/rooms'; import { createAnOnlineAgent, removeAgent } from '../../../data/livechat/users'; import { removePermissionFromAllRoles, restorePermissionToRoles, updateSetting, updateEESetting } from '../../../data/permissions.helper'; import { password } from '../../../data/user'; import type { TestUser } from '../../../data/users.helper'; import { setUserActiveStatus, createUser, deleteUser, getMe, getUserByUsername, login } from '../../../data/users.helper'; import { IS_EE } from '../../../e2e/config/constants'; import { retry } from '../helpers/retry'; describe('LIVECHAT - business hours', () => { before((done) => getCredentials(done)); before(async () => { await updateSetting('Livechat_enabled', true); await updateSetting('Livechat_enable_business_hours', true); await updateSetting('Omnichannel_enable_department_removal', true); await createAgent(); }); after(async () => { await updateSetting('Omnichannel_enable_department_removal', false); }); let defaultBhId: any; describe('[CE] livechat/business-hour', () => { after(async () => { const { _updatedAt, ts, ...cleanedBusinessHour } = defaultBhId; await saveBusinessHour({ ...cleanedBusinessHour, timezone: 'America/Sao_Paulo', timezoneName: 'America/Sao_Paulo', workHours: getWorkHours(true), }); }); it('should fail when user doesnt have view-livechat-business-hours permission', async () => { await removePermissionFromAllRoles('view-livechat-business-hours'); const response = await request.get(api('livechat/business-hour')).set(credentials).expect(403); expect(response.body.success).to.be.false; await restorePermissionToRoles('view-livechat-business-hours'); }); it('should fail when business hour type is not a valid BH type', async () => { const response = await request.get(api('livechat/business-hour')).set(credentials).query({ type: 'invalid' }).expect(200); expect(response.body.success).to.be.true; expect(response.body.businessHour).to.be.null; }); it('should return a business hour of type default', async () => { const response = await request.get(api('livechat/business-hour')).set(credentials).query({ type: 'default' }).expect(200); expect(response.body.success).to.be.true; expect(response.body.businessHour).to.be.an('object'); expect(response.body.businessHour._id).to.be.a('string'); expect(response.body.businessHour.workHours).to.be.an('array'); expect(response.body.businessHour.workHours[0]).to.be.an('object'); expect(response.body.businessHour.workHours[0].day) .to.be.an('string') .that.is.oneOf(['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']); expect(response.body.businessHour.workHours[0].start).to.be.an('object'); expect(response.body.businessHour.workHours[0].finish).to.be.an('object'); expect(response.body.businessHour.workHours[0].open).to.be.a('boolean'); expect(response.body.businessHour.timezone).to.be.an('object').that.has.property('name').that.is.an('string'); defaultBhId = response.body.businessHour; }); it('should not allow a user to be available if BH are closed', async () => { const { _updatedAt, ts, ...cleanedBusinessHour } = defaultBhId; await saveBusinessHour({ ...cleanedBusinessHour, timezone: 'America/Sao_Paulo', timezoneName: 'America/Sao_Paulo', workHours: [ { day: 'Monday', open: true, start: '00:00', finish: '00:01', }, ], }); const { body } = await makeAgentAvailable(credentials); expect(body).to.have.property('success', false); expect(body.error).to.be.equal('error-business-hours-are-closed'); }); it('should allow a user to be available if BH are open', async () => { const { _updatedAt, ts, ...cleanedBusinessHour } = defaultBhId; await saveBusinessHour({ ...cleanedBusinessHour, timezone: 'America/Sao_Paulo', timezoneName: 'America/Sao_Paulo', workHours: getWorkHours(true), }); const { body } = await makeAgentAvailable(credentials); expect(body).to.have.property('success', true); }); it('should save a default business hour with proper timezone settings', async () => { const { _updatedAt, ts, ...cleanedBusinessHour } = defaultBhId; await saveBusinessHour({ ...cleanedBusinessHour, timezone: 'Asia/Kolkata', workHours: getWorkHours(true), timezoneName: 'Asia/Kolkata', }); const { body } = await request .get(api('livechat/business-hour')) .set(credentials) .query({ type: LivechatBusinessHourTypes.DEFAULT }) .expect(200); expect(body.success).to.be.true; expect(body.businessHour).to.be.an('object'); expect(body.businessHour.timezone).to.be.an('object').that.has.property('name').that.is.equal('Asia/Kolkata'); expect(body.businessHour.workHours).to.be.an('array').with.lengthOf(7); const { workHours } = body.businessHour; expect(workHours[0].day).to.be.equal('Sunday'); expect(workHours[0].start.utc.dayOfWeek).to.be.equal('Saturday'); expect(workHours[0].finish.utc.dayOfWeek).to.be.equal('Sunday'); }); it('should allow agents to be available', async () => { const { body } = await makeAgentAvailable(credentials); expect(body).to.have.property('success', true); }); }); (IS_EE ? describe : describe.skip)('[EE] livechat/business-hour', () => { it('should fail if user doesnt have view-livechat-business-hours permission', async () => { await removePermissionFromAllRoles('view-livechat-business-hours'); const response = await request.get(api('livechat/business-hours')).set(credentials).expect(403); expect(response.body.success).to.be.false; await restorePermissionToRoles('view-livechat-business-hours'); }); it('should return a list of business hours', async () => { const response = await request.get(api('livechat/business-hours')).set(credentials).expect(200); expect(response.body.success).to.be.true; expect(response.body.businessHours).to.be.an('array').with.lengthOf.greaterThan(0); expect(response.body.businessHours[0]).to.be.an('object'); expect(response.body.businessHours[0]._id).to.be.a('string'); expect(response.body.businessHours[0].workHours).to.be.an('array'); expect(response.body.businessHours[0].workHours[0]).to.be.an('object'); expect(response.body.businessHours[0].workHours[0].day) .to.be.an('string') .that.is.oneOf(['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']); expect(response.body.businessHours[0].workHours[0].start).to.be.an('object'); expect(response.body.businessHours[0].workHours[0].finish).to.be.an('object'); expect(response.body.businessHours[0].workHours[0].open).to.be.a('boolean'); expect(response.body.businessHours[0].timezone).to.be.an('object').that.has.property('name').that.is.an('string'); expect(response.body.businessHours[0].active).to.be.a('boolean'); }); it('should return a just created custom business hour', async () => { const name = `business-hour-${Date.now()}`; await updateSetting('Livechat_business_hour_type', LivechatBusinessHourBehaviors.MULTIPLE); await saveBusinessHour({ name, active: true, type: LivechatBusinessHourTypes.CUSTOM, workHours: [{ day: 'Monday', start: '08:00', finish: '18:00', open: true }], daysOpen: ['Monday'], daysTime: [{ day: 'Monday', start: { time: '08:00' }, finish: { time: '18:00' }, open: true }], departmentsToApplyBusinessHour: '', timezone: 'America/Sao_Paulo', timezoneName: 'America/Sao_Paulo', }); const { body } = await request.get(api('livechat/business-hours')).set(credentials).query({ name }).expect(200); expect(body.success).to.be.true; expect(body.businessHours).to.be.an('array').with.lengthOf(1); expect(body.businessHours[0]).to.be.an('object'); expect(body.businessHours[0]._id).to.be.a('string'); expect(body.businessHours[0]).to.have.property('name', name); expect(body.businessHours[0]).to.have.property('active', true); expect(body.businessHours[0]).to.have.property('type', LivechatBusinessHourTypes.CUSTOM); expect(body.businessHours[0]).to.have.property('workHours').that.is.an('array').with.lengthOf(1); expect(body.businessHours[0].workHours[0]).to.be.an('object').with.property('day', 'Monday'); expect(body.businessHours[0].workHours[0]).to.have.property('start').that.is.an('object'); expect(body.businessHours[0].workHours[0]).to.have.property('finish').that.is.an('object'); expect(body.businessHours[0]).to.have.property('timezone').that.is.an('object').with.property('name', 'America/Sao_Paulo'); }); it('should fail if start and finish time are the same', async () => { const name = `business-hour-${Date.now()}`; await updateSetting('Livechat_business_hour_type', LivechatBusinessHourBehaviors.MULTIPLE); const result = await saveBusinessHour({ name, active: true, type: LivechatBusinessHourTypes.CUSTOM, workHours: [{ day: 'Monday', open: true, start: '08:00', finish: '08:00' }], daysOpen: ['Monday'], daysTime: [{ day: 'Monday', start: { time: '08:00' }, finish: { time: '08:00' }, open: true }], timezone: 'America/Sao_Paulo', departmentsToApplyBusinessHour: '', timezoneName: 'America/Sao_Paulo', }); expect(result).to.have.property('error'); }); it('should fail if finish is before start time', async () => { const name = `business-hour-${Date.now()}`; await updateSetting('Livechat_business_hour_type', LivechatBusinessHourBehaviors.MULTIPLE); const result = await saveBusinessHour({ name, active: true, type: LivechatBusinessHourTypes.CUSTOM, workHours: [{ day: 'Monday', open: true, start: '10:00', finish: '08:00' }], daysOpen: ['Monday'], daysTime: [{ day: 'Monday', start: { time: '10:00' }, finish: { time: '08:00' }, open: true }], timezone: 'America/Sao_Paulo', departmentsToApplyBusinessHour: '', timezoneName: 'America/Sao_Paulo', }); expect(result).to.have.property('error'); }); it('should fail if data is invalid', async () => { const name = `business-hour-${Date.now()}`; await updateSetting('Livechat_business_hour_type', LivechatBusinessHourBehaviors.MULTIPLE); const result = await saveBusinessHour({ name, active: true, type: LivechatBusinessHourTypes.CUSTOM, workHours: [{ day: 'Monday', open: true, start: '20000', finish: 'xxxxx' }], daysOpen: ['Monday'], daysTime: [{ day: 'Monday', start: { time: '20000' }, finish: { time: 'xxxxx' }, open: true }], timezone: 'America/Sao_Paulo', departmentsToApplyBusinessHour: '', timezoneName: 'America/Sao_Paulo', }); expect(result).to.have.property('error'); }); }); (IS_EE ? describe : describe.skip)('[EE][BH] On Business Hour created', () => { let defaultBusinessHour: ILivechatBusinessHour; before(async () => { await updateSetting('Livechat_business_hour_type', LivechatBusinessHourBehaviors.MULTIPLE); // wait for the callbacks to be registered await sleep(1000); // cleanup any existing business hours await removeAllCustomBusinessHours(); // get default business hour defaultBusinessHour = await getDefaultBusinessHour(); // close default business hour await openOrCloseBusinessHour(defaultBusinessHour, true); }); it('should create a custom business hour which is closed by default', async () => { // create custom business hour and link it to a department const { department, agent } = await createDepartmentWithAnOnlineAgent(); await createCustomBusinessHour([department._id], false); const latestAgent = await getMe(agent.credentials); expect(latestAgent).to.be.an('object'); expect(latestAgent.openBusinessHours).to.be.an('array').of.length(0); expect(latestAgent.statusLivechat).to.be.equal(ILivechatAgentStatus.NOT_AVAILABLE); }); it('should create a custom business hour which is closed by default, but a bot agent shouldnt be affected', async () => { const bot = await createUser({ roles: ['bot', 'livechat-agent'] }); const creds = await login(bot.username, password); await makeAgentAvailable(creds); const { department } = await createDepartmentWithAgent({ user: bot, credentials: creds }); await createCustomBusinessHour([department._id], false); const latestAgent: ILivechatAgent = await getMe(creds); expect(latestAgent).to.be.an('object'); expect(latestAgent.openBusinessHours).to.be.an('array').of.length(0); expect(latestAgent.statusLivechat).to.be.equal(ILivechatAgentStatus.AVAILABLE); }); }); // Scenario: A BH is linked to departments and gets re-saved with a smaller (or empty) department list // Expected result: the removed departments lose the link (and their agents lose the BH), // while the remaining departments stay linked (IS_EE ? describe : describe.skip)('[EE][BH] On Business Hour departments removed via save', () => { before(async () => { await updateSetting('Livechat_business_hour_type', LivechatBusinessHourBehaviors.MULTIPLE); // wait for the callbacks to be registered await sleep(1000); }); const createdDepartments: Awaited>[] = []; let keptDepartmentWithAgent: Awaited>; let removedDepartmentWithAgent: Awaited>; beforeEach(async () => { await removeAllCustomBusinessHours(); await openOrCloseBusinessHour(await getDefaultBusinessHour(), false); keptDepartmentWithAgent = await createDepartmentWithAnOnlineAgent(); removedDepartmentWithAgent = await createDepartmentWithAnOnlineAgent(); createdDepartments.push(keptDepartmentWithAgent, removedDepartmentWithAgent); }); after(async () => { await Promise.all(createdDepartments.map(({ department }) => deleteDepartment(department._id))); await Promise.all(createdDepartments.map(({ agent }) => deleteUser(agent.user))); }); const resaveWithDepartments = async (businessHour: ILivechatBusinessHour, departmentsToApplyBusinessHour: string) => { const { _updatedAt, ts, departments, ...cleanedBusinessHour } = businessHour; await saveBusinessHour({ ...cleanedBusinessHour, timezoneName: businessHour.timezone.name, timezone: businessHour.timezone.name, workHours: getWorkHours(true), daysOpen: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'], departmentsToApplyBusinessHour, } as any); }; it('should unlink only the removed departments when re-saving with a smaller department list', async () => { const { department: keptDepartment, agent: keptAgent } = keptDepartmentWithAgent; const { department: removedDepartment, agent: removedAgent } = removedDepartmentWithAgent; const customBusinessHour = await createCustomBusinessHour([keptDepartment._id, removedDepartment._id]); await openOrCloseBusinessHour(customBusinessHour, true); await resaveWithDepartments(customBusinessHour, keptDepartment._id); const latestKeptDepartment = await getDepartmentById(keptDepartment._id); expect(latestKeptDepartment.businessHourId).to.be.equal(customBusinessHour._id); const latestRemovedDepartment = await getDepartmentById(removedDepartment._id); expect(latestRemovedDepartment.businessHourId).to.be.undefined; const latestKeptAgent = await getMe(keptAgent.credentials); expect(latestKeptAgent.openBusinessHours).to.be.an('array').that.includes(customBusinessHour._id); const latestRemovedAgent = await getMe(removedAgent.credentials); expect(latestRemovedAgent.openBusinessHours).to.be.an('array').that.does.not.include(customBusinessHour._id); }); it('should unlink all departments when re-saving with an empty department list', async () => { const { department } = keptDepartmentWithAgent; const customBusinessHour = await createCustomBusinessHour([department._id]); await openOrCloseBusinessHour(customBusinessHour, true); await resaveWithDepartments(customBusinessHour, ''); const latestDepartment = await getDepartmentById(department._id); expect(latestDepartment.businessHourId).to.be.undefined; }); }); // Scenario: Assume we have a BH linked to a department, and we archive the department // Expected result: // 1) If BH is open and only linked to that department, it should be closed // 2) If BH is open and linked to other departments, it should remain open // 3) Agents within the archived department should be assigned to default BH // 3.1) We'll also need to handle the case where if an agent is assigned to "dep1" // and "dep2" and both these depts are connected to same BH, then in this case after // archiving "dep1", we'd still need to BH within this user's cache since he's part of // "dep2" which is linked to BH (IS_EE ? describe : describe.skip)('[EE][BH] On Department archived', () => { let defaultBusinessHour: ILivechatBusinessHour; let customBusinessHour: ILivechatBusinessHour; let deptLinkedToCustomBH: ILivechatDepartment; let agentLinkedToDept: Awaited>['agent']; before(async () => { await updateSetting('Livechat_business_hour_type', LivechatBusinessHourBehaviors.MULTIPLE); // wait for the callbacks to be registered await sleep(1000); }); beforeEach(async () => { // cleanup any existing business hours await removeAllCustomBusinessHours(); // get default business hour defaultBusinessHour = await getDefaultBusinessHour(); // close default business hour await openOrCloseBusinessHour(defaultBusinessHour, false); // create custom business hour and link it to a department const { department, agent } = await createDepartmentWithAnOnlineAgent(); customBusinessHour = await createCustomBusinessHour([department._id]); agentLinkedToDept = agent; deptLinkedToCustomBH = department; // open custom business hour await openOrCloseBusinessHour(customBusinessHour, true); }); it('upon archiving a department, if BH is open and only linked to that department, it should be closed', async () => { // archive department await archiveDepartment(deptLinkedToCustomBH._id); // verify if department is archived and BH link is removed const department = await getDepartmentById(deptLinkedToCustomBH._id); expect(department).to.be.an('object'); expect(department).to.have.property('archived', true); expect(department.businessHourId).to.be.undefined; // verify if BH is closed const latestCustomBH = await getCustomBusinessHourById(customBusinessHour._id); expect(latestCustomBH).to.be.an('object'); expect(latestCustomBH).to.have.property('active', false); expect(latestCustomBH.departments).to.be.an('array').that.is.empty; }); it('upon archiving a department, if BH is open and linked to other departments, it should remain open', async () => { // create another department and link it to the same BH const { department, agent } = await createDepartmentWithAnOnlineAgent(); await removeAllCustomBusinessHours(); customBusinessHour = await createCustomBusinessHour([deptLinkedToCustomBH._id, department._id]); // archive department await archiveDepartment(deptLinkedToCustomBH._id); // verify if department is archived and BH link is removed const archivedDepartment = await getDepartmentById(deptLinkedToCustomBH._id); expect(archivedDepartment).to.be.an('object'); expect(archivedDepartment).to.have.property('archived', true); expect(archivedDepartment.businessHourId).to.be.undefined; // verify if other department is not archived and BH link is not removed const otherDepartment = await getDepartmentById(department._id); expect(otherDepartment).to.be.an('object'); expect(otherDepartment.businessHourId).to.be.equal(customBusinessHour._id); // verify if BH is still open const latestCustomBH = await getCustomBusinessHourById(customBusinessHour._id); expect(latestCustomBH).to.be.an('object'); expect(latestCustomBH).to.have.property('active', true); expect(latestCustomBH.departments).to.be.an('array').of.length(1); expect(latestCustomBH?.departments?.[0]._id).to.be.equal(department._id); // cleanup await deleteDepartment(department._id); await deleteUser(agent.user); }); it('upon archiving a department, agents within the archived department should be assigned to default BH', async () => { await openOrCloseBusinessHour(defaultBusinessHour, true); // archive department await archiveDepartment(deptLinkedToCustomBH._id); const latestAgent: ILivechatAgent = await getMe(agentLinkedToDept.credentials); expect(latestAgent).to.be.an('object'); expect(latestAgent.openBusinessHours).to.be.an('array').of.length(1); expect(latestAgent?.openBusinessHours?.[0]).to.be.equal(defaultBusinessHour._id); }); it('upon archiving a department, overlapping agents should still have BH within their cache', async () => { // create another department and link it to the same BH const { department, agent } = await createDepartmentWithAnOnlineAgent(); await removeAllCustomBusinessHours(); customBusinessHour = await createCustomBusinessHour([deptLinkedToCustomBH._id, department._id]); // create overlapping agent by adding previous agent to newly created department await addOrRemoveAgentFromDepartment( department._id, { agentId: agentLinkedToDept.user._id, username: agentLinkedToDept.user.username || '', }, true, ); // archive department await archiveDepartment(deptLinkedToCustomBH._id); // verify if department is archived and BH link is removed const archivedDepartment = await getDepartmentById(deptLinkedToCustomBH._id); expect(archivedDepartment).to.be.an('object'); expect(archivedDepartment).to.have.property('archived', true); expect(archivedDepartment.businessHourId).to.be.undefined; // verify if BH is still open const latestCustomBH = await getCustomBusinessHourById(customBusinessHour._id); expect(latestCustomBH).to.be.an('object'); expect(latestCustomBH).to.have.property('active', true); expect(latestCustomBH.departments).to.be.an('array').of.length(1); expect(latestCustomBH?.departments?.[0]?._id).to.be.equal(department._id); // verify if overlapping agent still has BH within his cache const latestAgent: ILivechatAgent = await getMe(agentLinkedToDept.credentials); expect(latestAgent).to.be.an('object'); expect(latestAgent.openBusinessHours).to.be.an('array').of.length(1); expect(latestAgent?.openBusinessHours?.[0]).to.be.equal(customBusinessHour._id); // verify if other agent still has BH within his cache const otherAgent: ILivechatAgent = await getMe(agent.credentials); expect(otherAgent).to.be.an('object'); expect(otherAgent.openBusinessHours).to.be.an('array').of.length(1); expect(otherAgent?.openBusinessHours?.[0]).to.be.equal(customBusinessHour._id); // cleanup await deleteDepartment(department._id); await deleteUser(agent.user); }); afterEach(async () => { await deleteDepartment(deptLinkedToCustomBH._id); await deleteUser(agentLinkedToDept.user); }); }); (IS_EE ? describe : describe.skip)('[EE][BH] On Department disabled', () => { let defaultBusinessHour: ILivechatBusinessHour; let customBusinessHour: ILivechatBusinessHour; let deptLinkedToCustomBH: ILivechatDepartment; let agentLinkedToDept: Awaited>['agent']; before(async () => { await updateSetting('Livechat_business_hour_type', LivechatBusinessHourBehaviors.MULTIPLE); // wait for the callbacks to be registered await sleep(1000); }); beforeEach(async () => { // cleanup any existing business hours await removeAllCustomBusinessHours(); // get default business hour defaultBusinessHour = await getDefaultBusinessHour(); // close default business hour await openOrCloseBusinessHour(defaultBusinessHour, false); // create custom business hour and link it to a department const { department, agent } = await createDepartmentWithAnOnlineAgent(); customBusinessHour = await createCustomBusinessHour([department._id]); agentLinkedToDept = agent; deptLinkedToCustomBH = department; // open custom business hour await openOrCloseBusinessHour(customBusinessHour, true); }); it('upon disabling a department, if BH is open and only linked to that department, it should be closed', async () => { // disable department await disableDepartment(deptLinkedToCustomBH); // verify if BH link is removed const department = await getDepartmentById(deptLinkedToCustomBH._id); expect(department).to.be.an('object'); expect(department.businessHourId).to.be.undefined; // verify if BH is closed const latestCustomBH = await getCustomBusinessHourById(customBusinessHour._id); expect(latestCustomBH).to.be.an('object'); expect(latestCustomBH.active).to.be.false; expect(latestCustomBH.departments).to.be.an('array').that.is.empty; }); it('upon disabling a department, if BH is open and linked to other departments, it should remain open', async () => { // create another department and link it to the same BH const { department, agent } = await createDepartmentWithAnOnlineAgent(); await removeAllCustomBusinessHours(); customBusinessHour = await createCustomBusinessHour([deptLinkedToCustomBH._id, department._id]); // disable department await disableDepartment(deptLinkedToCustomBH); // verify if BH link is removed const disabledDepartment = await getDepartmentById(deptLinkedToCustomBH._id); expect(disabledDepartment).to.be.an('object'); expect(disabledDepartment.businessHourId).to.be.undefined; // verify if other department BH link is not removed const otherDepartment = await getDepartmentById(department._id); expect(otherDepartment).to.be.an('object'); expect(otherDepartment.businessHourId).to.be.equal(customBusinessHour._id); // verify if BH is still open const latestCustomBH = await getCustomBusinessHourById(customBusinessHour._id); expect(latestCustomBH).to.be.an('object'); expect(latestCustomBH).to.have.property('active', true); expect(latestCustomBH.departments).to.be.an('array').of.length(1); expect(latestCustomBH?.departments?.[0]._id).to.be.equal(department._id); // cleanup await deleteDepartment(department._id); await deleteUser(agent.user); }); it('upon disabling a department, agents within the disabled department should be assigned to default BH', async () => { await openOrCloseBusinessHour(defaultBusinessHour, true); // disable department await disableDepartment(deptLinkedToCustomBH); const latestAgent: ILivechatAgent = await getMe(agentLinkedToDept.credentials); expect(latestAgent).to.be.an('object'); expect(latestAgent.openBusinessHours).to.be.an('array').of.length(1); expect(latestAgent?.openBusinessHours?.[0]).to.be.equal(defaultBusinessHour._id); }); it('upon disabling a department, overlapping agents should still have BH within their cache', async () => { // create another department and link it to the same BH const { department, agent } = await createDepartmentWithAnOnlineAgent(); await removeAllCustomBusinessHours(); customBusinessHour = await createCustomBusinessHour([deptLinkedToCustomBH._id, department._id]); // create overlapping agent by adding previous agent to newly created department await addOrRemoveAgentFromDepartment( department._id, { agentId: agentLinkedToDept.user._id, username: agentLinkedToDept.user.username || '', }, true, ); // disable department await disableDepartment(deptLinkedToCustomBH); // verify if BH link is removed const disabledDepartment = await getDepartmentById(deptLinkedToCustomBH._id); expect(disabledDepartment).to.be.an('object'); expect(disabledDepartment.businessHourId).to.be.undefined; // verify if BH is still open const latestCustomBH = await getCustomBusinessHourById(customBusinessHour._id); expect(latestCustomBH).to.be.an('object'); expect(latestCustomBH).to.have.property('active', true); expect(latestCustomBH.departments).to.be.an('array').of.length(1); expect(latestCustomBH?.departments?.[0]?._id).to.be.equal(department._id); // verify if overlapping agent still has BH within his cache const latestAgent: ILivechatAgent = await getMe(agentLinkedToDept.credentials); expect(latestAgent).to.be.an('object'); expect(latestAgent.openBusinessHours).to.be.an('array').of.length(1); expect(latestAgent?.openBusinessHours?.[0]).to.be.equal(customBusinessHour._id); // verify if other agent still has BH within his cache const otherAgent: ILivechatAgent = await getMe(agent.credentials); expect(otherAgent).to.be.an('object'); expect(otherAgent.openBusinessHours).to.be.an('array').of.length(1); expect(otherAgent?.openBusinessHours?.[0]).to.be.equal(customBusinessHour._id); // cleanup await deleteDepartment(department._id); await deleteUser(agent.user); }); afterEach(async () => { await deleteDepartment(deptLinkedToCustomBH._id); await deleteUser(agentLinkedToDept.user); }); }); (IS_EE ? describe : describe.skip)('[EE][BH] On Department removed', () => { let defaultBusinessHour: ILivechatBusinessHour; let customBusinessHour: ILivechatBusinessHour; let deptLinkedToCustomBH: ILivechatDepartment; let agentLinkedToDept: Awaited>['agent']; before(async () => { await updateSetting('Livechat_business_hour_type', LivechatBusinessHourBehaviors.MULTIPLE); // wait for the callbacks to be registered await sleep(1000); }); beforeEach(async () => { // cleanup any existing business hours await removeAllCustomBusinessHours(); // get default business hour defaultBusinessHour = await getDefaultBusinessHour(); // close default business hour await openOrCloseBusinessHour(defaultBusinessHour, false); // create custom business hour and link it to a department const { department, agent } = await createDepartmentWithAnOnlineAgent(); customBusinessHour = await createCustomBusinessHour([department._id]); agentLinkedToDept = agent; deptLinkedToCustomBH = department; // open custom business hour await openOrCloseBusinessHour(customBusinessHour, true); }); it('upon deleting a department, if BH is open and only linked to that department, it should be closed', async () => { await deleteDepartment(deptLinkedToCustomBH._id); // verify if BH is closed const latestCustomBH = await getCustomBusinessHourById(customBusinessHour._id); expect(latestCustomBH).to.be.an('object'); expect(latestCustomBH.active).to.be.false; expect(latestCustomBH.departments).to.be.an('array').that.is.empty; }); it('upon deleting a department, if BH is open and linked to other departments, it should remain open', async () => { // create another department and link it to the same BH const { department, agent } = await createDepartmentWithAnOnlineAgent(); await removeAllCustomBusinessHours(); customBusinessHour = await createCustomBusinessHour([deptLinkedToCustomBH._id, department._id]); await deleteDepartment(deptLinkedToCustomBH._id); // verify if other department BH link is not removed const otherDepartment = await getDepartmentById(department._id); expect(otherDepartment).to.be.an('object'); expect(otherDepartment.businessHourId).to.be.equal(customBusinessHour._id); // verify if BH is still open const latestCustomBH = await getCustomBusinessHourById(customBusinessHour._id); expect(latestCustomBH).to.be.an('object'); expect(latestCustomBH).to.have.property('active', true); expect(latestCustomBH.departments).to.be.an('array').of.length(1); expect(latestCustomBH?.departments?.[0]._id).to.be.equal(department._id); // cleanup await deleteDepartment(department._id); await deleteUser(agent.user); }); it('upon deleting a department, agents within the disabled department should be assigned to default BH', async () => { await openOrCloseBusinessHour(defaultBusinessHour, true); await deleteDepartment(deptLinkedToCustomBH._id); const latestAgent: ILivechatAgent = await getMe(agentLinkedToDept.credentials); expect(latestAgent).to.be.an('object'); expect(latestAgent.openBusinessHours).to.be.an('array').of.length(1); expect(latestAgent?.openBusinessHours?.[0]).to.be.equal(defaultBusinessHour._id); }); it('upon deleting a department, overlapping agents should still have BH within their cache', async () => { // create another department and link it to the same BH const { department, agent } = await createDepartmentWithAnOnlineAgent(); await removeAllCustomBusinessHours(); customBusinessHour = await createCustomBusinessHour([deptLinkedToCustomBH._id, department._id]); // create overlapping agent by adding previous agent to newly created department await addOrRemoveAgentFromDepartment( department._id, { agentId: agentLinkedToDept.user._id, username: agentLinkedToDept.user.username || '', }, true, ); await deleteDepartment(deptLinkedToCustomBH._id); // verify if BH is still open const latestCustomBH = await getCustomBusinessHourById(customBusinessHour._id); expect(latestCustomBH).to.be.an('object'); expect(latestCustomBH).to.have.property('active', true); expect(latestCustomBH.departments).to.be.an('array').of.length(1); expect(latestCustomBH?.departments?.[0]?._id).to.be.equal(department._id); // verify if overlapping agent still has BH within his cache const latestAgent: ILivechatAgent = await getMe(agentLinkedToDept.credentials); expect(latestAgent).to.be.an('object'); expect(latestAgent.openBusinessHours).to.be.an('array').of.length(1); expect(latestAgent?.openBusinessHours?.[0]).to.be.equal(customBusinessHour._id); // verify if other agent still has BH within his cache const otherAgent: ILivechatAgent = await getMe(agent.credentials); expect(otherAgent).to.be.an('object'); expect(otherAgent.openBusinessHours).to.be.an('array').of.length(1); expect(otherAgent?.openBusinessHours?.[0]).to.be.equal(customBusinessHour._id); // cleanup await deleteDepartment(department._id); await deleteUser(agent.user); }); afterEach(async () => { await deleteUser(agentLinkedToDept.user); }); }); (IS_EE ? describe : describe.skip)('[EE][BH] On agent removed from department', () => { let defaultBusinessHour: ILivechatBusinessHour; let customBusinessHour: ILivechatBusinessHour; let deptLinkedToCustomBH: ILivechatDepartment; let agentLinkedToDept: Awaited>['agent']; // the business hour recomputation runs on a fire-and-forget callback, so poll until it lands const waitForAgentBusinessHours = async (agentCredentials: Credentials, expected: string[]): Promise => { const matches = (agent: ILivechatAgent) => JSON.stringify([...(agent.openBusinessHours || [])].sort()) === JSON.stringify([...expected].sort()); let latestAgent: ILivechatAgent = await getMe(agentCredentials); for (let attempts = 0; attempts < 10 && !matches(latestAgent); attempts++) { await sleep(500); latestAgent = await getMe(agentCredentials); } return latestAgent.openBusinessHours; }; before(async () => { await updateSetting('Livechat_business_hour_type', LivechatBusinessHourBehaviors.MULTIPLE); // wait for the callbacks to be registered await sleep(1000); }); beforeEach(async () => { await removeAllCustomBusinessHours(); defaultBusinessHour = await getDefaultBusinessHour(); await openOrCloseBusinessHour(defaultBusinessHour, true); const { department, agent } = await createDepartmentWithAnOnlineAgent(); deptLinkedToCustomBH = department; agentLinkedToDept = agent; customBusinessHour = await createCustomBusinessHour([department._id]); await openOrCloseBusinessHour(customBusinessHour, true); }); it('upon removing an agent from its only department, the agent should fall back to the default business hour', async () => { const openBusinessHoursBefore = await waitForAgentBusinessHours(agentLinkedToDept.credentials, [customBusinessHour._id]); expect(openBusinessHoursBefore).to.be.an('array').of.length(1); expect(openBusinessHoursBefore?.[0]).to.be.equal(customBusinessHour._id); await addOrRemoveAgentFromDepartment( deptLinkedToCustomBH._id, { agentId: agentLinkedToDept.user._id, username: agentLinkedToDept.user.username || '', }, false, ); const openBusinessHours = await waitForAgentBusinessHours(agentLinkedToDept.credentials, [defaultBusinessHour._id]); expect(openBusinessHours).to.be.an('array').of.length(1); expect(openBusinessHours?.[0]).to.be.equal(defaultBusinessHour._id); }); it('upon removing an agent from one department, the agent should keep the business hour shared with its other department', async () => { // link a second department (with its own agent) to the same business hour and make the first agent overlap const { department, agent } = await createDepartmentWithAnOnlineAgent(); await removeAllCustomBusinessHours(); customBusinessHour = await createCustomBusinessHour([deptLinkedToCustomBH._id, department._id]); await openOrCloseBusinessHour(customBusinessHour, true); await addOrRemoveAgentFromDepartment( department._id, { agentId: agentLinkedToDept.user._id, username: agentLinkedToDept.user.username || '', }, true, ); // control agent: belongs only to the first department and is removed in the same request as the // overlapping agent, so both are processed by the same recomputation callback. Observing the control // agent fall back to the default business hour proves that callback ran — only then is asserting the // overlapping agent's unchanged state meaningful (it equals the pre-removal state, so it can't // distinguish "correctly kept" from "callback never ran" on its own) const controlAgent = await createAnOnlineAgent(); await addOrRemoveAgentFromDepartment( deptLinkedToCustomBH._id, { agentId: controlAgent.user._id, username: controlAgent.user.username || '', }, true, ); const overlappingBefore = await waitForAgentBusinessHours(agentLinkedToDept.credentials, [customBusinessHour._id]); expect(overlappingBefore).to.be.an('array').of.length(1); const controlBefore = await waitForAgentBusinessHours(controlAgent.credentials, [customBusinessHour._id]); expect(controlBefore).to.be.an('array').of.length(1); await request .post(api(`livechat/department/${deptLinkedToCustomBH._id}/agents`)) .set(credentials) .send({ upsert: [], remove: [ { agentId: agentLinkedToDept.user._id, username: agentLinkedToDept.user.username || '' }, { agentId: controlAgent.user._id, username: controlAgent.user.username || '' }, ], }) .expect(200); const controlOpenBusinessHours = await waitForAgentBusinessHours(controlAgent.credentials, [defaultBusinessHour._id]); expect(controlOpenBusinessHours).to.be.an('array').of.length(1); expect(controlOpenBusinessHours?.[0]).to.be.equal(defaultBusinessHour._id); const latestAgent: ILivechatAgent = await getMe(agentLinkedToDept.credentials); expect(latestAgent.openBusinessHours).to.be.an('array').of.length(1); expect(latestAgent?.openBusinessHours?.[0]).to.be.equal(customBusinessHour._id); await deleteDepartment(department._id); await deleteUser(agent.user); await deleteUser(controlAgent.user); }); afterEach(async () => { await deleteDepartment(deptLinkedToCustomBH._id); await deleteUser(agentLinkedToDept.user); }); after(async () => { await removeAllCustomBusinessHours(); }); }); describe('[CE][BH] On Agent created/removed', () => { let defaultBH: ILivechatBusinessHour; let agent: ILivechatAgent; let agentCredentials: Credentials; before(async () => { await updateSetting('Livechat_enable_business_hours', true); await updateEESetting('Livechat_business_hour_type', LivechatBusinessHourBehaviors.SINGLE); // wait for callbacks to run await sleep(2000); defaultBH = await getDefaultBusinessHour(); await openOrCloseBusinessHour(defaultBH, true); agent = await createUser(); agentCredentials = await login(agent.username, password); }); after(async () => { await deleteUser(agent); }); it('should create a new agent and verify if it is assigned to the default business hour which is open', async () => { agent = await createAgent(agent.username); // the BH assignment runs on a fire-and-forget callback, so the create-agent request returns before it lands await retry( 'agent BH assignment is async, so openBusinessHours may still be unset on the first fetch', async () => { const latestAgent: ILivechatAgent = await getMe(agentCredentials); expect(latestAgent).to.be.an('object'); expect(latestAgent.openBusinessHours).to.be.an('array').of.length(1); expect(latestAgent?.openBusinessHours?.[0]).to.be.equal(defaultBH._id); }, { retries: 20, delayMs: 250 }, ); }); it('should create a new agent and verify if it is assigned to the default business hour which is closed', async () => { await openOrCloseBusinessHour(defaultBH, false); // the BH close recalculates agent statuses asynchronously; wait for it to propagate (existing agent loses the // BH assignment) before creating the new agent, otherwise it is created while the BH still counts as open await retry( 'BH close propagation is async, so the existing agent may still be assigned on the first fetch', async () => { const current: ILivechatAgent = await getMe(agentCredentials); expect(current.openBusinessHours ?? []).to.have.lengthOf(0); }, { retries: 20, delayMs: 250 }, ); const newUser: ILivechatAgent = await createUser(); const newUserCredentials = await login(newUser.username, password); await createAgent(newUser.username); // createAgent sets the agent as available first and the async callback moves it to not-available, so the // first fetch can still show the intermediate available state await retry( 'agent BH assignment is async, so statusLivechat may still be available on the first fetch', async () => { const latestAgent: ILivechatAgent = await getMe(newUserCredentials); expect(latestAgent).to.be.an('object'); expect(latestAgent.openBusinessHours).to.be.undefined; expect(latestAgent.statusLivechat).to.be.equal(ILivechatAgentStatus.NOT_AVAILABLE); }, { retries: 20, delayMs: 250 }, ); // cleanup await deleteUser(newUser); }); it('should verify if agent is assigned to BH when it is opened', async () => { // first verify if agent is not assigned to any BH let latestAgent: ILivechatAgent = await getMe(agentCredentials); expect(latestAgent).to.be.an('object'); expect(latestAgent.openBusinessHours).to.be.an('array').of.length(0); expect(latestAgent.statusLivechat).to.be.equal(ILivechatAgentStatus.NOT_AVAILABLE); // now open BH await openOrCloseBusinessHour(defaultBH, true); // verify if agent is assigned to BH latestAgent = await getMe(agentCredentials); expect(latestAgent).to.be.an('object'); expect(latestAgent.openBusinessHours).to.be.an('array').of.length(1); expect(latestAgent?.openBusinessHours?.[0]).to.be.equal(defaultBH._id); // verify if agent is able to make themselves available await makeAgentAvailable(agentCredentials as any); }); it('should verify if BH related props are cleared when an agent is deleted', async () => { await removeAgent(agent._id); const latestAgent: ILivechatAgent = await getMe(agentCredentials); expect(latestAgent).to.be.an('object'); expect(latestAgent.openBusinessHours).to.be.undefined; expect(latestAgent.statusLivechat).to.be.undefined; }); describe('Special Case - Agent created, BH already enabled', () => { let newAgent: ILivechatAgent; let newAgentCredentials: Credentials; before(async () => { newAgent = await createUser({ roles: ['user', 'livechat-agent'] }); newAgentCredentials = await login(newAgent.username, password); }); after(async () => { await deleteUser(newAgent); }); it('should verify a newly created agent to be assigned to the default business hour', async () => { const latestAgent: ILivechatAgent = await getMe(newAgentCredentials); expect(latestAgent).to.be.an('object'); expect(latestAgent.openBusinessHours).to.be.an('array').of.length(1); expect(latestAgent?.openBusinessHours?.[0]).to.be.equal(defaultBH._id); }); }); after(async () => { await deleteUser(agent); }); }); describe('[CE][BH] On Agent deactivated/activated', () => { let defaultBH: ILivechatBusinessHour; let agent: TestUser; before(async () => { await updateSetting('Livechat_enable_business_hours', true); await updateEESetting('Livechat_business_hour_type', LivechatBusinessHourBehaviors.SINGLE); // wait for callbacks to run await sleep(2000); defaultBH = await getDefaultBusinessHour(); await openOrCloseBusinessHour(defaultBH, true); agent = await createUser(); await createAgent(agent.username); }); after(async () => { await deleteUser(agent); await updateSetting('Livechat_enable_business_hours', false); }); it('should verify if agent becomes unavailable to take chats when user is deactivated', async () => { await makeAgentAvailable(await login(agent.username, password)); await setUserActiveStatus(agent._id, false); const latestAgent = await getUserByUsername(agent.username); expect(latestAgent).to.be.an('object'); expect(latestAgent.statusLivechat).to.be.equal(ILivechatAgentStatus.NOT_AVAILABLE); }); it('should verify if agent becomes available to take chats when user is activated, if business hour is active', async () => { await openOrCloseBusinessHour(defaultBH, true); await setUserActiveStatus(agent._id, true); const latestAgent = await getUserByUsername(agent.username); expect(latestAgent).to.be.an('object'); expect(latestAgent.statusLivechat).to.be.equal(ILivechatAgentStatus.AVAILABLE); }); it('should verify if agent becomes unavailable to take chats when user is activated, if business hour is inactive', async () => { await openOrCloseBusinessHour(defaultBH, false); await setUserActiveStatus(agent._id, false); await setUserActiveStatus(agent._id, true); const latestAgent = await getUserByUsername(agent.username); expect(latestAgent).to.be.an('object'); expect(latestAgent.statusLivechat).to.be.equal(ILivechatAgentStatus.NOT_AVAILABLE); }); it('should verify if managers are not able to make deactivated agents available', async () => { await createManager(); await setUserActiveStatus(agent._id, false); const response = await request .post(api('livechat/agent.status')) .set(credentials) .send({ status: 'available', agentId: agent._id, }) .expect(400); expect(response.body).to.have.property('success', false); expect(response.body).to.have.property('error', 'error-user-deactivated'); }); }); });