MM-34444 Detox/E2E: Prepare plugin API and webhook server (#5257)
* Detox/E2E: Prepare plugin API and webhook server * Added plugin to index * Used fork version of client-oauth2
This commit is contained in:
parent
64ccb58684
commit
d898286ec9
8 changed files with 895 additions and 2 deletions
47
detox/e2e/plugins/post_message_as.js
Normal file
47
detox/e2e/plugins/post_message_as.js
Normal file
|
|
@ -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};
|
||||
};
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
144
detox/e2e/support/server_api/plugin.js
Normal file
144
detox/e2e/support/server_api/plugin.js
Normal file
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
@ -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');
|
||||
|
|
|
|||
270
detox/e2e/utils/webhook_utils.js
Normal file
270
detox/e2e/utils/webhook_utils.js
Normal file
|
|
@ -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,
|
||||
};
|
||||
132
detox/package-lock.json
generated
132
detox/package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
296
detox/webhook_server.js
Normal file
296
detox/webhook_server.js
Normal file
|
|
@ -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);
|
||||
}
|
||||
Loading…
Reference in a new issue