import type { Credentials } from '@rocket.chat/api-client'; import { TeamType } from '@rocket.chat/core-typings'; import type { AtLeast, IIntegration, IMessage, IRoom, ITeam, IUser } from '@rocket.chat/core-typings'; import { Random } from '@rocket.chat/random'; import { assert, expect } from 'chai'; import { after, before, describe, it } from 'mocha'; import { getCredentials, api, request, credentials } from '../../data/api-data'; import { createIntegration, removeIntegration } from '../../data/integration.helper'; import { addPermissions, updatePermission, updateSetting } from '../../data/permissions.helper'; import { assignRoleToUser } from '../../data/roles.helper'; import { createRoom, deleteRoom } from '../../data/rooms.helper'; import { createTeam, deleteTeam } from '../../data/teams.helper'; import { adminUsername, password } from '../../data/user'; import type { TestUser } from '../../data/users.helper'; import { createUser, deleteUser, login, removeRoleFromUser } from '../../data/users.helper'; describe('[Incoming Integrations]', () => { let integration: IIntegration; let integrationCreatedByAnUser: IIntegration; let user: TestUser; let userCredentials: Credentials; let channel: IRoom; let testChannelName: string; before((done) => getCredentials(done)); before(async () => { await Promise.all([ updatePermission('manage-incoming-integrations', []), updatePermission('manage-own-incoming-integrations', []), updatePermission('manage-own-outgoing-integrations', []), updatePermission('manage-outgoing-integrations', []), ]); testChannelName = `channel.test.${Date.now()}-${Math.random()}`; channel = (await createRoom({ type: 'c', name: testChannelName })).body.channel; }); after(async () => { await Promise.all([ updatePermission('manage-incoming-integrations', ['admin']), updatePermission('manage-own-incoming-integrations', ['admin']), updatePermission('manage-own-outgoing-integrations', ['admin']), updatePermission('manage-outgoing-integrations', ['admin']), deleteRoom({ type: 'c', roomId: channel._id }), deleteUser(user), ]); }); describe('[/integrations.create]', () => { describe('Permission checks', () => { before(async () => { await Promise.all([updatePermission('manage-incoming-integrations', []), updatePermission('manage-own-incoming-integrations', [])]); }); after(async () => { await Promise.all([ updatePermission('manage-incoming-integrations', ['admin']), updatePermission('manage-own-incoming-integrations', ['admin']), ]); }); it('should return an error when the user DOES NOT have the permission "manage-incoming-integrations" to add an incoming integration', async () => { const res = await request .post(api('integrations.create')) .set(credentials) .send({ type: 'webhook-incoming', name: 'Incoming test', enabled: true, alias: 'test', username: 'rocket.cat', scriptEnabled: false, overrideDestinationChannelEnabled: true, channel: '#general', }) .expect('Content-Type', 'application/json') .expect(400); expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('errorType', 'not_authorized'); }); it('should return an error when the user DOES NOT have the permission "manage-own-incoming-integrations" to add an incoming integration', async () => { const res = await request .post(api('integrations.create')) .set(credentials) .send({ type: 'webhook-incoming', name: 'Incoming test', enabled: true, alias: 'test', username: 'rocket.cat', scriptEnabled: false, overrideDestinationChannelEnabled: true, channel: '#general', }) .expect('Content-Type', 'application/json') .expect(400); expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('errorType', 'not_authorized'); }); }); describe('With manage-incoming-integrations permission', () => { let tempIntegrationId: IIntegration['_id']; before(async () => { await updatePermission('manage-incoming-integrations', ['admin']); }); after(async () => { if (tempIntegrationId) { await removeIntegration(tempIntegrationId, 'incoming'); } }); it('should add the integration successfully when the user ONLY has the permission "manage-incoming-integrations" to add an incoming integration', async () => { const res = await request .post(api('integrations.create')) .set(credentials) .send({ type: 'webhook-incoming', name: 'Incoming test', enabled: true, alias: 'test', username: 'rocket.cat', scriptEnabled: false, overrideDestinationChannelEnabled: false, channel: '#general', }) .expect('Content-Type', 'application/json') .expect(200); expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('integration').and.to.be.an('object'); tempIntegrationId = res.body.integration._id; }); it('should set overrideDestinationChannelEnabled setting to false when it is not provided', async () => { const res = await request .post(api('integrations.create')) .set(credentials) .send({ type: 'webhook-incoming', name: 'Incoming test', enabled: true, alias: 'test', username: 'rocket.cat', scriptEnabled: false, channel: '#general', }) .expect('Content-Type', 'application/json') .expect(200); expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('integration').and.to.be.an('object'); expect(res.body.integration).to.have.property('overrideDestinationChannelEnabled', false); const integrationId = res.body.integration._id; await removeIntegration(integrationId, 'incoming'); }); }); describe('With manage-own-incoming-integrations permission', () => { before(async () => { await Promise.all([ updatePermission('manage-incoming-integrations', []), updatePermission('manage-own-incoming-integrations', ['admin']), ]); }); after(async () => { await Promise.all([ updatePermission('manage-incoming-integrations', ['admin']), updatePermission('manage-own-incoming-integrations', ['admin']), ]); }); it('should add the integration successfully when the user ONLY has the permission "manage-own-incoming-integrations" to add an incoming integration', async () => { const res = await request .post(api('integrations.create')) .set(credentials) .send({ type: 'webhook-incoming', name: 'Incoming test 2', enabled: true, alias: 'test2', username: 'rocket.cat', scriptEnabled: false, overrideDestinationChannelEnabled: false, channel: '#general', }) .expect('Content-Type', 'application/json') .expect(200); expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('integration').and.to.be.an('object'); integration = res.body.integration; }); }); describe('With both manage-own-incoming-integrations and manage-incoming-integrations', () => { it('should not allow users without a role with the message-impersonate permission to be assigned to new incoming integrations', async () => { await request .post(api('integrations.create')) .set(credentials) .send({ type: 'webhook-incoming', name: 'Incoming test', enabled: true, alias: 'test', username: adminUsername, scriptEnabled: false, overrideDestinationChannelEnabled: true, channel: '#general', }) .expect('Content-Type', 'application/json') .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); expect(res.body).to.have.property( 'error', "User selected for the incoming integration lacks the 'message-impersonate' permission. [error-user-lacks-message-impersonate-permission]", ); }); }); }); describe('Incoming Integration execution', () => { it('should return an error when the user sends an invalid type of integration', async () => { const res = await request .post(api('integrations.create')) .set(credentials) .send({ type: 'webhook-incoming-invalid', name: 'Incoming test', enabled: true, alias: 'test', username: 'rocket.cat', scriptEnabled: false, overrideDestinationChannelEnabled: true, channel: '#general', }) .expect('Content-Type', 'application/json') .expect(400); expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('error', 'Invalid integration type.'); }); it('should execute the incoming integration', async () => { await request .post(`/hooks/${integration._id}/${integration.token}`) .send({ text: 'Example message', }) .expect(200); }); it("should return an error when sending 'channel' field telling its configuration is disabled", async () => { const res = await request .post(`/hooks/${integration._id}/${integration.token}`) .send({ text: 'Example message', channel: [testChannelName], }) .expect(400); expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('error', 'overriding destination channel is disabled for this integration'); }); it("should return an error when sending 'roomId' field telling its configuration is disabled", async () => { const res = await request .post(`/hooks/${integration._id}/${integration.token}`) .send({ text: 'Example message', roomId: channel._id, }) .expect(400); expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('error', 'overriding destination channel is disabled for this integration'); }); it('should send a message for a channel that is specified in the webhooks configuration', (done) => { const successfulMessage = `Message sent successfully at #${Date.now()}`; void request .post(`/hooks/${integration._id}/${integration.token}`) .send({ text: successfulMessage, }) .expect(200) .end(() => { return request .get(api('channels.messages')) .set(credentials) .query({ roomId: 'GENERAL', }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('messages').and.to.be.an('array'); expect(!!(res.body.messages as IMessage[]).find((m) => m.msg === successfulMessage)).to.be.true; }) .end(done); }); }); it('should send a message for a channel that is not specified in the webhooks configuration', async () => { await request .put(api('integrations.update')) .set(credentials) .send({ type: 'webhook-incoming', overrideDestinationChannelEnabled: true, integrationId: integration._id, username: 'rocket.cat', channel: '#general', scriptEnabled: true, enabled: true, name: integration.name, }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('integration'); expect(res.body.integration.overrideDestinationChannelEnabled).to.be.equal(true); }); const successfulMessage = `Message sent successfully at #${Date.now()}`; await request .post(`/hooks/${integration._id}/${integration.token}`) .send({ text: successfulMessage, channel: [testChannelName], }) .expect(200); return request .get(api('channels.messages')) .set(credentials) .query({ roomId: channel._id, }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('messages').and.to.be.an('array'); expect(!!(res.body.messages as IMessage[]).find((m) => m.msg === successfulMessage)).to.be.true; }); }); it('should send a message if the payload is a application/x-www-form-urlencoded JSON', async () => { const payload = { msg: `Message as x-www-form-urlencoded JSON sent successfully at #${Date.now()}` }; await request .post(`/hooks/${integration._id}/${integration.token}`) .set('Content-Type', 'application/x-www-form-urlencoded') .send(`payload=${JSON.stringify(payload)}`) .expect(200); const messagesResult = await request .get(api('channels.messages')) .set(credentials) .query({ roomId: 'GENERAL', }) .expect('Content-Type', 'application/json') .expect(200); expect(messagesResult.body).to.have.property('success', true); expect(messagesResult.body).to.have.property('messages').and.to.be.an('array'); expect(!!(messagesResult.body.messages as IMessage[]).find((m) => m.msg === payload.msg)).to.be.true; }); describe('When the user does not have a role with the message-impersonate permission', () => { before(async () => { await addPermissions({ 'message-impersonate': ['user'], }); }); after(async () => { await addPermissions({ 'message-impersonate': ['bot', 'app'], }); }); it('should return an error when executing incoming integration', async () => { await request .post(`/hooks/${integration._id}/${integration.token}`) .send({ text: 'Example message', }) .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('error', 'error-user-lacks-message-impersonate-permission'); }); }); }); }); describe('Script integration tests', () => { let withScript: IIntegration; let withScriptDefaultContentType: IIntegration; let withSkipTranspile: IIntegration; const sloppyModeScript = 'const buildMessage = (obj) => {\n' + ' \n' + ' const template = `[#VALUE](${ obj.test })`;\n' + ' \n' + ' return {\n' + ' text: template\n' + ' };\n' + ' };\n' + ' \n' + ' class Script {\n' + ' process_incoming_request({ request }) {\n' + ' msg = buildMessage(request.content);\n' + ' \n' + ' return {\n' + ' content:{\n' + ' text: msg.text\n' + ' }\n' + ' };\n' + ' }\n' + ' }\n' + ' \n'; before(async () => { await updatePermission('manage-incoming-integrations', ['admin']); const res1 = await request .post(api('integrations.create')) .set(credentials) .send({ type: 'webhook-incoming', name: 'Incoming test with script', enabled: true, alias: 'test', username: 'rocket.cat', scriptEnabled: true, overrideDestinationChannelEnabled: false, channel: '#general', script: ` class Script { process_incoming_request({ request }) { return { content:{ text: request.content.text } }; } } `, }) .expect(200); withScript = res1.body.integration; const res2 = await request .post(api('integrations.create')) .set(credentials) .send({ type: 'webhook-incoming', name: 'Incoming test with script and default content-type', enabled: true, alias: 'test', username: 'rocket.cat', scriptEnabled: true, overrideDestinationChannelEnabled: false, channel: '#general', script: sloppyModeScript, }) .expect(200); withScriptDefaultContentType = res2.body.integration; // Same script but with skipTranspile: true — no Babel, class methods // run in strict mode so `msg = buildMessage(...)` throws ReferenceError. const res3 = await request .post(api('integrations.create')) .set(credentials) .send({ type: 'webhook-incoming', name: 'Incoming test with skipTranspile', enabled: true, alias: 'test', username: 'rocket.cat', scriptEnabled: true, skipTranspile: true, overrideDestinationChannelEnabled: false, channel: '#general', script: sloppyModeScript, }) .expect(200); withSkipTranspile = res3.body.integration; }); after(async () => { await Promise.all([ removeIntegration(withScript._id, 'incoming'), removeIntegration(withScriptDefaultContentType._id, 'incoming'), removeIntegration(withSkipTranspile._id, 'incoming'), ]); }); it('should send a message if the payload is a application/x-www-form-urlencoded JSON AND the integration has a valid script', async () => { const payload = { text: `Message as x-www-form-urlencoded JSON sent successfully at #${Date.now()}` }; await request .post(`/hooks/${withScript._id}/${withScript.token}`) .set('Content-Type', 'application/x-www-form-urlencoded') .send(`payload=${JSON.stringify(payload)}`) .expect(200); const messagesResult = await request .get(api('channels.messages')) .set(credentials) .query({ roomId: 'GENERAL', }) .expect('Content-Type', 'application/json') .expect(200); expect(messagesResult.body).to.have.property('success', true); expect(messagesResult.body).to.have.property('messages').and.to.be.an('array'); expect(!!(messagesResult.body.messages as IMessage[]).find((m) => m.msg === payload.text)).to.be.true; }); it('should send a message if the payload is application/json and the integration has a valid script', async () => { const payload = { test: 'test' }; await request .post(`/hooks/${withScriptDefaultContentType._id}/${withScriptDefaultContentType.token}`) .set('Content-Type', 'application/json') .send(JSON.stringify(payload)) .expect(200); const messagesResult = await request .get(api('channels.messages')) .set(credentials) .query({ roomId: 'GENERAL', }) .expect('Content-Type', 'application/json') .expect(200); expect(messagesResult.body).to.have.property('success', true); expect(messagesResult.body).to.have.property('messages').and.to.be.an('array'); expect(!!(messagesResult.body.messages as IMessage[]).find((m) => m.msg === '[#VALUE](test)')).to.be.true; }); it('should create the skipTranspile integration with scriptCompiled and no scriptError', () => { expect(withSkipTranspile).to.have.property('scriptCompiled'); expect(withSkipTranspile).to.not.have.property('scriptError'); expect(withSkipTranspile).to.have.property('skipTranspile', true); }); it('should fail to execute the same sloppy-mode script when skipTranspile is true', async () => { const payload = { test: 'test' }; await request .post(`/hooks/${withSkipTranspile._id}/${withSkipTranspile.token}`) .set('Content-Type', 'application/json') .send(JSON.stringify(payload)) .expect(400); }); }); describe('With manage-own-incoming-integrations permission', () => { let integrationId: string; before(async () => { await updatePermission('manage-own-incoming-integrations', ['admin']); }); after(async () => { if (integrationId) { await removeIntegration(integrationId, 'incoming'); } }); it('should return scriptCompiled and no scriptError', async () => { const res = await request .post(api('integrations.create')) .set(credentials) .send({ type: 'webhook-incoming', name: 'Incoming test', enabled: true, alias: 'test', username: 'rocket.cat', scriptEnabled: true, scriptEngine: 'isolated-vm', channel: '#general', script: ` class Script { process_incoming_request({ request }) { return { content:{ text: request.content.text } }; } } `, }) .expect(200); expect(res.body.integration).to.have.property('scriptCompiled'); expect(res.body.integration).to.not.have.property('scriptError'); integrationId = res.body.integration._id; }); }); }); describe('[/integrations.history]', () => { before(async () => { await Promise.all([updatePermission('manage-incoming-integrations', []), updatePermission('manage-own-incoming-integrations', [])]); }); after(async () => { await Promise.all([ updatePermission('manage-incoming-integrations', ['admin']), updatePermission('manage-own-incoming-integrations', ['admin']), ]); }); it('should return an error when trying to get history of incoming integrations if user does NOT have enough permissions', async () => { const res = await request .get(api('integrations.history')) .set(credentials) .query({ id: integration._id, }) .expect('Content-Type', 'application/json') .expect(403); expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('error', 'User does not have the permissions required for this action [error-unauthorized]'); }); }); describe('[/integrations.list]', () => { before((done) => { void createUser().then((createdUser) => { user = createdUser; void login(user.username, password).then((credentials) => { userCredentials = credentials; void updatePermission('manage-incoming-integrations', ['user']).then(() => { void createIntegration( { type: 'webhook-incoming', name: 'Incoming test', enabled: true, alias: 'test', username: 'rocket.cat', scriptEnabled: false, overrideDestinationChannelEnabled: true, channel: '#general', }, userCredentials, ).then((integration) => { integrationCreatedByAnUser = integration; done(); }); }); }); }); }); it('should return the list of incoming integrations', async () => { const res = await request.get(api('integrations.list')).set(credentials).expect('Content-Type', 'application/json').expect(200); expect(res.body).to.have.property('success', true); const integrationCreatedByAdmin = (res.body.integrations as IIntegration[]).find( (createdIntegration) => createdIntegration._id === integration._id, ); assert.isDefined(integrationCreatedByAdmin); expect(integrationCreatedByAdmin).to.be.an('object'); expect(integrationCreatedByAdmin._id).to.be.equal(integration._id); expect(res.body).to.have.property('offset'); expect(res.body).to.have.property('items'); expect(res.body).to.have.property('total'); }); describe('With manage-own-incoming-integrations permission', () => { before(async () => { await Promise.all([ updatePermission('manage-incoming-integrations', []), updatePermission('manage-own-incoming-integrations', ['user']), ]); }); after(async () => { await Promise.all([ updatePermission('manage-incoming-integrations', ['admin']), updatePermission('manage-own-incoming-integrations', ['admin']), ]); }); it('should return the list of integrations created by the user only', async () => { const res = await request.get(api('integrations.list')).set(userCredentials).expect('Content-Type', 'application/json').expect(200); expect(res.body).to.have.property('success', true); const integrationCreatedByAdmin = (res.body.integrations as IIntegration[]).find( (createdIntegration) => createdIntegration._id === integration._id, ); expect(integrationCreatedByAdmin).to.be.equal(undefined); expect(res.body).to.have.property('offset'); expect(res.body).to.have.property('items'); expect(res.body).to.have.property('total'); }); }); describe('Without any permissions', () => { before(async () => { await Promise.all([ updatePermission('manage-incoming-integrations', []), updatePermission('manage-own-incoming-integrations', []), updatePermission('manage-outgoing-integrations', []), updatePermission('manage-own-outgoing-integrations', []), ]); }); after(async () => { await Promise.all([ updatePermission('manage-incoming-integrations', ['admin']), updatePermission('manage-own-incoming-integrations', ['admin']), updatePermission('manage-outgoing-integrations', ['admin']), updatePermission('manage-own-outgoing-integrations', ['admin']), ]); }); it('should return unauthorized error when the user does not have any integrations permissions', async () => { await request .get(api('integrations.list')) .set(credentials) .expect('Content-Type', 'application/json') .expect(403) .expect((res) => { expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('error', 'User does not have the permissions required for this action [error-unauthorized]'); }); }); }); }); describe('[/integrations.get]', () => { describe('Invalid params', () => { it('should return an error when the required "integrationId" query parameters is not sent', async () => { const res = await request.get(api('integrations.get')).set(credentials).expect('Content-Type', 'application/json').expect(400); expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('error', `must have required property 'integrationId'`); }); it('should return an error when the user sends an invalid integration', async () => { const res = await request .get(api('integrations.get')) .query({ integrationId: 'invalid' }) .set(credentials) .expect('Content-Type', 'application/json') .expect(400); expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('error', 'The integration does not exists.'); }); }); describe('Without permissions', () => { before(async () => { await Promise.all([updatePermission('manage-incoming-integrations', []), updatePermission('manage-own-incoming-integrations', [])]); }); after(async () => { await Promise.all([ updatePermission('manage-incoming-integrations', ['admin']), updatePermission('manage-own-incoming-integrations', ['admin']), ]); }); it('should return an error when the user DOES NOT have the permission "manage-incoming-integrations" to get an incoming integration', async () => { const res = await request .get(api('integrations.get')) .query({ integrationId: integration._id }) .set(credentials) .expect('Content-Type', 'application/json') .expect(400); expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('error', 'not-authorized'); }); it('should return an error when the user DOES NOT have the permission "manage-incoming-integrations" to get an incoming integration created by another user', async () => { const res = await request .get(api('integrations.get')) .query({ integrationId: integrationCreatedByAnUser._id }) .set(credentials) .expect('Content-Type', 'application/json') .expect(400); expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('error', 'not-authorized'); }); }); describe('With manage-incoming-integrations permission', () => { before(async () => { await updatePermission('manage-incoming-integrations', ['admin']); }); it('should return the integration successfully', async () => { const res = await request .get(api('integrations.get')) .query({ integrationId: integration._id }) .set(credentials) .expect('Content-Type', 'application/json') .expect(200); expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('integration'); expect(res.body.integration._id).to.be.equal(integration._id); }); }); describe('With manage-own-incoming-integrations permission', () => { before(async () => { await Promise.all([ updatePermission('manage-incoming-integrations', []), updatePermission('manage-own-incoming-integrations', ['user']), ]); }); after(async () => { await Promise.all([ updatePermission('manage-incoming-integrations', ['admin']), updatePermission('manage-own-incoming-integrations', ['admin']), ]); }); it('should return the integration successfully when the user is able to see only your own integrations', async () => { const res = await request .get(api('integrations.get')) .query({ integrationId: integrationCreatedByAnUser._id }) .set(userCredentials) .expect('Content-Type', 'application/json') .expect(200); expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('integration'); expect(res.body.integration._id).to.be.equal(integrationCreatedByAnUser._id); }); }); }); describe('[/integrations.update]', () => { let senderUser: IUser; let sendUserCredentials: Credentials; before(async () => { await addPermissions({ 'message-impersonate': ['user'], }); senderUser = await createUser(); sendUserCredentials = await login(senderUser.username, password); }); after(async () => { await addPermissions({ 'message-impersonate': ['bot', 'app'], }); await deleteUser(senderUser); }); it('should update an integration by id and return the new data', async () => { const res = await request .put(api('integrations.update')) .set(credentials) .send({ type: 'webhook-incoming', name: 'Incoming test updated', enabled: true, alias: 'test updated', username: senderUser.username, scriptEnabled: true, overrideDestinationChannelEnabled: true, channel: '#general', integrationId: integration._id, }) .expect('Content-Type', 'application/json') .expect(200); expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('integration'); expect(res.body.integration._id).to.be.equal(integration._id); expect(res.body.integration.name).to.be.equal('Incoming test updated'); expect(res.body.integration.alias).to.be.equal('test updated'); integration = res.body.integration; }); it('should not allow users without a role with the message-impersonate permission to be assigned to existing incoming integrations', async () => { await request .put(api('integrations.update')) .set(credentials) .send({ type: 'webhook-incoming', name: 'Incoming test update attempt', enabled: true, alias: 'test update attempt', username: 'rocket.cat', scriptEnabled: true, overrideDestinationChannelEnabled: true, channel: '#general', integrationId: integration._id, }) .expect((res) => { expect(res.body).to.have.property('success', false); expect(res.body).to.have.property( 'error', "User selected for the incoming integration lacks the 'message-impersonate' permission. [error-user-lacks-message-impersonate-permission]", ); }); }); it('should have integration updated on subsequent gets', async () => { const res = await request .get(api('integrations.get')) .query({ integrationId: integration._id }) .set(credentials) .expect('Content-Type', 'application/json') .expect(200); expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('integration'); expect(res.body.integration._id).to.be.equal(integration._id); expect(res.body.integration.name).to.be.equal('Incoming test updated'); expect(res.body.integration.alias).to.be.equal('test updated'); }); it("should update an integration's username and associated userId correctly and return the new data", async () => { await request .put(api('integrations.update')) .set(credentials) .send({ type: 'webhook-incoming', name: 'Incoming test updated x2', enabled: true, alias: 'test updated x2', username: senderUser.username, scriptEnabled: true, overrideDestinationChannelEnabled: true, channel: '#general', integrationId: integration._id, }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('integration'); expect(res.body.integration._id).to.be.equal(integration._id); expect(res.body.integration.name).to.be.equal('Incoming test updated x2'); expect(res.body.integration.alias).to.be.equal('test updated x2'); expect(res.body.integration.username).to.be.equal(senderUser.username); expect(res.body.integration.userId).to.be.equal(sendUserCredentials['X-User-Id']); integration = res.body.integration; }); }); it('should send messages to the channel under the updated username', async () => { const successfulMessage = `Message sent successfully at #${Random.id()}`; await request .post(`/hooks/${integration._id}/${integration.token}`) .send({ text: successfulMessage, }) .expect(200); await request .get(api('channels.messages')) .set(credentials) .query({ roomId: 'GENERAL', }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('messages').and.to.be.an('array'); const message = (res.body.messages as IMessage[]).find((m) => m.msg === successfulMessage); expect(message?.u).have.property('username', senderUser.username); }); }); }); describe('[/integrations.remove]', () => { describe('Without permissions', () => { before(async () => { await Promise.all([ updatePermission('manage-incoming-integrations', []), updatePermission('manage-own-incoming-integrations', []), updatePermission('manage-outgoing-integrations', []), updatePermission('manage-own-outgoing-integrations', []), ]); }); after(async () => { await Promise.all([ updatePermission('manage-incoming-integrations', ['admin']), updatePermission('manage-own-incoming-integrations', ['admin']), updatePermission('manage-outgoing-integrations', ['admin']), updatePermission('manage-own-outgoing-integrations', ['admin']), ]); }); it('should return an error when the user DOES NOT have the permission "manage-incoming-integrations" to remove an incoming integration', async () => { const res = await request .post(api('integrations.remove')) .set(credentials) .send({ integrationId: integration._id, type: 'webhook-incoming', }) .expect('Content-Type', 'application/json') .expect(403); expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('error', 'User does not have the permissions required for this action [error-unauthorized]'); }); it('should return an error when the user DOES NOT have the permission "manage-own-incoming-integrations" to remove an incoming integration', async () => { const res = await request .post(api('integrations.remove')) .set(credentials) .send({ integrationId: integration._id, type: 'webhook-incoming', }) .expect('Content-Type', 'application/json') .expect(403); expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('error', 'User does not have the permissions required for this action [error-unauthorized]'); }); }); describe('Invalid params', () => { before(async () => { await updatePermission('manage-own-incoming-integrations', ['admin']); }); it('should return an error when the user sends an invalid type of integration', async () => { const res = await request .post(api('integrations.remove')) .set(credentials) .send({ integrationId: integration._id, type: 'invalid-type', }) .expect('Content-Type', 'application/json') .expect(400); expect(res.body).to.have.property('success', false); expect(res.body).to.have.property('error').include(`must match exactly one schema in oneOf`); }); }); describe('With manage-incoming-integrations permission', () => { before(async () => { await updatePermission('manage-incoming-integrations', ['admin']); }); it('should remove the integration successfully when the user at least one of the necessary permission to remove an incoming integration', async () => { const res = await request .post(api('integrations.remove')) .set(credentials) .send({ integrationId: integration._id, type: integration.type, }) .expect('Content-Type', 'application/json') .expect(200); expect(res.body).to.have.property('success', true); }); }); describe('Normal user with manage-own-incoming-integrations', () => { before(async () => { await updatePermission('manage-own-incoming-integrations', ['user']); }); after(async () => { await updatePermission('manage-own-incoming-integrations', ['admin']); }); it('the normal user should remove the integration successfully when the user have the "manage-own-incoming-integrations" to remove an incoming integration', async () => { const res = await request .post(api('integrations.remove')) .set(userCredentials) .send({ integrationId: integrationCreatedByAnUser._id, type: integrationCreatedByAnUser.type, }) .expect('Content-Type', 'application/json') .expect(200); expect(res.body).to.have.property('success', true); }); }); }); describe('Additional Tests for Message Delivery, Permissions, and Authentication', () => { let nonMemberUser: IUser; let privateTeam: ITeam; let publicChannelInPrivateTeam: IRoom; let privateRoom: IRoom; let publicRoom: IRoom; let integration2: IIntegration; let integration3: IIntegration; let integration4: IIntegration; let integrationMixed1: IIntegration; let integrationMixed2: IIntegration; before(async () => { nonMemberUser = await createUser({ username: `g_${Random.id()}`, roles: ['bot'] }); privateTeam = await createTeam(credentials, `private.team.${Random.id()}`, TeamType.PRIVATE); await assignRoleToUser({ username: adminUsername, roleId: 'bot' }); const [publicInPrivateResponse, privateRoomResponse, publicRoomResponse] = await Promise.all([ createRoom({ type: 'c', name: `teamPrivate.publicChannel.${Random.id()}`, credentials, extraData: { teamId: privateTeam._id, }, }), createRoom({ type: 'p', name: `privateChannel.${Random.id()}`, credentials, }), createRoom({ type: 'c', name: `publicChannel.${Random.id()}`, credentials, }), ]); publicChannelInPrivateTeam = publicInPrivateResponse.body.channel; privateRoom = privateRoomResponse.body.group; publicRoom = publicRoomResponse.body.channel; await updatePermission('manage-incoming-integrations', ['admin']); await request .post(api('integrations.create')) .set(credentials) .send({ type: 'webhook-incoming', name: 'Incoming test 2 - Sending Messages', enabled: true, alias: 'Incoming test 2 - Sending Messages', username: nonMemberUser.username as string, scriptEnabled: false, overrideDestinationChannelEnabled: false, channel: `#${privateRoom.fname}`, }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('integration').and.to.be.an('object'); integration2 = res.body.integration; }); await request .post(api('integrations.create')) .set(credentials) .send({ type: 'webhook-incoming', name: 'Incoming test 3 - Sending Messages', enabled: true, alias: 'Incoming test 3 - Sending Messages', username: nonMemberUser.username as string, scriptEnabled: false, overrideDestinationChannelEnabled: false, channel: `#${publicChannelInPrivateTeam.fname}`, }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('integration').and.to.be.an('object'); integration3 = res.body.integration; }); await request .post(api('integrations.create')) .set(credentials) .send({ type: 'webhook-incoming', name: 'Incoming test 4 - Sending Messages', enabled: true, alias: 'Incoming test 4 - Sending Messages', username: nonMemberUser.username as string, scriptEnabled: false, overrideDestinationChannelEnabled: false, channel: `#${publicRoom.fname}`, }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('integration').and.to.be.an('object'); integration4 = res.body.integration; }); await request .post(api('integrations.create')) .set(credentials) .send({ type: 'webhook-incoming', name: 'Incoming test Mixed - Sending Messages', enabled: true, alias: 'Incoming test Mixed - Sending Messages', username: nonMemberUser.username as string, scriptEnabled: false, overrideDestinationChannelEnabled: false, channel: `#${publicRoom.fname}, #${privateRoom.fname}`, }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('integration').and.to.be.an('object'); integrationMixed1 = res.body.integration; }); await request .post(api('integrations.create')) .set(credentials) .send({ type: 'webhook-incoming', name: 'Incoming test Mixed - Sending Messages', enabled: true, alias: 'Incoming test Mixed - Sending Messages', username: adminUsername, scriptEnabled: false, overrideDestinationChannelEnabled: false, channel: `#${publicRoom.fname}, #${privateRoom.fname}`, }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('integration').and.to.be.an('object'); integrationMixed2 = res.body.integration; }); }); after(async () => { await Promise.all( [publicRoom, privateRoom, publicChannelInPrivateTeam].map((room) => deleteRoom({ type: room.t as 'c' | 'p', roomId: room._id })), ); await deleteTeam(credentials, privateTeam.name); await deleteUser(nonMemberUser); await Promise.all([ ...[integration2, integration3, integration4, integrationMixed1, integrationMixed2].map((integration) => request.post(api('integrations.remove')).set(credentials).send({ integrationId: integration._id, type: integration.type, }), ), updatePermission('manage-incoming-integrations', ['admin']), ]); await removeRoleFromUser(adminUsername, 'bot'); }); it('should not send a message in public room if token is invalid', async () => { const successfulMessage = `Message sent successfully at #${Random.id()}`; await request .post(`/hooks/${integration4._id}/invalid-token`) .send({ text: successfulMessage, }) .expect(500) .expect((res) => { expect(res.text).to.be.equal('Internal Server Error'); }); await request .get(api('channels.messages')) .set(credentials) .query({ roomId: publicRoom._id, }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('messages').and.to.be.an('array'); expect((res.body.messages as IMessage[]).find((m) => m.msg === successfulMessage)).to.be.undefined; }); }); it('should not send a message in private room if token is invalid', async () => { const successfulMessage = `Message sent successfully at #${Random.id()}`; await request .post(`/hooks/${integration2._id}/invalid-token`) .send({ text: successfulMessage, }) .expect(500) .expect((res) => { expect(res.text).to.be.equal('Internal Server Error'); }); await request .get(api('groups.messages')) .set(credentials) .query({ roomId: privateRoom._id, }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('messages').and.to.be.an('array'); expect((res.body.messages as IMessage[]).find((m) => m.msg === successfulMessage)).to.be.undefined; }); }); it('should not send a message to a private rooms on behalf of a non member', async () => { const successfulMessage = `Message sent successfully at #${Random.id()}`; await request .post(`/hooks/${integration2._id}/${integration2.token}`) .send({ text: successfulMessage, }) .expect(400) .expect((res) => { expect(res.body).to.have.property('error', 'error-not-allowed'); }); await request .get(api('groups.messages')) .set(credentials) .query({ roomId: privateRoom._id, }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('messages').and.to.be.an('array'); expect((res.body.messages as IMessage[]).find((m) => m.msg === successfulMessage)).to.be.undefined; }); }); it('should not add non member to private rooms when sending message', async () => { const successfulMessage = `Message sent successfully at #${Random.id()}`; await request .post(`/hooks/${integration2._id}/${integration2.token}`) .send({ text: successfulMessage, }) .expect(400) .expect((res) => { expect(res.body).to.have.property('error', 'error-not-allowed'); }); await request .get(api('groups.members')) .set(credentials) .query({ roomId: privateRoom._id, }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('members').and.to.be.an('array'); expect((res.body.members as AtLeast[]).find((m) => m._id === nonMemberUser._id)).to.be.undefined; }); }); it('should not send a message to public channel of a private team on behalf of a non team member', async () => { const successfulMessage = `Message sent successfully at #${Random.id()}`; await request .post(`/hooks/${integration3._id}/${integration3.token}`) .send({ text: successfulMessage, }) .expect(400) .expect((res) => { expect(res.body).to.have.property('error', 'error-not-allowed'); }); await request .get(api('channels.messages')) .set(credentials) .query({ roomId: publicChannelInPrivateTeam._id, }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('messages').and.to.be.an('array'); expect((res.body.messages as IMessage[]).find((m) => m.msg === successfulMessage)).to.be.undefined; }); }); it('should not add non team member to the public channel in a private team when sending message', async () => { const successfulMessage = `Message sent successfully at #${Random.id()}`; await request .post(`/hooks/${integration3._id}/${integration3.token}`) .send({ text: successfulMessage, }) .expect(400) .expect((res) => { expect(res.body).to.have.property('error', 'error-not-allowed'); }); await request .get(api('channels.members')) .set(credentials) .query({ roomId: publicChannelInPrivateTeam._id, }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('members').and.to.be.an('array'); expect((res.body.members as AtLeast[]).find((m) => m._id === nonMemberUser._id)).to.be.undefined; }); }); it('should send messages from non-members to public rooms and add them as room members', async () => { const successfulMessage = `Message sent successfully at #${Random.id()}`; await request .post(`/hooks/${integration4._id}/${integration4.token}`) .send({ text: successfulMessage, }) .expect(200); await request .get(api('channels.messages')) .set(credentials) .query({ roomId: publicRoom._id, }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('messages').and.to.be.an('array'); expect((res.body.messages as IMessage[]).find((m) => m.msg === successfulMessage)).not.to.be.undefined; }); await request .get(api('channels.members')) .set(credentials) .query({ roomId: publicRoom._id, }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('members').and.to.be.an('array'); expect((res.body.members as AtLeast[]).find((m) => m._id === nonMemberUser._id)).not.to.be.undefined; }); }); describe('Message Settings', async () => { const maxSize = 5000; before(() => updateSetting('Message_MaxAllowedSize', maxSize)); after(() => updateSetting('Message_MaxAllowedSize', maxSize)); it('should not send a message if message size is greater than the Message_MaxAllowedSize', async () => { const largeMesssage = Array.from({ length: maxSize + 1 }) .map(() => 'A') .join(''); await request .post(`/hooks/${integration4._id}/${integration4.token}`) .send({ text: largeMesssage, }) .expect(400) .expect((res) => { expect(res.body.error).to.be.equal('error-message-size-exceeded'); }); await request .get(api('channels.messages')) .set(credentials) .query({ roomId: publicRoom._id, }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('messages').and.to.be.an('array'); expect((res.body.messages as IMessage[]).find((m) => m.msg === largeMesssage)).to.be.undefined; }); }); it('should send a message if message size is less than the Message_MaxAllowedSize', async () => { const smallerMessage = Array.from({ length: maxSize - 1 }) .map(() => 'A') .join(''); await request .post(`/hooks/${integration4._id}/${integration4.token}`) .send({ text: smallerMessage, }) .expect(200) .expect((res) => { expect(res.body.success).to.be.equal(true); }); await request .get(api('channels.messages')) .set(credentials) .query({ roomId: publicRoom._id, }) .expect('Content-Type', 'application/json') .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('messages').and.to.be.an('array'); expect((res.body.messages as IMessage[]).find((m) => m.msg === smallerMessage)).to.not.be.undefined; }); }); }); describe('Multiple channels delivery', () => { it('should not return separate responses when separateResponse is not provided', async () => { const testMsg = `msg-${Random.id()}`; await request .post(`/hooks/${integrationMixed1._id}/${integrationMixed1.token}`) .send({ text: testMsg, }) .expect(400) .expect((res) => { expect(res.body).to.not.have.property('responses'); }); }); it('should not return separate responses when separateResponse = false', async () => { const testMsg = `msg-${Random.id()}`; await request .post(`/hooks/${integrationMixed1._id}/${integrationMixed1.token}`) .send({ text: testMsg, separateResponse: false, }) .expect(400) .expect((res) => { expect(res.body).to.not.have.property('responses'); }); }); it('should return separate responses when separateResponse = true', async () => { const testMsg = `msg-${Random.id()}`; await request .post(`/hooks/${integrationMixed1._id}/${integrationMixed1.token}`) .send({ text: testMsg, separateResponse: true, }) .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body.responses).to.be.an('array').and.to.have.lengthOf(2); const publicResponse = res.body.responses.find((r: any) => r.channel === `#${publicRoom.fname}`); const privateResponse = res.body.responses.find((r: any) => r.channel === `#${privateRoom.fname}`); expect(publicResponse).not.to.have.property('error'); expect(privateResponse).to.have.property('error'); }); }); it('should not deliver to any channel if one fails when separateResponse is not provided', async () => { const testMsg = `msg-${Random.id()}`; await request .post(`/hooks/${integrationMixed1._id}/${integrationMixed1.token}`) .send({ text: testMsg, }) .expect(400) .expect((res) => { expect(res.body).to.have.property('error'); }); await request .get(api('groups.messages')) .set(credentials) .query({ roomId: privateRoom._id }) .expect(200) .expect((res) => { expect((res.body.messages as IMessage[]).find((m) => m.msg === testMsg)).to.be.undefined; }); await request .get(api('channels.messages')) .set(credentials) .query({ roomId: publicRoom._id }) .expect(200) .expect((res) => { expect((res.body.messages as IMessage[]).find((m) => m.msg === testMsg)).to.be.undefined; }); }); it('should not deliver to any channel if one fails when separateResponse = false', async () => { const testMsg = `msg-${Random.id()}`; await request .post(`/hooks/${integrationMixed1._id}/${integrationMixed1.token}`) .send({ text: testMsg, separateResponse: false, }) .expect(400) .expect((res) => { expect(res.body).to.have.property('error'); }); await request .get(api('groups.messages')) .set(credentials) .query({ roomId: privateRoom._id }) .expect(200) .expect((res) => { expect((res.body.messages as IMessage[]).find((m) => m.msg === testMsg)).to.be.undefined; }); await request .get(api('channels.messages')) .set(credentials) .query({ roomId: publicRoom._id }) .expect(200) .expect((res) => { expect((res.body.messages as IMessage[]).find((m) => m.msg === testMsg)).to.be.undefined; }); }); it('should send message to only public channel when separateResponse = true', async () => { const testMsg = `msg-${Random.id()}`; await request .post(`/hooks/${integrationMixed1._id}/${integrationMixed1.token}`) .send({ text: testMsg, separateResponse: true, }) .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); }); await request .get(api('groups.messages')) .set(credentials) .query({ roomId: privateRoom._id }) .expect(200) .expect((res) => { expect((res.body.messages as IMessage[]).find((m) => m.msg === testMsg)).to.be.undefined; }); await request .get(api('channels.messages')) .set(credentials) .query({ roomId: publicRoom._id }) .expect(200) .expect((res) => { const found = (res.body.messages as IMessage[]).filter((m) => m.msg === testMsg); expect(found).to.have.lengthOf(1); }); }); it('should send messages to all channels when message could be delivered to all channels', async () => { const testMsg = `msg-${Random.id()}`; await request .post(`/hooks/${integrationMixed2._id}/${integrationMixed2.token}`) .send({ text: testMsg, separateResponse: true, }) .expect(200) .expect((res) => { expect(res.body).to.have.property('success', true); expect(res.body.responses).to.be.an('array').and.to.have.lengthOf(2); const publicResponse = res.body.responses.find((r: any) => r.channel === `#${publicRoom.fname}`); const privateResponse = res.body.responses.find((r: any) => r.channel === `#${privateRoom.fname}`); expect(publicResponse).not.to.have.property('error'); expect(privateResponse).not.to.have.property('error'); }); await request .get(api('groups.messages')) .set(credentials) .query({ roomId: privateRoom._id }) .expect(200) .expect((res) => { const found = (res.body.messages as IMessage[]).filter((m) => m.msg === testMsg); expect(found).to.have.lengthOf(1); }); await request .get(api('channels.messages')) .set(credentials) .query({ roomId: publicRoom._id }) .expect(200) .expect((res) => { const found = (res.body.messages as IMessage[]).filter((m) => m.msg === testMsg); expect(found).to.have.lengthOf(1); }); }); }); }); });