From 997dd8a620f268206064a21c7a300ffed178b4a8 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Mon, 25 Jul 2022 10:16:30 -0600 Subject: [PATCH 1/9] Fix livechat department validatiors for endpoint --- apps/meteor/app/api/server/api.d.ts | 2 +- apps/meteor/app/api/server/api.js | 11 ++- .../imports/server/rest/departments.ts | 62 +++++++-------- packages/rest-typings/src/v1/omnichannel.ts | 75 ++++++++++++++++++- 4 files changed, 112 insertions(+), 38 deletions(-) diff --git a/apps/meteor/app/api/server/api.d.ts b/apps/meteor/app/api/server/api.d.ts index 594f956edaead..11d7537b82f31 100644 --- a/apps/meteor/app/api/server/api.d.ts +++ b/apps/meteor/app/api/server/api.d.ts @@ -68,7 +68,7 @@ type Options = ( twoFactorOptions?: ITwoFactorOptions; } ) & { - validateParams?: ValidateFunction; + validateParams?: ValidateFunction | { [key in Method]?: ValidateFunction }; authOrAnonRequired?: true; }; diff --git a/apps/meteor/app/api/server/api.js b/apps/meteor/app/api/server/api.js index 17b31a3b53c6c..3c2bb781fe0e0 100644 --- a/apps/meteor/app/api/server/api.js +++ b/apps/meteor/app/api/server/api.js @@ -425,9 +425,16 @@ export class APIClass extends Restivus { try { api.enforceRateLimit(objectForRateLimitMatch, this.request, this.response, this.userId); - if (_options.validateParams && !_options.validateParams(this.request.method === 'GET' ? this.queryParams : this.bodyParams)) { - throw new Meteor.Error('invalid-params', _options.validateParams.errors?.map((error) => error.message).join('\n ')); + if (_options.validateParams) { + const requestMethod = this.request.method; + const validatorFunc = + typeof _options.validateParams === 'function' ? _options.validateParams : _options.validateParams[requestMethod]; + + if (validatorFunc && !validatorFunc(requestMethod === 'GET' ? this.queryParams : this.bodyParams)) { + throw new Meteor.Error('invalid-params', _options.validateParams.errors?.map((error) => error.message).join('\n ')); + } } + if (shouldVerifyPermissions && (!this.userId || !hasAllPermission(this.userId, _options.permissionsRequired))) { throw new Meteor.Error('error-unauthorized', 'User does not have the permissions required for this action', { permissions: _options.permissionsRequired, diff --git a/apps/meteor/app/livechat/imports/server/rest/departments.ts b/apps/meteor/app/livechat/imports/server/rest/departments.ts index ce0d9e83143eb..52a4022220902 100644 --- a/apps/meteor/app/livechat/imports/server/rest/departments.ts +++ b/apps/meteor/app/livechat/imports/server/rest/departments.ts @@ -1,4 +1,4 @@ -import { isLivechatDepartmentProps } from '@rocket.chat/rest-typings'; +import { isGETLivechatDepartmentProps, isPOSTLivechatDepartmentProps } from '@rocket.chat/rest-typings'; import { Match, check } from 'meteor/check'; import { API } from '../../../../api/server'; @@ -15,7 +15,7 @@ import { API.v1.addRoute( 'livechat/department', - { authRequired: true, validateParams: isLivechatDepartmentProps }, + { authRequired: true, validateParams: { GET: isGETLivechatDepartmentProps, POST: isPOSTLivechatDepartmentProps } }, { async get() { if (!hasAtLeastOnePermission(this.userId, ['view-livechat-departments', 'view-l-room'])) { @@ -27,22 +27,20 @@ API.v1.addRoute( const { text, enabled, onlyMyDepartments, excludeDepartmentId } = this.queryParams; - const { departments, total } = Promise.await( - findDepartments({ - userId: this.userId, - text, - enabled: enabled === 'true', - onlyMyDepartments: onlyMyDepartments === 'true', - excludeDepartmentId, - pagination: { - offset, - count, - // IMO, sort type shouldn't be record, but a generic of the model we're trying to sort - // or the form { [k: keyof T]: number | string } - sort: sort as any, - }, - }), - ); + const { departments, total } = await findDepartments({ + userId: this.userId, + text, + enabled: enabled === 'true', + onlyMyDepartments: onlyMyDepartments === 'true', + excludeDepartmentId, + pagination: { + offset, + count, + // IMO, sort type shouldn't be record, but a generic of the model we're trying to sort + // or the form { [k: keyof T]: number | string } + sort: sort as any, + }, + }); return API.v1.success({ departments, count: departments.length, offset, total }); }, @@ -170,20 +168,18 @@ API.v1.addRoute( 'livechat/department.autocomplete', { authRequired: true }, { - get() { + async get() { const { selector, onlyMyDepartments } = this.queryParams; if (!selector) { return API.v1.failure("The 'selector' param is required"); } return API.v1.success( - Promise.await( - findDepartmentsToAutocomplete({ - uid: this.userId, - selector: JSON.parse(selector), - onlyMyDepartments: onlyMyDepartments === 'true', - }), - ), + await findDepartmentsToAutocomplete({ + uid: this.userId, + selector: JSON.parse(selector), + onlyMyDepartments: onlyMyDepartments === 'true', + }), ); }, }, @@ -239,7 +235,7 @@ API.v1.addRoute( 'livechat/department.listByIds', { authRequired: true }, { - get() { + async get() { const { ids } = this.queryParams; const { fields } = this.parseJsonQuery(); if (!ids) { @@ -250,13 +246,11 @@ API.v1.addRoute( } return API.v1.success( - Promise.await( - findDepartmentsBetweenIds({ - uid: this.userId, - ids, - fields, - }), - ), + await findDepartmentsBetweenIds({ + uid: this.userId, + ids, + fields, + }), ); }, }, diff --git a/packages/rest-typings/src/v1/omnichannel.ts b/packages/rest-typings/src/v1/omnichannel.ts index 674acb0b4f359..d0204a4512b5b 100644 --- a/packages/rest-typings/src/v1/omnichannel.ts +++ b/packages/rest-typings/src/v1/omnichannel.ts @@ -381,7 +381,80 @@ const LivechatDepartmentSchema = { additionalProperties: false, }; -export const isLivechatDepartmentProps = ajv.compile(LivechatDepartmentSchema); +export const isGETLivechatDepartmentProps = ajv.compile(LivechatDepartmentSchema); + +type POSTLivechatDepartmentProps = { + department: { + enabled: boolean; + name: string; + email: string; + description?: string; + showOnRegistration: boolean; + showOnOfflineForm: boolean; + requestTagsBeforeClosingChat?: boolean; + chatClosingTags?: string[]; + fallbackForwardDepartment?: string; + }; + agents: string[]; +}; + +const POSTLivechatDepartmentSchema = { + type: 'object', + properties: { + department: { + type: 'object', + properties: { + enabled: { + type: 'boolean', + }, + name: { + type: 'string', + }, + description: { + type: 'string', + nullable: true, + }, + showOnRegistration: { + type: 'boolean', + }, + showOnOfflineForm: { + type: 'boolean', + }, + requestTagsBeforeClosingChat: { + type: 'boolean', + nullable: true, + }, + chatClosingTags: { + type: 'array', + items: { + type: 'string', + }, + nullable: true, + }, + fallbackForwardDepartment: { + type: 'string', + nullable: true, + }, + email: { + type: 'string', + }, + }, + required: ['name', 'email', 'enabled', 'showOnRegistration', 'showOnOfflineForm'], + additionalProperties: true, + }, + agents: { + type: 'array', + items: { + type: 'string', + }, + nullable: true, + }, + }, + required: ['department'], + additionalProperties: false, +}; + +export const isPOSTLivechatDepartmentProps = ajv.compile(POSTLivechatDepartmentSchema); type LivechatDepartmentsAvailableByUnitIdProps = PaginatedRequest<{ text: string; onlyMyDepartments?: 'true' | 'false' }>; From 0759a20de325bf74b132dd2428c48bf7a5e353a1 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Mon, 25 Jul 2022 10:31:44 -0600 Subject: [PATCH 2/9] Get right errors from validatorFunc --- apps/meteor/app/api/server/api.js | 2 +- apps/meteor/app/livechat/server/lib/Livechat.js | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/meteor/app/api/server/api.js b/apps/meteor/app/api/server/api.js index 3c2bb781fe0e0..9619530116f83 100644 --- a/apps/meteor/app/api/server/api.js +++ b/apps/meteor/app/api/server/api.js @@ -431,7 +431,7 @@ export class APIClass extends Restivus { typeof _options.validateParams === 'function' ? _options.validateParams : _options.validateParams[requestMethod]; if (validatorFunc && !validatorFunc(requestMethod === 'GET' ? this.queryParams : this.bodyParams)) { - throw new Meteor.Error('invalid-params', _options.validateParams.errors?.map((error) => error.message).join('\n ')); + throw new Meteor.Error('invalid-params', validatorFunc.errors?.map((error) => error.message).join('\n ')); } } diff --git a/apps/meteor/app/livechat/server/lib/Livechat.js b/apps/meteor/app/livechat/server/lib/Livechat.js index 218576effe4d8..79c0e5c86bbd7 100644 --- a/apps/meteor/app/livechat/server/lib/Livechat.js +++ b/apps/meteor/app/livechat/server/lib/Livechat.js @@ -1079,6 +1079,10 @@ export const Livechat = { ); } + if (fallbackForwardDepartment && !LivechatDepartment.findOneById(fallbackForwardDepartment)) { + throw new Meteor.Error('error-fallback-department-not-found', 'Fallback department not found', { method: 'livechat:saveDepartment' }); + } + const departmentDB = LivechatDepartment.createOrUpdateDepartment(_id, departmentData); if (departmentDB && departmentAgents) { updateDepartmentAgents(departmentDB._id, departmentAgents, departmentDB.enabled); From 350e9f89512d9e55e854a65383cba8288ffdc9fd Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Mon, 25 Jul 2022 10:44:18 -0600 Subject: [PATCH 3/9] wrong typing for agents prop --- .../imports/server/rest/departments.ts | 32 ++++++++----------- packages/rest-typings/src/v1/omnichannel.ts | 22 ++++++++++--- 2 files changed, 32 insertions(+), 22 deletions(-) diff --git a/apps/meteor/app/livechat/imports/server/rest/departments.ts b/apps/meteor/app/livechat/imports/server/rest/departments.ts index 52a4022220902..0299ae5e2896d 100644 --- a/apps/meteor/app/livechat/imports/server/rest/departments.ts +++ b/apps/meteor/app/livechat/imports/server/rest/departments.ts @@ -44,31 +44,27 @@ API.v1.addRoute( return API.v1.success({ departments, count: departments.length, offset, total }); }, - post() { + async post() { if (!hasPermission(this.userId, 'manage-livechat-departments')) { return API.v1.unauthorized(); } - try { - check(this.bodyParams, { - department: Object, - agents: Match.Maybe(Array), - }); - - const agents = this.bodyParams.agents ? { upsert: this.bodyParams.agents } : {}; - const department = Livechat.saveDepartment(null, this.bodyParams.department, agents); + check(this.bodyParams, { + department: Object, + agents: Match.Maybe(Array), + }); - if (department) { - return API.v1.success({ - department, - agents: LivechatDepartmentAgents.find({ departmentId: department._id }).fetch(), - }); - } + const agents = this.bodyParams.agents ? { upsert: this.bodyParams.agents } : {}; + const department = Livechat.saveDepartment(null, this.bodyParams.department, agents); - return API.v1.failure(); - } catch (e) { - return API.v1.failure(e); + if (department) { + return API.v1.success({ + department, + agents: LivechatDepartmentAgents.find({ departmentId: department._id }).fetch(), + }); } + + return API.v1.failure(); }, }, ); diff --git a/packages/rest-typings/src/v1/omnichannel.ts b/packages/rest-typings/src/v1/omnichannel.ts index d0204a4512b5b..55a2a8ae5b798 100644 --- a/packages/rest-typings/src/v1/omnichannel.ts +++ b/packages/rest-typings/src/v1/omnichannel.ts @@ -395,7 +395,10 @@ type POSTLivechatDepartmentProps = { chatClosingTags?: string[]; fallbackForwardDepartment?: string; }; - agents: string[]; + agents: { + upsert: string[]; + remove: string[]; + }; }; const POSTLivechatDepartmentSchema = { @@ -443,9 +446,20 @@ const POSTLivechatDepartmentSchema = { additionalProperties: true, }, agents: { - type: 'array', - items: { - type: 'string', + type: 'object', + properties: { + upsert: { + type: 'array', + items: { + type: 'string', + }, + }, + remove: { + type: 'array', + items: { + type: 'string', + }, + }, }, nullable: true, }, From 6c4d088d13edc12e44d3632e65f23cc636352502 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Mon, 25 Jul 2022 19:37:29 -0600 Subject: [PATCH 4/9] add some tests --- .../imports/server/rest/departments.ts | 8 + .../livechat/server/api/lib/departments.ts | 8 - apps/meteor/tests/data/livechat/rooms.js | 15 + .../tests/end-to-end/api/livechat/00-rooms.ts | 2 + .../api/livechat/{agents.js => 01-agents.js} | 0 .../{appearance.js => 02-appearance.js} | 0 .../{custom-fields.js => 03-custom-fields.js} | 0 .../{dashboards.js => 04-dashboards.js} | 0 .../{inquiries.js => 05-inquiries.js} | 0 .../{integrations.js => 06-integrations.js} | 0 .../api/livechat/{queue.js => 07-queue.js} | 0 .../livechat/{triggers.js => 08-triggers.js} | 0 .../livechat/{visitors.ts => 09-visitors.ts} | 2 + .../end-to-end/api/livechat/10-departments.ts | 341 ++++++++++++++++++ 14 files changed, 368 insertions(+), 8 deletions(-) rename apps/meteor/tests/end-to-end/api/livechat/{agents.js => 01-agents.js} (100%) rename apps/meteor/tests/end-to-end/api/livechat/{appearance.js => 02-appearance.js} (100%) rename apps/meteor/tests/end-to-end/api/livechat/{custom-fields.js => 03-custom-fields.js} (100%) rename apps/meteor/tests/end-to-end/api/livechat/{dashboards.js => 04-dashboards.js} (100%) rename apps/meteor/tests/end-to-end/api/livechat/{inquiries.js => 05-inquiries.js} (100%) rename apps/meteor/tests/end-to-end/api/livechat/{integrations.js => 06-integrations.js} (100%) rename apps/meteor/tests/end-to-end/api/livechat/{queue.js => 07-queue.js} (100%) rename apps/meteor/tests/end-to-end/api/livechat/{triggers.js => 08-triggers.js} (100%) rename apps/meteor/tests/end-to-end/api/livechat/{visitors.ts => 09-visitors.ts} (99%) create mode 100644 apps/meteor/tests/end-to-end/api/livechat/10-departments.ts diff --git a/apps/meteor/app/livechat/imports/server/rest/departments.ts b/apps/meteor/app/livechat/imports/server/rest/departments.ts index 0299ae5e2896d..0307f41020fec 100644 --- a/apps/meteor/app/livechat/imports/server/rest/departments.ts +++ b/apps/meteor/app/livechat/imports/server/rest/departments.ts @@ -165,6 +165,10 @@ API.v1.addRoute( { authRequired: true }, { async get() { + if (!hasAtLeastOnePermission(this.userId, ['view-livechat-departments', 'view-l-room'])) { + return API.v1.unauthorized(); + } + const { selector, onlyMyDepartments } = this.queryParams; if (!selector) { return API.v1.failure("The 'selector' param is required"); @@ -232,6 +236,10 @@ API.v1.addRoute( { authRequired: true }, { async get() { + if (!hasAtLeastOnePermission(this.userId, ['view-livechat-departments', 'view-l-room'])) { + return API.v1.unauthorized(); + } + const { ids } = this.queryParams; const { fields } = this.parseJsonQuery(); if (!ids) { diff --git a/apps/meteor/app/livechat/server/api/lib/departments.ts b/apps/meteor/app/livechat/server/api/lib/departments.ts index 423e4f4318dc1..98f7939be0041 100644 --- a/apps/meteor/app/livechat/server/api/lib/departments.ts +++ b/apps/meteor/app/livechat/server/api/lib/departments.ts @@ -110,9 +110,6 @@ export async function findDepartmentsToAutocomplete({ selector, onlyMyDepartments = false, }: FindDepartmentToAutocompleteParams): Promise<{ items: ILivechatDepartmentRecord[] }> { - if (!(await hasPermissionAsync(uid, 'view-livechat-departments')) && !(await hasPermissionAsync(uid, 'view-l-room'))) { - return { items: [] }; - } const { exceptions = [] } = selector; let { conditions = {} } = selector; @@ -160,7 +157,6 @@ export async function findDepartmentAgents({ } export async function findDepartmentsBetweenIds({ - uid, ids, fields, }: { @@ -168,10 +164,6 @@ export async function findDepartmentsBetweenIds({ ids: string[]; fields: Record; }): Promise<{ departments: ILivechatDepartmentRecord[] }> { - if (!(await hasPermissionAsync(uid, 'view-livechat-departments')) && !(await hasPermissionAsync(uid, 'view-l-room'))) { - throw new Error('error-not-authorized'); - } - const departments = await LivechatDepartment.findInIds(ids, fields).toArray(); return { departments }; } diff --git a/apps/meteor/tests/data/livechat/rooms.js b/apps/meteor/tests/data/livechat/rooms.js index f915ee70bfa06..b95a087c21f5f 100644 --- a/apps/meteor/tests/data/livechat/rooms.js +++ b/apps/meteor/tests/data/livechat/rooms.js @@ -39,6 +39,21 @@ export const createVisitor = () => }); }); +export const createDepartment = () => { + return new Promise((resolve, reject) => { + request + .post(api('livechat/department')) + .set(credentials) + .send({ department: { name: `Department ${Date.now()}`, enabled: true, showOnOfflineForm: true, showOnRegistration: true, email: 'a@b.com' } }) + .end((err, res) => { + if (err) { + return reject(err); + } + resolve(res.body.department); + }); + }); +} + export const createAgent = () => new Promise((resolve, reject) => { request 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 5ef8b603990f6..bfd6885727131 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,3 +1,5 @@ +/* eslint-env mocha */ + import { expect } from 'chai'; import { IOmnichannelRoom, IVisitor } from '@rocket.chat/core-typings'; import { Response } from 'supertest'; diff --git a/apps/meteor/tests/end-to-end/api/livechat/agents.js b/apps/meteor/tests/end-to-end/api/livechat/01-agents.js similarity index 100% rename from apps/meteor/tests/end-to-end/api/livechat/agents.js rename to apps/meteor/tests/end-to-end/api/livechat/01-agents.js diff --git a/apps/meteor/tests/end-to-end/api/livechat/appearance.js b/apps/meteor/tests/end-to-end/api/livechat/02-appearance.js similarity index 100% rename from apps/meteor/tests/end-to-end/api/livechat/appearance.js rename to apps/meteor/tests/end-to-end/api/livechat/02-appearance.js diff --git a/apps/meteor/tests/end-to-end/api/livechat/custom-fields.js b/apps/meteor/tests/end-to-end/api/livechat/03-custom-fields.js similarity index 100% rename from apps/meteor/tests/end-to-end/api/livechat/custom-fields.js rename to apps/meteor/tests/end-to-end/api/livechat/03-custom-fields.js diff --git a/apps/meteor/tests/end-to-end/api/livechat/dashboards.js b/apps/meteor/tests/end-to-end/api/livechat/04-dashboards.js similarity index 100% rename from apps/meteor/tests/end-to-end/api/livechat/dashboards.js rename to apps/meteor/tests/end-to-end/api/livechat/04-dashboards.js diff --git a/apps/meteor/tests/end-to-end/api/livechat/inquiries.js b/apps/meteor/tests/end-to-end/api/livechat/05-inquiries.js similarity index 100% rename from apps/meteor/tests/end-to-end/api/livechat/inquiries.js rename to apps/meteor/tests/end-to-end/api/livechat/05-inquiries.js diff --git a/apps/meteor/tests/end-to-end/api/livechat/integrations.js b/apps/meteor/tests/end-to-end/api/livechat/06-integrations.js similarity index 100% rename from apps/meteor/tests/end-to-end/api/livechat/integrations.js rename to apps/meteor/tests/end-to-end/api/livechat/06-integrations.js diff --git a/apps/meteor/tests/end-to-end/api/livechat/queue.js b/apps/meteor/tests/end-to-end/api/livechat/07-queue.js similarity index 100% rename from apps/meteor/tests/end-to-end/api/livechat/queue.js rename to apps/meteor/tests/end-to-end/api/livechat/07-queue.js diff --git a/apps/meteor/tests/end-to-end/api/livechat/triggers.js b/apps/meteor/tests/end-to-end/api/livechat/08-triggers.js similarity index 100% rename from apps/meteor/tests/end-to-end/api/livechat/triggers.js rename to apps/meteor/tests/end-to-end/api/livechat/08-triggers.js diff --git a/apps/meteor/tests/end-to-end/api/livechat/visitors.ts b/apps/meteor/tests/end-to-end/api/livechat/09-visitors.ts similarity index 99% rename from apps/meteor/tests/end-to-end/api/livechat/visitors.ts rename to apps/meteor/tests/end-to-end/api/livechat/09-visitors.ts index 3d90e6890bb34..e6f9888687e9d 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/visitors.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/09-visitors.ts @@ -1,3 +1,5 @@ +/* eslint-env mocha */ + import { expect } from 'chai'; import type { ILivechatVisitor, IOmnichannelRoom } from '@rocket.chat/core-typings'; import { Response } from 'supertest'; diff --git a/apps/meteor/tests/end-to-end/api/livechat/10-departments.ts b/apps/meteor/tests/end-to-end/api/livechat/10-departments.ts new file mode 100644 index 0000000000000..8d69324e7beb1 --- /dev/null +++ b/apps/meteor/tests/end-to-end/api/livechat/10-departments.ts @@ -0,0 +1,341 @@ +/* eslint-env mocha */ +import { expect } from 'chai'; +import type { ILivechatDepartment } from '@rocket.chat/core-typings'; +import { Response } from 'supertest'; + +import { getCredentials, api, request, credentials } from '../../../data/api-data.js'; +import { updatePermission, updateSetting } from '../../../data/permissions.helper'; +import { makeAgentAvailable, createAgent, createDepartment } from '../../../data/livechat/rooms.js'; + +describe.only('LIVECHAT - Departments', function () { + before((done) => getCredentials(done)); + + before((done) => { + updateSetting('Livechat_enabled', true).then(() => + updatePermission('view-livechat-manager', ['admin']) + .then(() => createAgent()) + .then(() => makeAgentAvailable().then(() => done())), + ); + }); + + describe('GET livechat/department', () => { + it('should return unauthorized error when the user does not have the necessary permission', (done) => { + updatePermission('view-livechat-departments', []) + .then(() => updatePermission('view-l-room', [])) + .then(() => { + request.get(api('livechat/department')).set(credentials).expect('Content-Type', 'application/json').expect(403).end(done); + }); + }); + + it('should return a list of departments', (done) => { + updatePermission('view-livechat-departments', ['admin']).then(() => { + request + .get(api('livechat/department')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('departments'); + expect(res.body.departments).to.be.an('array'); + expect(res.body.departments).to.have.length.of.at.least(0); + }) + .end(done); + }); + }); + }); + + describe('POST livechat/departments', () => { + it('should return unauthorized error when the user does not have the necessary permission', (done) => { + updatePermission('manage-livechat-departments', []).then(() => { + request + .post(api('livechat/department')) + .set(credentials) + .send({ department: { name: 'Test', enabled: true, showOnOfflineForm: true, showOnRegistration: true, email: 'bla@bla' } }) + .expect('Content-Type', 'application/json') + .expect(403) + .end(done); + }); + }).timeout(5000); + + it('should return an error when no keys are provided', (done) => { + updatePermission('manage-livechat-departments', ['admin']).then(() => { + request + .post(api('livechat/department')) + .set(credentials) + .send({ department: {} }) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res: Response) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error'); + expect(res.body).to.have.property('errorType', 'invalid-params'); + }) + .end(done); + }); + }).timeout(5000); + + it('should create a new department', (done) => { + updatePermission('manage-livechat-departments', ['admin']).then(() => { + request + .post(api('livechat/department')) + .set(credentials) + .send({ department: { name: 'Test', enabled: true, showOnOfflineForm: true, showOnRegistration: true, email: 'bla@bla' } }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('department'); + expect(res.body.department).to.have.property('_id'); + expect(res.body.department).to.have.property('name', 'Test'); + expect(res.body.department).to.have.property('enabled', true); + expect(res.body.department).to.have.property('showOnOfflineForm', true); + expect(res.body.department).to.have.property('showOnRegistration', true); + }) + .end(done); + }); + }); + }); + + describe('GET livechat/department:_id', () => { + it('should return unauthorized error when the user does not have the necessary permission', (done) => { + updatePermission('view-livechat-departments', []).then(() => { + request + .get(api('livechat/department/testetetetstetete')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(403) + .end(done); + }); + }).timeout(5000); + + it('should return an error when the department does not exist', (done) => { + updatePermission('view-livechat-departments', ['admin']).then(() => { + request + .get(api('livechat/department/testesteteste')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('department'); + expect(res.body.department).to.be.null; + }) + .end(done); + }); + }).timeout(5000); + + it('should return the department', (done) => { + let dep: ILivechatDepartment; + updatePermission('view-livechat-departments', ['admin']) + .then(() => createDepartment()) + .then((department: ILivechatDepartment) => { + dep = department; + }) + .then(() => { + request + .get(api(`livechat/department/${dep._id}`)) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('department'); + expect(res.body.department).to.have.property('_id'); + expect(res.body.department).to.have.property('name', dep.name); + expect(res.body.department).to.have.property('enabled', dep.enabled); + expect(res.body.department).to.have.property('showOnOfflineForm', dep.showOnOfflineForm); + expect(res.body.department).to.have.property('showOnRegistration', dep.showOnRegistration); + expect(res.body.department).to.have.property('email', dep.email); + }) + .end(done); + }); + }); + }); + + describe('GET livechat/department.autocomplete', () => { + it('should return an error when the user does not have the necessary permission', (done) => { + updatePermission('view-livechat-departments', []) + .then(() => updatePermission('view-l-room', [])) + .then(() => { + request + .get(api('livechat/department.autocomplete')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(403) + .end(done); + }); + }); + it('should return an error when the query is not provided', (done) => { + updatePermission('view-livechat-departments', ['admin']) + .then(() => updatePermission('view-l-room', ['admin'])) + .then(() => { + request + .get(api('livechat/department.autocomplete')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res: Response) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error'); + }) + .end(done); + }); + }); + + it('should return an error when the query is empty', (done) => { + updatePermission('view-livechat-departments', ['admin']) + .then(() => updatePermission('view-l-room', ['admin'])) + .then(() => { + request + .get(api('livechat/department.autocomplete')) + .set(credentials) + .query({ selector: '' }) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res: Response) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error'); + }) + .end(done); + }); + }); + + it('should return an error when the query is not a string', (done) => { + updatePermission('view-livechat-departments', ['admin']) + .then(() => updatePermission('view-l-room', ['admin'])) + .then(() => { + request + .get(api('livechat/department.autocomplete')) + .set(credentials) + .query({ selector: { name: 'test' } }) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res: Response) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error'); + }) + .end(done); + }); + }); + + it('should return an error when selector is not valid JSON', (done) => { + updatePermission('view-livechat-departments', ['admin']) + .then(() => updatePermission('view-l-room', ['admin'])) + .then(() => { + request + .get(api('livechat/department.autocomplete')) + .set(credentials) + .query({ selector: '{name: "test"' }) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res: Response) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error'); + }) + .end(done); + }); + }); + + it('should return a list of departments that match selector.term', (done) => { + updatePermission('view-livechat-departments', ['admin']) + .then(() => updatePermission('view-l-room', ['admin'])) + .then(() => { + request + .get(api('livechat/department.autocomplete')) + .set(credentials) + .query({ selector: '{"term":"test"}' }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('items'); + expect(res.body.items).to.be.an('array'); + expect(res.body.items).to.have.length.of.at.least(1); + expect(res.body.items[0]).to.have.property('_id'); + expect(res.body.items[0]).to.have.property('name'); + }) + .end(done); + }); + }); + + it('should return a list of departments excluding the ids on selector.exceptions', (done) => { + let dep: ILivechatDepartment; + + updatePermission('view-livechat-departments', ['admin']) + .then(() => updatePermission('view-l-room', ['admin'])) + .then(() => createDepartment()) + .then((department: ILivechatDepartment) => { + dep = department; + }) + .then(() => { + request + .get(api('livechat/department.autocomplete')) + .set(credentials) + .query({ selector: `{"exceptions":["${dep._id}"]}` }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('items'); + expect(res.body.items).to.be.an('array'); + expect(res.body.items).to.have.length.of.at.least(1); + expect(res.body.items[0]).to.have.property('_id'); + expect(res.body.items[0]).to.have.property('name'); + expect(res.body.items.every((department: ILivechatDepartment) => department._id !== dep._id)).to.be.true; + }) + .end(done); + }); + }); + }); + + describe('GET livechat/departments.listByIds', () => { + it('should throw an error if the user doesnt have the permission to view the departments', (done) => { + updatePermission('view-livechat-departments', []) + .then(() => updatePermission('view-l-room', [])) + .then(() => { + request + .get(api('livechat/department.listByIds')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(403) + .end(done); + }); + }); + + it('should return an error when the query is not present', (done) => { + updatePermission('view-livechat-departments', ['admin']) + .then(() => updatePermission('view-l-room', ['admin'])) + .then(() => { + request + .get(api('livechat/department.listByIds')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res: Response) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error'); + }) + .end(done); + }); + }); + + it('should return an error when the query is not an array', (done) => { + updatePermission('view-livechat-departments', ['admin']) + .then(() => updatePermission('view-l-room', ['admin'])) + .then(() => { + request + .get(api('livechat/department.listByIds')) + .set(credentials) + .query({ ids: 'test' }) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res: Response) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error'); + }) + .end(done); + }); + }); + }); +}); From 611bc769fdcfb3c95aa6031dbd5182855e11ae39 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Mon, 25 Jul 2022 19:40:23 -0600 Subject: [PATCH 5/9] oops --- apps/meteor/tests/end-to-end/api/livechat/10-departments.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/tests/end-to-end/api/livechat/10-departments.ts b/apps/meteor/tests/end-to-end/api/livechat/10-departments.ts index 8d69324e7beb1..749d8f9735c85 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/10-departments.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/10-departments.ts @@ -97,7 +97,7 @@ describe.only('LIVECHAT - Departments', function () { }); }); - describe('GET livechat/department:_id', () => { + describe('GET livechat/department/:_id', () => { it('should return unauthorized error when the user does not have the necessary permission', (done) => { updatePermission('view-livechat-departments', []).then(() => { request From 6a5be7af279c6c9e78f2c32a1072429fe16751fa Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Tue, 26 Jul 2022 07:57:16 -0600 Subject: [PATCH 6/9] Update apps/meteor/tests/end-to-end/api/livechat/10-departments.ts --- apps/meteor/tests/end-to-end/api/livechat/10-departments.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/tests/end-to-end/api/livechat/10-departments.ts b/apps/meteor/tests/end-to-end/api/livechat/10-departments.ts index 749d8f9735c85..e35ecf5319ed2 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/10-departments.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/10-departments.ts @@ -7,7 +7,7 @@ import { getCredentials, api, request, credentials } from '../../../data/api-dat import { updatePermission, updateSetting } from '../../../data/permissions.helper'; import { makeAgentAvailable, createAgent, createDepartment } from '../../../data/livechat/rooms.js'; -describe.only('LIVECHAT - Departments', function () { +describe('LIVECHAT - Departments', function () { before((done) => getCredentials(done)); before((done) => { From facc4aea5de01b28c5861734b91ca52c733fe11b Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Tue, 26 Jul 2022 13:15:23 -0600 Subject: [PATCH 7/9] Update packages/rest-typings/src/v1/omnichannel.ts --- packages/rest-typings/src/v1/omnichannel.ts | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/packages/rest-typings/src/v1/omnichannel.ts b/packages/rest-typings/src/v1/omnichannel.ts index 55a2a8ae5b798..4d12ebf51e0a4 100644 --- a/packages/rest-typings/src/v1/omnichannel.ts +++ b/packages/rest-typings/src/v1/omnichannel.ts @@ -446,20 +446,16 @@ const POSTLivechatDepartmentSchema = { additionalProperties: true, }, agents: { - type: 'object', - properties: { - upsert: { - type: 'array', - items: { - type: 'string', - }, - }, - remove: { - type: 'array', - items: { + type: 'array', + items: { + type: 'object', + properties: { + agentId: { type: 'string', }, }, + required: ['agentId'], + additionalProperties: false, }, nullable: true, }, From 6f9a91b8d40ba723c6abb07f6fc6e55951393914 Mon Sep 17 00:00:00 2001 From: Murtaza Patrawala <34130764+murtaza98@users.noreply.github.com> Date: Fri, 29 Jul 2022 18:54:04 +0530 Subject: [PATCH 8/9] Chore: Sugggestions on PR #26357 (#26410) --- .../app/livechat/imports/server/rest/departments.ts | 1 - .../app/livechat/server/api/lib/departments.ts | 1 - apps/meteor/app/livechat/server/lib/Helper.js | 5 +++-- packages/rest-typings/src/v1/omnichannel.ts | 13 +++++++++---- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/apps/meteor/app/livechat/imports/server/rest/departments.ts b/apps/meteor/app/livechat/imports/server/rest/departments.ts index 0307f41020fec..d6e9481d0e0f5 100644 --- a/apps/meteor/app/livechat/imports/server/rest/departments.ts +++ b/apps/meteor/app/livechat/imports/server/rest/departments.ts @@ -251,7 +251,6 @@ API.v1.addRoute( return API.v1.success( await findDepartmentsBetweenIds({ - uid: this.userId, ids, fields, }), diff --git a/apps/meteor/app/livechat/server/api/lib/departments.ts b/apps/meteor/app/livechat/server/api/lib/departments.ts index 98f7939be0041..f482bd11bcfaa 100644 --- a/apps/meteor/app/livechat/server/api/lib/departments.ts +++ b/apps/meteor/app/livechat/server/api/lib/departments.ts @@ -160,7 +160,6 @@ export async function findDepartmentsBetweenIds({ ids, fields, }: { - uid: string; ids: string[]; fields: Record; }): Promise<{ departments: ILivechatDepartmentRecord[] }> { diff --git a/apps/meteor/app/livechat/server/lib/Helper.js b/apps/meteor/app/livechat/server/lib/Helper.js index 3a51f2343d1ef..810fa421a5c01 100644 --- a/apps/meteor/app/livechat/server/lib/Helper.js +++ b/apps/meteor/app/livechat/server/lib/Helper.js @@ -600,14 +600,15 @@ export const updateDepartmentAgents = (departmentId, agents, departmentEnabled) } upsert.forEach((agent) => { - if (!Users.findOneById(agent.agentId, { fields: { _id: 1 } })) { + const agentFromDb = Users.findOneById(agent.agentId, { fields: { _id: 1, username: 1 } }); + if (!agentFromDb) { return; } LivechatDepartmentAgents.saveAgent({ agentId: agent.agentId, departmentId, - username: agent.username, + username: agentFromDb.username, count: agent.count ? parseInt(agent.count) : 0, order: agent.order ? parseInt(agent.order) : 0, departmentEnabled, diff --git a/packages/rest-typings/src/v1/omnichannel.ts b/packages/rest-typings/src/v1/omnichannel.ts index 4d12ebf51e0a4..eb09cadfc8136 100644 --- a/packages/rest-typings/src/v1/omnichannel.ts +++ b/packages/rest-typings/src/v1/omnichannel.ts @@ -395,10 +395,7 @@ type POSTLivechatDepartmentProps = { chatClosingTags?: string[]; fallbackForwardDepartment?: string; }; - agents: { - upsert: string[]; - remove: string[]; - }; + agents: { agentId: string; count?: number; order?: number }[]; }; const POSTLivechatDepartmentSchema = { @@ -453,6 +450,14 @@ const POSTLivechatDepartmentSchema = { agentId: { type: 'string', }, + count: { + type: 'number', + nullable: true, + }, + order: { + type: 'number', + nullable: true, + }, }, required: ['agentId'], additionalProperties: false, From cac45b67d770c973c35cfa1eaa9f9890492f3e34 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Fri, 29 Jul 2022 15:07:23 -0300 Subject: [PATCH 9/9] Fix review --- apps/meteor/app/api/server/api.d.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/meteor/app/api/server/api.d.ts b/apps/meteor/app/api/server/api.d.ts index 11d7537b82f31..0f6db1f2f4396 100644 --- a/apps/meteor/app/api/server/api.d.ts +++ b/apps/meteor/app/api/server/api.d.ts @@ -93,6 +93,8 @@ type ActionThis } + ? T + : TOptions extends { validateParams: { GET: ValidateFunction } } ? T : Partial> : Record; @@ -101,6 +103,10 @@ type ActionThis : TOptions extends { validateParams: ValidateFunction } ? T + : TOptions extends { validateParams: infer V } + ? V extends { [key in TMethod]: ValidateFunction } + ? T + : Partial> : Partial>; readonly request: Request;