diff --git a/app/apps/server/orchestrator.js b/app/apps/server/orchestrator.js index 6f2a45bc01b..1cd352251e6 100644 --- a/app/apps/server/orchestrator.js +++ b/app/apps/server/orchestrator.js @@ -12,6 +12,10 @@ import { AppUploadsConverter } from './converters/uploads'; import { AppVisitorsConverter } from './converters/visitors'; import { AppRealLogsStorage, AppRealStorage } from './storage'; +function isTesting() { + return process.env.TEST_MODE === 'true'; +} + class AppServerOrchestrator { constructor() { @@ -97,7 +101,7 @@ class AppServerOrchestrator { } isDebugging() { - return settings.get('Apps_Framework_Development_Mode'); + return settings.get('Apps_Framework_Development_Mode') && !isTesting(); } getRocketChatLogger() { diff --git a/mocha_apps.opts b/mocha_apps.opts new file mode 100644 index 00000000000..7a631df0013 --- /dev/null +++ b/mocha_apps.opts @@ -0,0 +1,8 @@ +--require babel-mocha-es6-compiler +--require babel-polyfill +--reporter spec +--ui bdd +--timeout 10000 +--bail +--file tests/end-to-end/teardown.js +tests/end-to-end/apps/*.js diff --git a/package.json b/package.json index 7508dea0667..cb3ff0f6100 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,8 @@ "testui": "cypress run --project tests", "testunit": "mocha --opts ./mocha.opts \"`node -e \"console.log(require('./package.json').mocha.tests.join(' '))\"`\"", "testapi": "mocha --opts ./mocha_api.opts", - "testci": "npm run testapi && npm run testui", + "testapps": "mocha --opts ./mocha_apps.opts", + "testci": "npm run testapi && npm run testapps && npm run testui", "translation-diff": "node .scripts/translationDiff.js", "translation-fix-order": "node .scripts/fix-i18n.js", "version": "node .scripts/version.js", diff --git a/tests/data/apps/apps-data.js b/tests/data/apps/apps-data.js new file mode 100644 index 00000000000..1602396ed79 --- /dev/null +++ b/tests/data/apps/apps-data.js @@ -0,0 +1,4 @@ +export const APP_URL = 'https://github.com/RocketChat/Apps.RocketChat.Tester/blob/master/dist/appsrocketchattester_0.0.1.zip?raw=true'; +export const APP_NAME = 'Apps.RocketChat.Tester'; +export const APP_USERNAME = 'app.appsrocketchattester'; +export const apps = (path = '') => `/api/apps${ path }`; diff --git a/tests/data/apps/helper.js b/tests/data/apps/helper.js new file mode 100644 index 00000000000..1e3ba711d42 --- /dev/null +++ b/tests/data/apps/helper.js @@ -0,0 +1,35 @@ +import { request, credentials } from '../api-data'; +import { apps, APP_URL, APP_NAME } from './apps-data'; + +export const getApps = () => new Promise((resolve) => { + request.get(apps()) + .set(credentials) + .end((err, res) => { + resolve(res.body.apps); + }); +}); + +export const removeAppById = (id) => new Promise((resolve) => { + request.delete(apps(`/${ id }`)) + .set(credentials) + .end(resolve); +}); + +export const cleanupApps = async () => { + const apps = await getApps(); + const testApp = apps.find((app) => app.name === APP_NAME); + if (testApp) { + await removeAppById(testApp.id); + } +}; + +export const installTestApp = () => new Promise((resolve) => { + request.post(apps()) + .set(credentials) + .send({ + url: APP_URL, + }) + .end((err, res) => { + resolve(res.body.app); + }); +}); diff --git a/tests/data/chat.helper.js b/tests/data/chat.helper.js index 171c8d64051..8a385dd3f17 100644 --- a/tests/data/chat.helper.js +++ b/tests/data/chat.helper.js @@ -44,3 +44,17 @@ export const deleteMessage = ({ roomId, msgId }) => { msgId, }); }; + +export const getMessageById = ({ msgId }) => { + if (!msgId) { + throw new Error('"msgId" is required in "getMessageById" test helper'); + } + + return new Promise((resolve) => { + request.get(api(`chat.getMessage?msgId=${ msgId }`)) + .set(credentials) + .end((err, res) => { + resolve(res.body.message); + }); + }); +}; diff --git a/tests/data/users.helper.js b/tests/data/users.helper.js index 216ca7a9cd5..1490c1e69cc 100644 --- a/tests/data/users.helper.js +++ b/tests/data/users.helper.js @@ -23,3 +23,11 @@ export const login = (username, password) => new Promise((resolve) => { resolve(userCredentials); }); }); + +export const getUserByUsername = (username) => new Promise((resolve) => { + request.get(api(`users.info?username=${ username }`)) + .set(credentials) + .end((err, res) => { + resolve(res.body.user); + }); +}); diff --git a/tests/end-to-end/apps/00-installation.js b/tests/end-to-end/apps/00-installation.js new file mode 100644 index 00000000000..fd5238662d9 --- /dev/null +++ b/tests/end-to-end/apps/00-installation.js @@ -0,0 +1,107 @@ +import { expect } from 'chai'; + +import { getCredentials, request, credentials, api } from '../../data/api-data.js'; +import { updatePermission, updateSetting } from '../../data/permissions.helper'; +import { APP_URL, apps, APP_USERNAME } from '../../data/apps/apps-data.js'; +import { cleanupApps } from '../../data/apps/helper.js'; +import { getUserByUsername } from '../../data/users.helper.js'; + +describe('Apps - Installation', function() { + this.retries(0); + + before((done) => getCredentials(done)); + + before(async () => cleanupApps()); + + describe('[Installation]', () => { + it('should throw an error when trying to install an app and the apps framework is enabled but the user does not have the permission', (done) => { + updateSetting('Apps_Framework_Development_Mode', true) + .then(() => updatePermission('manage-apps', [])) + .then(() => { + request.post(apps()) + .set(credentials) + .send({ + url: APP_URL, + }) + .expect('Content-Type', 'application/json') + .expect(403) + .expect((res) => { + expect(res.body).to.have.a.property('success', false); + expect(res.body.error).to.be.equal('User does not have the permissions required for this action [error-unauthorized]'); + }) + .end(done); + }); + }); + it('should throw an error when trying to install an app and the apps framework is disabled', (done) => { + updateSetting('Apps_Framework_Development_Mode', false) + .then(() => updatePermission('manage-apps', ['admin'])) + .then(() => { + request.post(apps()) + .set(credentials) + .send({ + url: APP_URL, + }) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.a.property('success', false); + expect(res.body.error).to.be.equal('Installation from url is disabled.'); + }) + .end(done); + }); + }); + it('should install the app successfully from a URL', (done) => { + updateSetting('Apps_Framework_Development_Mode', true) + .then(() => { + request.post(apps()) + .set(credentials) + .send({ + url: APP_URL, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('success', true); + expect(res.body).to.have.a.property('app'); + expect(res.body.app).to.have.a.property('id'); + expect(res.body.app).to.have.a.property('version'); + expect(res.body.app).to.have.a.property('status').and.to.be.equal('auto_enabled'); + }) + .end(done); + }); + }); + it('should have created the app user successfully', (done) => { + getUserByUsername(APP_USERNAME) + .then((user) => { + expect(user.username).to.be.equal(APP_USERNAME); + }) + .then(done); + }); + describe('Slash commands registration', () => { + it('should have created the "test-simple" slash command successfully', (done) => { + request.get(api('commands.get?command=test-simple')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('success', true); + expect(res.body).to.have.a.property('command'); + expect(res.body.command.command).to.be.equal('test-simple'); + }) + .end(done); + }); + it('should have created the "test-with-arguments" slash command successfully', (done) => { + request.get(api('commands.get?command=test-with-arguments')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('success', true); + expect(res.body).to.have.a.property('command'); + expect(res.body.command.command).to.be.equal('test-with-arguments'); + }) + .end(done); + }); + }); + }); +}); diff --git a/tests/end-to-end/apps/01-send-messages.js b/tests/end-to-end/apps/01-send-messages.js new file mode 100644 index 00000000000..bda19f7caa9 --- /dev/null +++ b/tests/end-to-end/apps/01-send-messages.js @@ -0,0 +1,111 @@ +import { expect } from 'chai'; + +import { getCredentials, request, credentials } from '../../data/api-data.js'; +import { apps } from '../../data/apps/apps-data.js'; +import { cleanupApps, installTestApp } from '../../data/apps/helper.js'; +import { getMessageById } from '../../data/chat.helper.js'; +import { createRoom } from '../../data/rooms.helper'; + +describe('Apps - Send Messages As APP User', function() { + this.retries(0); + let app; + + before((done) => getCredentials(done)); + before(async () => { + await cleanupApps(); + app = await installTestApp(); + }); + + describe('[Send Message as app user]', () => { + it('should return an error when the room is not found', (done) => { + request.post(apps(`/public/${ app.id }/send-message-as-app-user`)) + .send({ + roomId: 'invalid-room', + }) + .set(credentials) + .expect(404) + .expect((err, res) => { + expect(err).to.have.a.property('error'); + expect(res).to.be.equal(undefined); + expect(err.error).to.have.a.property('text'); + expect(err.error.text).to.be.equal('Room "invalid-room" could not be found'); + }) + .end(done); + }); + describe('Send to a Public Channel', () => { + let publicMessageId; + it('should send a message as app user', (done) => { + request.post(apps(`/public/${ app.id }/send-message-as-app-user`)) + .set(credentials) + .send({ + roomId: 'GENERAL', + }) + .expect(200) + .expect((res) => { + const response = JSON.parse(res.text); + expect(response).to.have.a.property('messageId'); + publicMessageId = response.messageId; + }) + .end(done); + }); + it('should be a valid message', async () => { + const message = await getMessageById({ msgId: publicMessageId }); + expect(message.msg).to.be.equal('Executing send-message-as-app-user test endpoint'); + }); + }); + describe('Send to a Private Channel', () => { + let privateMessageId; + it('should send a message as app user', (done) => { + createRoom({ + type: 'p', + name: `apps-e2etest-room-${ Date.now() }`, + }) + .end((err, createdRoom) => { + request.post(apps(`/public/${ app.id }/send-message-as-app-user`)) + .set(credentials) + .send({ + roomId: createdRoom.body.group._id, + }) + .expect(200) + .expect((res) => { + const response = JSON.parse(res.text); + expect(response).to.have.a.property('messageId'); + privateMessageId = response.messageId; + }) + .end(done); + }); + }); + it('should be a valid message', async () => { + const message = await getMessageById({ msgId: privateMessageId }); + expect(message.msg).to.be.equal('Executing send-message-as-app-user test endpoint'); + }); + }); + describe('Send to a DM Channel', () => { + let DMMessageId; + it('should send a message as app user', (done) => { + createRoom({ + type: 'd', + username: 'rocket.cat', + }) + .end((err, createdRoom) => { + request.post(apps(`/public/${ app.id }/send-message-as-app-user`)) + .set(credentials) + .send({ + roomId: createdRoom.body.room._id, + }) + .expect(200) + .expect((res) => { + const response = JSON.parse(res.text); + expect(response).to.have.a.property('messageId'); + DMMessageId = response.messageId; + }) + .end(done); + }); + }); + it('should be a valid message', async () => { + const message = await getMessageById({ msgId: DMMessageId }); + expect(message.msg).to.be.equal('Executing send-message-as-app-user test endpoint'); + }); + }); + }); +}); diff --git a/tests/end-to-end/apps/02-send-messages-as-user.js b/tests/end-to-end/apps/02-send-messages-as-user.js new file mode 100644 index 00000000000..201071c1cff --- /dev/null +++ b/tests/end-to-end/apps/02-send-messages-as-user.js @@ -0,0 +1,150 @@ +import { expect } from 'chai'; + +import { getCredentials, request, credentials } from '../../data/api-data.js'; +import { apps } from '../../data/apps/apps-data.js'; +import { cleanupApps, installTestApp } from '../../data/apps/helper.js'; +import { getMessageById } from '../../data/chat.helper.js'; +import { createRoom } from '../../data/rooms.helper'; +import { adminUsername, password } from '../../data/user.js'; +import { createUser, login } from '../../data/users.helper.js'; + +describe('Apps - Send Messages As User', function() { + this.retries(0); + let app; + + before((done) => getCredentials(done)); + before(async () => { + await cleanupApps(); + app = await installTestApp(); + }); + + describe('[Send Message as user]', () => { + it('should return an error when the room is not found', (done) => { + request.post(apps(`/public/${ app.id }/send-message-as-user`)) + .send({ + roomId: 'invalid-room', + }) + .set(credentials) + .expect(404) + .expect((err, res) => { + expect(err).to.have.a.property('error'); + expect(res).to.be.equal(undefined); + expect(err.error).to.have.a.property('text'); + expect(err.error.text).to.be.equal('Room "invalid-room" could not be found'); + }) + .end(done); + }); + it('should return an error when the user is not found', (done) => { + request.post(apps(`/public/${ app.id }/send-message-as-user?userId=invalid-user`)) + .send({ + roomId: 'GENERAL', + }) + .set(credentials) + .expect(404) + .expect((err, res) => { + expect(err).to.have.a.property('error'); + expect(res).to.be.equal(undefined); + expect(err.error).to.have.a.property('text'); + expect(err.error.text).to.be.equal('User with id "invalid-user" could not be found'); + }) + .end(done); + }); + describe('Send to a Public Channel', () => { + let publicMessageId; + it('should send a message as app user', (done) => { + request.post(apps(`/public/${ app.id }/send-message-as-user?userId=${ adminUsername }`)) + .set(credentials) + .send({ + roomId: 'GENERAL', + }) + .expect(200) + .expect((res) => { + const response = JSON.parse(res.text); + expect(response).to.have.a.property('messageId'); + publicMessageId = response.messageId; + }) + .end(done); + }); + it('should be a valid message', async () => { + const message = await getMessageById({ msgId: publicMessageId }); + expect(message.msg).to.be.equal('Executing send-message-as-user test endpoint'); + }); + }); + describe('Send to a Private Channel', () => { + let privateMessageId; + it('should send a message as app user', (done) => { + createRoom({ + type: 'p', + name: `apps-e2etest-room-${ Date.now() }`, + }) + .end((err, createdRoom) => { + createUser() + .then((createdUser) => { + const user = createdUser; + login(user.username, password).then((credentials) => { + const userCredentials = credentials; + request.post(apps(`/public/${ app.id }/send-message-as-user?userId=${ user._id }`)) + .set(userCredentials) + .send({ + roomId: createdRoom.body.group._id, + }) + .expect(500) + .end(done); + }); + }); + }); + }); + it('should send a message as app user', (done) => { + createRoom({ + type: 'p', + name: `apps-e2etest-room-${ Date.now() }`, + }) + .end((err, createdRoom) => { + request.post(apps(`/public/${ app.id }/send-message-as-user?userId=${ adminUsername }`)) + .set(credentials) + .send({ + roomId: createdRoom.body.group._id, + }) + .expect(200) + .expect((res) => { + const response = JSON.parse(res.text); + expect(response).to.have.a.property('messageId'); + privateMessageId = response.messageId; + }) + .end(done); + }); + }); + it('should be a valid message', async () => { + const message = await getMessageById({ msgId: privateMessageId }); + expect(message.msg).to.be.equal('Executing send-message-as-user test endpoint'); + }); + }); + describe('Send to a DM Channel', () => { + let DMMessageId; + it('should send a message as app user', (done) => { + createRoom({ + type: 'd', + username: 'rocket.cat', + }) + .end((err, createdRoom) => { + request.post(apps(`/public/${ app.id }/send-message-as-user?userId=${ adminUsername }`)) + .set(credentials) + .send({ + roomId: createdRoom.body.room._id, + }) + .expect(200) + .expect((res) => { + const response = JSON.parse(res.text); + expect(response).to.have.a.property('messageId'); + DMMessageId = response.messageId; + }) + .end(done); + }); + }); + it('should be a valid message', async () => { + const message = await getMessageById({ msgId: DMMessageId }); + expect(message.msg).to.be.equal('Executing send-message-as-user test endpoint'); + }); + }); + }); +}); diff --git a/tests/end-to-end/apps/03-slash-command-test-simple.js b/tests/end-to-end/apps/03-slash-command-test-simple.js new file mode 100644 index 00000000000..da2c13ec809 --- /dev/null +++ b/tests/end-to-end/apps/03-slash-command-test-simple.js @@ -0,0 +1,44 @@ +import { expect } from 'chai'; + +import { getCredentials, request, credentials, api } from '../../data/api-data.js'; +import { cleanupApps, installTestApp } from '../../data/apps/helper.js'; + +describe('Apps - Slash Command "test-simple"', function() { + this.retries(0); + + before((done) => getCredentials(done)); + before(async () => { + await cleanupApps(); + await installTestApp(); + }); + + describe('[Slash command "test-simple"]', () => { + it('should execute the slash command successfully', (done) => { + request.post(api('commands.run')) + .send({ + roomId: 'GENERAL', + command: 'test-simple', + }) + .set(credentials) + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('success', true); + }) + .end(done); + }); + it('should have sent the message correctly', (done) => { + request.get(api('chat.search')) + .query({ + roomId: 'GENERAL', + searchText: 'Slashcommand \'test-simple\' successfully executed', + }) + .set(credentials) + .expect(200) + .expect((res) => { + const message = res.body.messages.find((message) => message.msg === 'Slashcommand \'test-simple\' successfully executed'); + expect(message).to.not.be.equal(undefined); + }) + .end(done); + }); + }); +}); diff --git a/tests/end-to-end/apps/04-slash-command-test-with-arguments.js b/tests/end-to-end/apps/04-slash-command-test-with-arguments.js new file mode 100644 index 00000000000..a4ba52c2851 --- /dev/null +++ b/tests/end-to-end/apps/04-slash-command-test-with-arguments.js @@ -0,0 +1,47 @@ +import { expect } from 'chai'; + +import { getCredentials, request, credentials, api } from '../../data/api-data.js'; +import { cleanupApps, installTestApp } from '../../data/apps/helper.js'; + +describe('Apps - Slash Command "test-with-arguments"', function() { + this.retries(0); + + before((done) => getCredentials(done)); + before(async () => { + await cleanupApps(); + await installTestApp(); + }); + + describe('[Slash command "test-with-arguments"]', () => { + const params = 'argument'; + it('should execute the slash command successfully', (done) => { + request.post(api('commands.run')) + .send({ + roomId: 'GENERAL', + command: 'test-with-arguments', + params, + }) + .set(credentials) + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('success', true); + }) + .end(done); + }); + it('should have sent the message correctly', (done) => { + const searchText = `Slashcommand \'test-with-arguments\' successfully executed with arguments: "${ params }"`; + request.get(api('chat.search')) + .query({ + roomId: 'GENERAL', + searchText, + }) + .set(credentials) + .expect(200) + .expect((res) => { + const message = res.body.messages.find((message) => message.msg === searchText); + expect(message).to.not.be.equal(undefined); + }) + .end(done); + }); + }); +}); diff --git a/tests/end-to-end/apps/apps-uninstall.js b/tests/end-to-end/apps/apps-uninstall.js new file mode 100644 index 00000000000..683e1c13599 --- /dev/null +++ b/tests/end-to-end/apps/apps-uninstall.js @@ -0,0 +1,45 @@ +import { expect } from 'chai'; + +import { getCredentials, request, credentials } from '../../data/api-data.js'; +import { apps } from '../../data/apps/apps-data.js'; +import { installTestApp, cleanupApps } from '../../data/apps/helper.js'; + +describe('Apps - Uninstall', function() { + this.retries(0); + let app; + + before((done) => getCredentials(done)); + + before(async () => { + await cleanupApps(); + app = await installTestApp(); + }); + + + describe('[Uninstall]', () => { + it('should throw an error when trying to uninstall an invalid app', (done) => { + request.delete(apps('/invalid-id')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(404) + .expect((res) => { + expect(res.body).to.have.a.property('success', false); + expect(res.body.error).to.be.equal('No App found by the id of: invalid-id'); + }) + .end(done); + }); + it('should remove the app successfully', (done) => { + request.delete(apps(`/${ app.id }`)) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('success', true); + expect(res.body).to.have.a.property('app'); + expect(res.body.app.id).to.be.equal(app.id); + expect(res.body.app.status).to.be.equal('disabled'); + }) + .end(done); + }); + }); +});