MM-41854 Detox/E2E: Setup detox infrastructure in Gekidou (#5979)

* MM-41854 Detox/E2E: Setup detox infrastructure in Gekidou

* Fix lint issues

* Fix lint issues

* Update API to include baseUrl for multiple servers

* Update init.js to have default siteUrl as baseUrl

* Update init.js to have default siteUrl as baseUrl

* Update import of testConfig

* Update import of testConfig

* Update postMessageAs signature

* Update detox/webhook_server.js

Co-authored-by: Avinash Lingaloo <avinashlng1080@gmail.com>

Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
Co-authored-by: Avinash Lingaloo <avinashlng1080@gmail.com>
This commit is contained in:
Joseph Baylon
2022-03-01 07:20:59 -08:00
committed by GitHub
parent 066775ca82
commit b87cf8358b
34 changed files with 3810 additions and 2011 deletions

1
.gitignore vendored
View File

@@ -97,6 +97,7 @@ coverage
mattermost-license.txt
*.mattermost-license
detox/artifacts
detox/detox_pixel_4_xl_api_30
# Bundle artifact
*.jsbundle

View File

@@ -6,7 +6,7 @@
"binaryPath": "../ios/Build/Products/Debug-iphonesimulator/Mattermost.app",
"type": "ios.simulator",
"device": {
"type": "iPhone 11"
"type": "iPhone 13"
}
},
"ios.sim.release": {
@@ -14,23 +14,23 @@
"binaryPath": "../ios/Build/Products/Release-iphonesimulator/Mattermost.app",
"build": "cd ../fastlane && NODE_ENV=production bundle exec fastlane ios simulator && cd ../detox",
"device": {
"type": "iPhone 11"
"type": "iPhone 13"
}
},
"android.emu.debug": {
"type": "android.emulator",
"binaryPath": "../android/app/build/outputs/apk/debug/app-debug.apk",
"build": "cd .. && ./node_modules/.bin/jetify && cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug && cd ../detox",
"build": "cd .. && ./node_modules/.bin/jetify && cd android && ./gradlew clean && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug && cd ../detox",
"device": {
"avdName": "detox_emu_api_30"
"avdName": "detox_pixel_4_xl_api_30"
}
},
"android.emu.release": {
"type": "android.emulator",
"binaryPath": "../android/app/build/outputs/apk/release/app-release.apk",
"build": "cd .. && ./node_modules/.bin/jetify && cd android && ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release && cd ../detox",
"build": "cd .. && ./node_modules/.bin/jetify && cd android && ./gradlew clean && ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release && cd ../detox",
"device": {
"avdName": "detox_emu_api_30"
"avdName": "detox_pixel_4_xl_api_30"
}
}
},

View File

@@ -0,0 +1,49 @@
AvdId = Detox_Pixel_4_XL_API_30
PlayStore.enabled = false
abi.type = x86
avd.ini.displayname = Detox Pixel 4 XL API 30
avd.ini.encoding = UTF-8
disk.dataPartition.size = 6g
fastboot.chosenSnapshotFile =
fastboot.forceChosenSnapshotBoot = no
fastboot.forceColdBoot = no
fastboot.forceFastBoot = yes
hw.accelerometer = no
hw.arc = false
hw.gyroscope = no
hw.audioInput = no
hw.audioOutput = no
hw.battery = yes
hw.camera.back = virtualscene
hw.camera.front = emulated
hw.cpu.arch = x86
hw.cpu.ncore = 4
hw.dPad = no
hw.device.hash2 = MD5:80326cf5b53c08af25d4243cb231faa9
hw.device.manufacturer = Google
hw.device.name = pixel_4_xl
hw.gps = no
hw.gpu.enabled = yes
hw.gpu.mode = auto
hw.initialOrientation = Portrait
hw.keyboard = yes
hw.lcd.density = 560
hw.lcd.height = 3040
hw.lcd.width = 1440
hw.mainKeys = no
hw.ramSize = 2048
hw.sdCard = no
hw.sensors.orientation = yes
hw.sensors.proximity = yes
hw.trackBall = no
image.sysdir.1 = system-images/android-30/google_apis/x86/
runtime.network.latency = none
runtime.network.speed = full
sdcard.size = 0
showDeviceFrame = yes
skin.dynamic = yes
skin.name = pixel_4_xl
skin.path = /change_to_absolute_path/pixel_4_xl_skin
tag.display = Google APIs
tag.id = google_apis
vm.heapSize = 576

Binary file not shown.

After

Width:  |  Height:  |  Size: 532 KiB

View File

@@ -0,0 +1,36 @@
parts {
device {
display {
width 1440
height 3040
x 0
y 0
}
}
portrait {
background {
image back.webp
}
foreground {
mask mask.webp
cutout emu01
}
}
}
layouts {
portrait {
width 1571
height 3332
event EV_SW:0:1
part1 {
name portrait
x 0
y 0
}
part2 {
name device
x 61
y 195
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -0,0 +1 @@
saveOnExit = true

View File

@@ -0,0 +1,19 @@
#!/bin/bash
set -ex
set -o pipefail
NAME=detox_pixel_4_xl_api_30
if emulator -list-avds | grep -q $NAME; then
echo "'${NAME}' Android virtual device already exists."
else
# Create virtual device in a relative "detox_pixel_4_xl_api_30" folder
avdmanager create avd -n $NAME -k 'system-images;android-30;google_apis;x86' -g google_apis -p $NAME -d 'pixel'
# Copy predefined config and skin
cp -r android_emulator/ $NAME/
sed -i -e "s|skin.path = /change_to_absolute_path/pixel_4_xl_skin|skin.path = $(pwd)/${NAME}/pixel_4_xl_skin|g" $NAME/config.ini
echo "Android virtual device successfully created: ${NAME}"
fi

View File

@@ -1,13 +1,16 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {System, User} from '@support/server_api';
import {Plugin, System, User} from '@support/server_api';
import testConfig from '@support/test_config';
beforeAll(async () => {
// Login as sysadmin and reset server configuration
await System.apiCheckSystemHealth();
await User.apiAdminLogin();
await System.apiUpdateConfig();
const baseUrl = testConfig.siteUrl;
await System.apiCheckSystemHealth(baseUrl);
await User.apiAdminLogin(baseUrl);
await System.apiUpdateConfig(baseUrl);
await Plugin.apiDisableNonPrepackagedPlugins(baseUrl);
await device.launchApp({
newInstance: false,

View File

@@ -0,0 +1,46 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
const axios = require('axios');
module.exports = async (baseUrl, {sender, message, channelId, rootId, createAt = 0}) => {
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,
root_id: rootId,
},
});
} catch (err) {
if (err.response) {
response = err.response;
}
}
return {status: response.status, data: response.data};
};

View File

@@ -0,0 +1,59 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {capitalize, getRandomId} from '@support/utils';
import client from './client';
import {getResponseFromError} from './common';
// ****************************************************************
// Bots
// See https://api.mattermost.com/#tag/bots
//
// 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`
// ****************************************************************
/**
* Create a bot.
* See https://api.mattermost.com/#operation/CreateBot
* @param {string} baseUrl - the base server URL
* @param {string} option.prefix - prefix to username and display name
* @param {Object} option.bot - bot object to be created
* @return {Object} returns {bot} on success or {error, status} on error
*/
export const apiCreateBot = async (baseUrl, {prefix = 'bot', bot = null} = {}) => {
try {
const newBot = bot || generateRandomBot({prefix});
const response = await client.post(
`${baseUrl}/api/v4/bots`,
newBot,
);
return {bot: response.data};
} catch (err) {
return getResponseFromError(err);
}
};
export const generateRandomBot = ({prefix = 'bot', randomIdLength = 6} = {}) => {
const randomId = getRandomId(randomIdLength);
return {
username: `${prefix}-${randomId}`,
display_name: `${capitalize(prefix)} ${randomId}`,
description: `Test bot description ${randomId}`,
};
};
export const Bot = {
apiCreateBot,
generateRandomBot,
};
export default Bot;

View File

@@ -19,18 +19,40 @@ import {getResponseFromError} from './common';
// ****************************************************************
/**
* Create a channel.
* See https://api.mattermost.com/#tag/channels/paths/~1channels/post
* @param {string} option.teamId - The team ID of the team to create the channel on
* @param {string} option.type - 'O' (default) for a public channel, 'P' for a private channel
* @param {string} option.prefix - option to add prefix to name and display name
* @param {Object} option.channel - fix channel object to be created
* @return {Object} returns {channel} on success or {error, status} on error
* Add user to channel.
* See https://api.mattermost.com/#operation/AddChannelMember
* @param {string} baseUrl - the base server URL
* @param {string} userId - The ID of user to add into the channel
* @param {string} channelId - The channel ID
* @return {Object} returns {member} on success or {error, status} on error
*/
export const apiCreateChannel = async ({teamId = null, type = 'O', prefix = 'channel', channel = null} = {}) => {
export const apiAddUserToChannel = async (baseUrl, userId, channelId) => {
try {
const response = await client.post(
'/api/v4/channels',
`${baseUrl}/api/v4/channels/${channelId}/members`,
{user_id: userId},
);
return {member: response.data};
} catch (err) {
return getResponseFromError(err);
}
};
/**
* Create a channel.
* See https://api.mattermost.com/#operation/CreateChannel
* @param {string} baseUrl - the base server URL
* @param {string} option.teamId - The team ID of the team to create the channel on
* @param {string} option.type - 'O' (default) for a public channel, 'P' for a private channel
* @param {string} option.prefix - prefix to name, display name, purpose, and header
* @param {Object} option.channel - channel object to be created
* @return {Object} returns {channel} on success or {error, status} on error
*/
export const apiCreateChannel = async (baseUrl, {teamId = null, type = 'O', prefix = 'channel', channel = null} = {}) => {
try {
const response = await client.post(
`${baseUrl}/api/v4/channels`,
channel || generateRandomChannel(teamId, type, prefix),
);
@@ -41,15 +63,18 @@ export const apiCreateChannel = async ({teamId = null, type = 'O', prefix = 'cha
};
/**
* Get a channel by name and team name.
* See https://api.mattermost.com/#tag/channels/paths/~1teams~1name~1{team_name}~1channels~1name~1{channel_name}/get
* @param {string} teamName - team name
* @param {string} channelName - channel name
* Create a direct message channel.
* See https://api.mattermost.com/#operation/CreateDirectChannel
* @param {string} baseUrl - the base server URL
* @param {Array} userIds - the two user IDs to be in the direct message
* @return {Object} returns {channel} on success or {error, status} on error
*/
export const apiGetChannelByName = async (teamName, channelName) => {
export const apiCreateDirectChannel = async (baseUrl, userIds = []) => {
try {
const response = await client.get(`/api/v4/teams/name/${teamName}/channels/name/${channelName}`);
const response = await client.post(
`${baseUrl}/api/v4/channels/direct`,
userIds,
);
return {channel: response.data};
} catch (err) {
@@ -58,20 +83,131 @@ export const apiGetChannelByName = async (teamName, channelName) => {
};
/**
* Add user to channel.
* See https://api.mattermost.com/#tag/channels/paths/~1channels~1{channel_id}~1members/post
* @param {string} userId - The ID of user to add into the channel
* @param {string} channelId - The channel ID
* @return {Object} returns {member} on success or {error, status} on error
* Create a group message channel.
* See https://api.mattermost.com/#operation/CreateGroupChannel
* @param {string} baseUrl - the base server URL
* @param {Array} userIds - user IDs to be in the group message channel
* @return {Object} returns {channel} on success or {error, status} on error
*/
export const apiAddUserToChannel = async (userId, channelId) => {
export const apiCreateGroupChannel = async (baseUrl, userIds = []) => {
try {
const response = await client.post(
`/api/v4/channels/${channelId}/members`,
{user_id: userId},
`${baseUrl}/api/v4/channels/group`,
userIds,
);
return {member: response.data};
return {channel: response.data};
} catch (err) {
return getResponseFromError(err);
}
};
/**
* Get a channel by name.
* See https://api.mattermost.com/#operation/GetChannelByName
* @param {string} baseUrl - the base server URL
* @param {string} teamId - team ID
* @param {string} channelName - channel name
* @return {Object} returns {channel} on success or {error, status} on error
*/
export const apiGetChannelByName = async (baseUrl, teamId, channelName) => {
try {
const response = await client.get(`${baseUrl}/api/v4/teams/${teamId}/channels/name/${channelName}`);
return {channel: response.data};
} catch (err) {
return getResponseFromError(err);
}
};
/**
* Get a channel by name and team name.
* See https://api.mattermost.com/#operation/GetChannelByNameForTeamName
* @param {string} baseUrl - the base server URL
* @param {string} teamName - team name
* @param {string} channelName - channel name
* @return {Object} returns {channel} on success or {error, status} on error
*/
export const apiGetChannelByNameAndTeamName = async (baseUrl, teamName, channelName) => {
try {
const response = await client.get(`${baseUrl}/api/v4/teams/name/${teamName}/channels/name/${channelName}`);
return {channel: response.data};
} catch (err) {
return getResponseFromError(err);
}
};
/**
* Get channels for user.
* See https://api.mattermost.com/#operation/GetChannelsForTeamForUser
* @param {string} baseUrl - the base server URL
* @param {string} userId - The user ID
* @param {string} teamId - The team ID the user belongs to
* @return {Object} returns {channels} on success or {error, status} on error
*/
export const apiGetChannelsForUser = async (baseUrl, userId, teamId) => {
try {
const response = await client.get(`${baseUrl}/api/v4/users/${userId}/teams/${teamId}/channels`);
return {channels: response.data};
} catch (err) {
return getResponseFromError(err);
}
};
/**
* Get unread messages.
* See https://api.mattermost.com/#operation/GetChannelUnread
* @param {string} baseUrl - the base server URL
* @param {string} userId - The user ID to perform view actions for
* @param {string} channelId - The channel ID that is being viewed
* @return {Object} returns response on success or {error, status} on error
*/
export const apiGetUnreadMessages = async (baseUrl, userId, channelId) => {
try {
return await client.get(`${baseUrl}/api/v4/users/${userId}/channels/${channelId}/unread`);
} catch (err) {
return getResponseFromError(err);
}
};
/**
* Remove user from channel.
* See https://api.mattermost.com/#operation/RemoveUserFromChannel
* @param {string} baseUrl - the base server URL
* @param {string} channelId - The channel ID
* @param {string} userId - The user ID to be removed from channel
* @return {Object} returns {status} on success or {error, status} on error
*/
export const apiRemoveUserFromChannel = async (baseUrl, channelId, userId) => {
try {
const response = await client.delete(
`${baseUrl}/api/v4/channels/${channelId}/members/${userId}`,
);
return {status: response.status};
} catch (err) {
return getResponseFromError(err);
}
};
/**
* View channel.
* See https://api.mattermost.com/#operation/ViewChannel
* @param {string} baseUrl - the base server URL
* @param {string} userId - The user ID to perform view actions for
* @param {string} channelId - The channel ID that is being viewed
* @return {Object} returns {viewed} on success or {error, status} on error
*/
export const apiViewChannel = async (baseUrl, userId, channelId) => {
try {
const response = await client.post(
`${baseUrl}/api/v4/channels/members/${userId}/view`,
{channel_id: channelId},
);
return {viewed: response.data};
} catch (err) {
return getResponseFromError(err);
}
@@ -93,7 +229,13 @@ function generateRandomChannel(teamId, type, prefix) {
export const Channel = {
apiAddUserToChannel,
apiCreateChannel,
apiCreateDirectChannel,
apiCreateGroupChannel,
apiGetChannelByName,
apiGetChannelsForUser,
apiGetUnreadMessages,
apiRemoveUserFromChannel,
apiViewChannel,
};
export default Channel;

View File

@@ -3,10 +3,7 @@
import axios from 'axios';
import testConfig from '../test_config';
export const client = axios.create({
baseURL: testConfig.siteUrl,
headers: {'X-Requested-With': 'XMLHttpRequest'},
});

View File

@@ -35,13 +35,11 @@ export const apiUploadFile = async (name, absFilePath, requestOptions = {}) => {
formData.append(name, fs.createReadStream(absFilePath));
try {
const response = await client.request({
return await client.request({
...requestOptions,
data: formData,
headers: formData.getHeaders(),
});
return response;
} catch (err) {
return getResponseFromError(err);
}

View File

@@ -90,6 +90,7 @@
"EnableUserCreation": true,
"EnableOpenServer": true,
"EnableUserDeactivation": false,
"EnableCustomUserStatuses": false,
"RestrictCreationToDomains": "",
"EnableCustomBrand": false,
"CustomBrandText": "",

View File

@@ -1,19 +1,27 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import Bot from './bot';
import Channel from './channel';
import Ldap from './ldap';
import Plugin from './plugin';
import Post from './post';
import Preference from './preference';
import Setup from './setup';
import Status from './status';
import System from './system';
import Team from './team';
import User from './user';
export {
Bot,
Channel,
Ldap,
Plugin,
Post,
Preference,
Setup,
Status,
System,
Team,
User,

View File

@@ -20,14 +20,13 @@ import {getResponseFromError} from './common';
/**
* Synchronize any user attribute changes in the configured AD/LDAP server with Mattermost.
* See https://api.mattermost.com/#tag/LDAP/paths/~1ldap~1sync/post
* @return {string} returns {status} on success or {error, status} on error
* See https://api.mattermost.com/#operation/SyncLdap
* @param {string} baseUrl - the base server URL
* @return {string} returns response on success or {error, status} on error
*/
export const apiLDAPSync = async () => {
export const apiLDAPSync = async (baseUrl) => {
try {
const response = await client.post('/api/v4/ldap/sync');
return response;
return await client.post(`${baseUrl}/api/v4/ldap/sync`);
} catch (err) {
return getResponseFromError(err);
}
@@ -35,14 +34,15 @@ export const apiLDAPSync = async () => {
/**
* Test the current AD/LDAP configuration to see if the AD/LDAP server can be contacted successfully.
* See https://api.mattermost.com/#tag/LDAP/paths/~1ldap~1test/post
* @return {string} returns {status} on success or {error, status} on error
* See https://api.mattermost.com/#operation/TestLdap
* @param {string} baseUrl - the base server URL
* @return {Object} returns {status} on success or {error, status} on error
*/
export const apiLDAPTest = async () => {
export const apiLDAPTest = async (baseUrl) => {
try {
const response = await client.post('/api/v4/ldap/test');
const response = await client.post(`${baseUrl}/api/v4/ldap/test`);
return response.data;
return {status: response.status};
} catch (err) {
return getResponseFromError(err);
}
@@ -50,12 +50,13 @@ export const apiLDAPTest = async () => {
/**
* Check that LDAP server can connect and is synchronized with Mattermost server.
* @param {string} baseUrl - the base server URL
*/
export const apiRequireLDAPServer = async () => {
const {error: testError} = await apiLDAPTest();
export const apiRequireLDAPServer = async (baseUrl) => {
const {error: testError} = await apiLDAPTest(baseUrl);
jestExpect(testError).toBeUndefined();
const {error: syncError} = await apiLDAPSync();
const {error: syncError} = await apiLDAPSync(baseUrl);
jestExpect(syncError).toBeUndefined();
};

View File

@@ -0,0 +1,158 @@
// 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`
// ****************************************************************
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.
* @param {string} baseUrl - the base server URL
*/
export const apiDisableNonPrepackagedPlugins = async (baseUrl) => {
const {plugins} = await apiGetAllPlugins(baseUrl);
if (!plugins) {
return;
}
plugins.active.forEach(async (plugin) => {
if (!prepackagedPlugins.includes(plugin.id)) {
await apiDisablePluginById(baseUrl, plugin.id);
}
});
};
/**
* Disable plugin.
* See https://api.mattermost.com/#operation/DisablePlugin
* @param {string} baseUrl - the base server URL
* @param {string} pluginId - the plugin ID
* @return {Object} returns response on success or {error, status} on error
*/
export const apiDisablePluginById = async (baseUrl, pluginId) => {
try {
return await client.post(`${baseUrl}/api/v4/plugins/${encodeURIComponent(pluginId)}/disable`);
} catch (err) {
return getResponseFromError(err);
}
};
/**
* Enable plugin.
* See https://api.mattermost.com/#operation/EnablePlugin
* @param {string} baseUrl - the base server URL
* @param {string} pluginId - the plugin ID
* @return {Object} returns response on success or {error, status} on error
*/
export const apiEnablePluginById = async (baseUrl, pluginId) => {
try {
return await client.post(`${baseUrl}/api/v4/plugins/${encodeURIComponent(pluginId)}/enable`);
} catch (err) {
return getResponseFromError(err);
}
};
/**
* Get plugins.
* See https://api.mattermost.com/#operation/GetPlugins
* @param {string} baseUrl - the base server URL
* @return {Object} returns {plugins} on success or {error, status} on error
*/
export const apiGetAllPlugins = async (baseUrl) => {
try {
const response = await client.get(`${baseUrl}/api/v4/plugins`);
return {plugins: response.data};
} catch (err) {
return getResponseFromError(err);
}
};
/**
* Install plugin from URL.
* See https://api.mattermost.com/#operation/InstallPluginFromUrl
* @param {string} baseUrl - the base server URL
* @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 (baseUrl, pluginDownloadUrl, force = false) => {
try {
const response = await client.post(`${baseUrl}/api/v4/plugins/install_from_url?plugin_download_url=${encodeURIComponent(pluginDownloadUrl)}&force=${force}`);
return {plugin: response.data};
} catch (err) {
return getResponseFromError(err);
}
};
/**
* Remove plugin.
* See https://api.mattermost.com/#operation/RemovePlugin
* @param {string} baseUrl - the base server URL
* @param {string} pluginId - the plugin ID
* @return {Object} returns response on success or {error, status} on error
*/
export const apiRemovePluginById = async (baseUrl, pluginId) => {
try {
return await client.delete(`${baseUrl}/api/v4/plugins/${encodeURIComponent(pluginId)}`);
} catch (err) {
return getResponseFromError(err);
}
};
/**
* Upload plugin.
* See https://api.mattermost.com/#operation/UploadPlugin
* @param {string} baseUrl - the base server URL
* @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 (baseUrl, filename) => {
try {
const absFilePath = path.resolve(__dirname, `../../support/fixtures/${filename}`);
return await apiUploadFile('plugin', absFilePath, {url: `${baseUrl}/api/v4/plugins`, method: 'POST'});
} catch (err) {
return getResponseFromError(err);
}
};
export const Plugin = {
apiDisableNonPrepackagedPlugins,
apiDisablePluginById,
apiEnablePluginById,
apiGetAllPlugins,
apiInstallPluginFromUrl,
apiRemovePluginById,
apiUploadPlugin,
};
export default Plugin;

View File

@@ -5,8 +5,8 @@ import client from './client';
import {getResponseFromError} from './common';
// ****************************************************************
// Channels
// See https://api.mattermost.com/#tag/channels
// Posts
// See https://api.mattermost.com/#tag/posts
//
// Exported API function should have the following:
// - documented using JSDoc
@@ -16,15 +16,44 @@ import {getResponseFromError} from './common';
// - return value defined by `@return`
// ****************************************************************
/**
* Create a new post in a channel. To create the post as a comment on another post, provide root_id.
* See https://api.mattermost.com/#operation/CreatePost
* @param {string} baseUrl - the base server URL
* @param {string} option.channelId - The channel ID to post in
* @param {string} option.message - The message contents, can be formatted with Markdown
* @param {string} option.rootId - The post ID to comment on
* @param {Object} option.props - A general object property bag to attach to the post
* @param {Date} option.createAt - The date the post is created at
* @return {Object} returns {post} on success or {error, status} on error
*/
export const apiCreatePost = async (baseUrl, {channelId, message, rootId, props = {}, createAt = 0}) => {
try {
const payload = {
channel_id: channelId,
message,
root_id: rootId,
props,
create_at: createAt,
};
const response = await client.post(`${baseUrl}/api/v4/posts`, payload);
return {post: response.data};
} catch (err) {
return getResponseFromError(err);
}
};
/**
* Get posts for a channel.
* See https://api.mattermost.com/#tag/posts/paths/~1channels~1{channel_id}~1posts/get
* See https://api.mattermost.com/#operation/GetPostsForChannel
* @param {string} baseUrl - the base server URL
* @param {string} channelId - The channel ID to get the posts for
* @return {Object} returns {posts} on success or {error, status} on error
*/
export const apiGetPostsInChannel = async (channelId) => {
export const apiGetPostsInChannel = async (baseUrl, channelId) => {
try {
const response = await client.get(`/api/v4/channels/${channelId}/posts`);
const response = await client.get(`${baseUrl}/api/v4/channels/${channelId}/posts`);
const {order, posts} = response.data;
@@ -38,17 +67,41 @@ export const apiGetPostsInChannel = async (channelId) => {
/**
* Get last post in a channel.
* @param {string} baseUrl - the base server URL
* @param {string} channelId - The channel ID to get the last post
* @return {Object} returns {post} on success or {error, status} on error
*/
export const apiGetLastPostInChannel = async (channelId) => {
const {posts} = await apiGetPostsInChannel(channelId);
export const apiGetLastPostInChannel = async (baseUrl, channelId) => {
const {posts} = await apiGetPostsInChannel(baseUrl, channelId);
return {post: posts[0]};
};
/**
* Patch a post.
* See https://api.mattermost.com/#operation/PatchPost
* @param {string} baseUrl - the base server URL
* @param {string} postId - the post ID
* @param {Object} postData - data to partially update a post
* @return {Object} returns {post} on success or {error, status} on error
*/
export const apiPatchPost = async (baseUrl, postId, postData) => {
try {
const response = await client.put(
`${baseUrl}/api/v4/posts/${postId}/patch`,
postData,
);
return {post: response.data};
} catch (err) {
return getResponseFromError(err);
}
};
export const Post = {
apiCreatePost,
apiGetLastPostInChannel,
apiGetPostsInChannel,
apiPatchPost,
};
export default Post;

View File

@@ -0,0 +1,101 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import client from './client';
import {getResponseFromError} from './common';
// ****************************************************************
// Preferences
// See https://api.mattermost.com/#tag/preferences
//
// 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`
// ****************************************************************
/**
* Save the user's favorite channel preference.
* @param {string} baseUrl - the base server URL
* @param {string} userId - the user ID
* @param {string} channelId - the channel id to be favorited
* @return {string} returns {status} on success or {error, status} on error
*/
export const apiSaveFavoriteChannelPreference = (baseUrl, userId, channelId) => {
const preference = {
user_id: userId,
category: 'favorite_channel',
name: channelId,
value: 'true',
};
return apiSaveUserPreferences(baseUrl, userId, [preference]);
};
/**
* Save the user's teammate name display preference.
* @param {string} baseUrl - the base server URL
* @param {string} userId - the user ID
* @param {string} nameFormat - one of "username" (default), "nickname_full_name" or "full_name"
* @returns
*/
export const apiSaveTeammateNameDisplayPreference = (baseUrl, userId, nameFormat = 'username') => {
const preference = {
user_id: userId,
category: 'display_settings',
name: 'name_format',
value: nameFormat,
};
return apiSaveUserPreferences(baseUrl, userId, [preference]);
};
/**
* Save the user's teams order preference.
* @param {string} baseUrl - the base server URL
* @param {string} userId - the user ID
* @param {Array} orderedTeamIds - ordered array of team IDs
* @return {string} returns {status} on success or {error, status} on error
*/
export const apiSaveTeamsOrderPreference = (baseUrl, userId, orderedTeamIds = []) => {
const preference = {
user_id: userId,
category: 'teams_order',
name: '',
value: orderedTeamIds.toString(),
};
return apiSaveUserPreferences(baseUrl, userId, [preference]);
};
/**
* Save the user's preferences.
* See https://api.mattermost.com/#operation/UpdatePreferences
* @param {string} baseUrl - the base server URL
* @param {string} userId - the user ID
* @param {Array} preferences - a list of user's preferences
* @return {string} returns {status} on success or {error, status} on error
*/
export const apiSaveUserPreferences = async (baseUrl, userId, preferences = []) => {
try {
const response = await client.put(
`${baseUrl}/api/v4/users/${userId}/preferences`,
preferences,
);
return {status: response.status};
} catch (err) {
return getResponseFromError(err);
}
};
export const Preference = {
apiSaveFavoriteChannelPreference,
apiSaveTeammateNameDisplayPreference,
apiSaveTeamsOrderPreference,
apiSaveUserPreferences,
};
export default Preference;

View File

@@ -7,20 +7,21 @@ import User from './user';
/**
* Creates new user, channel and team for test isolation.
* @param {string} baseUrl - the base server URL
* @param {Object} options - may pass options to predefine channel, team and user creation
* @return {Object} returns {channel, team, user} on success or {error, status} on error
*/
export const apiInit = async ({
export const apiInit = async (baseUrl, {
channelOptions = {type: 'O', prefix: 'channel'},
teamOptions = {type: 'O', prefix: 'team'},
userOptions = {prefix: 'user'},
} = {}) => {
const {team} = await Team.apiCreateTeam(teamOptions);
const {channel} = await Channel.apiCreateChannel({...channelOptions, teamId: team.id});
const {user} = await User.apiCreateUser(userOptions);
const {team} = await Team.apiCreateTeam(baseUrl, teamOptions);
const {channel} = await Channel.apiCreateChannel(baseUrl, {...channelOptions, teamId: team.id});
const {user} = await User.apiCreateUser(baseUrl, userOptions);
await Team.apiAddUserToTeam(user.id, team.id);
await Channel.apiAddUserToChannel(user.id, channel.id);
await Team.apiAddUserToTeam(baseUrl, user.id, team.id);
await Channel.apiAddUserToChannel(baseUrl, user.id, channel.id);
return {
channel,

View File

@@ -0,0 +1,62 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import client from './client';
import {getResponseFromError} from './common';
// ****************************************************************
// Status
// See https://api.mattermost.com/#tag/status
//
// 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 user status.
* See https://api.mattermost.com/#operation/GetUserStatus
* @param {string} baseUrl - the base server URL
* @param {string} userId - the user ID
* @return {Object} returns {userStatus} on success or {error, status} on error
*/
export const apiGetUserStatus = async (baseUrl, userId) => {
try {
const response = await client.get(`${baseUrl}/api/v4/users/${userId}/status`);
return {userStatus: response.data};
} catch (err) {
return getResponseFromError(err);
}
};
/**
* Update user status.
* See https://api.mattermost.com/#operation/UpdateUserStatus
* @param {string} baseUrl - the base server URL
* @param {string} userId - the user ID
* @param {string} status - the user status, can be online, away, offline and dnd
* @return {Object} returns {userStatus} on success or {error, status} on error
*/
export const apiUpdateUserStatus = async (baseUrl, userId, status = 'online') => {
try {
const response = await client.put(
`${baseUrl}/api/v4/users/${userId}/status`,
{user_id: userId, status},
);
return {userStatus: response.data};
} catch (err) {
return getResponseFromError(err);
}
};
export const Status = {
apiGetUserStatus,
apiUpdateUserStatus,
};
export default Status;

View File

@@ -27,73 +27,38 @@ import defaultServerConfig from './default_config.json';
/**
* Check system health.
* See https://api.mattermost.com/#tag/system/paths/~1system~1ping/get
* @return {Object} returns {data} on success or {error, status} on error
* @param {string} baseUrl - the base server URL
*/
export const apiCheckSystemHealth = async () => {
try {
const response = await client.get('/api/v4/system/ping?get_server_status=true');
return {data: response.data};
} catch (err) {
return getResponseFromError(err);
}
export const apiCheckSystemHealth = async (baseUrl) => {
const {data} = await apiPingServerStatus(baseUrl);
jestExpect(data.status).toEqual('OK');
jestExpect(data.database_status).toEqual('OK');
jestExpect(data.filestore_status).toEqual('OK');
};
/**
* Get configuration.
* See https://api.mattermost.com/#tag/system/paths/~1config/get
* Send a test email.
* See https://api.mattermost.com/#operation/TestEmail
* @param {string} baseUrl - the base server URL
* @return {Object} returns response on success or {error, status} on error
*/
export const apiGetConfig = async () => {
export const apiEmailTest = async (baseUrl) => {
try {
const response = await client.get('/api/v4/config');
return {config: response.data};
return await client.post(`${baseUrl}/api/v4/email/test`);
} catch (err) {
return getResponseFromError(err);
}
};
/**
* Update configuration.
* See https://api.mattermost.com/#tag/system/paths/~1config/put
* @param {Object} newConfig - specific config to update
*/
export const apiUpdateConfig = async (newConfig = {}) => {
try {
const {config: currentConfig} = await apiGetConfig();
const config = merge.all([currentConfig, getDefaultConfig(), newConfig]);
const response = await client.put(
'/api/v4/config',
config,
);
return {config: response.data};
} catch (err) {
return getResponseFromError(err);
}
};
function getDefaultConfig() {
const fromEnv = {
LdapSettings: {
LdapServer: testConfig.ldapServer,
LdapPort: testConfig.ldapPort,
},
ServiceSettings: {SiteURL: testConfig.siteUrl},
};
return merge(defaultServerConfig, fromEnv);
}
/**
* Get client license.
* See https://api.mattermost.com/#tag/system/paths/~1license~1client/get
* See https://api.mattermost.com/#operation/GetClientLicense
* @param {string} baseUrl - the base server URL
* @return {Object} returns {license} on success or {error, status} on error
*/
export const apiGetClientLicense = async () => {
export const apiGetClientLicense = async (baseUrl) => {
try {
const response = await client.get('/api/v4/license/client?format=old');
const response = await client.get(`${baseUrl}/api/v4/license/client?format=old`);
return {license: response.data};
} catch (err) {
@@ -101,12 +66,44 @@ export const apiGetClientLicense = async () => {
}
};
/**
* Get configuration.
* See https://api.mattermost.com/#operation/GetConfig
* @param {string} baseUrl - the base server URL
* @return {Object} returns {config} on success or {error, status} on error
*/
export const apiGetConfig = async (baseUrl) => {
try {
const response = await client.get(`${baseUrl}/api/v4/config`);
return {config: response.data};
} catch (err) {
return getResponseFromError(err);
}
};
/**
* Ping server status.
* See https://api.mattermost.com/#operation/GetPing
* @param {string} baseUrl - the base server URL
* @return {Object} returns {data} on success or {error, status} on error
*/
export const apiPingServerStatus = async (baseUrl) => {
try {
const response = await client.get(`${baseUrl}/api/v4/system/ping?get_server_status=true`);
return {data: response.data};
} catch (err) {
return getResponseFromError(err);
}
};
/**
* Require server license to successfully continue.
* @param {string} baseUrl - the base server URL
* @return {Object} returns {license} on success or fail when no license
*/
export const apiRequireLicense = async () => {
const {license} = await getClientLicense();
export const apiRequireLicense = async (baseUrl) => {
const {license} = await getClientLicense(baseUrl);
if (license.IsLicensed !== 'true') {
console.error('Server has no Enterprise license.');
@@ -118,11 +115,12 @@ export const apiRequireLicense = async () => {
/**
* Require server license with specific feature to successfully continue.
* @param {string} baseUrl - the base server URL
* @param {string} key - feature, e.g. LDAP
* @return {Object} returns {license} on success or fail when no license or no license to specific feature.
*/
export const apiRequireLicenseForFeature = async (key = '') => {
const {license} = await getClientLicense();
export const apiRequireLicenseForFeature = async (baseUrl, key = '') => {
const {license} = await getClientLicense(baseUrl);
if (license.IsLicensed !== 'true') {
console.error('Server has no Enterprise license.');
@@ -146,44 +144,93 @@ export const apiRequireLicenseForFeature = async (key = '') => {
};
/**
* Upload server license with file expected at "/detox/e2e/support/fixtures/mattermost-license.txt"
* Require SMTP server to be running.
* @param {string} baseUrl - the base server URL
*/
export const apiUploadLicense = async () => {
const absFilePath = path.resolve(__dirname, '../../support/fixtures/mattermost-license.txt');
const response = await apiUploadFile('license', absFilePath, {url: '/api/v4/license', method: 'POST'});
export const apiRequireSMTPServer = async (baseUrl) => {
const {status} = await apiEmailTest(baseUrl);
jestExpect(status).toEqual(200);
};
return response;
/**
* Update configuration.
* See https://api.mattermost.com/#operation/UpdateConfig
* @param {string} baseUrl - the base server URL
* @param {Object} newConfig - specific config to update
* @return {Object} returns {config} on success or {error, status} on error
*/
export const apiUpdateConfig = async (baseUrl, newConfig = {}) => {
try {
const {config: currentConfig} = await apiGetConfig(baseUrl);
const config = merge.all([currentConfig, getDefaultConfig(), newConfig]);
const response = await client.put(
`${baseUrl}/api/v4/config`,
config,
);
return {config: response.data};
} catch (err) {
return getResponseFromError(err);
}
};
/**
* Upload server license with file expected at "/detox/e2e/support/fixtures/mattermost-license.txt"
* See https://api.mattermost.com/#operation/UploadLicenseFile
* @param {string} baseUrl - the base server URL
* @return {Object} returns response on success or {error, status} on error
*/
export const apiUploadLicense = async (baseUrl) => {
const absFilePath = path.resolve(__dirname, '../../support/fixtures/mattermost-license.txt');
return apiUploadFile('license', absFilePath, {url: `${baseUrl}/api/v4/license`, method: 'POST'});
};
/**
* Get client license.
* If no license, try to upload if license file is available at "/support/fixtures/mattermost-license.txt".
* @return {Object} returns {license} on success or upload when no license or get updated license.
*/
async function getClientLicense() {
const {license} = await apiGetClientLicense();
async function getClientLicense(baseUrl) {
const {license} = await apiGetClientLicense(baseUrl);
if (license.IsLicensed === 'true') {
return {license};
}
// Upload a license if server is currently not loaded with license
const response = await apiUploadLicense();
const response = await apiUploadLicense(baseUrl);
if (response.error) {
console.warn(response.error.message);
return {license};
}
// Get an updated client license
const out = await apiGetClientLicense();
const out = await apiGetClientLicense(baseUrl);
return {license: out.license};
}
function getDefaultConfig() {
const fromEnv = {
LdapSettings: {
LdapServer: testConfig.ldapServer,
LdapPort: testConfig.ldapPort,
},
ServiceSettings: {SiteURL: testConfig.siteUrl},
};
return merge(defaultServerConfig, fromEnv);
}
export const System = {
apiCheckSystemHealth,
apiGetConfig,
apiUpdateConfig,
apiEmailTest,
apiGetClientLicense,
apiGetConfig,
apiPingServerStatus,
apiRequireLicense,
apiRequireLicenseForFeature,
apiRequireSMTPServer,
apiUpdateConfig,
apiUploadLicense,
};

View File

@@ -2,6 +2,7 @@
// See LICENSE.txt for license information.
import {capitalize, getRandomId} from '@support/utils';
import jestExpect from 'expect';
import client from './client';
import {getResponseFromError} from './common';
@@ -18,38 +19,18 @@ import {getResponseFromError} from './common';
// - return value defined by `@return`
// ****************************************************************
/**
* Create a team.
* See https://api.mattermost.com/#tag/teams/paths/~1teams/post
* @param {string} option.type - 'O' (default) for open, 'I' for invite only
* @param {string} option.prefix - option to add prefix to name and display name
* @param {Object} team - fix team object to be created
* @return {Object} returns {team} on success or {error, status} on error
*/
export const apiCreateTeam = async ({type = 'O', prefix = 'team', team = null} = {}) => {
try {
const response = await client.post(
'/api/v4/teams',
team || generateRandomTeam(type, prefix),
);
return {team: response.data};
} catch (err) {
return getResponseFromError(err);
}
};
/**
* Add user to team.
* See https://api.mattermost.com/#tag/teams/paths/~1teams~1{team_id}~1members/post
* See https://api.mattermost.com/#operation/AddTeamMember
* @param {string} baseUrl - the base server URL
* @param {string} userId - The ID of user to add into the team
* @param {string} teamId - The team ID
* @return {Object} returns {member} on success or {error, status} on error
*/
export const apiAddUserToTeam = async (userId, teamId) => {
export const apiAddUserToTeam = async (baseUrl, userId, teamId) => {
try {
const response = await client.post(
`/api/v4/teams/${teamId}/members`,
`${baseUrl}/api/v4/teams/${teamId}/members`,
{team_id: teamId, user_id: userId},
);
@@ -60,14 +41,92 @@ export const apiAddUserToTeam = async (userId, teamId) => {
};
/**
* Get team members for user.
* See https://api.mattermost.com/#tag/teams/paths/~1users~1{user_id}~1teams~1members/get
* @param {string} userId
* Create a team.
* See https://api.mattermost.com/#operation/CreateTeam
* @param {string} baseUrl - the base server URL
* @param {string} option.type - 'O' (default) for open, 'I' for invite only
* @param {string} option.prefix - prefix to name and display name
* @param {Object} option.team - team object to be created
* @return {Object} returns {team} on success or {error, status} on error
*/
export const apiCreateTeam = async (baseUrl, {type = 'O', prefix = 'team', team = null} = {}) => {
try {
const response = await client.post(
`${baseUrl}/api/v4/teams`,
team || generateRandomTeam(type, prefix),
);
return {team: response.data};
} catch (err) {
return getResponseFromError(err);
}
};
/**
* Delete a team.
* See https://api.mattermost.com/#operation/SoftDeleteTeam
* @param {string} baseUrl - the base server URL
* @param {string} teamId - The team ID
* @return {Object} returns {status} on success or {error, status} on error
*/
export const apiDeleteTeam = async (baseUrl, teamId) => {
try {
const response = await client.delete(
`${baseUrl}/api/v4/teams/${teamId}`,
);
return {status: response.status};
} catch (err) {
return getResponseFromError(err);
}
};
/**
* Delete teams.
* @param {string} baseUrl - the base server URL
* @param {Array} teams - array of teams
*/
export const apiDeleteTeams = async (baseUrl, teams = []) => {
let teamArray = teams;
if (!teamArray.length > 0) {
({teams: teamArray} = await Team.apiGetTeams(baseUrl));
}
teamArray.forEach(async (team) => {
const {status} = await Team.apiDeleteTeam(baseUrl, team.id);
jestExpect(status).toEqual(200);
});
};
/**
* Remove user from team.
* See https://api.mattermost.com/#operation/RemoveTeamMember
* @param {string} baseUrl - the base server URL
* @param {string} teamId - The team ID
* @param {string} userId - The user ID to be removed from team
* @return {Object} returns {status} on success or {error, status} on error
*/
export const apiDeleteUserFromTeam = async (baseUrl, teamId, userId) => {
try {
const response = await client.delete(
`${baseUrl}/api/v4/teams/${teamId}/members/${userId}`,
);
return {status: response.status};
} catch (err) {
return getResponseFromError(err);
}
};
/**
* Get teams.
* See https://api.mattermost.com/#operation/GetAllTeams
* @param {string} baseUrl - the base server URL
* @return {Object} returns {teams} on success or {error, status} on error
*/
export const apiGetTeamMembersForUser = async (userId = 'me') => {
export const apiGetTeams = async (baseUrl) => {
try {
const response = await client.get(`/api/v4/users/${userId}/teams`);
const response = await client.get(`${baseUrl}/api/v4/teams`);
return {teams: response.data};
} catch (err) {
@@ -75,6 +134,71 @@ export const apiGetTeamMembersForUser = async (userId = 'me') => {
}
};
/**
* Get teams for user.
* See https://api.mattermost.com/#operation/GetTeamsForUser
* @param {string} baseUrl - the base server URL
* @param {string} userId - The user ID
* @return {Object} returns {teams} on success or {error, status} on error
*/
export const apiGetTeamsForUser = async (baseUrl, userId = 'me') => {
try {
const response = await client.get(`${baseUrl}/api/v4/users/${userId}/teams`);
return {teams: response.data};
} catch (err) {
return getResponseFromError(err);
}
};
/**
* Patch a team.
* See https://api.mattermost.com/#operation/PatchTeam
* @param {string} baseUrl - the base server URL
* @param {string} teamId - The team ID
* @param {string} patch.display_name - Display name
* @param {string} patch.description - Description
* @param {string} patch.company_name - Company name
* @param {string} patch.allowed_domains - Allowed domains
* @param {boolean} patch.allow_open_invite - Allow open invite
* @param {boolean} patch.group_constrained - Group constrained
* @return {Object} returns {team} on success or {error, status} on error
*/
export const apiPatchTeam = async (baseUrl, teamId, teamData) => {
try {
const response = await client.put(
`${baseUrl}/api/v4/teams/${teamId}/patch`,
teamData,
);
return {team: response.data};
} catch (err) {
return getResponseFromError(err);
}
};
/**
* Patch teams.
* @param {string} baseUrl - the base server URL
* @param {string} patch.display_name - Display name
* @param {string} patch.description - Description
* @param {string} patch.company_name - Company name
* @param {string} patch.allowed_domains - Allowed domains
* @param {boolean} patch.allow_open_invite - Allow open invite
* @param {boolean} patch.group_constrained - Group constrained
* @param {Array} teams - array of teams
*/
export const apiPatchTeams = async (baseUrl, teamData, teams = []) => {
let teamArray = teams;
if (!teamArray.length > 0) {
({teams: teamArray} = await Team.apiGetTeams(baseUrl));
}
teamArray.forEach(async (team) => {
await Team.apiPatchTeam(baseUrl, team.id, teamData);
});
};
function generateRandomTeam(type, prefix) {
const randomId = getRandomId();
@@ -88,7 +212,13 @@ function generateRandomTeam(type, prefix) {
export const Team = {
apiAddUserToTeam,
apiCreateTeam,
apiGetTeamMembersForUser,
apiDeleteTeam,
apiDeleteTeams,
apiDeleteUserFromTeam,
apiGetTeams,
apiGetTeamsForUser,
apiPatchTeam,
apiPatchTeams,
};
export default Team;

View File

@@ -19,16 +19,130 @@ import {getResponseFromError} from './common';
// - return value defined by `@return`
// ****************************************************************
/**
* Login to Mattermost server as sysadmin.
* @param {string} baseUrl - the base server URL
* @return {Object} returns {user, status} on success or {error, status} on error
*/
export const apiAdminLogin = (baseUrl) => {
return apiLogin(baseUrl, {
username: testConfig.adminUsername,
password: testConfig.adminPassword,
});
};
/**
* Create a user.
* See https://api.mattermost.com/#operation/CreateUser
* @param {string} baseUrl - the base server URL
* @param {string} option.prefix - prefix to email and username
* @param {Object} option.user - user object to be created
* @return {Object} returns {user} on success or {error, status} on error
*/
export const apiCreateUser = async (baseUrl, {prefix = 'user', user = null} = {}) => {
try {
const newUser = user || generateRandomUser({prefix});
const response = await client.post(
`${baseUrl}/api/v4/users`,
newUser,
);
return {user: {...response.data, password: newUser.password}};
} catch (err) {
return getResponseFromError(err);
}
};
/**
* Deactivate a user account.
* See https://api.mattermost.com/#operation/DeleteUser
* @param {string} baseUrl - the base server URL
* @param {string} userId - the user ID
* @return {Object} returns {status} on success or {error, status} on error
*/
export const apiDeactivateUser = async (baseUrl, userId) => {
try {
const response = await client.delete(`${baseUrl}/api/v4/users/${userId}`);
return {status: response.status};
} catch (err) {
return getResponseFromError(err);
}
};
/**
* Demote a user to a guest.
* See https://api.mattermost.com/#operation/DemoteUserToGuest
* @param {string} baseUrl - the base server URL
* @param {string} userId - the user ID
* @return {Object} returns {status} on success or {error, status} on error
*/
export const apiDemoteUserToGuest = async (baseUrl, userId) => {
try {
const response = await client.post(`${baseUrl}/api/v4/users/${userId}/demote`);
return {status: response.status};
} catch (err) {
return getResponseFromError(err);
}
};
/**
* Get user from a current session.
* @param {string} baseUrl - the base server URL
* @return {Object} returns {user} on success or {error, status} on error
*/
export const apiGetMe = (baseUrl) => {
return apiGetUserById(baseUrl, 'me');
};
/**
* Get a user by ID.
* See https://api.mattermost.com/#operation/GetUser
* @param {string} baseUrl - the base server URL
* @param {string} userId - the user ID
* @return {Object} returns {user} on success or {error, status} on error
*/
export const apiGetUserById = async (baseUrl, userId) => {
try {
const response = await client.get(`${baseUrl}/api/v4/users/${userId}`);
return {user: response.data};
} catch (err) {
return getResponseFromError(err);
}
};
/**
* Get a user by username.
* See https://api.mattermost.com/#operation/GetUserByUsername
* @param {string} baseUrl - the base server URL
* @param {string} username - the username
* @return {Object} returns {user} on success or {error, status} on error
*/
export const apiGetUserByUsername = async (baseUrl, username) => {
try {
const response = await client.get(`${baseUrl}/api/v4/users/username/${username}`);
return {user: response.data};
} catch (err) {
return getResponseFromError(err);
}
};
/**
* Login to Mattermost server.
* See https://api.mattermost.com/#tag/users/paths/~1users~1login/post
* See https://api.mattermost.com/#operation/Login
* @param {string} baseUrl - the base server URL
* @param {string} user.username - username of a user
* @param {string} user.password - password of a user
* @return {Object} returns {user, status} on success or {error, status} on error
*/
export const apiLogin = async (user) => {
export const apiLogin = async (baseUrl, user) => {
try {
const response = await client.post(
'/api/v4/users/login',
`${baseUrl}/api/v4/users/login`,
{login_id: user.username, password: user.password},
);
@@ -47,64 +161,45 @@ export const apiLogin = async (user) => {
}
};
/**
* Login to Mattermost server as sysadmin.
*/
export const apiAdminLogin = async () => {
return apiLogin({
username: testConfig.adminUsername,
password: testConfig.adminPassword,
});
};
/**
* Logout from the Mattermost server.
* See https://api.mattermost.com/#tag/users/paths/~1users~1logout/post
* See https://api.mattermost.com/#operation/Logout
* @param {string} baseUrl - the base server URL
* @return {Object} returns {status} on success
*/
export const apiLogout = async () => {
const response = await client.post('/api/v4/users/logout');
export const apiLogout = async (baseUrl) => {
const response = await client.post(`${baseUrl}/api/v4/users/logout`);
client.defaults.headers.Cookie = '';
return response.data;
return {status: response.status};
};
/**
* Create a user.
* See https://api.mattermost.com/#tag/users/paths/~1users/post
* @param {Object} user - user object to be created
* Patch user from a current session.
* @param {string} baseUrl - the base server URL
* @param {Object} userData - data to partially update a user
* @return {Object} returns {user} on success or {error, status} on error
*/
export const apiCreateUser = async ({prefix = 'user', user = null} = {}) => {
try {
const newUser = user || generateRandomUser(prefix);
export const apiPatchMe = (baseUrl, userData) => {
return apiPatchUser(baseUrl, 'me', userData);
};
const response = await client.post(
'/api/v4/users',
newUser,
/**
* Patch a user.
* See https://api.mattermost.com/#operation/PatchUser
* @param {string} baseUrl - the base server URL
* @param {string} userId - the user ID
* @param {Object} userData - data to partially update a user
* @return {Object} returns {user} on success or {error, status} on error
*/
export const apiPatchUser = async (baseUrl, userId, userData) => {
try {
const response = await client.put(
`${baseUrl}/api/v4/users/${userId}/patch`,
userData,
);
return {user: {...response.data, password: newUser.password}};
} catch (err) {
return getResponseFromError(err);
}
};
/**
* Get user from a current session.
*/
export const apiGetMe = () => {
return apiGetUserById('me');
};
/**
* Get a user by ID.
* See https://api.mattermost.com/#tag/users/paths/~1users~1{user_id}/get
* @param {string} userId
*/
export const apiGetUserById = async (userId) => {
try {
const response = await client.get(`/api/v4/users/${userId}`);
return {user: response.data};
} catch (err) {
return getResponseFromError(err);
@@ -112,41 +207,54 @@ export const apiGetUserById = async (userId) => {
};
/**
* Get a user by username.
* See https://api.mattermost.com/#tag/users/paths/~1users~1username~1{username}/get
* @param {string} username
* Update user active status.
* See https://api.mattermost.com/#operation/UpdateUserActive
* @param {string} baseUrl - the base server URL
* @param {string} userId - the user ID
* @param {boolean} active - use true to set the user active, false for inactive
* @return {Object} returns {status} on success or {error, status} on error
*/
export const apiGetUserByUsername = async (username) => {
export const apiUpdateUserActiveStatus = async (baseUrl, userId, active) => {
try {
const response = await client.get(`/api/v4/users/username/${username}`);
const response = await client.put(
`${baseUrl}/api/v4/users/${userId}/active`,
{active},
);
return {user: response.data};
return {status: response.status};
} catch (err) {
return getResponseFromError(err);
}
};
function generateRandomUser(prefix) {
const randomId = getRandomId();
export const generateRandomUser = ({prefix = 'user', randomIdLength = 6} = {}) => {
const randomId = getRandomId(randomIdLength);
return {
email: `${prefix}${randomId}@sample.mattermost.com`,
username: `${prefix}${randomId}`,
password: 'passwd',
first_name: `First${randomId}`,
last_name: `Last${randomId}`,
nickname: `Nickname${randomId}`,
first_name: `F${randomId}`,
last_name: `L${randomId}`,
nickname: `N${randomId}`,
position: `P${randomId}`,
};
}
};
export const User = {
apiAdminLogin,
apiLogin,
apiLogout,
apiCreateUser,
apiDeactivateUser,
apiDemoteUserToGuest,
apiGetMe,
apiGetUserById,
apiGetUserByUsername,
apiLogin,
apiLogout,
apiPatchMe,
apiPatchUser,
apiUpdateUserActiveStatus,
generateRandomUser,
};
export default User;

View File

@@ -4,6 +4,8 @@
module.exports = {
serverUrl: process.env.SITE_URL || (process.env.IOS ? 'http://localhost:8065' : 'http://10.0.2.2:8065'),
siteUrl: process.env.SITE_URL || 'http://localhost:8065',
smtpUrl: process.env.SMTP_URL || 'http://localhost:9001',
adminEmail: process.env.ADMIN_EMAIL || 'sysadmin@sample.mattermost.com',
adminUsername: process.env.ADMIN_USERNAME || 'sysadmin',
adminPassword: process.env.ADMIN_PASSWORD || 'Sys@dmin-sample1',
ldapServer: process.env.LDAP_SERVER || 'localhost',

View File

@@ -0,0 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Imports here
// Exports here
export {
};

View File

@@ -0,0 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Imports here
// Exports here
export {
};

View File

@@ -0,0 +1,238 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import testConfig from '@support/test_config';
import axios from 'axios';
import jestExpect from 'expect';
/**
* Get email url.
* @returns {string} email url
*/
export const getEmailUrl = () => {
const smtpUrl = testConfig.smtpUrl || 'http://localhost:9001';
return `${smtpUrl}/api/v1/mailbox`;
};
/**
* Get email reset email template.
* @param {string} userEmail - the destination user email
* @returns {string} email template
*/
export const getEmailResetEmailTemplate = (userEmail) => {
return [
'----------------------',
'You updated your email',
'----------------------',
'',
`Your email address for Mattermost has been changed to ${userEmail}.`,
'If you did not make this change, please contact the system administrator.',
'',
'To change your notification preferences, log in to your team site and go to Settings > Notifications.',
];
};
/**
* Get join email template.
* @param {string} sender - the email sender
* @param {string} userEmail - the destination user email
* @param {Object} team - the team to join
* @param {boolean} isGuest - true if guest; otherwise false
* @returns {string} email template
*/
export const getJoinEmailTemplate = (sender, userEmail, team, isGuest = false) => {
const baseUrl = testConfig.siteUrl;
return [
`${sender} invited you to join the ${team.display_name} team.`,
`${isGuest ? 'You were invited as a guest to collaborate with the team' : 'Start collaborating with your team on Mattermost'}`,
'',
`<join-link-check> Join now ( ${baseUrl}/signup_user_complete/?d=${encodeURIComponent(JSON.stringify({display_name: team.display_name.replace(' ', '+'), email: userEmail, name: team.name}))}&t=<actual-token> )`,
'',
'What is Mattermost?',
'Mattermost is a flexible, open source messaging platform that enables secure team collaboration.',
'Learn more ( mattermost.com )',
'',
'© 2021 Mattermost, Inc. 530 Lytton Avenue, Second floor, Palo Alto, CA, 94301',
];
};
/**
* Get mention email template.
* @param {string} sender - the email sender
* @param {string} message - the email message
* @param {string} postId - the post id where user is mentioned
* @param {string} siteName - the site name
* @param {string} teamName - the team name where user is mentioned
* @param {string} channelDisplayName - the channel display name where user is mentioned
* @@returns {string} email template
*/
export const getMentionEmailTemplate = (sender, message, postId, siteName, teamName, channelDisplayName) => {
const baseUrl = testConfig.siteUrl;
return [
`@${sender} mentioned you in a message`,
`While you were away, @${sender} mentioned you in the ${channelDisplayName} channel.`,
'',
`View Message ( ${baseUrl}/landing#/${teamName}/pl/${postId} )`,
'',
`@${sender}`,
'<skip-local-time-check>',
channelDisplayName,
'',
message,
'',
'Want to change your notifications settings?',
`Login to ${siteName} ( ${baseUrl} ) and go to Settings > Notifications`,
'',
'© 2021 Mattermost, Inc. 530 Lytton Avenue, Second floor, Palo Alto, CA, 94301',
];
};
/**
* Get password reset email template.
* @returns {string} email template
*/
export const getPasswordResetEmailTemplate = () => {
const baseUrl = testConfig.siteUrl;
return [
'Reset Your Password',
'Click the button below to reset your password. If you didnt request this, you can safely ignore this email.',
'',
`<reset-password-link-check> Reset Password ( http://${baseUrl}/reset_password_complete?token=<actual-token> )`,
'',
'The password reset link expires in 24 hours.',
'',
'© 2021 Mattermost, Inc. 530 Lytton Avenue, Second floor, Palo Alto, CA, 94301',
];
};
/**
* Get email verify email template.
* @param {string} userEmail - the destination user email
* @returns {string} email template
*/
export const getEmailVerifyEmailTemplate = (userEmail) => {
const baseUrl = testConfig.siteUrl;
return [
'Verify your email address',
`Thanks for joining ${baseUrl.split('/')[2]}. ( ${baseUrl} )`,
'Click below to verify your email address.',
'',
`<email-verify-link-check> Verify Email ( ${baseUrl}/do_verify_email?token=<actual-token>&email=${encodeURIComponent(userEmail)} )`,
'',
'This email address was used to create an account with Mattermost.',
'If it was not you, you can safely ignore this email.',
'',
'© 2021 Mattermost, Inc. 530 Lytton Avenue, Second floor, Palo Alto, CA, 94301',
];
};
/**
* Get welcome email template.
* @param {string} userEmail - the destination user email
* @param {string} siteName - the site name
* @param {string} teamName - the team name where user is welcome
* @returns {string} email template
*/
export const getWelcomeEmailTemplate = (userEmail, siteName, teamName) => {
const baseUrl = testConfig.siteUrl;
return [
'Welcome to the team',
`Thanks for joining ${baseUrl.split('/')[2]}. ( ${baseUrl} )`,
'Click below to verify your email address.',
'',
`<email-verify-link-check> Verify Email ( ${baseUrl}/do_verify_email?token=<actual-token>&email=${encodeURIComponent(userEmail)}&redirect_to=/${teamName} )`,
'',
`This email address was used to create an account with ${siteName}.`,
'If it was not you, you can safely ignore this email.',
'',
'Download the desktop and mobile apps',
'For the best experience, download the apps for PC, Mac, iOS and Android.',
'',
'Download ( https://mattermost.com/download/#mattermostApps )',
'',
'© 2021 Mattermost, Inc. 530 Lytton Avenue, Second floor, Palo Alto, CA, 94301',
];
};
/**
* Verify email body.
* @param {string} expectedBody - expected email body
* @param {*} actualBody - actual email body
*/
export const verifyEmailBody = (expectedBody, actualBody) => {
jestExpect(expectedBody.length).toEqual(actualBody.length);
for (let i = 0; i < expectedBody.length; i++) {
if (expectedBody[i].includes('skip-local-time-check')) {
continue;
}
if (expectedBody[i].includes('email-verify-link-check')) {
jestExpect(actualBody[i]).toContain('Verify Email');
jestExpect(actualBody[i]).toContain('do_verify_email?token=');
continue;
}
if (expectedBody[i].includes('join-link-check')) {
jestExpect(actualBody[i]).toContain('Join now');
jestExpect(actualBody[i]).toContain('signup_user_complete/?d=');
continue;
}
if (expectedBody[i].includes('reset-password-link-check')) {
jestExpect(actualBody[i]).toContain('Reset Password');
jestExpect(actualBody[i]).toContain('reset_password_complete?token=');
continue;
}
jestExpect(expectedBody[i]).toEqual(actualBody[i]);
}
};
/**
* Get recent email.
* @param {string} username - username of email recipient
* @param {string} mailUrl - url of email
*/
export const getRecentEmail = async (username, mailUrl = getEmailUrl()) => {
const mailboxUrl = `${mailUrl}/${username}`;
let response;
let recentEmail;
try {
response = await axios({url: mailboxUrl, method: 'get'});
recentEmail = response.data[response.data.length - 1];
} catch (error) {
return {status: error.status, data: null};
}
if (!recentEmail || !recentEmail.id) {
return {status: 501, data: null};
}
let recentEmailMessage;
const mailMessageUrl = `${mailboxUrl}/${recentEmail.id}`;
try {
response = await axios({url: mailMessageUrl, method: 'get'});
recentEmailMessage = response.data;
} catch (error) {
return {status: error.status, data: null};
}
return {status: response.status, data: recentEmailMessage};
};
/**
* Split email body text.
* @param {string} text
* @return {string} split text
*/
export const splitEmailBodyText = (text) => {
return text.split('\n').map((d) => d.trim());
};

View File

@@ -1,25 +1,38 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import testConfig from '@support/test_config';
import {v4 as uuidv4} from 'uuid';
export * from './email';
/**
* Explicit `wait` should not normally used but made available for special cases.
* @param {number} ms - duration in millisecond
* @return {Promise} promise with timeout
*/
export const wait = async (ms) => {
return new Promise((resolve) => setTimeout(resolve, ms));
};
/**
* Check if android.
* @return {boolean} true if android
*/
export const isAndroid = () => {
return device.getPlatform() === 'android';
};
/**
* Check if ios.
* @return {boolean} true if ios
*/
export const isIos = () => {
return device.getPlatform() === 'ios';
};
/**
* Get random id.
* @param {number} length - length on random string to return, e.g. 6 (default)
* @return {string} random string
*/
@@ -30,6 +43,7 @@ export const getRandomId = (length = 6) => {
};
/**
* Capitalize first character of text.
* @param {string} text
* @return {string} capitalized text
*/
@@ -37,6 +51,17 @@ export const capitalize = (text) => {
return text.charAt(0).toUpperCase() + text.slice(1);
};
/**
* Get admin account.
*/
export const getAdminAccount = () => {
return {
username: testConfig.adminUsername,
password: testConfig.adminPassword,
email: testConfig.adminEmail,
};
};
const SECOND = 1000;
const MINUTE = 60 * 1000;

View 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,
};

3481
detox/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,21 +5,22 @@
"author": "Mattermost, Inc.",
"devDependencies": {
"@babel/plugin-proposal-class-properties": "7.16.7",
"@babel/plugin-transform-modules-commonjs": "7.16.7",
"@babel/plugin-transform-runtime": "7.16.7",
"@babel/preset-env": "7.16.7",
"axios": "0.24.0",
"babel-jest": "27.4.5",
"@babel/plugin-transform-modules-commonjs": "7.16.8",
"@babel/plugin-transform-runtime": "7.17.0",
"@babel/preset-env": "7.16.11",
"axios": "0.26.0",
"babel-jest": "27.5.1",
"babel-plugin-module-resolver": "4.1.0",
"client-oauth2": "github:larkox/js-client-oauth2#e24e2eb5dfcbbbb3a59d095e831dbe0012b0ac49",
"client-oauth2": "4.3.3",
"deepmerge": "4.2.2",
"detox": "19.4.1",
"detox": "19.4.5",
"form-data": "4.0.0",
"jest": "27.4.5",
"jest-circus": "27.4.5",
"jest-cli": "27.4.5",
"jest-html-reporters": "3.0.3",
"jest": "27.5.1",
"jest-circus": "27.5.1",
"jest-cli": "27.5.1",
"jest-html-reporters": "3.0.5",
"jest-junit": "13.0.0",
"moment-timezone": "0.5.34",
"sanitize-filename": "1.6.3",
"uuid": "8.3.2"
},

295
detox/webhook_server.js Normal file
View File

@@ -0,0 +1,295 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/* eslint-disable camelcase, no-console */
const axios = require('axios');
const ClientOAuth2 = require('client-oauth2');
const express = require('express');
const postMessageAs = require('./e2e/plugins/post_message_as');
const webhookUtils = require('./e2e/utils/webhook_utils');
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,
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(baseUrl, {sender: {username, password}, message, channelId});
}
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);
}