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,209 @@
import type { ILivechatBusinessHour } from '@rocket.chat/core-typings';
import { LivechatBusinessHourTypes } from '@rocket.chat/core-typings';
import type { POSTLivechatBusinessHoursSaveParams } from '@rocket.chat/rest-typings';
import moment from 'moment';
import { api, credentials, request } from '../api-data';
import { updateEESetting, updateSetting } from '../permissions.helper';
type ISaveBhApiWorkHour = Omit<ILivechatBusinessHour, '_id' | 'ts' | 'timezone'> & {
workHours: { day: string; start: string; finish: string; open: boolean }[];
} & { departmentsToApplyBusinessHour?: string } & { timezoneName: string };
export const saveBusinessHour = async (businessHour: POSTLivechatBusinessHoursSaveParams) => {
const { body } = await request.post(api('livechat/business-hours.save')).set(credentials).send(businessHour);
return body;
};
export const createCustomBusinessHour = async (departments: string[], open = true): Promise<ILivechatBusinessHour> => {
const name = `business-hour-${Date.now()}`;
const businessHour: POSTLivechatBusinessHoursSaveParams = {
name,
active: true,
type: LivechatBusinessHourTypes.CUSTOM,
workHours: getWorkHours(open),
timezoneName: 'Asia/Calcutta',
timezone: 'Asia/Calcutta',
departmentsToApplyBusinessHour: '',
daysOpen: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'],
daysTime: [
{ day: 'Monday', start: { time: '08:00' }, finish: { time: '18:00' }, open },
{ day: 'Tuesday', start: { time: '08:00' }, finish: { time: '18:00' }, open },
{ day: 'Wednesday', start: { time: '08:00' }, finish: { time: '18:00' }, open },
{ day: 'Thursday', start: { time: '08:00' }, finish: { time: '18:00' }, open },
{ day: 'Friday', start: { time: '08:00' }, finish: { time: '18:00' }, open },
],
};
if (departments.length) {
businessHour.departmentsToApplyBusinessHour = departments.join(',');
}
await saveBusinessHour(businessHour);
const existingBusinessHours: ILivechatBusinessHour[] = await getAllCustomBusinessHours();
const createdBusinessHour = existingBusinessHours.find((bh) => bh.name === name);
if (!createdBusinessHour) {
throw new Error('Could not create business hour');
}
return createdBusinessHour;
};
export const makeDefaultBusinessHourActiveAndClosed = async () => {
// enable settings
await updateSetting('Livechat_enable_business_hours', true);
await updateEESetting('Livechat_business_hour_type', 'Single');
// create business hours
const {
body: { businessHour },
} = await request.get(api('livechat/business-hour')).query({ type: 'default' }).set(credentials).send();
const { workHours } = businessHour;
// Remove properties not accepted by the endpoint schema
const { _updatedAt, ts, ...cleanedBusinessHour } = businessHour;
const enabledBusinessHour = {
...cleanedBusinessHour,
timezoneName: 'America/Sao_Paulo',
timezone: 'America/Sao_Paulo',
departmentsToApplyBusinessHour: '',
daysOpen: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'],
daysTime: workHours.map((workHour: { open: boolean; start: { time: string }; finish: { time: string }; day: string }) => {
return {
open: true,
start: { time: '00:00' },
finish: { time: '00:01' },
day: workHour.day,
};
}),
workHours: workHours.map((workHour: { open: boolean; start: string; finish: string; day: string; code?: number }) => {
workHour.open = true;
workHour.start = '00:00';
workHour.finish = '00:01'; // if a job runs between 00:00 and 00:01, then this test will fail :P
delete workHour.code;
return workHour;
}),
};
return request.post(api('livechat/business-hours.save')).set(credentials).send(enabledBusinessHour).expect(200);
};
export const disableDefaultBusinessHour = async () => {
// disable settings
await updateSetting('Livechat_enable_business_hours', false);
await updateEESetting('Livechat_business_hour_type', 'Single');
// create business hours
const {
body: { businessHour },
} = await request.get(api('livechat/business-hour')).query({ type: 'default' }).set(credentials).send();
const { workHours } = businessHour;
// Remove properties not accepted by the endpoint schema
const { _updatedAt, ts, ...cleanedBusinessHour } = businessHour;
const disabledBusinessHour = {
...cleanedBusinessHour,
timezoneName: 'America/Sao_Paulo',
timezone: 'America/Sao_Paulo',
departmentsToApplyBusinessHour: '',
daysOpen: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'],
daysTime: workHours.map((workHour: { open: boolean; start: { time: string }; finish: { time: string }; day: string }) => {
return {
open: false,
start: { time: '00:00' },
finish: { time: '23:59' },
day: workHour.day,
};
}),
workHours: workHours.map((workHour: { open: boolean; start: string; finish: string; day: string }) => {
workHour.open = false;
workHour.start = '00:00';
workHour.finish = '23:59';
return workHour;
}),
};
return request.post(api('livechat/business-hours.save')).set(credentials).send(disabledBusinessHour).expect(200);
};
const removeCustomBusinessHour = async (businessHourId: string) => {
return request
.post(api('livechat/business-hours.remove'))
.set(credentials)
.send({ _id: businessHourId, type: LivechatBusinessHourTypes.CUSTOM })
.expect(200);
};
const getAllCustomBusinessHours = async (): Promise<ILivechatBusinessHour[]> => {
const response = await request.get(api('livechat/business-hours')).set(credentials).expect(200);
return (response.body.businessHours || []).filter(
(businessHour: ILivechatBusinessHour) => businessHour.type === LivechatBusinessHourTypes.CUSTOM,
);
};
export const removeAllCustomBusinessHours = async () => {
const existingBusinessHours: ILivechatBusinessHour[] = await getAllCustomBusinessHours();
const promises = existingBusinessHours.map((businessHour) => removeCustomBusinessHour(businessHour._id));
await Promise.all(promises);
};
export const getDefaultBusinessHour = async (): Promise<ILivechatBusinessHour> => {
const response = await request
.get(api('livechat/business-hour'))
.set(credentials)
.query({ type: LivechatBusinessHourTypes.DEFAULT })
.expect(200);
return response.body.businessHour;
};
export const getCustomBusinessHourById = async (businessHourId: string): Promise<ILivechatBusinessHour> => {
const response = await request
.get(api('livechat/business-hour'))
.set(credentials)
.query({ type: LivechatBusinessHourTypes.CUSTOM, _id: businessHourId })
.expect(200);
return response.body.businessHour;
};
// TODO: Refactor logic so object passed is of the correct type for POST /livechat/business-hours.save. See: CORE-1552
export const openOrCloseBusinessHour = async (businessHour: ILivechatBusinessHour, open: boolean) => {
const { _updatedAt, ts, ...cleanedBusinessHour } = businessHour;
const timezoneName = businessHour.timezone.name;
const enabledBusinessHour = {
...cleanedBusinessHour,
timezoneName,
timezone: timezoneName,
workHours: getWorkHours().map((workHour) => {
return {
...workHour,
open,
};
}),
departmentsToApplyBusinessHour: businessHour.departments?.map((department) => department._id).join(',') || '',
};
await saveBusinessHour(enabledBusinessHour as any);
};
export const getWorkHours = (open = true): ISaveBhApiWorkHour['workHours'] => {
const workHours: ISaveBhApiWorkHour['workHours'] = [];
for (let i = 0; i < 7; i++) {
workHours.push({
day: moment().day(i).format('dddd'),
start: '00:00',
finish: '23:59',
open,
});
}
return workHours;
};

View File

@@ -0,0 +1,25 @@
import { faker } from '@faker-js/faker';
import type { IOmnichannelCannedResponse } from '@rocket.chat/core-typings';
import { api, credentials, request } from '../api-data';
import type { DummyResponse } from './utils';
export const createCannedResponse = (): Promise<Partial<IOmnichannelCannedResponse>> =>
new Promise((resolve, reject) => {
const response = {
shortcut: `${faker.lorem.word()}-${Date.now()}`,
scope: 'user',
tags: [faker.string.uuid()],
text: faker.lorem.sentence(),
};
return request
.post(api(`canned-responses`))
.set(credentials)
.send(response)
.end((_err: Error, _res: DummyResponse<boolean>) => {
if (_err) {
return reject(_err);
}
resolve(response);
});
});

View File

@@ -0,0 +1,49 @@
import type { ILivechatCustomField } from '@rocket.chat/core-typings';
import type { Response } from 'supertest';
import { credentials, request, api } from '../api-data';
type ExtendedCustomField = Omit<ILivechatCustomField, '_id' | '_updatedAt'> & { field: string };
export const createCustomField = (customField: ExtendedCustomField): Promise<ILivechatCustomField> =>
new Promise((resolve, reject) => {
void request
.get(api(`livechat/custom-fields/${customField.label}`))
.set(credentials)
.send()
.end((err: Error, res: Response) => {
if (err) {
return reject(err);
}
if (res.body.customField !== null && res.body.customField !== undefined) {
resolve(res.body.customField);
} else {
void request
.post(api('livechat/custom-fields.save'))
.set(credentials)
.send({ customFieldId: null, customFieldData: customField })
.end((err: Error, res: Response): void => {
if (err) {
return reject(err);
}
resolve(res.body.customField);
});
}
});
});
export const deleteCustomField = (customFieldID: string) =>
new Promise((resolve, reject) => {
void request
.post(api('livechat/custom-fields.delete'))
.set(credentials)
.send({
customFieldId: customFieldID,
})
.end((err: Error, res: Response): void => {
if (err) {
return reject(err);
}
resolve(res.body);
});
});

View File

@@ -0,0 +1,225 @@
import { faker } from '@faker-js/faker';
import type { Credentials } from '@rocket.chat/api-client';
import type { ILivechatDepartment, IUser, LivechatDepartmentDTO } from '@rocket.chat/core-typings';
import { Random } from '@rocket.chat/random';
import { expect } from 'chai';
import { api, credentials, request } from '../api-data';
import { makeAgentAvailable } from './rooms';
import { createAgentAndReLogin, createAnAwayAgent, createAnOfflineAgent } from './users';
import type { WithRequiredProperty } from './utils';
const NewDepartmentData = ((): Partial<ILivechatDepartment> => ({
enabled: true,
name: `new department ${Random.id()}`,
description: 'created from api',
showOnRegistration: true,
email: faker.internet.email(),
showOnOfflineForm: true,
}))();
export const createDepartment = async (
departmentData?: Partial<ILivechatDepartment>,
agents?: { agentId: string; count?: string; order?: string }[],
): Promise<ILivechatDepartment> => {
const department = { ...NewDepartmentData, ...departmentData };
const response = await request
.post(api('livechat/department'))
.set(credentials)
.send({
department,
...(agents && { agents }),
})
.expect(200);
return response.body.department;
};
const updateDepartment = async (departmentId: string, departmentData: Partial<LivechatDepartmentDTO>): Promise<ILivechatDepartment> => {
const response = await request
.put(api(`livechat/department/${departmentId}`))
.set(credentials)
.send({
department: departmentData,
})
.expect(200);
return response.body.department;
};
export type OnlineAgent = {
user: WithRequiredProperty<IUser, 'username'>;
credentials: Credentials;
};
export const createDepartmentWithAnOnlineAgent = async (): Promise<{ department: ILivechatDepartment; agent: OnlineAgent }> => {
const { user, credentials: agentCredentials } = await createAgentAndReLogin();
const department = await createDepartment();
await addOrRemoveAgentFromDepartment(department._id, { agentId: user._id, username: user.username }, true);
await makeAgentAvailable(agentCredentials);
return {
department,
agent: {
credentials: agentCredentials,
user,
},
};
};
export const createDepartmentWith2OnlineAgents = async (): Promise<{
department: ILivechatDepartment;
agent1: OnlineAgent;
agent2: OnlineAgent;
}> => {
const [agent1, agent2] = await Promise.all([createAgentAndReLogin(), createAgentAndReLogin()]);
const department = await createDepartment(
{
enabled: true,
name: Random.id(),
showOnRegistration: true,
email: `${Random.id()}@example.com`,
showOnOfflineForm: true,
},
[{ agentId: agent1.user._id }, { agentId: agent2.user._id }],
);
await Promise.all([makeAgentAvailable(agent1.credentials), makeAgentAvailable(agent2.credentials)]);
return {
department,
agent1,
agent2,
};
};
export const createDepartmentWithAgent = async (agent: OnlineAgent): Promise<{ department: ILivechatDepartment; agent: OnlineAgent }> => {
const { user, credentials } = agent;
const department = await createDepartment();
await addOrRemoveAgentFromDepartment(department._id, { agentId: user._id, username: user.username }, true);
return {
department,
agent: {
credentials,
user,
},
};
};
export const addOrRemoveAgentFromDepartment = async (
departmentId: string,
agent: { agentId: string; username: string; count?: number; order?: number },
add: boolean,
) => {
const response = await request
.post(api(`livechat/department/${departmentId}/agents`))
.set(credentials)
.send({
...(add ? { upsert: [agent], remove: [] } : { remove: [agent], upsert: [] }),
});
if (response.status !== 200) {
throw new Error(`Failed to add or remove agent from department. Status code: ${response.status}\n${response.body}`);
}
};
export const createDepartmentWithAnOfflineAgent = async ({
allowReceiveForwardOffline = false,
fallbackForwardDepartment,
departmentsAllowedToForward,
}: {
allowReceiveForwardOffline?: boolean;
fallbackForwardDepartment?: string;
departmentsAllowedToForward?: string[];
}): Promise<{
department: ILivechatDepartment;
agent: {
credentials: Credentials;
user: WithRequiredProperty<IUser, 'username'>;
};
}> => {
const { user, credentials } = await createAnOfflineAgent();
const department = (await createDepartment({
allowReceiveForwardOffline,
fallbackForwardDepartment,
departmentsAllowedToForward,
})) as ILivechatDepartment;
await addOrRemoveAgentFromDepartment(department._id, { agentId: user._id, username: user.username }, true);
return {
department,
agent: {
credentials,
user,
},
};
};
export const createDepartmentWithAnAwayAgent = async ({
allowReceiveForwardOffline = false,
fallbackForwardDepartment,
departmentsAllowedToForward,
}: {
allowReceiveForwardOffline?: boolean;
fallbackForwardDepartment?: string;
departmentsAllowedToForward?: string[];
}): Promise<{
department: ILivechatDepartment;
agent: {
credentials: Credentials;
user: WithRequiredProperty<IUser, 'username'>;
};
}> => {
const { user, credentials } = await createAnAwayAgent();
const department = (await createDepartment({
allowReceiveForwardOffline,
fallbackForwardDepartment,
departmentsAllowedToForward,
})) as ILivechatDepartment;
await addOrRemoveAgentFromDepartment(department._id, { agentId: user._id, username: user.username }, true);
return {
department,
agent: {
credentials,
user,
},
};
};
export const archiveDepartment = async (departmentId: string): Promise<void> => {
await request
.post(api(`livechat/department/${departmentId}/archive`))
.set(credentials)
.expect(200);
};
export const disableDepartment = async (department: Omit<ILivechatDepartment, '_updatedAt'>): Promise<void> => {
department.enabled = false;
delete department._updatedAt;
const updatedDepartment = await updateDepartment(department._id, department);
expect(updatedDepartment.enabled).to.be.false;
};
export const deleteDepartment = async (departmentId: string): Promise<void> => {
await request
.delete(api(`livechat/department/${departmentId}`))
.set(credentials)
.expect(200);
};
export const getDepartmentById = async (departmentId: string): Promise<ILivechatDepartment> => {
const response = await request
.get(api(`livechat/department/${departmentId}`))
.set(credentials)
.expect(200);
return response.body.department;
};

View File

@@ -0,0 +1,31 @@
import { getCredentials, api, request, credentials } from '../api-data';
export const createEmailInbox = async (): Promise<{ _id: string }> => {
await new Promise((resolve) => getCredentials(resolve));
const { body } = await request
.post(api('email-inbox'))
.set(credentials)
.send({
name: 'test',
active: false,
email: `test${new Date().getTime()}@test.com`,
description: 'test',
senderInfo: 'test',
smtp: {
server: 'smtp.example.com',
port: 587,
username: 'xxxx',
password: 'xxxx',
secure: true,
},
imap: {
server: 'imap.example.com',
port: 993,
username: 'xxxx',
password: 'xxxx',
secure: true,
maxRetries: 10,
},
});
return body;
};

View File

@@ -0,0 +1,31 @@
import type { Credentials } from '@rocket.chat/api-client';
import type { ILivechatInquiryRecord } from '@rocket.chat/core-typings';
import type { PaginatedResult } from '@rocket.chat/rest-typings';
import { api, request } from '../api-data';
export const fetchAllInquiries = async (credentials: Credentials, department?: string): Promise<ILivechatInquiryRecord[]> => {
const inquiries: ILivechatInquiryRecord[] = [];
let hasMore = true;
let offset = 0;
while (hasMore) {
const { body } = (await request
.get(api('livechat/inquiries.queuedForUser'))
.set(credentials)
.query({
...(department && { department }),
offset,
})
.expect('Content-Type', 'application/json')
.expect(200)) as { body: PaginatedResult<{ inquiries: Array<ILivechatInquiryRecord> }> };
inquiries.push(...body.inquiries);
hasMore = body.total > inquiries.length;
offset += body.count;
}
return inquiries;
};

View File

@@ -0,0 +1,71 @@
import type { ILivechatPriority, IOmnichannelServiceLevelAgreements } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import { generateRandomSLAData } from '../../e2e/utils/omnichannel/sla';
import { api, credentials, request } from '../api-data';
import type { DummyResponse } from './utils';
export const createSLA = (): Promise<Omit<IOmnichannelServiceLevelAgreements, '_updated'>> => {
return new Promise((resolve, reject) => {
void request
.post(api('livechat/sla'))
.set(credentials)
.send(generateRandomSLAData())
.end((err: Error, res: DummyResponse<{ sla: Omit<IOmnichannelServiceLevelAgreements, '_updated'> }, 'unwrapped'>) => {
if (err) {
return reject(err);
}
resolve(res.body.sla);
});
});
};
export const deleteSLA = (id: string): Promise<void> => {
return new Promise((resolve, reject) => {
void request
.delete(api(`livechat/sla/${id}`))
.set(credentials)
.send()
.end((err: Error, _res: DummyResponse<void, 'not-wrapped'>) => {
if (err) {
return reject(err);
}
resolve();
});
});
};
export const bulkCreateSLA = (amount: number): Promise<Omit<IOmnichannelServiceLevelAgreements, '_updated'>[]> => {
const promises = [];
for (let i = 0; i < amount; i++) {
promises.push(createSLA());
}
return Promise.all(promises);
};
export const deleteAllSLA = async (): Promise<void> => {
const response = await request.get(api('livechat/sla')).set(credentials).expect('Content-Type', 'application/json').expect(200);
expect(response.body).to.have.property('success', true);
expect(response.body.sla).to.be.an('array');
const {
body: { sla },
} = response as { body: { sla: IOmnichannelServiceLevelAgreements[] } };
const promises = sla.map((slaObj) =>
request
.delete(api(`livechat/sla/${slaObj._id}`))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200),
);
await Promise.all(promises);
};
export const getRandomPriority = async (): Promise<ILivechatPriority> => {
const response = await request.get(api('livechat/priorities')).set(credentials).expect('Content-Type', 'application/json').expect(200);
expect(response.body).to.have.property('success', true);
expect(response.body.priorities).to.be.an('array');
const {
body: { priorities },
} = response as { body: { priorities: ILivechatPriority[] } };
return priorities[Math.floor(Math.random() * priorities.length)];
};

View File

@@ -0,0 +1,494 @@
import { faker } from '@faker-js/faker';
import type { Credentials } from '@rocket.chat/api-client';
import type {
ILivechatInquiryRecord,
ILivechatAgent,
ILivechatDepartment,
ILivechatVisitor,
IMessage,
IOmnichannelRoom,
} from '@rocket.chat/core-typings';
import type { Response } from 'supertest';
import { api, credentials, methodCall, request } from '../api-data';
import { imgURL } from '../interactions';
import { getSettingValueById, restorePermissionToRoles, updateSetting } from '../permissions.helper';
import { adminUsername } from '../user';
import { getRandomVisitorToken } from './users';
import type { DummyResponse } from './utils';
import { sleep } from './utils';
export const createLivechatRoom = async (visitorToken: string, extraRoomParams?: Record<string, string>): Promise<IOmnichannelRoom> => {
const urlParams = new URLSearchParams();
urlParams.append('token', visitorToken);
if (extraRoomParams) {
for (const [key, value] of Object.entries(extraRoomParams)) {
urlParams.append(key, value);
}
}
const response = await request.get(api('livechat/room')).query(urlParams.toString()).set(credentials).expect(200);
return response.body.room;
};
export const createLivechatRoomWidget = async (
visitorToken: string,
extraRoomParams?: Record<string, string>,
): Promise<IOmnichannelRoom> => {
const urlParams = new URLSearchParams();
urlParams.append('token', visitorToken);
if (extraRoomParams) {
for (const [key, value] of Object.entries(extraRoomParams)) {
urlParams.append(key, value);
}
}
const response = await request
.get(api('livechat/room'))
.set('Cookie', [`rc_room_type=l`, `rc_is_widget=t`])
.query(urlParams.toString())
.set(credentials)
.expect(200);
return response.body.room;
};
export const createVisitorWithCustomData = async ({
department,
visitorName,
customPhone,
customFields,
customToken,
customEmail,
ignoreEmail = false,
ignorePhone = false,
}: {
department?: string;
customPhone?: string;
visitorName?: string;
customEmail?: string;
customFields?: { key: string; value: string; overwrite: boolean }[];
customToken?: string;
ignoreEmail?: boolean;
ignorePhone?: boolean;
}): Promise<ILivechatVisitor> => {
const token = customToken || getRandomVisitorToken();
const email = customEmail || `${token}@${token}.com`;
const phone = customPhone || `${Math.floor(Math.random() * 10000000000)}`;
try {
const res = await request.get(api(`livechat/visitor/${token}`));
if (res?.body?.visitor) {
return res.body.visitor as ILivechatVisitor;
}
} catch {
// Ignore errors from GET; we will create the visitor below.
}
const res = await request
.post(api('livechat/visitor'))
.set(credentials)
.send({
visitor: {
name: visitorName || `Visitor ${Date.now()}`,
token,
customFields: customFields || [{ key: 'address', value: 'Rocket.Chat street', overwrite: true }],
...(department ? { department } : {}),
...(!ignoreEmail ? { email } : {}),
...(!ignorePhone ? { phone } : {}),
},
});
return res.body.visitor as ILivechatVisitor;
};
export const createVisitor = (
department?: string,
visitorName?: string,
customEmail?: string,
customPhone?: string,
): Promise<ILivechatVisitor> =>
new Promise((resolve, reject) => {
const token = getRandomVisitorToken();
const email = customEmail || `${token}@${token}.com`;
const phone = customPhone || `${Math.floor(Math.random() * 10000000000)}`;
void request.get(api(`livechat/visitor/${token}`)).end((err: Error, res: DummyResponse<ILivechatVisitor>) => {
if (!err && res && res.body && res.body.visitor) {
return resolve(res.body.visitor);
}
void request
.post(api('livechat/visitor'))
.set(credentials)
.send({
visitor: {
name: visitorName || `Visitor ${Date.now()}`,
email,
token,
phone,
customFields: [{ key: 'address', value: 'Rocket.Chat street', overwrite: true }],
...(department ? { department } : {}),
},
})
.end((err: Error, res: DummyResponse<ILivechatVisitor>) => {
if (err) {
return reject(err);
}
resolve(res.body.visitor);
});
});
});
export const deleteVisitor = async (token: string): Promise<void> => {
await request.delete(api(`livechat/visitor/${token}`));
};
export const takeInquiry = async (inquiryId: string, agentCredentials?: Credentials): Promise<void> => {
const userId = agentCredentials ? agentCredentials['X-User-Id'] : credentials['X-User-Id'];
await request
.post(api('livechat/inquiries.take'))
.set(agentCredentials || credentials)
.send({ userId, inquiryId })
.expect(200);
};
export const fetchInquiry = (roomId: string): Promise<ILivechatInquiryRecord> => {
return new Promise((resolve, reject) => {
void request
.get(api('livechat/inquiries.getOne'))
.query({ roomId })
.set(credentials)
.end((err: Error, res: DummyResponse<ILivechatInquiryRecord>) => {
if (err) {
return reject(err);
}
resolve(res.body.inquiry);
});
});
};
export const createDepartment = (
agents?: { agentId: string; count?: number }[],
name?: string,
enabled = true,
opts: Record<string, any> = {},
departmentUnit?: { _id?: string },
userCredentials: Credentials = credentials,
): Promise<ILivechatDepartment> => {
return new Promise((resolve, reject) => {
void request
.post(api('livechat/department'))
.set(userCredentials)
.send({
department: {
name: name || `Department ${Date.now()}`,
enabled,
showOnOfflineForm: true,
showOnRegistration: true,
email: 'a@b.com',
...opts,
},
agents,
departmentUnit,
})
.end((err: Error, res: DummyResponse<ILivechatDepartment>) => {
if (err) {
return reject(err);
}
resolve(res.body.department);
});
});
};
export const updateDepartment = ({
departmentId,
userCredentials,
agents,
name,
enabled = true,
opts = {},
departmentUnit,
}: {
departmentId: string;
userCredentials: Credentials;
agents?: { agentId: string }[];
name?: string;
enabled?: boolean;
opts?: Record<string, any>;
departmentUnit?: { _id?: string };
}): Promise<ILivechatDepartment> => {
return new Promise((resolve, reject) => {
void request
.put(api(`livechat/department/${departmentId}`))
.set(userCredentials)
.send({
department: {
name: name || `Department ${Date.now()}`,
enabled,
showOnOfflineForm: true,
showOnRegistration: true,
email: 'a@b.com',
...opts,
},
agents,
departmentUnit,
})
.end((err: Error, res: DummyResponse<ILivechatDepartment>) => {
if (err) {
return reject(err);
}
resolve(res.body.department);
});
});
};
export const deleteAgent = async (agentId: string = credentials['X-User-Id']): Promise<void> => {
await request
.delete(api(`livechat/users/agent/${agentId}`))
.set(credentials)
.expect(200);
};
export const createAgent = (overrideUsername?: string): Promise<ILivechatAgent> =>
new Promise((resolve, reject) => {
void request
.post(api('livechat/users/agent'))
.set(credentials)
.send({
username: overrideUsername || adminUsername,
})
.end((err: Error, res: DummyResponse<ILivechatAgent>) => {
if (err) {
return reject(err);
}
resolve(res.body.user);
});
});
export const createManager = (overrideUsername?: string): Promise<ILivechatAgent> =>
new Promise((resolve, reject) => {
void request
.post(api('livechat/users/manager'))
.set(credentials)
.send({
username: overrideUsername || adminUsername,
})
.end((err: Error, res: DummyResponse<ILivechatAgent>) => {
if (err) {
return reject(err);
}
resolve(res.body.user);
});
});
export const switchLivechatStatus = async (status: 'available' | 'not-available', overrideCredentials?: Credentials): Promise<void> => {
await request
.post(api('livechat/agent.status'))
.set(overrideCredentials || credentials)
.send({ status })
.expect(200);
};
export const makeAgentAvailable = async (overrideCredentials?: Credentials): Promise<Response> => {
await restorePermissionToRoles('view-l-room');
await request
.post(api('users.setStatus'))
.set(overrideCredentials || credentials)
.send({
message: '',
status: 'online',
});
return request
.post(api('livechat/agent.status'))
.set(overrideCredentials || credentials)
.send({
status: 'available',
});
};
export const makeAgentUnavailable = async (overrideCredentials?: Credentials): Promise<void> => {
await request
.post(api('users.setStatus'))
.set(overrideCredentials || credentials)
.send({ message: '', status: 'offline' })
.expect(200);
await request
.post(api('livechat/agent.status'))
.set(overrideCredentials || credentials)
.send({
status: 'not-available',
})
.expect(200);
};
export const getLivechatRoomInfo = (roomId: string): Promise<IOmnichannelRoom> => {
return new Promise((resolve /* , reject*/) => {
void request
.get(api('channels.info'))
.set(credentials)
.query({
roomId,
})
.end((_err: Error, res: DummyResponse<IOmnichannelRoom>) => {
resolve(res.body.channel);
});
});
};
/**
* @summary Sends message as visitor
*/
export const sendMessage = (roomId: string, message: string, visitorToken: string): Promise<IMessage> => {
return new Promise((resolve, reject) => {
void request
.post(api('livechat/message'))
.set(credentials)
.send({
rid: roomId,
msg: message,
token: visitorToken,
})
.end((err: Error, res: DummyResponse<IMessage>) => {
if (err) {
return reject(err);
}
resolve(res.body.message);
});
});
};
export const uploadFile = (roomId: string, visitorToken: string): Promise<IMessage> => {
return new Promise((resolve, reject) => {
void request
.post(api(`livechat/upload/${roomId}`))
.set({ 'x-visitor-token': visitorToken, ...credentials })
.attach('file', imgURL)
.end((err: Error, res: DummyResponse<IMessage>) => {
if (err) {
return reject(err);
}
resolve(res.body as unknown as IMessage);
});
});
};
// Sends a message using sendMessage method from agent
export const sendAgentMessage = (roomId: string, msg?: string, userCredentials: Credentials = credentials): Promise<IMessage> => {
return new Promise((resolve, reject) => {
void request
.post(methodCall('sendMessage'))
.set(userCredentials)
.send({
message: JSON.stringify({
method: 'sendMessage',
params: [{ rid: roomId, msg: msg || faker.lorem.sentence() }],
id: 'id',
msg: 'method',
}),
})
.end((err: Error, res: any) => {
if (err) {
return reject(err);
}
resolve(JSON.parse(res.body.message).result);
});
});
};
export const fetchMessages = (roomId: string, visitorToken: string): Promise<IMessage[]> => {
return new Promise((resolve, reject) => {
void request
.get(api(`livechat/messages.history/${roomId}`))
.set(credentials)
.query({
token: visitorToken,
})
.end((err: Error, res: DummyResponse<IMessage[]>) => {
if (err) {
return reject(err);
}
if (!res.body.success) {
reject(res.body);
return;
}
resolve(res.body.messages);
});
});
};
export const closeOmnichannelRoom = async (roomId: string, tags?: string[]): Promise<void> => {
await request
.post(api('livechat/room.closeByUser'))
.set(credentials)
.send({ rid: roomId, ...(tags && { tags }), comment: faker.lorem.sentence() })
.expect(200);
};
export const bulkCreateLivechatRooms = async (
amount: number,
department?: string,
resolveRoomExtraParams?: (index: number) => Record<string, string> | undefined,
): Promise<IOmnichannelRoom[]> => {
const rooms: IOmnichannelRoom[] = [];
for (let i = 0; i < amount; i++) {
const visitor = await createVisitor(department);
const extraRoomParams = resolveRoomExtraParams ? resolveRoomExtraParams(i) : {};
const room = await createLivechatRoom(visitor.token, extraRoomParams);
rooms.push(room);
}
return rooms;
};
export const startANewLivechatRoomAndTakeIt = async ({
departmentId,
agent,
}: {
departmentId?: string;
agent?: Credentials;
} = {}): Promise<{ room: IOmnichannelRoom; visitor: ILivechatVisitor }> => {
const currentRoutingMethod = await getSettingValueById('Livechat_Routing_Method');
const routingMethodChanged = false;
if (currentRoutingMethod !== 'Manual_Selection') {
await updateSetting('Livechat_Routing_Method', 'Manual_Selection');
// wait for routing algorithm to stop
await sleep(1000);
}
const visitor = await createVisitor(departmentId);
const room = await createLivechatRoom(visitor.token);
const { _id: roomId } = room;
const inq = await fetchInquiry(roomId);
await takeInquiry(inq._id, agent);
await sendMessage(roomId, 'test message', visitor.token);
if (routingMethodChanged) {
await updateSetting('Livechat_Routing_Method', currentRoutingMethod);
// wait for routing algorithm to start
await sleep(1000);
}
return { room, visitor };
};
export const placeRoomOnHold = async (roomId: string): Promise<void> => {
await request.post(api('livechat/room.onHold')).set(credentials).send({ roomId }).expect(200);
};
export const moveBackToQueue = async (roomId: string, overrideCredentials?: Credentials): Promise<void> => {
await request
.post(api('livechat/inquiries.returnAsInquiry'))
.set(overrideCredentials || credentials)
.send({ roomId })
.expect('Content-Type', 'application/json')
.expect(200);
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,50 @@
import { faker } from '@faker-js/faker';
import type { ILivechatTag, FindTagsResult } from '@rocket.chat/core-typings';
import { credentials, request, api } from '../api-data';
import type { DummyResponse } from './utils';
export const listTags = async (): Promise<FindTagsResult> => {
const { body } = await request.get(api('livechat/tags')).set(credentials).query({ viewAll: 'true' });
return body;
};
export const saveTags = async (departments: string[] = []): Promise<ILivechatTag> => {
const { body } = await request
.post(api('livechat/tags.save'))
.set(credentials)
.send({
tagData: {
name: faker.string.uuid(),
description: faker.lorem.sentence(),
},
...(departments.length > 0 && { tagDepartments: departments }),
});
return body;
};
export const removeAllTags = async (): Promise<boolean> => {
const tagsList = await listTags();
await Promise.all(tagsList.tags.map((tag) => removeTag(tag._id)));
const response = await request.get(api('livechat/tags')).set(credentials).expect('Content-Type', 'application/json').expect(200);
return response.body.tags.length === 0;
};
export const removeTag = (id: string): Promise<boolean> => {
return new Promise((resolve, reject) => {
void request
.post(api('livechat/tags.delete'))
.set(credentials)
.send({ id })
.end((err: Error, res: DummyResponse<string, 'wrapped'>) => {
if (err) {
return reject(err);
}
resolve(JSON.parse(res.body.success).result);
});
});
};

View File

@@ -0,0 +1,55 @@
import { faker } from '@faker-js/faker';
import type { ILivechatTrigger } from '@rocket.chat/core-typings';
import { api, credentials, request } from '../api-data';
import type { DummyResponse } from './utils';
export const createTrigger = (name: string): Promise<boolean> => {
return new Promise((resolve, reject) => {
void request
.post(api('livechat/triggers'))
.set(credentials)
.send({
name,
description: faker.lorem.sentence(),
enabled: true,
runOnce: faker.datatype.boolean(),
actions: [
{
name: 'send-message',
params: {
msg: faker.lorem.sentence(),
name: faker.person.firstName(),
sender: faker.helpers.arrayElement(['queue', 'custom']),
},
},
],
conditions: [
{
name: faker.helpers.arrayElement(['time-on-site', 'page-url', 'chat-opened-by-visitor', 'after-guest-registration']),
value: faker.string.alpha(),
},
],
})
.end((err: Error, _res: DummyResponse<boolean, 'unwrapped'>) => {
if (err) {
return reject(err);
}
resolve(true);
});
});
};
export const fetchTriggers = (): Promise<ILivechatTrigger[]> => {
return new Promise((resolve, reject) => {
void request
.get(api('livechat/triggers'))
.set(credentials)
.end((err: Error, res: DummyResponse<ILivechatTrigger[], 'wrapped'>) => {
if (err) {
return reject(err);
}
resolve(res.body.triggers);
});
});
};

View File

@@ -0,0 +1,85 @@
import { faker } from '@faker-js/faker';
import type { IOmnichannelBusinessUnit } from '@rocket.chat/core-typings';
import { methodCall, credentials, request, api } from '../api-data';
import type { DummyResponse } from './utils';
export const createMonitor = async (username: string): Promise<{ _id: string; username: string; role: string[] }> => {
return new Promise((resolve, reject) => {
void request
.post(api('livechat/monitors.create'))
.set(credentials)
.send({ username })
.end((err: Error, res: DummyResponse<{ _id: string; username: string; role: string[] }, 'not-wrapped'>) => {
if (err) {
return reject(err);
}
resolve(res.body);
});
});
};
export const createUnit = async (
monitorId: string,
username: string,
departmentIds: string[],
name?: string,
extraMonitor: { monitorId: string; username: string }[] = [],
): Promise<IOmnichannelBusinessUnit> => {
return new Promise((resolve, reject) => {
void request
.post(api('livechat/units'))
.set(credentials)
.send({
unitData: {
name: name || `${faker.person.firstName()} ${faker.string.uuid()}`,
visibility: faker.helpers.arrayElement(['public', 'private']),
},
unitMonitors: [{ monitorId, username }, ...extraMonitor],
unitDepartments: departmentIds.map((departmentId) => ({ departmentId })),
})
.end((err: Error, res: DummyResponse<IOmnichannelBusinessUnit, 'not-wrapped'>) => {
if (err) {
return reject(err);
}
resolve(res.body);
});
});
};
export const deleteUnit = async (unit: IOmnichannelBusinessUnit): Promise<IOmnichannelBusinessUnit> => {
return new Promise((resolve, reject) => {
void request
.post(methodCall(`livechat:removeUnit`))
.set(credentials)
.send({
message: JSON.stringify({
method: 'livechat:removeUnit',
params: [unit._id],
id: '101',
msg: 'method',
}),
})
.end((err: Error, res: DummyResponse<string, 'wrapped'>) => {
if (err) {
return reject(err);
}
resolve(JSON.parse(res.body.message).result);
});
});
};
export const getUnit = (unitId: string): Promise<IOmnichannelBusinessUnit> => {
return new Promise((resolve, reject) => {
void request
.get(api(`livechat/units/${unitId}`))
.set(credentials)
.end((err: Error, res: DummyResponse<IOmnichannelBusinessUnit, 'not-wrapped'>) => {
if (err) {
return reject(err);
}
resolve(res.body);
});
});
};

View File

@@ -0,0 +1,128 @@
import { faker } from '@faker-js/faker';
import type { Credentials } from '@rocket.chat/api-client';
import { UserStatus, type ILivechatAgent, type IUser } from '@rocket.chat/core-typings';
import { Random } from '@rocket.chat/random';
import { api, credentials, request } from '../api-data';
import { password } from '../user';
import { createUser, login, setUserAway, setUserStatus } from '../users.helper';
import { createAgent, makeAgentAvailable, makeAgentUnavailable } from './rooms';
export const createBotAgent = async (): Promise<{
credentials: Credentials;
user: IUser;
}> => {
const agent = await createUser({
roles: ['bot'],
});
const createdUserCredentials = await login(agent.username, password);
await createAgent(agent.username);
await makeAgentAvailable(createdUserCredentials);
return {
credentials: createdUserCredentials,
user: agent,
};
};
export const getRandomVisitorToken = (): string => faker.string.alphanumeric(17);
export const getAgent = async (userId: string): Promise<ILivechatAgent> => {
const { body } = await request
.get(api(`livechat/users/agent/${userId}`))
.set(credentials)
.expect(200);
return body.user;
};
export const removeAgent = async (userId: string): Promise<void> => {
await request
.delete(api(`livechat/users/agent/${userId}`))
.set(credentials)
.expect(200);
};
export const createAgentAndReLogin = async (): Promise<{
credentials: Credentials;
user: IUser & { username: string };
}> => {
const username = `user.test.${Random.id()}`;
const email = `${username}@rocket.chat`;
const { body } = await request.post(api('users.create')).set(credentials).send({ email, name: username, username, password });
const agent = body.user;
const createdUserCredentials = await login(agent.username, password);
await createAgent(agent.username);
return {
credentials: createdUserCredentials,
user: agent,
};
};
export const createAnOnlineAgent = async (): Promise<{
credentials: Credentials;
user: IUser & { username: string };
}> => {
const { credentials: createdUserCredentials, user: agent } = await createAgentAndReLogin();
await makeAgentAvailable(createdUserCredentials);
return {
credentials: createdUserCredentials,
user: agent,
};
};
export const createAnOfflineAgent = async (): Promise<{
credentials: Credentials;
user: IUser & { username: string };
}> => {
const username = `user.test.${Date.now()}.offline`;
const email = `${username}.offline@rocket.chat`;
const { body } = await request.post(api('users.create')).set(credentials).send({ email, name: username, username, password });
const agent = body.user;
const createdUserCredentials = await login(agent.username, password);
await createAgent(agent.username);
await makeAgentUnavailable(createdUserCredentials);
return {
credentials: createdUserCredentials,
user: agent,
};
};
export const createAnAwayAgent = async (): Promise<{
credentials: Credentials;
user: IUser & { username: string };
}> => {
const username = `user.test.${Date.now()}.away`;
const email = `${username}.offline@rocket.chat`;
const { body } = await request.post(api('users.create')).set(credentials).send({ email, name: username, username, password });
const agent = body.user;
const createdUserCredentials = await login(agent.username, password);
await createAgent(agent.username);
await makeAgentAvailable(createdUserCredentials);
await setUserStatus(createdUserCredentials, UserStatus.AWAY);
await setUserAway(createdUserCredentials);
return {
credentials: createdUserCredentials,
user: agent,
};
};
export const updateLivechatSettingsForUser = async (
agentId: string,
livechatSettings: Record<string, any>,
agentDepartments: string[] = [],
): Promise<void> => {
await request
.post(api('livechat/agents.saveInfo'))
.set(credentials)
.send({
agentId,
agentData: livechatSettings,
agentDepartments,
})
.expect('Content-Type', 'application/json')
.expect(200);
};

View File

@@ -0,0 +1,17 @@
export type DummyResponse<T, E = 'wrapped'> = E extends 'wrapped' ? { body: { [k: string]: T } } : { body: T };
export type WithRequiredProperty<Type, Key extends keyof Type> = Type & {
[Property in Key]-?: Type[Property];
};
export const sleep = (ms: number) => {
return new Promise((resolve) => setTimeout(resolve, ms));
};
export const parseMethodResponse = (response: any) => {
if (response.message) {
return JSON.parse(response.message);
}
return {};
};

View File

@@ -0,0 +1,13 @@
import type { ILivechatVisitor } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import { api, credentials, request } from '../api-data';
export const getLivechatVisitorByToken = async (token: string): Promise<ILivechatVisitor> => {
const response = await request
.get(api(`livechat/visitor/${token}`))
.set(credentials)
.expect(200);
expect(response.body).to.have.property('visitor');
return response.body.visitor;
};