mattermost-mobile-upstream/detox/e2e/support/server_api/plugin.ts
yasser khan ebc91be6e0
E2E: run shared channels tests on CI, demo plugin v0.12.2 on all full-suite servers (#10267)
* E2E: run the shared channels tests on CI instead of skipping them

CI servers now boot with Shared Channels running (matterwick#106), so the suite runs there and fails setup if the service is missing.

* E2E: run demo plugin v0.12.2 in DialogOnlyMode

Moves the demo plugin to v0.12.2 and writes DialogOnlyMode: true into its settings entry before it is installed on server B, so it no longer creates a demo user and channels or posts in every team on every server event. Only /dialog is kept, which is all the dialog specs use. The entry still keeps an older build on a reused server out of config saves.

* E2E: give a neutral reason when Shared Channels is unavailable

hasSharedChannels is false both when the license lacks Shared Channels and when GET /remotecluster fails; point to the globalSetup log line that names the actual reason.

* E2E: run the read-only dialog specs on the pool servers

With demo plugin v0.12.2 in DialogOnlyMode it no longer posts in every team, so it is installed on every full-suite server (pool A and mutation B) instead of B only. interactive_dialog_plugin and interactive_dialog_collapsible change no server-wide state, so they leave the mutation manifest and run on the pool. interactive_dialog_mobile_upload_disabled stays on B. The shared SITE_3 and smoke servers still get no demo plugin, and CMT still rejects a site 2 that is also a pool site 1.

* E2E: remove unused demo plugin helpers

apiUploadAndEnablePlugin and apiClearDemoPluginFileUploadKV had no callers (the KV route does not exist). Provisioning installs the plugin, so the install plan they used goes too.

* fix(e2e): say why a spec has no result (#10280)

* chore: Update NOTICE.txt file with updated dependencies (#10277)

Co-authored-by: unified-ci-app[bot] <121569378+unified-ci-app[bot]@users.noreply.github.com>

* fix(e2e): say whether a spec with no result had a silent shard or a stopped one

A spec that produced no result now says which: no spec of its shard reported
(the shard ran or uploaded nothing), or other specs of its shard reported (its
Jest process stopped before reaching this spec). E2E triage re-runs the first
and judges the second by the spec's own history. The shard of each spec comes
from the generate-specs matrix rows; without them the old wording stays.

---------

Co-authored-by: unified-ci-app[bot] <121569378+unified-ci-app[bot]@users.noreply.github.com>

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
Co-authored-by: unified-ci-app[bot] <121569378+unified-ci-app[bot]@users.noreply.github.com>
2026-10-27 11:42:07 +05:30

354 lines
12 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import path from 'path';
import {
DEMO_PLUGIN_FIXTURE_FILENAME,
DEMO_PLUGIN_ID,
} from '../../../shared/demo-plugin-fixture';
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 = new Set([
'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.calls',
'com.mattermost.nps',
'com.mattermost.welcomebot',
'zoom',
]);
/**
* Get the latest release version from GitHub releases
* @param {string} repo - GitHub repository in format 'owner/repo'
* @return {Promise<string>} returns latest version string without 'v' prefix
*/
export const apiGetLatestPluginVersion = async (repo: string): Promise<string> => {
try {
const response = await client.get(`https://api.github.com/repos/${repo}/releases/latest`);
const tagName = response.data.tag_name;
// Remove 'v' prefix if present (e.g., 'v0.10.2' -> '0.10.2')
return tagName.startsWith('v') ? tagName.substring(1) : tagName;
} catch (err) {
// Fallback to hardcoded version if API fails
return '0.10.3';
}
};
// Agents Plugin Constants
export const AgentsPlugin = {
id: 'mattermost-ai',
} as const;
// Calls Plugin Constants
export const CallsPlugin = {
id: 'com.mattermost.calls',
url: 'https://github.com/mattermost/mattermost-plugin-calls/releases/download/v1.5.0/com.mattermost.calls-1.5.0.tar.gz',
} as const;
// Demo Plugin Constants
export const DemoPlugin = {
id: DEMO_PLUGIN_ID,
filename: DEMO_PLUGIN_FIXTURE_FILENAME,
} as const;
/**
* Disable non-prepackaged plugins.
* @param {string} baseUrl - the base server URL
*/
export const apiDisableNonPrepackagedPlugins = async (baseUrl: string): Promise<any> => {
const {plugins} = await apiGetAllPlugins(baseUrl);
if (!plugins) {
return;
}
plugins.active.forEach(async (plugin: any) => {
if (plugin.id !== DemoPlugin.id && !prepackagedPlugins.has(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: string, pluginId: string): Promise<any> => {
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: string, pluginId: string): Promise<any> => {
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: string): Promise<any> => {
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: string, pluginDownloadUrl: string, force = false): Promise<any> => {
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: string, pluginId: string): Promise<any> => {
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
* @param {boolean} force - overwrite an existing plugin install
* @return {Object} returns response on success or {error, status} on error
*/
export const apiUploadPlugin = async (baseUrl: string, filename: string, force = false): Promise<any> => {
try {
const absFilePath = path.resolve(__dirname, `../../support/fixtures/${filename}`);
const forceQuery = force ? '?force=true' : '';
return await apiUploadFile('plugin', absFilePath, {url: `${baseUrl}/api/v4/plugins${forceQuery}`, method: 'POST'});
} catch (err) {
return getResponseFromError(err);
}
};
/**
* Get plugin status - whether it's installed and/or active.
* @param {string} baseUrl - the base server URL
* @param {string} pluginId - the plugin ID
* @param {string} version - the expected plugin version
* @return {Object} returns {isInstalled, isActive, plugin} on success or {error, status} on error
*/
export const apiGetPluginStatus = async (baseUrl: string, pluginId: string, version?: string): Promise<any> => {
const allPluginsResult = await apiGetAllPlugins(baseUrl);
if (allPluginsResult.error) {
return allPluginsResult;
}
const {plugins} = allPluginsResult;
if (!plugins) {
return {isInstalled: false, isActive: false};
}
try {
// Check if plugin is installed (in either active or inactive list)
let plugin = plugins.active?.find((p: any) => p.id === pluginId);
if (plugin) {
const isVersionMatch = !version || plugin.version === version;
return {
isInstalled: true,
isActive: true,
plugin,
isVersionMatch,
};
}
plugin = plugins.inactive?.find((p: any) => p.id === pluginId);
if (plugin) {
const isVersionMatch = !version || plugin.version === version;
return {
isInstalled: true,
isActive: false,
plugin,
isVersionMatch,
};
}
return {isInstalled: false, isActive: false};
} catch (err) {
return getResponseFromError(err);
}
};
/**
* Install a plugin from the Marketplace.
* See https://api.mattermost.com/#operation/InstallMarketplacePlugin
* @param {string} baseUrl - the base server URL
* @param {string} pluginId - the plugin ID to install from Marketplace
* @return {Object} returns {plugin} on success or {error, status} on error
*/
export const apiInstallPluginFromMarketplace = async (baseUrl: string, pluginId: string): Promise<any> => {
try {
const response = await client.post(`${baseUrl}/api/v4/plugins/marketplace`, {id: pluginId});
return {plugin: response.data};
} catch (err) {
return getResponseFromError(err);
}
};
/**
* Ensure a plugin is installed and active, installing from Marketplace if needed.
* @param {string} baseUrl - the base server URL
* @param {string} pluginId - the plugin ID
* @return {Object} returns {isInstalled, isActive} status
*/
export const apiEnsurePluginInstalled = async (baseUrl: string, pluginId: string): Promise<any> => {
// Check if already installed
const status = await apiGetPluginStatus(baseUrl, pluginId);
if (status.isActive) {
return status;
}
if (status.isInstalled && !status.isActive) {
// eslint-disable-next-line no-console
console.log(`[apiEnsurePluginInstalled] Plugin ${pluginId} installed but inactive, enabling...`);
await apiEnablePluginById(baseUrl, pluginId);
await new Promise((resolve) => setTimeout(resolve, 2000));
return apiGetPluginStatus(baseUrl, pluginId);
}
// Not installed — try Marketplace
// eslint-disable-next-line no-console
console.log(`[apiEnsurePluginInstalled] Plugin ${pluginId} not installed, installing from Marketplace...`);
const installResult = await apiInstallPluginFromMarketplace(baseUrl, pluginId);
if (installResult.error) {
// eslint-disable-next-line no-console
console.warn(`[apiEnsurePluginInstalled] Marketplace install failed for ${pluginId}:`, installResult.error.message || installResult.error);
return {isInstalled: false, isActive: false};
}
// Enable after install
await apiEnablePluginById(baseUrl, pluginId);
await new Promise((resolve) => setTimeout(resolve, 2000));
const finalStatus = await apiGetPluginStatus(baseUrl, pluginId);
// eslint-disable-next-line no-console
console.log(`[apiEnsurePluginInstalled] Plugin ${pluginId}: installed=${finalStatus.isInstalled}, active=${finalStatus.isActive}`);
return finalStatus;
};
/**
* Submit the demo plugin file-upload interactive dialog (populates plugin KV for re-open hydration).
* @param {string} baseUrl - the base server URL
* @param {Object} params - dialog submit payload
* @return {Object} returns {status} on success or {error, status} on error
*/
export const apiSubmitDemoPluginFileUploadDialog = async (
baseUrl: string,
{
userId,
channelId,
teamId,
submission,
fileIds = [],
}: {
userId: string;
channelId: string;
teamId: string;
submission: Record<string, string>;
fileIds?: string[];
},
): Promise<any> => {
try {
const response = await client.post(
`${baseUrl}/plugins/${DemoPlugin.id}/dialog/file-upload`,
{
user_id: userId,
channel_id: channelId,
team_id: teamId,
submission,
file_ids: fileIds,
cancelled: false,
state: 'somestate',
},
);
return {status: response.status};
} catch (err) {
return getResponseFromError(err);
}
};
export const Plugin = {
apiDisableNonPrepackagedPlugins,
apiDisablePluginById,
apiEnablePluginById,
apiEnsurePluginInstalled,
apiGetAllPlugins,
apiGetLatestPluginVersion,
apiGetPluginStatus,
apiInstallPluginFromMarketplace,
apiInstallPluginFromUrl,
apiRemovePluginById,
apiUploadPlugin,
apiSubmitDemoPluginFileUploadDialog,
};
export default Plugin;