From 9cf668926e3864b1f263653150dd5c21cb0bc6c7 Mon Sep 17 00:00:00 2001 From: murtaza98 Date: Wed, 27 Jul 2022 21:17:50 +0530 Subject: [PATCH 1/7] Chore: Add tests for livechat/room.forward endpoint --- .../meteor/app/livechat/server/api/v1/room.js | 14 +- .../app/livechat/server/lib/Livechat.js | 17 ++- .../app/models/server/models/Messages.js | 23 ---- apps/meteor/tests/data/livechat/department.js | 30 +++-- apps/meteor/tests/data/livechat/rooms.js | 24 +++- .../tests/end-to-end/api/livechat/00-rooms.ts | 123 +++++++++++++++++- packages/rest-typings/src/v1/omnichannel.ts | 40 ++++++ 7 files changed, 226 insertions(+), 45 deletions(-) diff --git a/apps/meteor/app/livechat/server/api/v1/room.js b/apps/meteor/app/livechat/server/api/v1/room.js index 6a0f991795ae..d405f84bc04d 100644 --- a/apps/meteor/app/livechat/server/api/v1/room.js +++ b/apps/meteor/app/livechat/server/api/v1/room.js @@ -199,10 +199,20 @@ API.v1.addRoute('livechat/room.survey', { API.v1.addRoute( 'livechat/room.forward', - { authRequired: true }, + { authRequired: true, permissionsRequired: ['view-l-room', 'transfer-livechat-guest'] }, { post() { - API.v1.success(Meteor.runAsUser(this.userId, () => Meteor.call('livechat:transfer', this.bodyParams))); + let result = false; + + Meteor.runAsUser(this.userId, () => { + result = Meteor.call('livechat:transfer', this.bodyParams); + }); + + if (result) { + return API.v1.success(); + } + + return API.v1.failure(); }, }, ); diff --git a/apps/meteor/app/livechat/server/lib/Livechat.js b/apps/meteor/app/livechat/server/lib/Livechat.js index 218576effe4d..92784d96d555 100644 --- a/apps/meteor/app/livechat/server/lib/Livechat.js +++ b/apps/meteor/app/livechat/server/lib/Livechat.js @@ -710,7 +710,22 @@ export const Livechat = { }, }; - return Messages.createTransferHistoryWithRoomIdMessageAndUser(room._id, '', { _id, username }, transfer); + const type = 'livechat_transfer_history'; + const transferMessage = { + t: type, + rid: room._id, + ts: new Date(), + msg: '', + u: { + _id, + username, + }, + groupable: false, + }; + + Object.assign(transferMessage, transfer); + + sendMessage(transferredBy, transferMessage, room); }, async transfer(room, guest, transferData) { diff --git a/apps/meteor/app/models/server/models/Messages.js b/apps/meteor/app/models/server/models/Messages.js index 813568a6234f..e570c67ed779 100644 --- a/apps/meteor/app/models/server/models/Messages.js +++ b/apps/meteor/app/models/server/models/Messages.js @@ -818,29 +818,6 @@ export class Messages extends Base { return record; } - createTransferHistoryWithRoomIdMessageAndUser(roomId, message, user, extraData) { - const type = 'livechat_transfer_history'; - const record = { - t: type, - rid: roomId, - ts: new Date(), - msg: message, - u: { - _id: user._id, - username: user.username, - }, - groupable: false, - }; - - if (settings.get('Message_Read_Receipt_Enabled')) { - record.unread = true; - } - Object.assign(record, extraData); - - record._id = this.insertOrUpsert(record); - return record; - } - createTranscriptHistoryWithRoomIdMessageAndUser(roomId, message, user, extraData) { const type = 'livechat_transcript_history'; const record = { diff --git a/apps/meteor/tests/data/livechat/department.js b/apps/meteor/tests/data/livechat/department.js index e6502fb54d2e..bb8741f1e8e6 100644 --- a/apps/meteor/tests/data/livechat/department.js +++ b/apps/meteor/tests/data/livechat/department.js @@ -1,24 +1,30 @@ -import { api, credentials, request } from '../api-data'; +import { api, credentials, methodCall, request } from '../api-data'; +import { faker } from '@faker-js/faker'; export const createDepartment = () => new Promise((resolve, reject) => { request - .post(api('livechat/department')) + .post(methodCall('livechat:saveDepartment')) + .set(credentials) .send({ - department: { - enabled: false, - email: 'email@email.com', - showOnRegistration: true, - showOnOfflineForm: true, - name: `new department ${Date.now()}`, - description: 'created from api', - }, + message: JSON.stringify({ + method: 'livechat:saveDepartment', + params: ['', { + enabled: true, + email: faker.internet.email(), + showOnRegistration: true, + showOnOfflineForm: true, + name: `new department ${Date.now()}`, + description: 'created from api', + }, []], + id: 'id', + msg: 'method', + }), }) - .set(credentials) .end((err, res) => { if (err) { return reject(err); } - resolve(res.body.department); + resolve(JSON.parse(res.body.message).result); }); }); diff --git a/apps/meteor/tests/data/livechat/rooms.js b/apps/meteor/tests/data/livechat/rooms.js index f915ee70bfa0..6bbdac258796 100644 --- a/apps/meteor/tests/data/livechat/rooms.js +++ b/apps/meteor/tests/data/livechat/rooms.js @@ -9,6 +9,20 @@ export const createLivechatRoom = (visitorToken) => .end((err, res) => resolve(res.body.room)); }); +export const getLivechatRoomInfo = (roomId) => { + return new Promise((resolve /* , reject*/) => { + request + .get(api('channels.info')) + .set(credentials) + .query({ + roomId, + }) + .end((_err, req) => { + resolve(req.body.channel); + }); + }); +} + export const createVisitor = () => new Promise((resolve, reject) => { const token = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); @@ -39,13 +53,13 @@ export const createVisitor = () => }); }); -export const createAgent = () => +export const createAgent = (overrideUsername) => new Promise((resolve, reject) => { request .post(api('livechat/users/agent')) .set(credentials) .send({ - username: adminUsername, + username: overrideUsername || adminUsername, }) .end((err, res) => { if (err) { @@ -71,9 +85,9 @@ export const createManager = () => }); }); -export const makeAgentAvailable = () => +export const makeAgentAvailable = (overrideCredentials) => new Promise((resolve, reject) => { - request.post(api('users.setStatus')).set(credentials).send({ + request.post(api('users.setStatus')).set(overrideCredentials || credentials).send({ message: '', status: 'online', }).end((err, res) => { @@ -82,7 +96,7 @@ export const makeAgentAvailable = () => } request .post(methodCall('livechat/changeLivechatStatus')) - .set(credentials) + .set(overrideCredentials || credentials) .send({ message: JSON.stringify({ method: 'livechat/changeLivechatStatus', diff --git a/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts b/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts index 5ef8b603990f..e37fadf12aeb 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts @@ -1,10 +1,13 @@ import { expect } from 'chai'; -import { IOmnichannelRoom, IVisitor } from '@rocket.chat/core-typings'; +import { IOmnichannelRoom, IUser, IVisitor } from '@rocket.chat/core-typings'; import { Response } from 'supertest'; import { getCredentials, api, request, credentials } from '../../../data/api-data.js'; -import { createVisitor, createLivechatRoom, createAgent, makeAgentAvailable } from '../../../data/livechat/rooms.js'; +import { createVisitor, createLivechatRoom, createAgent, makeAgentAvailable, getLivechatRoomInfo } from '../../../data/livechat/rooms.js'; import { updatePermission, updateSetting } from '../../../data/permissions.helper'; +import { createUser, login } from '../../../data/users.helper.js'; +import { adminUsername, password } from '../../../data/user.js'; +import { createDepartment } from '../../../data/livechat/department.js'; describe('LIVECHAT - rooms', function () { this.retries(0); @@ -240,4 +243,120 @@ describe('LIVECHAT - rooms', function () { .end(done); }); }); + + describe('livechat/room.forward', () => { + it('should return an "unauthorized error" when the user does not have "view-l-room" permission', async () => { + await updatePermission('transfer-livechat-guest', ['admin']); + await updatePermission('view-l-room', []); + + await request + .post(api('livechat/room.forward')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(403) + .expect((res: Response) => { + expect(res.body).to.have.property('success', false); + expect(res.body.error).to.have.string('unauthorized'); + }); + }); + + it('should return an "unauthorized error" when the user does not have "transfer-livechat-guest" permission', async () => { + await updatePermission('transfer-livechat-guest', []); + await updatePermission('view-l-room', ['admin']); + + await request + .post(api('livechat/room.forward')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(403) + .expect((res: Response) => { + expect(res.body).to.have.property('success', false); + expect(res.body.error).to.have.string('unauthorized'); + }); + }); + + it('should not be successful when no target (userId or departmentId) was specified', async () => { + await updatePermission('transfer-livechat-guest', ['admin']); + await updatePermission('view-l-room', ['admin']); + + await request + .post(api('livechat/room.forward')) + .set(credentials) + .send({ + roomId: room._id, + }) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res: Response) => { + expect(res.body).to.have.property('success', false); + }); + }); + + it('should return a success message when transferred successfully to agent', async () => { + const user: IUser = await createUser(); + const createdUserCredentials = await login(user.username, password); + await createAgent(user.username); + await makeAgentAvailable(createdUserCredentials); + + const newVisitor = await createVisitor(); + const newRoom = await createLivechatRoom(newVisitor.token); + + await request + .post(api('livechat/room.forward')) + .set(credentials) + .send({ + roomId: newRoom._id, + userId: user._id, + clientAction: true, + comment: 'test comment', + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + }); + + const latestRoom = await getLivechatRoomInfo(newRoom._id); + + expect(latestRoom).to.have.property('lastMessage'); + expect(latestRoom.lastMessage?.t).to.be.equal('livechat_transfer_history'); + expect(latestRoom.lastMessage?.u?.username).to.be.equal(adminUsername); + expect((latestRoom.lastMessage as any)?.transferData?.comment).to.be.equal('test comment'); + expect((latestRoom.lastMessage as any)?.transferData?.scope).to.be.equal('agent'); + expect((latestRoom.lastMessage as any)?.transferData?.transferredTo?.username).to.be.equal(user.username); + }); + + it('should return a success message when transferred successfully to a department', async () => { + const department = await createDepartment(); + const newVisitor = await createVisitor(); + const newRoom = await createLivechatRoom(newVisitor.token); + + await request + .post(api('livechat/room.forward')) + .set(credentials) + .send({ + roomId: newRoom._id, + departmentId: department._id, + clientAction: true, + comment: 'test comment', + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + }); + + const latestRoom = await getLivechatRoomInfo(newRoom._id); + + expect(latestRoom).to.have.property('departmentId'); + expect(latestRoom.departmentId).to.be.equal(department._id); + + expect(latestRoom).to.have.property('lastMessage'); + expect(latestRoom.lastMessage?.t).to.be.equal('livechat_transfer_history'); + expect(latestRoom.lastMessage?.u?.username).to.be.equal(adminUsername); + expect((latestRoom.lastMessage as any)?.transferData?.comment).to.be.equal('test comment'); + expect((latestRoom.lastMessage as any)?.transferData?.scope).to.be.equal('department'); + expect((latestRoom.lastMessage as any)?.transferData?.nextDepartment?._id).to.be.equal(department._id); + }); + }); }); diff --git a/packages/rest-typings/src/v1/omnichannel.ts b/packages/rest-typings/src/v1/omnichannel.ts index 674acb0b4f35..3b0eab027932 100644 --- a/packages/rest-typings/src/v1/omnichannel.ts +++ b/packages/rest-typings/src/v1/omnichannel.ts @@ -268,6 +268,43 @@ const LiveChatRoomJoinSchema = { export const isLiveChatRoomJoinProps = ajv.compile(LiveChatRoomJoinSchema); +type LiveChatRoomForward = { + roomId: string; + userId?: string; + departmentId?: string; + comment?: string; + clientAction?: boolean; +}; + +const LiveChatRoomForwardSchema = { + type: 'object', + properties: { + roomId: { + type: 'string', + }, + userId: { + type: 'string', + nullable: true, + }, + departmentId: { + type: 'string', + nullable: true, + }, + comment: { + type: 'string', + nullable: true, + }, + clientAction: { + type: 'boolean', + nullable: true, + }, + }, + required: ['roomId'], + additionalProperties: false, +}; + +export const isLiveChatRoomForwardProps = ajv.compile(LiveChatRoomForwardSchema); + type LivechatMonitorsListProps = PaginatedRequest<{ text: string }>; const LivechatMonitorsListSchema = { @@ -942,6 +979,9 @@ export type OmnichannelEndpoints = { '/v1/livechat/room.join': { GET: (params: LiveChatRoomJoin) => { success: boolean }; }; + '/v1/livechat/room.forward': { + GET: (params: LiveChatRoomForward) => { success: boolean }; + }; '/v1/livechat/monitors': { GET: (params: LivechatMonitorsListProps) => PaginatedResult<{ monitors: ILivechatMonitor[]; From ac0f02bac69216d67d378946898ce07ba247bbd0 Mon Sep 17 00:00:00 2001 From: murtaza98 Date: Fri, 29 Jul 2022 13:39:43 +0530 Subject: [PATCH 2/7] Avoid transferring to same agent & department within tests --- apps/meteor/tests/data/livechat/department.js | 30 --------- apps/meteor/tests/data/livechat/department.ts | 61 +++++++++++++++++++ apps/meteor/tests/data/livechat/rooms.js | 3 +- .../tests/end-to-end/api/livechat/00-rooms.ts | 32 ++++++---- 4 files changed, 83 insertions(+), 43 deletions(-) delete mode 100644 apps/meteor/tests/data/livechat/department.js create mode 100644 apps/meteor/tests/data/livechat/department.ts diff --git a/apps/meteor/tests/data/livechat/department.js b/apps/meteor/tests/data/livechat/department.js deleted file mode 100644 index bb8741f1e8e6..000000000000 --- a/apps/meteor/tests/data/livechat/department.js +++ /dev/null @@ -1,30 +0,0 @@ -import { api, credentials, methodCall, request } from '../api-data'; -import { faker } from '@faker-js/faker'; - -export const createDepartment = () => - new Promise((resolve, reject) => { - request - .post(methodCall('livechat:saveDepartment')) - .set(credentials) - .send({ - message: JSON.stringify({ - method: 'livechat:saveDepartment', - params: ['', { - enabled: true, - email: faker.internet.email(), - showOnRegistration: true, - showOnOfflineForm: true, - name: `new department ${Date.now()}`, - description: 'created from api', - }, []], - id: 'id', - msg: 'method', - }), - }) - .end((err, res) => { - if (err) { - return reject(err); - } - resolve(JSON.parse(res.body.message).result); - }); - }); diff --git a/apps/meteor/tests/data/livechat/department.ts b/apps/meteor/tests/data/livechat/department.ts new file mode 100644 index 000000000000..8c8a73d3352a --- /dev/null +++ b/apps/meteor/tests/data/livechat/department.ts @@ -0,0 +1,61 @@ +import { api, credentials, methodCall, request } from '../api-data'; +import { faker } from '@faker-js/faker'; +import { createUser, login } from '../users.helper'; +import { password } from '../user'; +import { createAgent, makeAgentAvailable } from './rooms'; +import { ILivechatDepartment, IUser } from '@rocket.chat/core-typings'; + +export const createDepartmentWithMethod = (initialAgents: { agentId: string, username: string }[] = []) => + new Promise((resolve, reject) => { + request + .post(methodCall('livechat:saveDepartment')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'livechat:saveDepartment', + params: ['', { + enabled: true, + email: faker.internet.email(), + showOnRegistration: true, + showOnOfflineForm: true, + name: `new department ${Date.now()}`, + description: 'created from api', + }, initialAgents], + id: 'id', + msg: 'method', + }), + }) + .end((err: any, res: any) => { + if (err) { + return reject(err); + } + resolve(JSON.parse(res.body.message).result); + }); + }); + + +export const createDepartmentWithAnOnlineAgent = async (): Promise<{department: ILivechatDepartment, agent: IUser}> => { + const agent: IUser = await createUser(); + const createdUserCredentials = await login(agent.username, password); + await createAgent(agent.username); + await makeAgentAvailable(createdUserCredentials); + + const department = await createDepartmentWithMethod() as ILivechatDepartment; + + await addOrRemoveAgentFromDepartment(department._id, {agentId: agent._id, username: (agent.username as string)}, true); + + return { + department, + agent, + }; +}; + +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); + } +} diff --git a/apps/meteor/tests/data/livechat/rooms.js b/apps/meteor/tests/data/livechat/rooms.js index 6bbdac258796..2b8571846de7 100644 --- a/apps/meteor/tests/data/livechat/rooms.js +++ b/apps/meteor/tests/data/livechat/rooms.js @@ -23,7 +23,7 @@ export const getLivechatRoomInfo = (roomId) => { }); } -export const createVisitor = () => +export const createVisitor = (departmentId) => new Promise((resolve, reject) => { const token = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); const email = `${token}@${token}.com`; @@ -42,6 +42,7 @@ export const createVisitor = () => token, phone, customFields: [{ key: 'address', value: 'Rocket.Chat street', overwrite: true }], + ...departmentId && { department: departmentId }, }, }) .end((err, res) => { diff --git a/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts b/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts index e37fadf12aeb..67d873a589ed 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts @@ -7,7 +7,7 @@ import { createVisitor, createLivechatRoom, createAgent, makeAgentAvailable, get import { updatePermission, updateSetting } from '../../../data/permissions.helper'; import { createUser, login } from '../../../data/users.helper.js'; import { adminUsername, password } from '../../../data/user.js'; -import { createDepartment } from '../../../data/livechat/department.js'; +import { createDepartmentWithAnOnlineAgent } from '../../../data/livechat/department'; describe('LIVECHAT - rooms', function () { this.retries(0); @@ -293,20 +293,26 @@ describe('LIVECHAT - rooms', function () { }); it('should return a success message when transferred successfully to agent', async () => { - const user: IUser = await createUser(); - const createdUserCredentials = await login(user.username, password); - await createAgent(user.username); - await makeAgentAvailable(createdUserCredentials); + const initialAgentAssignedToChat: IUser = await createUser(); + const initialAgentCredentials = await login(initialAgentAssignedToChat.username, password); + await createAgent(initialAgentAssignedToChat.username); + await makeAgentAvailable(initialAgentCredentials); const newVisitor = await createVisitor(); + // at this point, the chat will get transferred to agent "user" const newRoom = await createLivechatRoom(newVisitor.token); + const forwardChatToUser: IUser = await createUser(); + const forwardChatToUserCredentials = await login(forwardChatToUser.username, password); + await createAgent(forwardChatToUser.username); + await makeAgentAvailable(forwardChatToUserCredentials); + await request .post(api('livechat/room.forward')) .set(credentials) .send({ roomId: newRoom._id, - userId: user._id, + userId: forwardChatToUser._id, clientAction: true, comment: 'test comment', }) @@ -323,12 +329,14 @@ describe('LIVECHAT - rooms', function () { expect(latestRoom.lastMessage?.u?.username).to.be.equal(adminUsername); expect((latestRoom.lastMessage as any)?.transferData?.comment).to.be.equal('test comment'); expect((latestRoom.lastMessage as any)?.transferData?.scope).to.be.equal('agent'); - expect((latestRoom.lastMessage as any)?.transferData?.transferredTo?.username).to.be.equal(user.username); + expect((latestRoom.lastMessage as any)?.transferData?.transferredTo?.username).to.be.equal(forwardChatToUser.username); }); it('should return a success message when transferred successfully to a department', async () => { - const department = await createDepartment(); - const newVisitor = await createVisitor(); + const { department: initialDepartment } = await createDepartmentWithAnOnlineAgent(); + const { department: forwardToDepartment } = await createDepartmentWithAnOnlineAgent(); + + const newVisitor = await createVisitor(initialDepartment._id); const newRoom = await createLivechatRoom(newVisitor.token); await request @@ -336,7 +344,7 @@ describe('LIVECHAT - rooms', function () { .set(credentials) .send({ roomId: newRoom._id, - departmentId: department._id, + departmentId: forwardToDepartment._id, clientAction: true, comment: 'test comment', }) @@ -349,14 +357,14 @@ describe('LIVECHAT - rooms', function () { const latestRoom = await getLivechatRoomInfo(newRoom._id); expect(latestRoom).to.have.property('departmentId'); - expect(latestRoom.departmentId).to.be.equal(department._id); + expect(latestRoom.departmentId).to.be.equal(forwardToDepartment._id); expect(latestRoom).to.have.property('lastMessage'); expect(latestRoom.lastMessage?.t).to.be.equal('livechat_transfer_history'); expect(latestRoom.lastMessage?.u?.username).to.be.equal(adminUsername); expect((latestRoom.lastMessage as any)?.transferData?.comment).to.be.equal('test comment'); expect((latestRoom.lastMessage as any)?.transferData?.scope).to.be.equal('department'); - expect((latestRoom.lastMessage as any)?.transferData?.nextDepartment?._id).to.be.equal(department._id); + expect((latestRoom.lastMessage as any)?.transferData?.nextDepartment?._id).to.be.equal(forwardToDepartment._id); }); }); }); From 28d7551476292f11746131570496ca5a2de46673 Mon Sep 17 00:00:00 2001 From: murtaza98 Date: Fri, 5 Aug 2022 18:43:12 +0530 Subject: [PATCH 3/7] Fix builds --- apps/meteor/tests/data/livechat/department.ts | 3 ++- apps/meteor/tests/data/livechat/rooms.ts | 6 ++---- apps/meteor/tests/data/livechat/utils.ts | 2 ++ 3 files changed, 6 insertions(+), 5 deletions(-) create mode 100644 apps/meteor/tests/data/livechat/utils.ts diff --git a/apps/meteor/tests/data/livechat/department.ts b/apps/meteor/tests/data/livechat/department.ts index 91cf1bd29e45..8a04f4297b46 100644 --- a/apps/meteor/tests/data/livechat/department.ts +++ b/apps/meteor/tests/data/livechat/department.ts @@ -4,6 +4,7 @@ import { api, credentials, methodCall, request } from '../api-data'; import { password } from '../user'; import { createUser, login } from '../users.helper'; import { createAgent, makeAgentAvailable } from './rooms'; +import { DummyResponse } from './utils'; export const createDepartment = (): Promise => new Promise((resolve, reject) => { @@ -20,7 +21,7 @@ export const createDepartment = (): Promise => }, }) .set(credentials) - .end((err, res) => { + .end((err: Error, res: DummyResponse) => { if (err) { return reject(err); } diff --git a/apps/meteor/tests/data/livechat/rooms.ts b/apps/meteor/tests/data/livechat/rooms.ts index 412bd7102f61..14904bcba5bd 100644 --- a/apps/meteor/tests/data/livechat/rooms.ts +++ b/apps/meteor/tests/data/livechat/rooms.ts @@ -1,9 +1,7 @@ import type { IInquiry, ILivechatAgent, ILivechatDepartment, ILivechatVisitor, IOmnichannelRoom } from '@rocket.chat/core-typings'; import { api, credentials, methodCall, request } from '../api-data'; import { adminUsername } from '../user'; - -type DummyResponse = - E extends 'wrapped' ? { body: { [k: string]: T } } : { body: T }; +import { DummyResponse } from './utils'; export const createLivechatRoom = (visitorToken: string): Promise => new Promise((resolve) => { @@ -165,7 +163,7 @@ export const getLivechatRoomInfo = (roomId: string): Promise = .query({ roomId, }) - .end((_err, req) => { + .end((_err: Error, req: DummyResponse) => { resolve(req.body.channel); }); }); diff --git a/apps/meteor/tests/data/livechat/utils.ts b/apps/meteor/tests/data/livechat/utils.ts new file mode 100644 index 000000000000..676d3edb8d63 --- /dev/null +++ b/apps/meteor/tests/data/livechat/utils.ts @@ -0,0 +1,2 @@ +export type DummyResponse = + E extends 'wrapped' ? { body: { [k: string]: T } } : { body: T }; From 445dee48dbed1dc5cd3c3351fd0524b8807e6203 Mon Sep 17 00:00:00 2001 From: murtaza98 Date: Sun, 7 Aug 2022 18:20:09 +0530 Subject: [PATCH 4/7] Fix error translations not working on client --- .../meteor/app/livechat/server/api/v1/room.js | 34 ++++++++++++++----- apps/meteor/app/livechat/server/lib/Helper.js | 28 ++++----------- .../app/livechat/server/lib/Livechat.js | 2 +- .../QuickActions/hooks/useQuickActions.tsx | 4 +-- .../hooks/checkAgentBeforeTakeInquiry.ts | 3 +- .../tests/end-to-end/api/livechat/00-rooms.ts | 6 ++++ packages/rest-typings/src/v1/omnichannel.ts | 2 +- 7 files changed, 43 insertions(+), 36 deletions(-) diff --git a/apps/meteor/app/livechat/server/api/v1/room.js b/apps/meteor/app/livechat/server/api/v1/room.js index be50781e10a1..b21dbaef6ba8 100644 --- a/apps/meteor/app/livechat/server/api/v1/room.js +++ b/apps/meteor/app/livechat/server/api/v1/room.js @@ -3,6 +3,8 @@ import { Match, check } from 'meteor/check'; import { Random } from 'meteor/random'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { OmnichannelSourceType } from '@rocket.chat/core-typings'; +import { LivechatVisitors, Users } from '@rocket.chat/models'; +import { isLiveChatRoomForwardProps } from '@rocket.chat/rest-typings'; import { settings as rcSettings } from '../../../../settings/server'; import { Messages, LivechatRooms } from '../../../../models/server'; @@ -195,20 +197,34 @@ API.v1.addRoute('livechat/room.survey', { API.v1.addRoute( 'livechat/room.forward', - { authRequired: true, permissionsRequired: ['view-l-room', 'transfer-livechat-guest'] }, + { authRequired: true, permissionsRequired: ['view-l-room', 'transfer-livechat-guest'], validateParams: isLiveChatRoomForwardProps }, { - post() { - let result = false; + async post() { + const transferData = this.bodyParams; - Meteor.runAsUser(this.userId, () => { - result = Meteor.call('livechat:transfer', this.bodyParams); - }); + const room = await LivechatRooms.findOneById(this.bodyParams.roomId); + if (!room || room.t !== 'l') { + throw new Error('error-invalid-room', 'Invalid room'); + } - if (result) { - return API.v1.success(); + if (!room.open) { + throw new Error('This_conversation_is_already_closed'); } - return API.v1.failure(); + const guest = await LivechatVisitors.findOneById(room.v && room.v._id); + transferData.transferredBy = normalizeTransferredByData(Meteor.user() || {}, room); + if (transferData.userId) { + const userToTransfer = await Users.findOneById(transferData.userId); + transferData.transferredTo = { + _id: userToTransfer._id, + username: userToTransfer.username, + name: userToTransfer.name, + }; + } + + const chatForwardedResult = await Livechat.transfer(room, guest, transferData); + + return chatForwardedResult ? API.v1.success() : API.v1.failure(); }, }, ); diff --git a/apps/meteor/app/livechat/server/lib/Helper.js b/apps/meteor/app/livechat/server/lib/Helper.js index 810fa421a5c0..97778819d2ac 100644 --- a/apps/meteor/app/livechat/server/lib/Helper.js +++ b/apps/meteor/app/livechat/server/lib/Helper.js @@ -349,24 +349,18 @@ export const forwardRoomToAgent = async (room, transferData) => { const user = Users.findOneOnlineAgentById(agentId); if (!user) { logger.debug(`Agent ${agentId} is offline. Cannot forward`); - throw new Meteor.Error('error-user-is-offline', 'User is offline', { - function: 'forwardRoomToAgent', - }); + throw new Error('error-user-is-offline'); } const { _id: rid, servedBy: oldServedBy } = room; const inquiry = LivechatInquiry.findOneByRoomId(rid); if (!inquiry) { logger.debug(`No inquiries found for room ${room._id}. Cannot forward`); - throw new Meteor.Error('error-invalid-inquiry', 'Invalid inquiry', { - function: 'forwardRoomToAgent', - }); + throw new Error('error-invalid-inquiry'); } if (oldServedBy && agentId === oldServedBy._id) { - throw new Meteor.Error('error-selected-agent-room-agent-are-same', 'The selected agent and the room agent are the same', { - function: 'forwardRoomToAgent', - }); + throw new Error('error-selected-agent-room-agent-are-same'); } const { username } = user; @@ -445,16 +439,12 @@ export const forwardRoomToDepartment = async (room, guest, transferData) => { const inquiry = LivechatInquiry.findOneByRoomId(rid); if (!inquiry) { logger.debug(`Cannot forward room ${room._id}. No inquiries found`); - throw new Meteor.Error('error-transferring-inquiry'); + throw new Error('error-transferring-inquiry'); } const { departmentId } = transferData; if (oldDepartmentId === departmentId) { - throw new Meteor.Error( - 'error-forwarding-chat-same-department', - 'The selected department and the current room department are the same', - { function: 'forwardRoomToDepartment' }, - ); + throw new Error('error-forwarding-chat-same-department'); } const { userId: agentId, clientAction } = transferData; @@ -462,15 +452,11 @@ export const forwardRoomToDepartment = async (room, guest, transferData) => { logger.debug(`Forwarding room ${room._id} to department ${departmentId} (to user ${agentId})`); let user = Users.findOneOnlineAgentById(agentId); if (!user) { - throw new Meteor.Error('error-user-is-offline', 'User is offline', { - function: 'forwardRoomToAgent', - }); + throw new Error('error-user-is-offline'); } user = LivechatDepartmentAgents.findOneByAgentIdAndDepartmentId(agentId, departmentId); if (!user) { - throw new Meteor.Error('error-user-not-belong-to-department', 'The selected user does not belong to this department', { - function: 'forwardRoomToDepartment', - }); + throw new Error('error-user-not-belong-to-department'); } const { username } = user; agent = { agentId, username }; diff --git a/apps/meteor/app/livechat/server/lib/Livechat.js b/apps/meteor/app/livechat/server/lib/Livechat.js index 66bf6cbef557..7c554c5e14e7 100644 --- a/apps/meteor/app/livechat/server/lib/Livechat.js +++ b/apps/meteor/app/livechat/server/lib/Livechat.js @@ -732,7 +732,7 @@ export const Livechat = { Livechat.logger.debug(`Transfering room ${room._id} [Transfered by: ${transferData?.transferredBy?._id}]`); if (room.onHold) { Livechat.logger.debug('Cannot transfer. Room is on hold'); - throw new Meteor.Error('error-room-onHold', 'Room On Hold', { method: 'livechat:transfer' }); + throw new Error('error-room-onHold'); } if (transferData.departmentId) { diff --git a/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useQuickActions.tsx b/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useQuickActions.tsx index 12a5dc517e19..99fd355894da 100644 --- a/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useQuickActions.tsx +++ b/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useQuickActions.tsx @@ -138,7 +138,7 @@ export const useQuickActions = ( } }, [closeModal, discardTranscript, dispatchToastMessage, rid, t]); - const forwardChat = useMethod('livechat:transfer'); + const forwardChat = useEndpoint('POST', '/v1/livechat/room.forward'); const handleForwardChat = useCallback( async (departmentId?: string, userId?: string, comment?: string) => { @@ -173,7 +173,7 @@ export const useQuickActions = ( FlowRouter.go('/'); closeModal(); } catch (error: any) { - handleError(error); + dispatchToastMessage({ type: 'error', message: error as any }); } }, [closeModal, dispatchToastMessage, forwardChat, rid, t], diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/checkAgentBeforeTakeInquiry.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/checkAgentBeforeTakeInquiry.ts index d95326f1a28b..0fc525c84984 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/checkAgentBeforeTakeInquiry.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/checkAgentBeforeTakeInquiry.ts @@ -1,4 +1,3 @@ -import { Meteor } from 'meteor/meteor'; import { Users } from '@rocket.chat/models'; import { callbacks } from '../../../../../lib/callbacks'; @@ -73,7 +72,7 @@ callbacks.add( cbLogger.debug('Callback with error. Agent reached max amount of simultaneous chats'); callbacks.run('livechat.onMaxNumberSimultaneousChatsReached', inquiry); if (options.clientAction && !options.forwardingToDepartment) { - throw new Meteor.Error('error-max-number-simultaneous-chats-reached', 'Not allowed'); + throw new Error('error-max-number-simultaneous-chats-reached'); } return null; diff --git a/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts b/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts index 20eabf3d26ea..249c80ae18fb 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts @@ -257,6 +257,9 @@ describe('LIVECHAT - rooms', function () { await request .post(api('livechat/room.forward')) .set(credentials) + .send({ + roomId: 'invalid-room-id', + }) .expect('Content-Type', 'application/json') .expect(403) .expect((res: Response) => { @@ -272,6 +275,9 @@ describe('LIVECHAT - rooms', function () { await request .post(api('livechat/room.forward')) .set(credentials) + .send({ + roomId: 'invalid-room-id', + }) .expect('Content-Type', 'application/json') .expect(403) .expect((res: Response) => { diff --git a/packages/rest-typings/src/v1/omnichannel.ts b/packages/rest-typings/src/v1/omnichannel.ts index c0a9cf2da7fa..3c43a6e3ab03 100644 --- a/packages/rest-typings/src/v1/omnichannel.ts +++ b/packages/rest-typings/src/v1/omnichannel.ts @@ -1068,7 +1068,7 @@ export type OmnichannelEndpoints = { GET: (params: LiveChatRoomJoin) => { success: boolean }; }; '/v1/livechat/room.forward': { - GET: (params: LiveChatRoomForward) => { success: boolean }; + POST: (params: LiveChatRoomForward) => { success: boolean }; }; '/v1/livechat/monitors': { GET: (params: LivechatMonitorsListProps) => PaginatedResult<{ From 244f5b35fa1cf80a4c8d15292dc2922670542c93 Mon Sep 17 00:00:00 2001 From: murtaza98 Date: Sun, 7 Aug 2022 18:27:23 +0530 Subject: [PATCH 5/7] Mark "livechat:transfer" as deprecated since its not getting used on client anymore --- apps/meteor/app/livechat/server/methods/transfer.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/meteor/app/livechat/server/methods/transfer.js b/apps/meteor/app/livechat/server/methods/transfer.js index 52a189c97e5c..f9f556386aca 100644 --- a/apps/meteor/app/livechat/server/methods/transfer.js +++ b/apps/meteor/app/livechat/server/methods/transfer.js @@ -7,6 +7,8 @@ import { LivechatRooms, Subscriptions, Users } from '../../../models/server'; import { Livechat } from '../lib/Livechat'; import { normalizeTransferredByData } from '../lib/Helper'; +// Deprecated in favor of "livechat/room.forward" endpoint +// TODO: Deprecated: Remove in v6.0.0 Meteor.methods({ async 'livechat:transfer'(transferData) { if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'view-l-room')) { From 307087e55217bde6815de86e744d327079e1cc5c Mon Sep 17 00:00:00 2001 From: murtaza98 Date: Sun, 7 Aug 2022 18:32:22 +0530 Subject: [PATCH 6/7] CRS --- apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts b/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts index 249c80ae18fb..93dda2238a72 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts @@ -4,7 +4,7 @@ import fs from 'fs'; import path from 'path'; import { expect } from 'chai'; -import { IOmnichannelRoom, ILivechatVisitor, IUser } from '@rocket.chat/core-typings'; +import { IOmnichannelRoom, ILivechatVisitor, IUser, IOmnichannelSystemMessage } from '@rocket.chat/core-typings'; import { Response } from 'supertest'; import { getCredentials, api, request, credentials } from '../../../data/api-data'; @@ -338,9 +338,10 @@ describe('LIVECHAT - rooms', function () { expect(latestRoom).to.have.property('lastMessage'); expect(latestRoom.lastMessage?.t).to.be.equal('livechat_transfer_history'); expect(latestRoom.lastMessage?.u?.username).to.be.equal(adminUsername); - expect((latestRoom.lastMessage as any)?.transferData?.comment).to.be.equal('test comment'); - expect((latestRoom.lastMessage as any)?.transferData?.scope).to.be.equal('agent'); - expect((latestRoom.lastMessage as any)?.transferData?.transferredTo?.username).to.be.equal(forwardChatToUser.username); + const { lastMessage } = latestRoom as { lastMessage: IOmnichannelSystemMessage }; + expect(lastMessage?.transferData?.comment).to.be.equal('test comment'); + expect(lastMessage?.transferData?.scope).to.be.equal('agent'); + expect(lastMessage?.transferData?.transferredTo?.username).to.be.equal(forwardChatToUser.username); }); it('should return a success message when transferred successfully to a department', async () => { From 4410853d062881a1ae7695d1d5bd5f2b33da3c0e Mon Sep 17 00:00:00 2001 From: murtaza98 Date: Wed, 10 Aug 2022 17:52:08 +0530 Subject: [PATCH 7/7] Fix lint issues within the build --- apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts b/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts index d8f6b2f567d5..7a9eb6718534 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts @@ -4,7 +4,7 @@ import fs from 'fs'; import path from 'path'; import { expect } from 'chai'; -import type { IOmnichannelRoom, ILivechatVisitor, IUser, IOmnichannelSystemMessage } from '@rocket.chat/core-typings'; +import type { IOmnichannelRoom, ILivechatVisitor, IUser, IOmnichannelSystemMessage } from '@rocket.chat/core-typings'; import type { Response } from 'supertest'; import { getCredentials, api, request, credentials } from '../../../data/api-data';