diff --git a/detox/e2e/plugins/post_message_as.js b/detox/e2e/plugins/post_message_as.js new file mode 100644 index 000000000..8780376d1 --- /dev/null +++ b/detox/e2e/plugins/post_message_as.js @@ -0,0 +1,47 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +const axios = require('axios'); + +module.exports = async ({sender, message, channelId, rootId, createAt = 0, baseUrl}) => { + const loginResponse = await axios({ + url: `${baseUrl}/api/v4/users/login`, + headers: {'X-Requested-With': 'XMLHttpRequest'}, + method: 'post', + data: {login_id: sender.username, password: sender.password}, + }); + + const setCookie = loginResponse.headers['set-cookie']; + let cookieString = ''; + setCookie.forEach((cookie) => { + const nameAndValue = cookie.split(';')[0]; + cookieString += nameAndValue + ';'; + }); + + let response; + try { + response = await axios({ + url: `${baseUrl}/api/v4/posts`, + headers: { + 'Content-Type': 'application/json', + 'X-Requested-With': 'XMLHttpRequest', + Cookie: cookieString, + }, + method: 'post', + data: { + channel_id: channelId, + message, + type: '', + create_at: createAt, + parent_id: rootId, + root_id: rootId, + }, + }); + } catch (err) { + if (err.response) { + response = err.response; + } + } + + return {status: response.status, data: response.data}; +}; diff --git a/detox/e2e/support/server_api/index.js b/detox/e2e/support/server_api/index.js index 5504a5c5e..ed46c7a7b 100644 --- a/detox/e2e/support/server_api/index.js +++ b/detox/e2e/support/server_api/index.js @@ -3,6 +3,7 @@ import Channel from './channel'; import Ldap from './ldap'; +import Plugin from './plugin'; import Post from './post'; import Preference from './preference'; import Setup from './setup'; @@ -14,6 +15,7 @@ import User from './user'; export { Channel, Ldap, + Plugin, Post, Preference, Setup, diff --git a/detox/e2e/support/server_api/plugin.js b/detox/e2e/support/server_api/plugin.js new file mode 100644 index 000000000..5474090fa --- /dev/null +++ b/detox/e2e/support/server_api/plugin.js @@ -0,0 +1,144 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import path from 'path'; + +import client from './client'; +import {apiUploadFile, getResponseFromError} from './common'; + +// **************************************************************** +// Plugins +// https://api.mattermost.com/#tag/plugins +// +// Exported API function should have the following: +// - documented using JSDoc +// - meaningful description +// - match the referenced API endpoints +// - parameter/s defined by `@param` +// - return value defined by `@return` +// **************************************************************** + +/** + * Get plugins. + * See https://api.mattermost.com/#tag/plugins/paths/~1plugins/get + * @return {Object} returns {plugins} on success or {error, status} on error + */ +export const apiGetAllPlugins = async () => { + try { + const response = await client.get('/api/v4/plugins'); + + return {plugins: response.data}; + } catch (err) { + return getResponseFromError(err); + } +}; + +/** + * Upload plugin. + * See https://api.mattermost.com/#tag/plugins/paths/~1plugins/post + * @param {string} filename - the filename of plugin to be uploaded + * @return {Object} returns response on success or {error, status} on error + */ +export const apiUploadPlugin = async (filename) => { + try { + const absFilePath = path.resolve(__dirname, `../../support/fixtures/${filename}`); + const response = await apiUploadFile('plugin', absFilePath, {url: '/api/v4/plugins', method: 'POST'}); + + return response; + } catch (err) { + return getResponseFromError(err); + } +}; + +/** + * Install plugin from URL. + * See https://api.mattermost.com/#tag/plugins/paths/~1plugins~1install_from_url/post + * @param {string} pluginDownloadUrl - URL used to download the plugin + * @param {string} force - Set to 'true' to overwrite a previously installed plugin with the same ID, if any + * @return {Object} returns {plugin} on success or {error, status} on error + */ +export const apiInstallPluginFromUrl = async (pluginDownloadUrl, force = false) => { + try { + const response = await client.post(`/api/v4/plugins/install_from_url?plugin_download_url=${encodeURIComponent(pluginDownloadUrl)}&force=${force}`); + + return {plugin: response.data}; + } catch (err) { + return getResponseFromError(err); + } +}; + +/** + * Enable plugin. + * See https://api.mattermost.com/#tag/plugins/paths/~1plugins~1{plugin_id}~1enable/post + * @param {string} pluginId - the plugin ID + * @return {Object} returns response on success or {error, status} on error + */ +export const apiEnablePluginById = async (pluginId) => { + try { + const response = await client.post(`/api/v4/plugins/${encodeURIComponent(pluginId)}/enable`); + + return response; + } catch (err) { + return getResponseFromError(err); + } +}; + +/** + * Disable plugin. + * See https://api.mattermost.com/#tag/plugins/paths/~1plugins~1{plugin_id}~1disable/post + * @param {string} pluginId - the plugin ID + * @return {Object} returns response on success or {error, status} on error + */ +export const apiDisablePluginById = async (pluginId) => { + try { + const response = await client.post(`/api/v4/plugins/${encodeURIComponent(pluginId)}/disable`); + + return response; + } catch (err) { + return getResponseFromError(err); + } +}; + +const prepackagedPlugins = [ + 'antivirus', + 'mattermost-autolink', + 'com.mattermost.aws-sns', + 'com.mattermost.plugin-channel-export', + 'com.mattermost.custom-attributes', + 'github', + 'com.github.manland.mattermost-plugin-gitlab', + 'com.mattermost.plugin-incident-management', + 'jenkins', + 'jira', + 'com.mattermost.nps', + 'com.mattermost.welcomebot', + 'zoom', +]; + +/** + * Disable non-prepackaged plugins. + */ +export const apiDisableNonPrepackagedPlugins = async () => { + const {plugins} = await apiGetAllPlugins(); + plugins.active.forEach(async (plugin) => { + if (!prepackagedPlugins.includes(plugin.id)) { + await apiDisablePluginById(plugin.id); + } + }); +}; + +/** + * Remove plugin. + * See https://api.mattermost.com/#tag/plugins/paths/~1plugins~1{plugin_id}/delete + * @param {string} pluginId - the plugin ID + * @return {Object} returns response on success or {error, status} on error + */ +export const apiRemovePluginById = async (pluginId) => { + try { + const response = await client.delete(`/api/v4/plugins/${encodeURIComponent(pluginId)}`); + + return response; + } catch (err) { + return getResponseFromError(err); + } +}; diff --git a/detox/e2e/support/server_api/system.js b/detox/e2e/support/server_api/system.js index 7948a25c6..3cde16372 100644 --- a/detox/e2e/support/server_api/system.js +++ b/detox/e2e/support/server_api/system.js @@ -181,7 +181,7 @@ export const apiRequireLicenseForFeature = async (key = '') => { /** * Upload server license with file expected at "/detox/e2e/support/fixtures/mattermost-license.txt" - * @return {Object} returns response + * @return {Object} returns response on success or {error, status} on error */ export const apiUploadLicense = async () => { const absFilePath = path.resolve(__dirname, '../../support/fixtures/mattermost-license.txt'); diff --git a/detox/e2e/utils/webhook_utils.js b/detox/e2e/utils/webhook_utils.js new file mode 100644 index 000000000..dfc86a554 --- /dev/null +++ b/detox/e2e/utils/webhook_utils.js @@ -0,0 +1,270 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +function getFullDialog(triggerId, webhookBaseUrl) { + return { + trigger_id: triggerId, + url: `${webhookBaseUrl}/dialog_submit`, + dialog: { + callback_id: 'somecallbackid', + title: 'Title for Full Dialog Test', + icon_url: + 'http://www.mattermost.org/wp-content/uploads/2016/04/icon.png', + elements: [ + { + display_name: 'Display Name', + name: 'realname', + type: 'text', + subtype: '', + default: 'default text', + placeholder: 'placeholder', + help_text: + 'This a regular input in an interactive dialog triggered by a test integration.', + optional: false, + min_length: 0, + max_length: 0, + data_source: '', + options: null, + }, + { + display_name: 'Email', + name: 'someemail', + type: 'text', + subtype: 'email', + default: '', + placeholder: 'placeholder@bladekick.com', + help_text: + 'This a regular email input in an interactive dialog triggered by a test integration.', + optional: false, + min_length: 0, + max_length: 0, + data_source: '', + options: null, + }, + { + display_name: 'Number', + name: 'somenumber', + type: 'text', + subtype: 'number', + default: '', + placeholder: '', + help_text: '', + optional: false, + min_length: 0, + max_length: 0, + data_source: '', + options: null, + }, + { + display_name: 'Password', + name: 'somepassword', + type: 'text', + subtype: 'password', + default: 'p@ssW0rd', + placeholder: 'placeholder', + help_text: + 'This a password input in an interactive dialog triggered by a test integration.', + optional: true, + min_length: 0, + max_length: 0, + data_source: '', + options: null, + }, + { + display_name: 'Display Name Long Text Area', + name: 'realnametextarea', + type: 'textarea', + subtype: '', + default: '', + placeholder: 'placeholder', + help_text: '', + optional: true, + min_length: 5, + max_length: 100, + data_source: '', + options: null, + }, + { + display_name: 'User Selector', + name: 'someuserselector', + type: 'select', + subtype: '', + default: '', + placeholder: 'Select a user...', + help_text: '', + optional: false, + min_length: 0, + max_length: 0, + data_source: 'users', + options: null, + }, + { + display_name: 'Channel Selector', + name: 'somechannelselector', + type: 'select', + subtype: '', + default: '', + placeholder: 'Select a channel...', + help_text: 'Choose a channel from the list.', + optional: true, + min_length: 0, + max_length: 0, + data_source: 'channels', + options: null, + }, + { + display_name: 'Option Selector', + name: 'someoptionselector', + type: 'select', + subtype: '', + default: '', + placeholder: 'Select an option...', + help_text: '', + optional: false, + min_length: 0, + max_length: 0, + data_source: '', + options: [ + { + text: 'Option1', + value: 'opt1', + }, + { + text: 'Option2', + value: 'opt2', + }, + { + text: 'Option3', + value: 'opt3', + }, + ], + }, + { + display_name: 'Radio Option Selector', + name: 'someradiooptions', + type: 'radio', + help_text: '', + optional: false, + options: [ + { + text: 'Engineering', + value: 'engineering', + }, + { + text: 'Sales', + value: 'sales', + }, + ], + }, + { + display_name: 'Boolean Selector', + placeholder: 'Was this modal helpful?', + name: 'boolean_input', + type: 'bool', + default: 'True', + optional: true, + help_text: 'This is the help text', + }, + ], + submit_label: 'Submit', + notify_on_cancel: true, + state: 'somestate', + }, + }; +} + +function getSimpleDialog(triggerId, webhookBaseUrl) { + return { + trigger_id: triggerId, + url: `${webhookBaseUrl}/dialog_submit`, + dialog: { + callback_id: 'somecallbackid', + title: 'Title for Dialog Test without elements', + icon_url: + 'http://www.mattermost.org/wp-content/uploads/2016/04/icon.png', + submit_label: 'Submit Test', + notify_on_cancel: true, + state: 'somestate', + }, + }; +} + +function getUserAndChannelDialog(triggerId, webhookBaseUrl) { + return { + trigger_id: triggerId, + url: `${webhookBaseUrl}/dialog_submit`, + dialog: { + callback_id: 'somecallbackid', + title: 'Title for Dialog Test with user and channel element', + icon_url: + 'http://www.mattermost.org/wp-content/uploads/2016/04/icon.png', + submit_label: 'Submit Test', + notify_on_cancel: true, + state: 'somestate', + elements: [ + { + display_name: 'User Selector', + name: 'someuserselector', + type: 'select', + subtype: '', + default: '', + placeholder: 'Select a user...', + help_text: '', + optional: false, + min_length: 0, + max_length: 0, + data_source: 'users', + options: null, + }, + { + display_name: 'Channel Selector', + name: 'somechannelselector', + type: 'select', + subtype: '', + default: '', + placeholder: 'Select a channel...', + help_text: 'Choose a channel from the list.', + optional: true, + min_length: 0, + max_length: 0, + data_source: 'channels', + options: null, + }, + ], + }, + }; +} + +function getBooleanDialog(triggerId, webhookBaseUrl) { + return { + trigger_id: triggerId, + url: `${webhookBaseUrl}/dialog_submit`, + dialog: { + callback_id: 'somecallbackid', + title: 'Title for Dialog Test with boolean element', + icon_url: + 'http://www.mattermost.org/wp-content/uploads/2016/04/icon.png', + submit_label: 'Submit Test', + notify_on_cancel: true, + state: 'somestate', + elements: [ + { + display_name: 'Boolean Selector', + placeholder: 'Was this modal helpful?', + name: 'boolean_input', + type: 'bool', + default: 'True', + optional: true, + help_text: 'This is the help text', + }, + ], + }, + }; +} + +module.exports = { + getFullDialog, + getSimpleDialog, + getUserAndChannelDialog, + getBooleanDialog, +}; diff --git a/detox/package-lock.json b/detox/package-lock.json index 990f1efa1..16724491e 100644 --- a/detox/package-lock.json +++ b/detox/package-lock.json @@ -3454,6 +3454,12 @@ } } }, + "@servie/events": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@servie/events/-/events-1.0.0.tgz", + "integrity": "sha512-sBSO19KzdrJCM3gdx6eIxV8M9Gxfgg6iDQmH5TIAGaUu+X9VDdsINXJOnoiZ1Kx3TrHdH4bt5UVglkjsEGBcvw==", + "dev": true + }, "@sinonjs/commons": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.1.tgz", @@ -3570,6 +3576,12 @@ "integrity": "sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw==", "dev": true }, + "@types/tough-cookie": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-2.3.7.tgz", + "integrity": "sha512-rMQbgMGxnLsdn8e9aPVyuN+zMQLrZ2QW8xlv7eWS1mydfGXN+tsTKffcIzd8rGCcLdmi3xvQw2MDaZI1bBNTaw==", + "dev": true + }, "@types/yargs": { "version": "15.0.9", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.9.tgz", @@ -4061,6 +4073,12 @@ "exception-formatter": "^1.0.4" } }, + "byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/byte-length/-/byte-length-1.0.2.tgz", + "integrity": "sha512-ovBpjmsgd/teRmgcPh23d4gJvxDoXtAzEL9xTfMU8Yc2kqCDb7L9jAG0XHl1nzuGl+h3ebCIF1i62UFyA9V/2Q==", + "dev": true + }, "cache-base": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", @@ -4196,6 +4214,23 @@ } } }, + "client-oauth2": { + "version": "github:larkox/js-client-oauth2#e24e2eb5dfcbbbb3a59d095e831dbe0012b0ac49", + "from": "github:larkox/js-client-oauth2#e24e2eb5dfcbbbb3a59d095e831dbe0012b0ac49", + "dev": true, + "requires": { + "popsicle": "^12.0.5", + "safe-buffer": "^5.2.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } + } + }, "cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -7360,6 +7395,21 @@ } } }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "make-error-cause": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/make-error-cause/-/make-error-cause-2.3.0.tgz", + "integrity": "sha512-etgt+n4LlOkGSJbBTV9VROHA5R7ekIPS4vfh+bCAoJgRrJWdqJCBbpS3osRJ/HrT7R68MzMiY3L3sDJ/Fd8aBg==", + "dev": true, + "requires": { + "make-error": "^1.3.5" + } + }, "makeerror": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz", @@ -7907,6 +7957,65 @@ } } }, + "popsicle": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/popsicle/-/popsicle-12.1.0.tgz", + "integrity": "sha512-muNC/cIrWhfR6HqqhHazkxjob3eyECBe8uZYSQ/N5vixNAgssacVleerXnE8Are5fspR0a+d2qWaBR1g7RYlmw==", + "dev": true, + "requires": { + "popsicle-content-encoding": "^1.0.0", + "popsicle-cookie-jar": "^1.0.0", + "popsicle-redirects": "^1.1.0", + "popsicle-transport-http": "^1.0.8", + "popsicle-transport-xhr": "^2.0.0", + "popsicle-user-agent": "^1.0.0", + "servie": "^4.3.3", + "throwback": "^4.1.0" + } + }, + "popsicle-content-encoding": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/popsicle-content-encoding/-/popsicle-content-encoding-1.0.0.tgz", + "integrity": "sha512-4Df+vTfM8wCCJVTzPujiI6eOl3SiWQkcZg0AMrOkD1enMXsF3glIkFUZGvour1Sj7jOWCsNSEhBxpbbhclHhzw==", + "dev": true + }, + "popsicle-cookie-jar": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/popsicle-cookie-jar/-/popsicle-cookie-jar-1.0.0.tgz", + "integrity": "sha512-vrlOGvNVELko0+J8NpGC5lHWDGrk8LQJq9nwAMIVEVBfN1Lib3BLxAaLRGDTuUnvl45j5N9dT2H85PULz6IjjQ==", + "dev": true, + "requires": { + "@types/tough-cookie": "^2.3.5", + "tough-cookie": "^3.0.1" + } + }, + "popsicle-redirects": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/popsicle-redirects/-/popsicle-redirects-1.1.0.tgz", + "integrity": "sha512-XCpzVjVk7tty+IJnSdqWevmOr1n8HNDhL86v7mZ6T1JIIf2KGybxUk9mm7ZFOhWMkGB0e8XkacHip7BV8AQWQA==", + "dev": true + }, + "popsicle-transport-http": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/popsicle-transport-http/-/popsicle-transport-http-1.1.3.tgz", + "integrity": "sha512-NYXgiwBw87vzh/r7Igwa80f6nyYwvMMjLJsL6hnQugIy0sJ7c4JRkUDCja31WHdVukCfMj6s+BVzISYdQHg37g==", + "dev": true, + "requires": { + "make-error-cause": "^2.2.0" + } + }, + "popsicle-transport-xhr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/popsicle-transport-xhr/-/popsicle-transport-xhr-2.0.0.tgz", + "integrity": "sha512-5Sbud4Widngf1dodJE5cjEYXkzEUIl8CzyYRYR57t6vpy9a9KPGQX6KBKdPjmBZlR5A06pOBXuJnVr23l27rtA==", + "dev": true + }, + "popsicle-user-agent": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/popsicle-user-agent/-/popsicle-user-agent-1.0.0.tgz", + "integrity": "sha512-epKaq3TTfTzXcxBxjpoKYMcTTcAX8Rykus6QZu77XNhJuRHSRxMd+JJrbX/3PFI0opFGSN0BabbAYCbGxbu0mA==", + "dev": true + }, "posix-character-classes": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", @@ -8530,6 +8639,17 @@ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", "dev": true }, + "servie": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/servie/-/servie-4.3.3.tgz", + "integrity": "sha512-b0IrY3b1gVMsWvJppCf19g1p3JSnS0hQi6xu4Hi40CIhf0Lx8pQHcvBL+xunShpmOiQzg1NOia812NAWdSaShw==", + "dev": true, + "requires": { + "@servie/events": "^1.0.0", + "byte-length": "^1.0.2", + "ts-expect": "^1.1.0" + } + }, "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -9068,6 +9188,12 @@ "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==", "dev": true }, + "throwback": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/throwback/-/throwback-4.1.0.tgz", + "integrity": "sha512-dLFe8bU8SeH0xeqeKL7BNo8XoPC/o91nz9/ooeplZPiso+DZukhoyZcSz9TFnUNScm+cA9qjU1m1853M6sPOng==", + "dev": true + }, "tmpl": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", @@ -9150,6 +9276,12 @@ "utf8-byte-length": "^1.0.1" } }, + "ts-expect": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-expect/-/ts-expect-1.3.0.tgz", + "integrity": "sha512-e4g0EJtAjk64xgnFPD6kTBUtpnMVzDrMb12N1YZV0VvSlhnVT3SGxiYTLdGy8Q5cYHOIC/FAHmZ10eGrAguicQ==", + "dev": true + }, "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", diff --git a/detox/package.json b/detox/package.json index 19a9c6e4e..b08edc608 100644 --- a/detox/package.json +++ b/detox/package.json @@ -11,6 +11,7 @@ "axios": "0.21.1", "babel-jest": "26.6.3", "babel-plugin-module-resolver": "4.1.0", + "client-oauth2": "github:larkox/js-client-oauth2#e24e2eb5dfcbbbb3a59d095e831dbe0012b0ac49", "deepmerge": "4.2.2", "detox": "18.6.2", "form-data": "4.0.0", @@ -30,7 +31,8 @@ "e2e:android-test-release": "detox test -c android.emu.release --record-logs failing --take-screenshots failing", "e2e:ios-test": "IOS=true detox test -c ios.sim.debug", "e2e:ios-build-release": "detox build -c ios.sim.release", - "e2e:ios-test-release": "IOS=true detox test -c ios.sim.release --record-logs failing --take-screenshots failing" + "e2e:ios-test-release": "IOS=true detox test -c ios.sim.release --record-logs failing --take-screenshots failing", + "start:webhook": "node webhook_server.js" }, "jest": { "transform": { diff --git a/detox/webhook_server.js b/detox/webhook_server.js new file mode 100644 index 000000000..b9f26716d --- /dev/null +++ b/detox/webhook_server.js @@ -0,0 +1,296 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +/* eslint-disable camelcase, no-console */ + +const express = require('express'); +const axios = require('axios'); +var ClientOAuth2 = require('client-oauth2'); + +const webhookUtils = require('./e2e/utils/webhook_utils'); +const postMessageAs = require('./e2e/plugins/post_message_as'); +const port = 3000; + +const { + SITE_URL, + WEBHOOK_BASE_URL, + ADMIN_USERNAME, + ADMIN_PASSWORD, +} = process.env; // eslint-disable-line no-process-env + +const server = express(); +server.use(express.json()); +server.use(express.urlencoded({extended: true})); + +process.title = process.argv[2]; + +server.get('/', ping); +server.post('/message_menus', postMessageMenus); +server.post('/dialog_request', onDialogRequest); +server.post('/simple_dialog_request', onSimpleDialogRequest); +server.post('/user_and_channel_dialog_request', onUserAndChannelDialogRequest); +server.post('/dialog_submit', onDialogSubmit); +server.post('/boolean_dialog_request', onBooleanDialogRequest); +server.post('/slack_compatible_message_response', postSlackCompatibleMessageResponse); +server.post('/send_message_to_channel', postSendMessageToChannel); +server.post('/post_outgoing_webhook', postOutgoingWebhook); +server.post('/send_oauth_credentials', postSendOauthCredentials); +server.get('/start_oauth', getStartOAuth); +server.get('/complete_oauth', getCompleteOauth); +server.post('/postOAuthMessage', postOAuthMessage); + +function ping(req, res) { + const baseUrl = SITE_URL || 'http://localhost:8065'; + const webhookBaseUrl = WEBHOOK_BASE_URL || 'http://localhost:3000'; + + return res.json({ + message: 'I\'m alive!', + baseUrl, + webhookBaseUrl, + }); +} + +server.listen(port, () => console.log(`Webhook test server listening on port ${port}!`)); + +let appID; +let appSecret; +let client; +let authedUser; +function postSendOauthCredentials(req, res) { + appID = req.body.appID.trim(); + appSecret = req.body.appSecret.trim(); + client = new ClientOAuth2({ + clientId: appID, + clientSecret: appSecret, + authorizationUri: getBaseUrl() + '/oauth/authorize', + accessTokenUri: getBaseUrl() + '/oauth/access_token', + redirectUri: getWebhookBaseUrl() + '/complete_oauth', + }); + return res.status(200).send('OK'); +} + +function getStartOAuth(req, res) { + return res.redirect(client.code.getUri()); +} + +function getCompleteOauth(req, res) { + client.code.getToken(req.originalUrl).then((user) => { + authedUser = user; + return res.status(200).send('OK'); + }).catch((reason) => { + return res.status(reason.status).send(reason); + }); +} + +async function postOAuthMessage(req, res) { + const {channelId, message, rootId, createAt} = req.body; + const apiUrl = getBaseUrl() + '/api/v4/posts'; + authedUser.sign({ + method: 'post', + url: apiUrl, + }); + try { + await axios({ + url: apiUrl, + headers: { + 'Content-Type': 'application/json', + 'X-Requested-With': 'XMLHttpRequest', + Authorization: 'Bearer ' + authedUser.accessToken, + }, + method: 'post', + data: { + channel_id: channelId, + message, + type: '', + create_at: createAt, + parent_id: rootId, + root_id: rootId, + }, + }); + } catch (err) { + // Do nothing + } + return res.status(200).send('OK'); +} + +function postSlackCompatibleMessageResponse(req, res) { + const {spoiler, skipSlackParsing} = req.body.context; + + res.setHeader('Content-Type', 'application/json'); + return res.json({ + ephemeral_text: spoiler, + skip_slack_parsing: skipSlackParsing, + }); +} + +function postMessageMenus(req, res) { + let responseData = {}; + const {body} = req; + if (body && body.context.action === 'do_something') { + responseData = { + ephemeral_text: `Ephemeral | ${body.type} ${body.data_source} option: ${body.context.selected_option}`, + }; + } + + res.setHeader('Content-Type', 'application/json'); + return res.json(responseData); +} + +async function openDialog(dialog) { + const baseUrl = getBaseUrl(); + await axios({ + method: 'post', + url: `${baseUrl}/api/v4/actions/dialogs/open`, + data: dialog, + }); +} + +function onDialogRequest(req, res) { + const {body} = req; + if (body.trigger_id) { + const webhookBaseUrl = getWebhookBaseUrl(); + const dialog = webhookUtils.getFullDialog(body.trigger_id, webhookBaseUrl); + openDialog(dialog); + } + + res.setHeader('Content-Type', 'application/json'); + return res.json({text: 'Full dialog triggered via slash command!'}); +} + +function onSimpleDialogRequest(req, res) { + const {body} = req; + if (body.trigger_id) { + const webhookBaseUrl = getWebhookBaseUrl(); + const dialog = webhookUtils.getSimpleDialog(body.trigger_id, webhookBaseUrl); + openDialog(dialog); + } + + res.setHeader('Content-Type', 'application/json'); + return res.json({text: 'Simple dialog triggered via slash command!'}); +} + +function onUserAndChannelDialogRequest(req, res) { + const {body} = req; + if (body.trigger_id) { + const webhookBaseUrl = getWebhookBaseUrl(); + const dialog = webhookUtils.getUserAndChannelDialog(body.trigger_id, webhookBaseUrl); + openDialog(dialog); + } + + res.setHeader('Content-Type', 'application/json'); + return res.json({text: 'Simple dialog triggered via slash command!'}); +} + +function onBooleanDialogRequest(req, res) { + const {body} = req; + if (body.trigger_id) { + const webhookBaseUrl = getWebhookBaseUrl(); + const dialog = webhookUtils.getBooleanDialog(body.trigger_id, webhookBaseUrl); + openDialog(dialog); + } + + res.setHeader('Content-Type', 'application/json'); + return res.json({text: 'Simple dialog triggered via slash command!'}); +} + +function onDialogSubmit(req, res) { + const {body} = req; + + res.setHeader('Content-Type', 'application/json'); + + let message; + if (body.cancelled) { + message = 'Dialog cancelled'; + sendSysadminResponse(message, body.channel_id); + } else { + message = 'Dialog submitted'; + sendSysadminResponse(message, body.channel_id); + } + + return res.json({text: message}); +} + +/** + * @route "POST /send_message_to_channel?type={messageType}&channel_id={channelId}" + * @query type - message type of empty string for regular message if not provided (default), "system_message", etc + * @query channel_id - channel where to send the message + */ +function postSendMessageToChannel(req, res) { + const channelId = req.query.channel_id; + const response = { + response_type: 'in_channel', + text: 'Extra response 2', + channel_id: channelId, + extra_responses: [{ + response_type: 'in_channel', + text: 'Hello World', + channel_id: channelId, + }], + }; + + if (req.query.type) { + response.type = req.query.type; + } + + res.json(response); +} + +function getWebhookBaseUrl() { + return WEBHOOK_BASE_URL || 'http://localhost:3000'; +} + +function getBaseUrl() { + return SITE_URL || 'http://localhost:8065'; +} + +// Convenient way to send response in a channel by using sysadmin account +function sendSysadminResponse(message, channelId) { + const username = ADMIN_USERNAME || 'sysadmin'; + const password = ADMIN_PASSWORD || 'Sys@dmin-sample1'; + const baseUrl = getBaseUrl(); + postMessageAs({sender: {username, password}, message, channelId, baseUrl}); +} + +const responseTypes = ['in_channel', 'comment']; + +function getWebhookResponse(body, {responseType, username, iconUrl}) { + const payload = Object.entries(body).map(([key, value]) => `- ${key}: "${value}"`).join('\n'); + + return ` +\`\`\` +#### Outgoing Webhook Payload +${payload} +#### Webhook override to Mattermost instance +- response_type: "${responseType}" +- type: "" +- username: "${username}" +- icon_url: "${iconUrl}" +\`\`\` +`; +} + +/** + * @route "POST /post_outgoing_webhook?override_username={username}&override_icon_url={iconUrl}&response_type={comment}" + * @query override_username - the user name that overrides the user name defined by the outgoing webhook + * @query override_icon_url - the user icon url that overrides the user icon url defined by the outgoing webhook + * @query response_type - "in_channel" (default) or "comment" + */ +function postOutgoingWebhook(req, res) { + const {body, query} = req; + if (!body) { + res.status(404).send({error: 'Invalid data'}); + } + + const responseType = query.response_type || responseTypes[0]; + const username = query.override_username || ''; + const iconUrl = query.override_icon_url || ''; + + const response = { + text: getWebhookResponse(body, {responseType, username, iconUrl}), + username, + icon_url: iconUrl, + type: '', + response_type: responseType, + }; + res.status(200).send(response); +}