refactor: Deprecate transcript & triggers endpoints and create tests (#29861)

Co-authored-by: Murtaza Patrawala <34130764+murtaza98@users.noreply.github.com>
pull/30092/head^2
Kevin Aleman 2 years ago committed by GitHub
parent 0194971c52
commit 06d24c19a7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 50
      apps/meteor/app/livechat/imports/server/rest/appearance.ts
  2. 23
      apps/meteor/app/livechat/imports/server/rest/triggers.ts
  3. 47
      apps/meteor/app/livechat/server/api/v1/transcript.ts
  4. 2
      apps/meteor/app/livechat/server/methods/discardTranscript.ts
  5. 2
      apps/meteor/app/livechat/server/methods/requestTranscript.ts
  6. 2
      apps/meteor/app/livechat/server/methods/saveAppearance.ts
  7. 2
      apps/meteor/app/livechat/server/methods/saveTrigger.ts
  8. 9
      apps/meteor/client/views/omnichannel/appearance/AppearancePage.tsx
  9. 4
      apps/meteor/client/views/omnichannel/triggers/EditTriggerPage.js
  10. 4
      apps/meteor/client/views/omnichannel/triggers/NewTriggerPage.js
  11. 12
      apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useQuickActions.tsx
  12. 2
      apps/meteor/server/models/raw/LivechatRooms.ts
  13. 2
      apps/meteor/server/models/raw/LivechatTrigger.ts
  14. 51
      apps/meteor/tests/end-to-end/api/livechat/02-appearance.ts
  15. 167
      apps/meteor/tests/end-to-end/api/livechat/08-triggers.ts
  16. 138
      apps/meteor/tests/end-to-end/api/livechat/11-livechat.ts
  17. 2
      packages/model-typings/src/models/ILivechatTriggerModel.ts
  18. 145
      packages/rest-typings/src/v1/omnichannel.ts

@ -1,9 +1,18 @@
import { Settings } from '@rocket.chat/models';
import { isPOSTLivechatAppearanceParams } from '@rocket.chat/rest-typings';
import { API } from '../../../../api/server';
import { findAppearance } from '../../../server/api/lib/appearance';
API.v1.addRoute(
'livechat/appearance',
{ authRequired: true, permissionsRequired: ['view-livechat-manager'] },
{
authRequired: true,
permissionsRequired: ['view-livechat-manager'],
validateParams: {
POST: isPOSTLivechatAppearanceParams,
},
},
{
async get() {
const { appearance } = await findAppearance();
@ -12,5 +21,44 @@ API.v1.addRoute(
appearance,
});
},
async post() {
const settings = this.bodyParams;
const validSettingList = [
'Livechat_title',
'Livechat_title_color',
'Livechat_enable_message_character_limit',
'Livechat_message_character_limit',
'Livechat_show_agent_info',
'Livechat_show_agent_email',
'Livechat_display_offline_form',
'Livechat_offline_form_unavailable',
'Livechat_offline_message',
'Livechat_offline_success_message',
'Livechat_offline_title',
'Livechat_offline_title_color',
'Livechat_offline_email',
'Livechat_conversation_finished_message',
'Livechat_conversation_finished_text',
'Livechat_registration_form',
'Livechat_name_field_registration_form',
'Livechat_email_field_registration_form',
'Livechat_registration_form_message',
];
const valid = settings.every((setting) => validSettingList.includes(setting._id));
if (!valid) {
throw new Error('invalid-setting');
}
await Promise.all(
settings.map((setting) => {
return Settings.updateValueById(setting._id, setting.value);
}),
);
return API.v1.success();
},
},
);

@ -1,4 +1,5 @@
import { isGETLivechatTriggersParams } from '@rocket.chat/rest-typings';
import { LivechatTrigger } from '@rocket.chat/models';
import { isGETLivechatTriggersParams, isPOSTLivechatTriggersParams } from '@rocket.chat/rest-typings';
import { API } from '../../../../api/server';
import { getPaginationItems } from '../../../../api/server/helpers/getPaginationItems';
@ -6,7 +7,14 @@ import { findTriggers, findTriggerById } from '../../../server/api/lib/triggers'
API.v1.addRoute(
'livechat/triggers',
{ authRequired: true, permissionsRequired: ['view-livechat-manager'], validateParams: isGETLivechatTriggersParams },
{
authRequired: true,
permissionsRequired: ['view-livechat-manager'],
validateParams: {
GET: isGETLivechatTriggersParams,
POST: isPOSTLivechatTriggersParams,
},
},
{
async get() {
const { offset, count } = await getPaginationItems(this.queryParams);
@ -22,6 +30,17 @@ API.v1.addRoute(
return API.v1.success(triggers);
},
async post() {
const { _id, name, description, enabled, runOnce, conditions, actions } = this.bodyParams;
if (_id) {
await LivechatTrigger.updateById(_id, { name, description, enabled, runOnce, conditions, actions });
} else {
await LivechatTrigger.insertOne({ name, description, enabled, runOnce, conditions, actions });
}
return API.v1.success();
},
},
);

@ -1,7 +1,10 @@
import { isPOSTLivechatTranscriptParams } from '@rocket.chat/rest-typings';
import type { IOmnichannelRoom } from '@rocket.chat/core-typings';
import { LivechatRooms, Users } from '@rocket.chat/models';
import { isPOSTLivechatTranscriptParams, isPOSTLivechatTranscriptRequestParams } from '@rocket.chat/rest-typings';
import { i18n } from '../../../../../server/lib/i18n';
import { API } from '../../../../api/server';
import { Livechat as LivechatJS } from '../../lib/Livechat';
import { Livechat } from '../../lib/LivechatTyped';
API.v1.addRoute(
@ -18,3 +21,45 @@ API.v1.addRoute(
},
},
);
API.v1.addRoute(
'livechat/transcript/:rid',
{
authRequired: true,
permissionsRequired: ['send-omnichannel-chat-transcript'],
validateParams: {
POST: isPOSTLivechatTranscriptRequestParams,
},
},
{
async delete() {
const { rid } = this.urlParams;
const room = await LivechatRooms.findOneById<Pick<IOmnichannelRoom, 'open' | 'transcriptRequest'>>(rid, {
projection: { open: 1, transcriptRequest: 1 },
});
if (!room?.open) {
throw new Error('error-invalid-room');
}
if (!room.transcriptRequest) {
throw new Error('error-transcript-not-requested');
}
await LivechatRooms.unsetEmailTranscriptRequestedByRoomId(rid);
return API.v1.success();
},
async post() {
const { rid } = this.urlParams;
const { email, subject } = this.bodyParams;
const user = await Users.findOneById(this.userId, {
projection: { _id: 1, username: 1, name: 1, utcOffset: 1 },
});
await LivechatJS.requestTranscript({ rid, email, subject, user });
return API.v1.success();
},
},
);

@ -4,6 +4,7 @@ import { check } from 'meteor/check';
import { Meteor } from 'meteor/meteor';
import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission';
import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger';
declare module '@rocket.chat/ui-contexts' {
// eslint-disable-next-line @typescript-eslint/naming-convention
@ -14,6 +15,7 @@ declare module '@rocket.chat/ui-contexts' {
Meteor.methods<ServerMethods>({
async 'livechat:discardTranscript'(rid: string) {
methodDeprecationLogger.method('livechat:discardTranscript', '7.0.0');
check(rid, String);
const user = Meteor.userId();

@ -4,6 +4,7 @@ import { check } from 'meteor/check';
import { Meteor } from 'meteor/meteor';
import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission';
import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger';
import { Livechat } from '../lib/Livechat';
declare module '@rocket.chat/ui-contexts' {
@ -15,6 +16,7 @@ declare module '@rocket.chat/ui-contexts' {
Meteor.methods<ServerMethods>({
async 'livechat:requestTranscript'(rid, email, subject) {
methodDeprecationLogger.method('livechat:requestTranscript', '7.0.0');
check(rid, String);
check(email, String);

@ -3,6 +3,7 @@ import type { ServerMethods } from '@rocket.chat/ui-contexts';
import { Meteor } from 'meteor/meteor';
import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission';
import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger';
declare module '@rocket.chat/ui-contexts' {
// eslint-disable-next-line @typescript-eslint/naming-convention
@ -13,6 +14,7 @@ declare module '@rocket.chat/ui-contexts' {
Meteor.methods<ServerMethods>({
async 'livechat:saveAppearance'(settings) {
methodDeprecationLogger.method('livechat:saveAppearance', '7.0.0');
const uid = Meteor.userId();
if (!uid || !(await hasPermissionAsync(uid, 'view-livechat-manager'))) {
throw new Meteor.Error('error-not-allowed', 'Not allowed', {

@ -5,6 +5,7 @@ import { Match, check } from 'meteor/check';
import { Meteor } from 'meteor/meteor';
import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission';
import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger';
declare module '@rocket.chat/ui-contexts' {
// eslint-disable-next-line @typescript-eslint/naming-convention
@ -15,6 +16,7 @@ declare module '@rocket.chat/ui-contexts' {
Meteor.methods<ServerMethods>({
async 'livechat:saveTrigger'(trigger) {
methodDeprecationLogger.method('livechat:saveTrigger', '7.0.0');
const uid = Meteor.userId();
if (!uid || !(await hasPermissionAsync(uid, 'view-livechat-manager'))) {
throw new Meteor.Error('error-not-allowed', 'Not allowed', {

@ -1,7 +1,7 @@
import type { ISetting, Serialized } from '@rocket.chat/core-typings';
import { ButtonGroup, Button, Box } from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { useToastMessageDispatch, useMethod, useTranslation } from '@rocket.chat/ui-contexts';
import { useToastMessageDispatch, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts';
import type { FC } from 'react';
import React from 'react';
@ -47,12 +47,15 @@ const AppearancePage: FC<AppearancePageProps> = ({ settings }) => {
const t = useTranslation();
const dispatchToastMessage = useToastMessageDispatch();
const save = useMethod('livechat:saveAppearance');
const save = useEndpoint('POST', '/v1/livechat/appearance');
const { values, handlers, commit, reset, hasUnsavedChanges } = useForm(reduceAppearance(settings));
const handleSave = useMutableCallback(async () => {
const mappedAppearance = Object.entries(values).map(([_id, value]) => ({ _id, value }));
const mappedAppearance = Object.entries(values).map(([_id, value]) => ({ _id, value })) as {
_id: string;
value: string | boolean | number;
}[];
try {
await save(mappedAppearance);

@ -1,6 +1,6 @@
import { FieldGroup, Button, ButtonGroup } from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { useToastMessageDispatch, useRoute, useMethod, useTranslation } from '@rocket.chat/ui-contexts';
import { useToastMessageDispatch, useRoute, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts';
import React from 'react';
import { ContextualbarScrollableContent, ContextualbarFooter } from '../../../components/Contextualbar';
@ -44,7 +44,7 @@ const EditTriggerPage = ({ data, onSave }) => {
const router = useRoute('omnichannel-triggers');
const save = useMethod('livechat:saveTrigger');
const save = useEndpoint('POST', '/v1/livechat/triggers');
const { values, handlers, hasUnsavedChanges } = useForm(getInitialValues(data));

@ -1,6 +1,6 @@
import { Button, FieldGroup, ButtonGroup } from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { useToastMessageDispatch, useRoute, useMethod, useTranslation } from '@rocket.chat/ui-contexts';
import { useToastMessageDispatch, useRoute, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts';
import React, { useMemo } from 'react';
import { ContextualbarScrollableContent, ContextualbarFooter } from '../../../components/Contextualbar';
@ -13,7 +13,7 @@ const NewTriggerPage = ({ onSave }) => {
const router = useRoute('omnichannel-triggers');
const save = useMethod('livechat:saveTrigger');
const save = useEndpoint('POST', '/v1/livechat/triggers');
const { values, handlers } = useForm({
name: '',

@ -72,12 +72,12 @@ export const useQuickActions = (): {
const closeModal = useCallback(() => setModal(null), [setModal]);
const requestTranscript = useMethod('livechat:requestTranscript');
const requestTranscript = useEndpoint('POST', '/v1/livechat/transcript/:rid', { rid });
const handleRequestTranscript = useCallback(
async (email: string, subject: string) => {
try {
await requestTranscript(rid, email, subject);
await requestTranscript({ email, subject });
closeModal();
dispatchToastMessage({
type: 'success',
@ -87,7 +87,7 @@ export const useQuickActions = (): {
dispatchToastMessage({ type: 'error', message: error });
}
},
[closeModal, dispatchToastMessage, requestTranscript, rid, t],
[closeModal, dispatchToastMessage, requestTranscript, t],
);
const sendTranscriptPDF = useEndpoint('POST', '/v1/omnichannel/:rid/request-transcript', { rid });
@ -118,11 +118,11 @@ export const useQuickActions = (): {
[closeModal, dispatchToastMessage, rid, sendTranscript],
);
const discardTranscript = useMethod('livechat:discardTranscript');
const discardTranscript = useEndpoint('DELETE', '/v1/livechat/transcript/:rid', { rid });
const handleDiscardTranscript = useCallback(async () => {
try {
await discardTranscript(rid);
await discardTranscript();
dispatchToastMessage({
type: 'success',
message: t('Livechat_transcript_request_has_been_canceled'),
@ -131,7 +131,7 @@ export const useQuickActions = (): {
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
}
}, [closeModal, discardTranscript, dispatchToastMessage, rid, t]);
}, [closeModal, discardTranscript, dispatchToastMessage, t]);
const forwardChat = useEndpoint('POST', '/v1/livechat/room.forward');

@ -180,7 +180,7 @@ export class LivechatRoomsRaw extends BaseRaw<IOmnichannelRoom> implements ILive
};
const params = [...firstParams, usersGroup, project, facet];
return this.col.aggregate(params, { readPreference: readSecondaryPreferred() }).toArray();
return this.col.aggregate(params, { readPreference: readSecondaryPreferred(), allowDiskUse: true }).toArray();
}
async findAllNumberOfAbandonedRooms({

@ -17,7 +17,7 @@ export class LivechatTriggerRaw extends BaseRaw<ILivechatTrigger> implements ILi
return this.find({ enabled: true });
}
updateById(_id: string, data: ILivechatTrigger): Promise<UpdateResult> {
updateById(_id: string, data: Omit<ILivechatTrigger, '_id' | '_updatedAt'>): Promise<UpdateResult> {
return this.updateOne({ _id }, { $set: data } as UpdateFilter<ILivechatTrigger>); // TODO: remove this cast when TypeScript is updated
}
}

@ -3,7 +3,7 @@ import { before, describe, it } from 'mocha';
import type { Response } from 'supertest';
import { getCredentials, api, request, credentials } from '../../../data/api-data';
import { updatePermission, updateSetting } from '../../../data/permissions.helper';
import { removePermissionFromAllRoles, restorePermissionToRoles, updatePermission, updateSetting } from '../../../data/permissions.helper';
describe('LIVECHAT - appearance', function () {
this.retries(0);
@ -32,4 +32,53 @@ describe('LIVECHAT - appearance', function () {
});
});
});
describe('POST livechat/appearance', () => {
it('should fail if user is not logged in', async () => {
await request.post(api('livechat/appearance')).send({}).expect(401);
});
it('should fail if body is not an array', async () => {
await request.post(api('livechat/appearance')).set(credentials).send({}).expect(400);
});
it('should fail if body is an empty array', async () => {
await request.post(api('livechat/appearance')).set(credentials).send([]).expect(400);
});
it('should fail if body does not contain value', async () => {
await request
.post(api('livechat/appearance'))
.set(credentials)
.send([{ name: 'Livechat_title' }])
.expect(400);
});
it('should fail if body does not contain name', async () => {
await request
.post(api('livechat/appearance'))
.set(credentials)
.send([{ value: 'test' }])
.expect(400);
});
it('should fail if user does not have the necessary permission', async () => {
await removePermissionFromAllRoles('view-livechat-manager');
await request
.post(api('livechat/appearance'))
.set(credentials)
.send([{ _id: 'invalid', value: 'test' }])
.expect(403);
});
it('should fail if body contains invalid _id', async () => {
await restorePermissionToRoles('view-livechat-manager');
await request
.post(api('livechat/appearance'))
.set(credentials)
.send([{ _id: 'invalid', value: 'test' }])
.expect(400);
});
it('should update the settings', async () => {
await request
.post(api('livechat/appearance'))
.set(credentials)
.send([{ _id: 'Livechat_title', value: 'test' }])
.expect(200);
});
});
});

@ -4,7 +4,7 @@ import type { Response } from 'supertest';
import { getCredentials, api, request, credentials } from '../../../data/api-data';
import { createTrigger, fetchTriggers } from '../../../data/livechat/triggers';
import { updatePermission, updateSetting } from '../../../data/permissions.helper';
import { removePermissionFromAllRoles, restorePermissionToRoles, updatePermission, updateSetting } from '../../../data/permissions.helper';
describe('LIVECHAT - triggers', function () {
this.retries(0);
@ -17,12 +17,12 @@ describe('LIVECHAT - triggers', function () {
describe('livechat/triggers', () => {
it('should return an "unauthorized error" when the user does not have the necessary permission', async () => {
await updatePermission('view-livechat-manager', []);
await removePermissionFromAllRoles('view-livechat-manager');
await request.get(api('livechat/triggers')).set(credentials).expect('Content-Type', 'application/json').expect(403);
});
it('should return an array of triggers', async () => {
await updatePermission('view-livechat-manager', ['admin']);
await restorePermissionToRoles('view-livechat-manager');
await createTrigger(`test${Date.now()}`);
await request
.get(api('livechat/triggers'))
@ -46,6 +46,167 @@ describe('LIVECHAT - triggers', function () {
});
});
describe('POST livechat/triggers', () => {
it('should fail if user is not logged in', async () => {
await request.post(api('livechat/triggers')).send({}).expect(401);
});
it('should fail if no data is sent', async () => {
await request.post(api('livechat/triggers')).set(credentials).send({}).expect(400);
});
it('should fail if invalid data is sent', async () => {
await request.post(api('livechat/triggers')).set(credentials).send({ name: 'test' }).expect(400);
});
it('should fail if name is not an string', async () => {
await request
.post(api('livechat/triggers'))
.set(credentials)
.send({ name: 1, description: 'test', enabled: true, runOnce: true, conditions: [], actions: [] })
.expect(400);
});
it('should fail if description is not an string', async () => {
await request
.post(api('livechat/triggers'))
.set(credentials)
.send({ name: 'test', description: 1, enabled: true, runOnce: true, conditions: [], actions: [] })
.expect(400);
});
it('should fail if enabled is not an boolean', async () => {
await request
.post(api('livechat/triggers'))
.set(credentials)
.send({ name: 'test', description: 'test', enabled: 1, runOnce: true, conditions: [], actions: [] })
.expect(400);
});
it('should fail if runOnce is not an boolean', async () => {
await request
.post(api('livechat/triggers'))
.set(credentials)
.send({ name: 'test', description: 'test', enabled: true, runOnce: 1, conditions: [], actions: [] })
.expect(400);
});
it('should fail if conditions is not an array', async () => {
await request
.post(api('livechat/triggers'))
.set(credentials)
.send({ name: 'test', description: 'test', enabled: true, runOnce: true, conditions: 1, actions: [] })
.expect(400);
});
it('should fail if actions is not an array', async () => {
await request
.post(api('livechat/triggers'))
.set(credentials)
.send({ name: 'test', description: 'test', enabled: true, runOnce: true, conditions: [], actions: 1 })
.expect(400);
});
it('should fail if conditions is an array with invalid data', async () => {
await request
.post(api('livechat/triggers'))
.set(credentials)
.send({ name: 'test', description: 'test', enabled: true, runOnce: true, conditions: [1], actions: [] })
.expect(400);
});
it('should fail if conditions is an array of objects, but name is not a valid value', async () => {
await request
.post(api('livechat/triggers'))
.set(credentials)
.send({ name: 'test', description: 'test', enabled: true, runOnce: true, conditions: [{ name: 'invalid' }], actions: [] })
.expect(400);
});
it('should fail if actions is an array of invalid values', async () => {
await request
.post(api('livechat/triggers'))
.set(credentials)
.send({ name: 'test', description: 'test', enabled: true, runOnce: true, conditions: [{ name: 'page-url' }], actions: [1] })
.expect(400);
});
it('should fail if actions is an array of objects, but name is not a valid value', async () => {
await request
.post(api('livechat/triggers'))
.set(credentials)
.send({
name: 'test',
description: 'test',
enabled: true,
runOnce: true,
conditions: [{ name: 'page-url', value: 'http://localhost:3000' }],
actions: [{ name: 'invalid' }],
})
.expect(400);
});
it('should fail if actions is an array of objects, but sender is not a valid value', async () => {
await request
.post(api('livechat/triggers'))
.set(credentials)
.send({
name: 'test',
description: 'test',
enabled: true,
runOnce: true,
conditions: [{ name: 'page-url' }],
actions: [{ name: 'send-message', params: { sender: 'invalid' } }],
})
.expect(400);
});
it('should fail if actions is an array of objects, but msg is not a valid value', async () => {
await request
.post(api('livechat/triggers'))
.set(credentials)
.send({
name: 'test',
description: 'test',
enabled: true,
runOnce: true,
conditions: [{ name: 'page-url', value: 'http://localhost:3000' }],
actions: [{ name: 'send-message', params: { sender: 'custom' } }],
})
.expect(400);
});
it('should fail if actions is an array of objects, but name is not a valid value', async () => {
await request
.post(api('livechat/triggers'))
.set(credentials)
.send({
name: 'test',
description: 'test',
enabled: true,
runOnce: true,
conditions: [{ name: 'page-url', value: 'http://localhost:3000' }],
actions: [{ name: 'send-message', params: { sender: 'custom', msg: 'test', name: {} } }],
})
.expect(400);
});
it('should fail if user doesnt have view-livechat-manager permission', async () => {
await removePermissionFromAllRoles('view-livechat-manager');
await request
.post(api('livechat/triggers'))
.set(credentials)
.send({
name: 'test',
description: 'test',
enabled: true,
runOnce: true,
conditions: [{ name: 'page-url', value: 'http://localhost:3000' }],
actions: [{ name: 'send-message', params: { sender: 'custom', msg: 'test', name: 'test' } }],
})
.expect(403);
});
it('should save a new trigger', async () => {
await restorePermissionToRoles('view-livechat-manager');
await request
.post(api('livechat/triggers'))
.set(credentials)
.send({
name: 'test',
description: 'test',
enabled: true,
runOnce: true,
conditions: [{ name: 'page-url', value: 'http://localhost:3000' }],
actions: [{ name: 'send-message', params: { sender: 'custom', msg: 'test', name: 'test' } }],
})
.expect(200);
});
});
describe('livechat/triggers/:id', () => {
it('should return an "unauthorized error" when the user does not have the necessary permission', async () => {
await updatePermission('view-livechat-manager', []);

@ -4,9 +4,9 @@ import { before, describe, it } from 'mocha';
import { getCredentials, api, request, credentials } from '../../../data/api-data';
import { createCustomField, deleteCustomField } from '../../../data/livechat/custom-fields';
import { addOrRemoveAgentFromDepartment, createDepartmentWithAnOnlineAgent } from '../../../data/livechat/department';
import { createVisitor, createLivechatRoom, makeAgentUnavailable } from '../../../data/livechat/rooms';
import { createVisitor, createLivechatRoom, makeAgentUnavailable, closeOmnichannelRoom } from '../../../data/livechat/rooms';
import { createBotAgent, getRandomVisitorToken } from '../../../data/livechat/users';
import { updateSetting } from '../../../data/permissions.helper';
import { removePermissionFromAllRoles, restorePermissionToRoles, updatePermission, updateSetting } from '../../../data/permissions.helper';
import { IS_EE } from '../../../e2e/config/constants';
describe('LIVECHAT - Utils', function () {
@ -276,6 +276,140 @@ describe('LIVECHAT - Utils', function () {
expect(body).to.have.property('success', true);
});
});
describe('livechat/transcript/:rid', () => {
it('should fail if user is not authenticated', async () => {
await request.delete(api('livechat/transcript/rid')).send({}).expect(401);
});
it('should fail if user doesnt have "send-omnichannel-chat-transcript" permission', async () => {
const user = await createVisitor();
const room = await createLivechatRoom(user.token);
await removePermissionFromAllRoles('send-omnichannel-chat-transcript');
await request
.delete(api(`livechat/transcript/${room._id}`))
.set(credentials)
.send({})
.expect(403);
});
it('should fail if rid is not a valid room id', async () => {
await restorePermissionToRoles('send-omnichannel-chat-transcript');
await request.delete(api('livechat/transcript/rid')).set(credentials).send({}).expect(400);
});
it('should fail if room is not open', async () => {
const user = await createVisitor();
const room = await createLivechatRoom(user.token);
await closeOmnichannelRoom(room._id);
await request
.delete(api(`livechat/transcript/${room._id}`))
.set(credentials)
.send({})
.expect(400);
});
it('should fail if room doesnt have transcript requested', async () => {
const user = await createVisitor();
const room = await createLivechatRoom(user.token);
await request
.delete(api(`livechat/transcript/${room._id}`))
.set(credentials)
.send({})
.expect(400);
});
it('should remove transcript if all good', async () => {
const user = await createVisitor();
const room = await createLivechatRoom(user.token);
await request
.post(api(`livechat/transcript/${room._id}`))
.set(credentials)
.send({ email: 'abc@abc.com', subject: 'test' })
.expect(200);
await request
.delete(api(`livechat/transcript/${room._id}`))
.set(credentials)
.send({})
.expect(200);
});
});
describe('POST livechat/transcript/:rid', () => {
it('should fail if user is not authenticated', async () => {
await request.post(api('livechat/transcript/rid')).send({}).expect(401);
});
it('should fail if "email" param is not sent', async () => {
const user = await createVisitor();
const room = await createLivechatRoom(user.token);
await request
.post(api(`livechat/transcript/${room._id}`))
.set(credentials)
.send({})
.expect(400);
});
it('should fail if "subject" param is not sent', async () => {
const user = await createVisitor();
const room = await createLivechatRoom(user.token);
await request
.post(api(`livechat/transcript/${room._id}`))
.set(credentials)
.send({ email: 'abc@abc.xmz' })
.expect(400);
});
it('should fail if user doesnt have "send-omnichannel-chat-transcript" permission', async () => {
const user = await createVisitor();
const room = await createLivechatRoom(user.token);
await updatePermission('send-omnichannel-chat-transcript', []);
await request
.post(api(`livechat/transcript/${room._id}`))
.set(credentials)
.send({ email: 'abc@abc.com', subject: 'test' })
.expect(403);
});
it('should fail if rid is not a valid room id', async () => {
await updatePermission('send-omnichannel-chat-transcript', ['livechat-manager', 'admin']);
await request.post(api('livechat/transcript/rid')).set(credentials).send({ email: 'abc@abc.com', subject: 'test' }).expect(400);
});
it('should fail if room is not open', async () => {
const user = await createVisitor();
const room = await createLivechatRoom(user.token);
await closeOmnichannelRoom(room._id);
await request
.post(api(`livechat/transcript/${room._id}`))
.set(credentials)
.send({ email: 'abc@abc.com', subject: 'test' })
.expect(400);
});
it('should fail if room already has transcript requested', async () => {
const user = await createVisitor();
const room = await createLivechatRoom(user.token);
await request
.post(api(`livechat/transcript/${room._id}`))
.set(credentials)
.send({ email: 'abc@abc.com', subject: 'test' })
.expect(200);
await request
.post(api(`livechat/transcript/${room._id}`))
.set(credentials)
.send({ email: 'abc@abc.com', subject: 'test' })
.expect(400);
});
it('should request transcript if all good', async () => {
const user = await createVisitor();
const room = await createLivechatRoom(user.token);
await request
.post(api(`livechat/transcript/${room._id}`))
.set(credentials)
.send({ email: 'abc@abc.com', subject: 'test' })
.expect(200);
});
});
describe('livechat/visitor.callStatus', () => {
it('should fail if token is not in body params', async () => {
const { body } = await request.post(api('livechat/visitor.callStatus')).set(credentials).send({});

@ -5,5 +5,5 @@ import type { IBaseModel } from './IBaseModel';
export interface ILivechatTriggerModel extends IBaseModel<ILivechatTrigger> {
findEnabled(): FindCursor<ILivechatTrigger>;
updateById(_id: string, data: ILivechatTrigger): Promise<UpdateResult>;
updateById(_id: string, data: Omit<ILivechatTrigger, '_id' | '_updatedAt'>): Promise<UpdateResult>;
}

@ -21,6 +21,8 @@ import type {
IOmnichannelServiceLevelAgreements,
ILivechatPriority,
LivechatDepartmentDTO,
ILivechatTriggerCondition,
ILivechatTriggerAction,
} from '@rocket.chat/core-typings';
import { ILivechatAgentStatus } from '@rocket.chat/core-typings';
import Ajv from 'ajv';
@ -2982,11 +2984,149 @@ const POSTomnichannelIntegrationsSchema = {
export const isPOSTomnichannelIntegrations = ajv.compile<POSTomnichannelIntegrations>(POSTomnichannelIntegrationsSchema);
type POSTLivechatTranscriptRequestParams = {
email: string;
subject: string;
};
const POSTLivechatTranscriptRequestParamsSchema = {
type: 'object',
properties: {
email: {
type: 'string',
},
subject: {
type: 'string',
},
},
required: ['email', 'subject'],
additionalProperties: false,
};
export const isPOSTLivechatTranscriptRequestParams = ajv.compile<POSTLivechatTranscriptRequestParams>(
POSTLivechatTranscriptRequestParamsSchema,
);
type POSTLivechatTriggersParams = {
name: string;
description: string;
enabled: boolean;
runOnce: boolean;
conditions: ILivechatTriggerCondition[];
actions: ILivechatTriggerAction[];
_id?: string;
};
const POSTLivechatTriggersParamsSchema = {
type: 'object',
properties: {
name: {
type: 'string',
},
description: {
type: 'string',
},
enabled: {
type: 'boolean',
},
runOnce: {
type: 'boolean',
},
conditions: {
type: 'array',
items: {
type: 'object',
properties: {
name: {
type: 'string',
enum: ['time-on-site', 'page-url', 'chat-opened-by-visitor'],
},
value: {
type: 'string',
nullable: true,
},
},
required: ['name', 'value'],
additionalProperties: false,
},
minItems: 1,
},
actions: {
type: 'array',
items: {
type: 'object',
properties: {
name: {
type: 'string',
enum: ['send-message'],
},
params: {
type: 'object',
nullable: true,
properties: {
sender: {
type: 'string',
enum: ['queue', 'custom'],
},
msg: {
type: 'string',
},
name: {
type: 'string',
nullable: true,
},
},
required: ['sender', 'msg'],
additionalProperties: false,
},
},
required: ['name'],
additionalProperties: false,
},
minItems: 1,
},
_id: {
type: 'string',
nullable: true,
},
},
required: ['name', 'description', 'enabled', 'runOnce', 'conditions', 'actions'],
additionalProperties: false,
};
export const isPOSTLivechatTriggersParams = ajv.compile<POSTLivechatTriggersParams>(POSTLivechatTriggersParamsSchema);
type POSTLivechatAppearanceParams = {
_id: string;
value: string | boolean | number;
}[];
const POSTLivechatAppearanceParamsSchema = {
type: 'array',
items: {
type: 'object',
properties: {
_id: {
type: 'string',
},
value: {
type: ['string', 'boolean', 'number'],
},
},
required: ['_id', 'value'],
additionalProperties: false,
},
minItems: 1,
};
export const isPOSTLivechatAppearanceParams = ajv.compile<POSTLivechatAppearanceParams>(POSTLivechatAppearanceParamsSchema);
export type OmnichannelEndpoints = {
'/v1/livechat/appearance': {
GET: () => {
appearance: ISetting[];
};
POST: (params: POSTLivechatAppearanceParams) => void;
};
'/v1/livechat/visitors.info': {
GET: (params: LivechatVisitorsInfo) => {
@ -3287,6 +3427,10 @@ export type OmnichannelEndpoints = {
'/v1/livechat/transcript': {
POST: (params: POSTLivechatTranscriptParams) => { message: string };
};
'/v1/livechat/transcript/:rid': {
DELETE: () => void;
POST: (params: POSTLivechatTranscriptRequestParams) => void;
};
'/v1/livechat/offline.message': {
POST: (params: POSTLivechatOfflineMessageParams) => { message: string };
};
@ -3351,6 +3495,7 @@ export type OmnichannelEndpoints = {
};
'/v1/livechat/triggers': {
GET: (params: GETLivechatTriggersParams) => PaginatedResult<{ triggers: WithId<ILivechatTrigger>[] }>;
POST: (params: POSTLivechatTriggersParams) => void;
};
'/v1/livechat/triggers/:_id': {
GET: () => { trigger: ILivechatTrigger | null };

Loading…
Cancel
Save