snapshot of https://github.com/mattermost/mattermost.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s

This commit is contained in:
upstream-mirror
2026-09-16 00:29:55 +05:00
commit 240db4e2a7
15309 changed files with 2173406 additions and 0 deletions

View File

@@ -0,0 +1,139 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {Bot, BotPatch} from '@mattermost/types/bots';
import {getRandomId} from '../../utils';
import type {ChainableT} from '@/types';
// *****************************************************************************
// Bots
// https://api.mattermost.com/#tag/bots
// *****************************************************************************
/**
* Create a bot.
* See https://api.mattermost.com/#tag/bots/paths/~1bots/post
* @param {string} options.bot - predefined `bot` object instead of random bot
* @param {string} options.prefix - 'bot' (default) or any prefix to easily identify a bot
* @returns {Bot} out.bot: `Bot` object
*
* @example
* cy.apiCreateBot().then(({bot}) => {
* // do something with bot
* });
*/
function apiCreateBot({prefix, bot}: Partial<{prefix: string; bot: BotPatch}> = {}): ChainableT<{bot: Bot & {fullDisplayName: string}}> {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: '/api/v4/bots',
method: 'POST',
body: bot || createBotPatch(prefix),
}).then((response) => {
expect(response.status).to.equal(201);
const {body} = response;
return cy.wrap({
bot: {
...body,
fullDisplayName: `${body.display_name} (@${body.username})`,
},
});
});
}
Cypress.Commands.add('apiCreateBot', apiCreateBot);
/**
* Get bots.
* See https://api.mattermost.com/#tag/bots/paths/~1bots/get
* @param {number} options.page - The page to select
* @param {number} options.perPage - The number of users per page. There is a maximum limit of 200 users per page
* @param {boolean} options.includeDeleted - If deleted bots should be returned
* @returns {Bot[]} out.bots: `Bot[]` object
*
* @example
* cy.apiGetBots();
*/
function apiGetBots(page = 0, perPage = 200, includeDeleted = false): ChainableT<{bots: Bot[]}> {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: `/api/v4/bots?page=${page}&per_page=${perPage}&include_deleted=${includeDeleted}`,
method: 'GET',
}).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap({bots: response.body});
});
}
Cypress.Commands.add('apiGetBots', apiGetBots);
/**
* Disable bot.
* See https://api.mattermost.com/#tag/bots/operation/DisableBot
* @param {string} userId - User ID
* @returns {Response} response: Cypress-chainable response which should have successful HTTP status of 200 OK to continue or pass.
*
* @example
* cy.apiDisableBot('user-id);
*/
function apiDisableBot(userId: string) {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: `/api/v4/bots/${userId}/disable`,
method: 'POST',
}).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap(response);
});
}
Cypress.Commands.add('apiDisableBot', apiDisableBot);
/**
* Patches bot
* @param {string} prefix - bot prefix
* @returns {BotPatch} botPatch: Cypress-chainable bot
*/
export function createBotPatch(prefix = 'bot'): BotPatch {
const randomId = getRandomId();
return {
username: `${prefix}-${randomId}`,
display_name: `Test Bot ${randomId}`,
description: `Test bot description ${randomId}`,
} as BotPatch;
}
/**
* Deactivate test bots.
*
* @example
* cy.apiDeactivateTestBots();
*/
function apiDeactivateTestBots() {
return cy.apiGetBots().then(({bots}) => {
bots.forEach((bot) => {
if (bot?.display_name?.includes('Test Bot') || bot?.username.startsWith('bot-')) {
cy.apiDisableBot(bot.user_id);
cy.apiDeactivateUser(bot.user_id);
// Log for debugging
cy.log(`Deactivated Bot: "${bot.username}"`);
}
});
});
}
Cypress.Commands.add('apiDeactivateTestBots', apiDeactivateTestBots);
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
interface Chainable {
apiCreateBot: typeof apiCreateBot;
apiDeactivateTestBots: typeof apiDeactivateTestBots;
apiGetBots: typeof apiGetBots;
apiDisableBot: typeof apiDisableBot;
}
}
}

View File

@@ -0,0 +1,32 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/// <reference types="cypress" />
// ***************************************************************
// Each command should be properly documented using JSDoc.
// See https://jsdoc.app/index.html for reference.
// Basic requirements for documentation are the following:
// - Meaningful description
// - Specific link to https://api.mattermost.com
// - Each parameter with `@params`
// - Return value with `@returns`
// - Example usage with `@example`
// Custom command should follow naming convention of having `api` prefix, e.g. `apiLogin`.
// ***************************************************************
declare namespace Cypress {
interface Chainable {
/**
* Delete the custom brand image.
* See https://api.mattermost.com/#tag/brand/paths/~1brand~1image/delete
* @returns {Response} response: Cypress-chainable response which should have either a successful HTTP status of 200 OK
* or a 404 Not Found in case that the image didn't exists to continue or pass.
*
* @example
* cy.apiDeleteBrandImage();
*/
apiDeleteBrandImage(): Chainable<Record<string, any>>;
}
}

View File

@@ -0,0 +1,20 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// *****************************************************************************
// Brand
// https://api.mattermost.com/#tag/brand
// *****************************************************************************
Cypress.Commands.add('apiDeleteBrandImage', () => {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: '/api/v4/brand/image',
method: 'DELETE',
failOnStatusCode: false,
}).then((response) => {
// both deleted and not existing responses are valid
expect(response.status).to.be.oneOf([200, 404]);
return cy.wrap(response);
});
});

View File

@@ -0,0 +1,437 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {Channel, ChannelMembership, ChannelType} from '@mattermost/types/channels';
import type {UserProfile} from '@mattermost/types/users';
import {getRandomId} from '../../utils';
import type {ChainableT} from '@/types';
// *****************************************************************************
// Channels
// https://api.mattermost.com/#tag/channels
// *****************************************************************************
export function createChannelPatch(teamId: string, name: string, displayName: string, type: ChannelType = 'O', purpose = '', header = '', unique = true): Partial<Channel> {
const randomSuffix = getRandomId();
return {
team_id: teamId,
name: unique ? `${name}-${randomSuffix}` : name,
display_name: unique ? `${displayName} ${randomSuffix}` : displayName,
type,
purpose,
header,
};
}
/**
* Create a new channel.
* See https://api.mattermost.com/#tag/channels/paths/~1channels/post
* @param {String} teamId - Unique handler for a team, will be present in the team URL
* @param {String} name - Unique handler for a channel, will be present in the team URL
* @param {String} displayName - Non-unique UI name for the channel
* @param {String} type - 'O' for a public channel (default), 'P' for a private channel
* @param {String} purpose - A short description of the purpose of the channel
* @param {String} header - Markdown-formatted text to display in the header of the channel
* @param {Boolean} [unique=true] - if true (default), it will create with unique/random channel name.
* @returns {Channel} `out.channel` as `Channel`
*
* @example
* cy.apiCreateChannel('team-id', 'test-channel', 'Test Channel').then(({channel}) => {
* // do something with channel
* });
*/
function apiCreateChannel(
teamId: string,
name: string,
displayName: string,
type?: ChannelType,
purpose?: string,
header?: string,
unique: boolean = true): ChainableT<{channel: Channel}> {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: '/api/v4/channels',
method: 'POST',
body: createChannelPatch(teamId, name, displayName, type, purpose, header, unique),
}).then((response) => {
expect(response.status).to.equal(201);
return cy.wrap({channel: response.body});
});
}
Cypress.Commands.add('apiCreateChannel', apiCreateChannel);
/**
* Create a new direct message channel between two users.
* See https://api.mattermost.com/#tag/channels/paths/~1channels~1direct/post
* @param {string[]} userIds - The two user ids to be in the direct message
* @returns {Channel} `out.channel` as `Channel`
*
* @example
* cy.apiCreateDirectChannel(['user-1-id', 'user-2-id']).then(({channel}) => {
* // do something with channel
* });
*/
function apiCreateDirectChannel(userIds: string[] = []): ChainableT<{channel: Channel}> {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: '/api/v4/channels/direct',
method: 'POST',
body: userIds,
}).then((response) => {
expect(response.status).to.equal(201);
return cy.wrap({channel: response.body});
});
}
Cypress.Commands.add('apiCreateDirectChannel', apiCreateDirectChannel);
/**
* Create a new group message channel to group of users via API. If the logged in user's id is not included in the list, it will be appended to the end.
* See https://api.mattermost.com/#tag/channels/paths/~1channels~1group/post
* @param {string[]} userIds - User ids to be in the group message channel
* @returns {Channel} `out.channel` as `Channel`
*
* @example
* cy.apiCreateGroupChannel(['user-1-id', 'user-2-id', 'current-user-id']).then(({channel}) => {
* // do something with channel
* });
*/
function apiCreateGroupChannel(userIds: string[]): ChainableT<{channel: Channel}> {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: '/api/v4/channels/group',
method: 'POST',
body: userIds,
}).then((response) => {
expect(response.status).to.equal(201);
return cy.wrap({channel: response.body});
});
}
Cypress.Commands.add('apiCreateGroupChannel', apiCreateGroupChannel);
/**
* Update a channel.
* The fields that can be updated are listed as parameters. Omitted fields will be treated as blanks.
* See https://api.mattermost.com/#tag/channels/paths/~1channels~1{channel_id}/put
* @param {string} channelId - The channel ID to be updated
* @param {Channel} channel - Channel object to be updated
* @param {string} channel.name - The unique handle for the channel, will be present in the channel URL
* @param {string} channel.display_name - The non-unique UI name for the channel
* @param {string} channel.purpose - A short description of the purpose of the channel
* @param {string} channel.header - Markdown-formatted text to display in the header of the channel
* @returns {Channel} `out.channel` as `Channel`
*
* @example
* cy.apiUpdateChannel('channel-id', {name: 'new-name', display_name: 'New Display Name'. 'purpose': 'Updated purpose', 'header': 'Updated header'});
*/
function apiUpdateChannel(channelId: string, channel: Channel): ChainableT<{channel: Channel}> {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: '/api/v4/channels/' + channelId,
method: 'PUT',
body: {
...channel,
id: channelId,
},
}).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap({channel: response.body});
});
}
Cypress.Commands.add('apiUpdateChannel', apiUpdateChannel);
/**
* Partially update a channel by providing only the fields you want to update.
* Omitted fields will not be updated.
* The fields that can be updated are defined in the request body, all other provided fields will be ignored.
* See https://api.mattermost.com/#tag/channels/paths/~1channels~1{channel_id}~1patch/put
* @param {string} channelId - The channel ID to be patched
* @param {Channel} channel - Channel object to be patched
* @param {string} channel.name - The unique handle for the channel, will be present in the channel URL
* @param {string} channel.display_name - The non-unique UI name for the channel
* @param {string} channel.purpose - A short description of the purpose of the channel
* @param {string} channel.header - Markdown-formatted text to display in the header of the channel
* @returns {Channel} `out.channel` as `Channel`
*
* @example
* cy.apiPatchChannel('channel-id', {name: 'new-name', display_name: 'New Display Name'});
*/
function apiPatchChannel(channelId: string, channel: Partial<Channel>): ChainableT<{channel: Channel}> {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
method: 'PUT',
url: `/api/v4/channels/${channelId}/patch`,
body: channel,
}).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap({channel: response.body});
});
}
Cypress.Commands.add('apiPatchChannel', apiPatchChannel);
/**
* Updates channel's privacy allowing changing a channel from Public to Private and back.
* See https://api.mattermost.com/#tag/channels/paths/~1channels~1{channel_id}~1privacy/put
* @param {string} channelId - The channel ID to be patched
* @param {string} privacy - The privacy the channel should be set too. P = Private, O = Open
* @returns {Channel} `out.channel` as `Channel`
*
* @example
* cy.apiPatchChannelPrivacy('channel-id', 'P');
*/
function apiPatchChannelPrivacy(channelId: string, privacy = 'O'): ChainableT<{channel: Channel}> {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
method: 'PUT',
url: `/api/v4/channels/${channelId}/privacy`,
body: {privacy},
}).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap({channel: response.body});
});
}
Cypress.Commands.add('apiPatchChannelPrivacy', apiPatchChannelPrivacy);
/**
* Get channel from the provided channel id string.
* See https://api.mattermost.com/#tag/channels/paths/~1channels~1{channel_id}/get
* @param {string} channelId - Channel ID
* @returns {Channel} `out.channel` as `Channel`
*
* @example
* cy.apiGetChannel('channel-id').then(({channel}) => {
* // do something with channel
* });
*/
function apiGetChannel(channelId: string): ChainableT<{channel: Channel}> {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: `/api/v4/channels/${channelId}`,
}).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap({channel: response.body});
});
}
Cypress.Commands.add('apiGetChannel', apiGetChannel);
/**
* Gets a channel from the provided team name and channel name strings.
* 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
* @returns {Channel} `out.channel` as `Channel`
*
* @example
* cy.apiGetChannelByName('team-name', 'channel-name').then(({channel}) => {
* // do something with channel
* });
*/
function apiGetChannelByName(teamName: string, channelName: string): ChainableT<{channel: Channel}> {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: `/api/v4/teams/name/${teamName}/channels/name/${channelName}`,
}).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap({channel: response.body});
});
}
Cypress.Commands.add('apiGetChannelByName', apiGetChannelByName);
/**
* Get a list of all channels.
* See https://api.mattermost.com/#tag/channels/paths/~1channels/get
* @returns {Channel[]} `out.channels` as `Channel[]`
*
* @example
* cy.apiGetAllChannels().then(({channels}) => {
* // do something with channels
* });
*/
function apiGetAllChannels(): ChainableT<{channels: Channel[]}> {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: '/api/v4/channels',
}).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap({channels: response.body});
});
}
Cypress.Commands.add('apiGetAllChannels', apiGetAllChannels);
/**
* Get channels for user.
* See https://api.mattermost.com/#tag/channels/paths/~1users~1{user_id}~1teams~1{team_id}~1channels/get
* @returns {Channel[]} `out.channels` as `Channel[]`
*
* @example
* cy.apiGetChannelsForUser().then(({channels}) => {
* // do something with channels
* });
*/
function apiGetChannelsForUser(userId: string, teamId: string): ChainableT<{channels: Channel[]}> {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: `/api/v4/users/${userId}/teams/${teamId}/channels`,
}).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap({channels: response.body});
});
}
Cypress.Commands.add('apiGetChannelsForUser', apiGetChannelsForUser);
/**
* Soft deletes a channel, by marking the channel as deleted in the database.
* Soft deleted channels will not be accessible in the user interface.
* Direct and group message channels cannot be deleted.
* See https://api.mattermost.com/#tag/channels/paths/~1channels~1{channel_id}/delete
* @param {string} channelId - The channel ID to be deleted
* @returns {Response} response: Cypress-chainable response which should have successful HTTP status of 200 OK to continue or pass.
*
* @example
* cy.apiDeleteChannel('channel-id');
*/
function apiDeleteChannel(channelId: string): ChainableT<Cypress.Response<unknown>> {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: '/api/v4/channels/' + channelId,
method: 'DELETE',
}).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap(response);
});
}
Cypress.Commands.add('apiDeleteChannel', apiDeleteChannel);
/**
* Add a user to a channel by creating a channel member object.
* See https://api.mattermost.com/#tag/channels/paths/~1channels~1{channel_id}~1members/post
* @param {string} channelId - Channel ID
* @param {string} userId - User ID to add to the channel
* @returns {ChannelMembership} `out.member` as `ChannelMembership`
*
* @example
* cy.apiAddUserToChannel('channel-id', 'user-id').then(({member}) => {
* // do something with member
* });
*/
function apiAddUserToChannel(channelId: string, userId: string): ChainableT<{member: ChannelMembership}> {
return cy.request<ChannelMembership>({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: '/api/v4/channels/' + channelId + '/members',
method: 'POST',
body: {
user_id: userId,
},
}).then((response) => {
expect(response.status).to.equal(201);
return cy.wrap({member: response.body});
});
}
Cypress.Commands.add('apiAddUserToChannel', apiAddUserToChannel);
function apiRemoveUserFromChannel(channelId: string, userId: string): ChainableT<{member: unknown}> {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: '/api/v4/channels/' + channelId + '/members/' + userId,
method: 'DELETE',
}).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap({member: response.body});
});
}
Cypress.Commands.add('apiRemoveUserFromChannel', apiRemoveUserFromChannel);
/**
* Convenient command that create, post into and then archived a channel.
* @param {string} name - name of channel to be created
* @param {string} displayName - display name of channel to be created
* @param {string} type - type of channel
* @param {string} teamId - team Id where the channel will be added
* @param {string[]} [messages] - messages to be posted before archiving a channel
* @param {UserProfile} [user] - user who will be posting the messages
* @returns {Channel} archived channel
*
* @example
* cy.apiCreateArchivedChannel('channel-name', 'channel-display-name', 'team-id', messages, user).then((channel) => {
* // do something with channel
* });
*/
function apiCreateArchivedChannel(name: string, displayName: string, type: ChannelType = 'O', teamId: string, messages: string[] = [], user?: UserProfile): ChainableT<Channel> {
return cy.apiCreateChannel(teamId, name, displayName, type).then(({channel}) => {
Cypress._.forEach(messages, (message) => {
cy.postMessageAs({
sender: user as {username: string; password: string},
message,
channelId: channel.id,
});
});
cy.apiDeleteChannel(channel.id);
return cy.wrap(channel);
});
}
Cypress.Commands.add('apiCreateArchivedChannel', apiCreateArchivedChannel);
/**
* Command to convert a GM to a private channel
* @param {string} channelId - channel id of GM to be converted
* @param {string} teamId - id of team to move the converted private channel to
* @param {string} displayName - display name of converted channel
* @param {string} name - name of converted channel
*/
function apiConvertGMToPrivateChannel(channelId: string, teamId: string, displayName: string, name: string): ChainableT<Cypress.Response<void>> {
const body = {
channel_id: channelId,
team_id: teamId,
display_name: displayName,
name,
};
return cy.request<void>({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: `/api/v4/channels/${channelId}/convert_to_channel?team_id=${teamId}`,
method: 'POST',
body,
});
}
Cypress.Commands.add('apiConvertGMToPrivateChannel', apiConvertGMToPrivateChannel);
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
interface Chainable {
apiCreateChannel: typeof apiCreateChannel;
apiCreateDirectChannel: typeof apiCreateDirectChannel;
apiCreateGroupChannel: typeof apiCreateGroupChannel;
apiUpdateChannel: typeof apiUpdateChannel;
apiPatchChannel: typeof apiPatchChannel;
apiPatchChannelPrivacy: typeof apiPatchChannelPrivacy;
apiGetChannel: typeof apiGetChannel;
apiGetChannelByName: typeof apiGetChannelByName;
apiGetAllChannels: typeof apiGetAllChannels;
apiGetChannelsForUser: typeof apiGetChannelsForUser;
apiDeleteChannel: typeof apiDeleteChannel;
apiAddUserToChannel: typeof apiAddUserToChannel;
apiRemoveUserFromChannel: typeof apiRemoveUserFromChannel;
apiCreateArchivedChannel: typeof apiCreateArchivedChannel;
apiConvertGMToPrivateChannel: typeof apiConvertGMToPrivateChannel;
}
}
}

View File

@@ -0,0 +1,41 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/// <reference types="cypress" />
// ***************************************************************
// Each command should be properly documented using JSDoc.
// See https://jsdoc.app/index.html for reference.
// Basic requirements for documentation are the following:
// - Meaningful description
// - Specific link to https://api.mattermost.com
// - Each parameter with `@params`
// - Return value with `@returns`
// - Example usage with `@example`
// Custom command should follow naming convention of having `api` prefix, e.g. `apiLogin`.
// ***************************************************************
declare namespace Cypress {
interface Chainable {
/**
* Get products.
* See https://api.mattermost.com/#operation/GetCloudProducts
* @returns {Product[]} out.Products: `Product[]` object
*
* @example
* cy.apiGetCloudProducts();
*/
apiGetCloudProducts(): Chainable<{products: Product[]}>;
/**
* Get subscriptions.
* See https://api.mattermost.com/#operation/GetSubscription
* @returns {Subscription} out.subscription: `Subscription` object
*
* @example
* cy.apiGetCloudSubscription();
*/
apiGetCloudSubscription(): Chainable<{subscription: Subscription}>;
}
}

View File

@@ -0,0 +1,24 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
Cypress.Commands.add('apiGetCloudProducts', () => {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: '/api/v4/cloud/products',
method: 'GET',
}).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap({products: response.body});
});
});
Cypress.Commands.add('apiGetCloudSubscription', () => {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: '/api/v4/cloud/subscription',
method: 'GET',
}).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap({subscription: response.body});
});
});

View File

@@ -0,0 +1,452 @@
{
"ServiceSettings": {
"SiteURL": "http://localhost:8065",
"WebsocketURL": "",
"LicenseFileLocation": "",
"ListenAddress": ":8065",
"ConnectionSecurity": "",
"TLSCertFile": "",
"TLSKeyFile": "",
"TLSMinVer": "1.2",
"TLSStrictTransport": false,
"TLSStrictTransportMaxAge": 63072000,
"TLSOverwriteCiphers": [],
"UseLetsEncrypt": false,
"LetsEncryptCertificateCacheFile": "./config/letsencrypt.cache",
"Forward80To443": false,
"TrustedProxyIPHeader": [],
"ReadTimeout": 300,
"WriteTimeout": 300,
"IdleTimeout": 300,
"MaximumLoginAttempts": 10,
"GoroutineHealthThreshold": -1,
"EnableOAuthServiceProvider": false,
"EnableIncomingWebhooks": true,
"EnableOutgoingWebhooks": true,
"EnableCommands": true,
"EnablePostUsernameOverride": false,
"EnablePostIconOverride": false,
"GoogleDeveloperKey": "",
"EnableLinkPreviews": false,
"EnablePermalinkPreviews": true,
"RestrictLinkPreviews": "",
"EnableTesting": false,
"EnableDeveloper": false,
"DeveloperFlags": "",
"EnableClientPerformanceDebugging": false,
"EnableSecurityFixAlert": false,
"EnableInsecureOutgoingConnections": false,
"AllowedUntrustedInternalConnections": "localhost",
"EnableMultifactorAuthentication": false,
"EnforceMultifactorAuthentication": false,
"EnableUserAccessTokens": false,
"AllowCorsFrom": "",
"CorsExposedHeaders": "",
"CorsAllowCredentials": false,
"CorsDebug": false,
"AllowCookiesForSubdomains": false,
"ExtendSessionLengthWithActivity": true,
"SessionLengthWebInDays": 30,
"SessionLengthWebInHours": 720,
"SessionLengthMobileInDays": 30,
"SessionLengthMobileInHours": 720,
"SessionLengthSSOInDays": 30,
"SessionLengthSSOInHours": 720,
"SessionCacheInMinutes": 10,
"SessionIdleTimeoutInMinutes": 43200,
"WebsocketSecurePort": 443,
"WebsocketPort": 80,
"WebserverMode": "gzip",
"EnableGifPicker": false,
"GiphySdkKey": "s0glxvzVg9azvPipKxcPLpXV0q1x1fVP",
"EnableCustomEmoji": false,
"EnableEmojiPicker": true,
"PostEditTimeLimit": -1,
"TimeBetweenUserTypingUpdatesMilliseconds": 5000,
"EnablePostSearch": true,
"EnableFileSearch": true,
"MinimumHashtagLength": 3,
"EnableUserTypingMessages": true,
"EnableChannelViewedMessages": true,
"EnableUserStatuses": true,
"ExperimentalEnableAuthenticationTransfer": true,
"ClusterLogTimeoutMilliseconds": 2000,
"EnableTutorial": true,
"EnableOnboardingFlow": false,
"ExperimentalEnableDefaultChannelLeaveJoinMessages": true,
"ExperimentalGroupUnreadChannels": "disabled",
"EnableAPITeamDeletion": true,
"EnableAPITriggerAdminNotifications": false,
"EnableAPIUserDeletion": false,
"EnableHardenedMode": false,
"ExperimentalStrictCSRFEnforcement": false,
"EnableEmailInvitations": true,
"DisableBotsWhenOwnerIsDeactivated": true,
"EnableBotAccountCreation": true,
"EnableSVGs": true,
"EnableLatex": false,
"EnableInlineLatex": true,
"PostPriority": true,
"AllowPersistentNotifications": true,
"AllowPersistentNotificationsForGuests": false,
"PersistentNotificationIntervalMinutes": 5,
"PersistentNotificationMaxCount": 6,
"PersistentNotificationMaxRecipients": 5,
"EnableAPIChannelDeletion": false,
"EnableLocalMode": false,
"LocalModeSocketLocation": "/var/tmp/mattermost_local.socket",
"EnableAWSMetering": false,
"AWSMeteringTimeoutSeconds": 30,
"SplitKey": "",
"FeatureFlagSyncIntervalSeconds": 30,
"DebugSplit": false,
"ThreadAutoFollow": true,
"CollapsedThreads": "disabled",
"ManagedResourcePaths": "",
"EnableCustomGroups": true,
"AllowSyncedDrafts": true,
"UniqueEmojiReactionLimitPerPost": 50,
"RefreshPostStatsRunTime": "00:00",
"MaximumPayloadSizeBytes": 100000,
"MaximumURLLength": 2048
},
"TeamSettings": {
"SiteName": "Mattermost",
"MaxUsersPerTeam": 2000,
"EnableJoinLeaveMessageByDefault": true,
"EnableUserCreation": true,
"EnableOpenServer": true,
"EnableUserDeactivation": false,
"RestrictCreationToDomains": "",
"EnableCustomUserStatuses": true,
"EnableCustomBrand": false,
"CustomBrandText": "",
"CustomDescriptionText": "",
"RestrictDirectMessage": "any",
"EnableLastActiveTime": true,
"UserStatusAwayTimeout": 300,
"MaxChannelsPerTeam": 2000,
"MaxNotificationsPerChannel": 1000,
"EnableConfirmNotificationsToChannel": true,
"TeammateNameDisplay": "username",
"ExperimentalEnableAutomaticReplies": false,
"LockTeammateNameDisplay": false,
"ExperimentalPrimaryTeam": "",
"ExperimentalDefaultChannels": []
},
"ClientRequirements": {
"AndroidLatestVersion": "",
"AndroidMinVersion": "",
"IosLatestVersion": "",
"IosMinVersion": ""
},
"PasswordSettings": {
"MinimumLength": 14,
"Lowercase": false,
"Number": false,
"Uppercase": false,
"Symbol": false,
"EnableForgotLink": true
},
"EmailSettings": {
"EnableSignUpWithEmail": true,
"EnableSignInWithEmail": true,
"EnableSignInWithUsername": true,
"SendEmailNotifications": true,
"UseChannelInEmailNotifications": false,
"RequireEmailVerification": false,
"FeedbackName": "",
"FeedbackEmail": "test@example.com",
"ReplyToAddress": "test@example.com",
"FeedbackOrganization": "",
"EnableSMTPAuth": false,
"SMTPUsername": "",
"SMTPPassword": "",
"SMTPServer": "localhost",
"SMTPPort": "10025",
"SMTPServerTimeout": 10,
"ConnectionSecurity": "",
"SendPushNotifications": true,
"PushNotificationServer": "https://push-test.mattermost.com",
"PushNotificationContents": "generic",
"PushNotificationBuffer": 1000,
"EnableEmailBatching": false,
"EmailBatchingBufferSize": 256,
"EmailBatchingInterval": 30,
"EnablePreviewModeBanner": true,
"SkipServerCertificateVerification": false,
"EmailNotificationContentsType": "full"
},
"PrivacySettings": {
"ShowEmailAddress": true,
"ShowFullName": true
},
"SupportSettings": {
"TermsOfServiceLink": "https://mattermost.com/pl/terms-of-use/",
"PrivacyPolicyLink": "https://mattermost.com/pl/privacy-policy/",
"AboutLink": "https://mattermost.com/pl/about-mattermost",
"HelpLink": "https://mattermost.com/pl/help/",
"ReportAProblemLink": "https://mattermost.com/pl/report-a-bug",
"ReportAProblemType": "link",
"ForgotPasswordLink": "",
"SupportEmail": "",
"CustomTermsOfServiceEnabled": false,
"CustomTermsOfServiceReAcceptancePeriod": 365,
"EnableAskCommunityLink": true
},
"AnnouncementSettings": {
"EnableBanner": false,
"BannerText": "",
"BannerColor": "#f2a93b",
"BannerTextColor": "#333333",
"AllowBannerDismissal": true,
"AdminNoticesEnabled": false,
"UserNoticesEnabled": false,
"NoticesURL": "https://notices.mattermost.com/",
"NoticesFetchFrequency": 3600,
"NoticesSkipCache": false
},
"ThemeSettings": {
"EnableThemeSelection": true,
"DefaultTheme": "default",
"AllowCustomThemes": true,
"AllowedThemes": []
},
"GitLabSettings": {
"Enable": false,
"Secret": "",
"Id": "",
"Scope": "",
"AuthEndpoint": "",
"TokenEndpoint": "",
"UserAPIEndpoint": "",
"DiscoveryEndpoint": "",
"ButtonText": "",
"ButtonColor": ""
},
"GoogleSettings": {
"Enable": false,
"Secret": "",
"Id": "",
"Scope": "profile email",
"AuthEndpoint": "https://accounts.google.com/o/oauth2/v2/auth",
"TokenEndpoint": "https://www.googleapis.com/oauth2/v4/token",
"UserAPIEndpoint": "https://people.googleapis.com/v1/people/me?personFields=names,emailAddresses,nicknames,metadata",
"DiscoveryEndpoint": "",
"ButtonText": "",
"ButtonColor": ""
},
"Office365Settings": {
"Enable": false,
"Secret": "",
"Id": "",
"Scope": "User.Read",
"AuthEndpoint": "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
"TokenEndpoint": "https://login.microsoftonline.com/common/oauth2/v2.0/token",
"UserAPIEndpoint": "https://graph.microsoft.com/v1.0/me",
"DiscoveryEndpoint": "",
"DirectoryId": ""
},
"OpenIdSettings": {
"Enable": false,
"Secret": "",
"Id": "",
"Scope": "profile openid email",
"AuthEndpoint": "",
"TokenEndpoint": "",
"UserAPIEndpoint": "",
"DiscoveryEndpoint": "",
"ButtonText": "",
"ButtonColor": "#145DBF"
},
"LdapSettings": {
"Enable": true,
"EnableSync": false,
"LdapServer": "localhost",
"LdapPort": 389,
"ConnectionSecurity": "",
"BaseDN": "dc=mm,dc=test,dc=com",
"BindUsername": "cn=admin,dc=mm,dc=test,dc=com",
"BindPassword": "mostest_password",
"UserFilter": "",
"GroupFilter": "",
"GuestFilter": "",
"EnableAdminFilter": false,
"AdminFilter": "",
"GroupDisplayNameAttribute": "cn",
"GroupIdAttribute": "entryUUID",
"FirstNameAttribute": "cn",
"LastNameAttribute": "sn",
"EmailAttribute": "mail",
"UsernameAttribute": "uid",
"NicknameAttribute": "cn",
"IdAttribute": "uid",
"PositionAttribute": "sAMAccountType",
"LoginIdAttribute": "uid",
"PictureAttribute": "",
"SyncIntervalMinutes": 10000,
"SkipCertificateVerification": true,
"PublicCertificateFile": "",
"PrivateKeyFile": "",
"QueryTimeout": 60,
"AnalyticsQueryTimeout": 300,
"MaxPageSize": 500,
"LoginFieldName": ""
},
"ComplianceSettings": {
"Enable": false,
"Directory": "./data/",
"EnableDaily": false,
"BatchSize": 30000
},
"LocalizationSettings": {
"DefaultServerLocale": "en",
"DefaultClientLocale": "en",
"AvailableLocales": ""
},
"SamlSettings": {
"Enable": false,
"EnableSyncWithLdap": false,
"EnableSyncWithLdapIncludeAuth": false,
"IgnoreGuestsLdapSync": false,
"Verify": true,
"Encrypt": true,
"SignRequest": false,
"IdpURL": "",
"IdpDescriptorURL": "",
"IdpMetadataURL": "",
"ServiceProviderIdentifier": "",
"AssertionConsumerServiceURL": "",
"SignatureAlgorithm": "RSAwithSHA1",
"CanonicalAlgorithm": "Canonical1.0",
"ScopingIDPProviderId": "",
"ScopingIDPName": "",
"IdpCertificateFile": "saml-idp.crt",
"PublicCertificateFile": "saml-public.crt",
"PrivateKeyFile": "saml-private.key",
"IdAttribute": "",
"GuestAttribute": "",
"EnableAdminAttribute": false,
"AdminAttribute": "",
"FirstNameAttribute": "",
"LastNameAttribute": "",
"EmailAttribute": "Email",
"UsernameAttribute": "Username",
"NicknameAttribute": "",
"LocaleAttribute": "",
"PositionAttribute": "",
"LoginButtonText": "SAML"
},
"NativeAppSettings": {
"AppCustomURLSchemes": [
"mmauth://",
"mmauthbeta://"
],
"AppDownloadLink": "https://mattermost.com/pl/download-apps",
"AndroidAppDownloadLink": "https://mattermost.com/pl/android-app/",
"IosAppDownloadLink": "https://mattermost.com/pl/ios-app/"
},
"ClusterSettings": {
"Enable": false
},
"ExperimentalSettings": {
"RestrictSystemAdmin": true
},
"AnalyticsSettings": {
"MaxUsersForStatistics": 2500
},
"DataRetentionSettings": {
"EnableMessageDeletion": false,
"EnableFileDeletion": false,
"EnableBoardsDeletion": false,
"MessageRetentionDays": 365,
"MessageRetentionHours": 0,
"FileRetentionDays": 365,
"FileRetentionHours": 0,
"BoardsRetentionDays": 365,
"DeletionJobStartTime": "02:00",
"BatchSize": 3000,
"TimeBetweenBatchesMilliseconds": 100,
"RetentionIdsBatchSize": 100
},
"MessageExportSettings": {
"EnableExport": false,
"ExportFormat": "actiance",
"DailyRunTime": "01:00",
"ExportFromTimestamp": 0,
"BatchSize": 10000,
"DownloadExportResults": false,
"GlobalRelaySettings": {
"CustomerType": "A9",
"SMTPUsername": "",
"SMTPPassword": "",
"EmailAddress": "",
"SMTPServerTimeout": 1800,
"CustomSMTPServerName": "",
"CustomSMTPPort": "25",
"CustomHeaderName": "",
"CustomHeaderValue": "",
"SenderAddress": ""
}
},
"PluginSettings": {
"Enable": true,
"EnableUploads": true,
"AllowInsecureDownloadURL": false,
"EnableHealthCheck": true,
"Directory": "./plugins",
"ClientDirectory": "./client/plugins",
"Plugins": {},
"PluginStates": {
"com.mattermost.calls": {
"Enable": false
},
"com.mattermost.nps": {
"Enable": false
},
"playbooks": {
"Enable": true
}
},
"EnableMarketplace": true,
"EnableRemoteMarketplace": true,
"AutomaticPrepackagedPlugins": true,
"RequirePluginSignature": false,
"MarketplaceURL": "https://api.integrations.mattermost.com",
"SignaturePublicKeyFiles": [],
"ChimeraOAuthProxyURL": ""
},
"DisplaySettings": {
"CustomURLSchemes": [],
"MaxMarkdownNodes": 0
},
"GuestAccountsSettings": {
"Enable": true,
"HideTags": false,
"EnforceMultifactorAuthentication": false,
"RestrictCreationToDomains": ""
},
"ImageProxySettings": {
"Enable": true,
"ImageProxyType": "local",
"RemoteImageProxyURL": "",
"RemoteImageProxyOptions": ""
},
"ImportSettings": {
"Directory": "./import",
"RetentionDays": 30
},
"ExportSettings": {
"Directory": "./export",
"RetentionDays": 30
},
"WranglerSettings": {
"PermittedWranglerRoles": [],
"AllowedEmailDomain": [],
"MoveThreadMaxCount": 100,
"MoveThreadToAnotherTeamEnable": false,
"MoveThreadFromPrivateChannelEnable": false,
"MoveThreadFromDirectMessageChannelEnable": false,
"MoveThreadFromGroupMessageChannelEnable": false
}
}

View File

@@ -0,0 +1,31 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/// <reference types="cypress" />
// ***************************************************************
// Each command should be properly documented using JSDoc.
// See https://jsdoc.app/index.html for reference.
// Basic requirements for documentation are the following:
// - Meaningful description
// - Specific link to https://api.mattermost.com
// - Each parameter with `@params`
// - Return value with `@returns`
// - Example usage with `@example`
// Custom command should follow naming convention of having `api` prefix, e.g. `apiLogin`.
// ***************************************************************
declare namespace Cypress {
interface Chainable {
/**
* Get cluster status
* See https://api.mattermost.com/#tag/cluster/operation/GetClusterStatus
* @returns {ClusterInfo[]} out.clusterInfo: `ClusterInfo[]` object
*
* @example
* cy.apiGetClusterStatus();
*/
apiGetClusterStatus(): Chainable<{clusterInfo: ClusterInfo[]}>;
}
}

View File

@@ -0,0 +1,13 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
Cypress.Commands.add('apiGetClusterStatus', () => {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: '/api/v4/cluster/status',
method: 'GET',
}).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap({clusterInfo: response.body});
});
});

View File

@@ -0,0 +1,42 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/// <reference types="cypress" />
// ***************************************************************
// Each command should be properly documented using JSDoc.
// See https://jsdoc.app/index.html for reference.
// Basic requirements for documentation are the following:
// - Meaningful description
// - Specific link to https://api.mattermost.com
// - Each parameter with `@params`
// - Return value with `@returns`
// - Example usage with `@example`
// Custom command should follow naming convention of having `api` prefix, e.g. `apiLogin`.
// ***************************************************************
declare namespace Cypress {
interface Chainable {
/**
* Upload file directly via API.
* @param {String} name - name of form
* @param {String} filePath - path of the file to upload; can be relative or absolute
* @param {Object} options - request options
* @param {String} options.url - HTTP resource URL
* @param {String} options.method - HTTP request method
* @param {Number} options.successStatus - HTTP status code
*
* @example
* cy.apiUploadFile('certificate', filePath, {url: '/api/v4/saml/certificate/public', method: 'POST', successStatus: 200});
*/
apiUploadFile(name: string, filePath: string, options: Record<string, unknown>): Chainable<Response>;
/**
* Verify export file content-type
* @param {String} fileURL - Export file URL
* @param {String} contentType - File content-Type
*/
apiDownloadFileAndVerifyContentType(fileURL: string, contentType: string): Chainable;
}
}

View File

@@ -0,0 +1,68 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import * as TIMEOUTS from '../../fixtures/timeouts';
const path = require('path');
// *****************************************************************************
// Common / Helper commands
// *****************************************************************************
Cypress.Commands.add('apiUploadFile', (name, filePath, options = {}) => {
const formData = new FormData();
const filename = path.basename(filePath);
cy.fixture(filePath, 'binary', {timeout: TIMEOUTS.TWENTY_MIN}).
then(Cypress.Blob.binaryStringToBlob).
then((blob) => {
formData.set(name, blob, filename);
formRequest(options.method, options.url, formData, options.successStatus);
});
});
Cypress.Commands.add('apiDownloadFileAndVerifyContentType', (fileURL, contentType = 'application/zip') => {
cy.request(fileURL).then((response) => {
// * Verify the download
expect(response.status).to.equal(200);
// * Confirm its content type
expect(response.headers['content-type']).to.equal(contentType);
});
});
/**
* Process binary file HTTP form request.
* @param {String} method - HTTP request method
* @param {String} url - HTTP resource URL
* @param {FormData} formData - Key value pairs representing form fields and value
* @param {Number} successStatus - HTTP status code
*/
function formRequest(method, url, formData, successStatus) {
const baseUrl = Cypress.config('baseUrl');
const xhr = new XMLHttpRequest();
xhr.open(method, url, false);
let cookies = '';
cy.getCookie('MMCSRF', {log: false}).then((token) => {
//get MMCSRF cookie value
const csrfToken = token.value;
cy.getCookies({log: false}).then((cookieValues) => {
//prepare cookie string
cookieValues.forEach((cookie) => {
cookies += cookie.name + '=' + cookie.value + '; ';
});
//set headers
xhr.setRequestHeader('Access-Control-Allow-Origin', baseUrl);
xhr.setRequestHeader('Access-Control-Allow-Methods', 'GET, POST, PUT');
xhr.setRequestHeader('X-CSRF-Token', csrfToken);
xhr.setRequestHeader('Cookie', cookies);
xhr.send(formData);
if (xhr.readyState === 4) {
expect(xhr.status, 'Expected form request to be processed successfully').to.equal(successStatus);
} else {
expect(xhr.status, 'Form request process delayed').to.equal(successStatus);
}
});
});
}

View File

@@ -0,0 +1,33 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/// <reference types="cypress" />
// ***************************************************************
// Each command should be properly documented using JSDoc.
// See https://jsdoc.app/index.html for reference.
// Basic requirements for documentation are the following:
// - Meaningful description
// - Each parameter with `@params`
// - Return value with `@returns`
// - Example usage with `@example`
// ***************************************************************
declare namespace Cypress {
interface Chainable {
/**
* Delete all custom retention policies
*/
apiDeleteAllCustomRetentionPolicies(): Chainable;
/**
* Create a post with create_at prop via API
* @param {string} channelId - Channel ID
* @param {string} message - Post a message
* @param {string} token - token
* @param {number} createat - epoch date
*/
apiPostWithCreateDate(channelId: string, message: string, token: string, createat: number): Chainable;
}
}

View File

@@ -0,0 +1,157 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// *****************************************************************************
// Data Retention
// https://api.mattermost.com/#tag/data-retention
// *****************************************************************************
/**
* Get all Custom Retention Policies
* @param {Integer} page - The page to select
* @param {Integer} perPage - The number of policies per page
*/
Cypress.Commands.add('apiGetCustomRetentionPolicies', (page = 0, perPage = 100) => {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: `/api/v4/data_retention/policies?page=${page}&per_page=${perPage}`,
method: 'GET',
}).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap(response);
});
});
/**
* Get a Custom Retention Policy
* @param {string} id - The id of the policy
*/
Cypress.Commands.add('apiGetCustomRetentionPolicy', (id) => {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: `/api/v4/data_retention/policies/${id}`,
method: 'GET',
}).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap(response);
});
});
/**
* Delete Custom Retention Policy
* @param {string} id - The id of the policy
*/
Cypress.Commands.add('apiDeleteCustomRetentionPolicy', (id) => {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: `/api/v4/data_retention/policies/${id}`,
method: 'DELETE',
}).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap(response);
});
});
/**
* Get Custom Retention Policy teams
* @param {string} id - The id of the policy
* @param {Integer} page - The page to select
* @param {Integer} perPage - The number of policy teams per page
*/
Cypress.Commands.add('apiGetCustomRetentionPolicyTeams', (id, page = 0, perPage = 100) => {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: `/api/v4/data_retention/policies/${id}/teams?page=${page}&per_page=${perPage}`,
method: 'GET',
}).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap(response);
});
});
/**
* Get Custom Retention Policy channels
* @param {string} id - The id of the policy
* @param {Integer} page - The page to select
* @param {Integer} perPage - The number of policy channels per page
*/
Cypress.Commands.add('apiGetCustomRetentionPolicyChannels', (id, page = 0, perPage = 100) => {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: `/api/v4/data_retention/policies/${id}/channels?page=${page}&per_page=${perPage}`,
method: 'GET',
}).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap(response);
});
});
/**
* Search Custom Retention Policy teams
* @param {string} id - The id of the policy
* @param {string} term - The team search term
*/
Cypress.Commands.add('apiSearchCustomRetentionPolicyTeams', (id, term) => {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: `/api/v4/data_retention/policies/${id}/teams/search`,
method: 'POST',
body: {term},
}).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap(response);
});
});
/**
* Search Custom Retention Policy teams
* @param {string} id - The id of the policy
* @param {string} term - The channel search term
*/
Cypress.Commands.add('apiSearchCustomRetentionPolicyChannels', (id, term) => {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: `/api/v4/data_retention/policies/${id}/channels/search`,
method: 'POST',
body: {term},
}).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap(response);
});
});
/**
* Delete all custom retention policies
*/
Cypress.Commands.add('apiDeleteAllCustomRetentionPolicies', () => {
cy.apiGetCustomRetentionPolicies().then((result) => {
result.body.policies.forEach((policy) => {
cy.apiDeleteCustomRetentionPolicy(policy.id);
});
});
});
/**
* Create a post with create_at prop via API
* @param {string} channelId - Channel ID
* @param {string} message - Post a message
* @param {string} token - token
* @param {number} createAt - epoch date
*/
Cypress.Commands.add('apiPostWithCreateDate', (channelId, message, token, createAt) => {
const headers = {'X-Requested-With': 'XMLHttpRequest'};
if (token !== '') {
headers.Authorization = `Bearer ${token}`;
}
return cy.request({
headers,
url: '/api/v4/posts',
method: 'POST',
body: {
channel_id: channelId,
create_at: createAt,
message,
},
});
});

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,45 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// *****************************************************************************
// Groups
// https://api.mattermost.com/#tag/groups
// *****************************************************************************
import type {ChainableT} from '../../types';
function apiCreateCustomUserGroup(displayName: string, name: string, userIds: string[]): ChainableT<Cypress.Response<any>> {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: '/api/v4/groups',
method: 'POST',
body: {
display_name: displayName,
name,
source: 'custom',
allow_reference: true,
user_ids: userIds,
},
}).then((response) => {
expect(response.status).to.equal(201);
return cy.wrap(response);
});
}
Cypress.Commands.add('apiCreateCustomUserGroup', apiCreateCustomUserGroup);
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
interface Chainable {
/**
* Create custom user group
* @param {string} displayName - the display name of the group
* @param {string} name - the @ mentionable name of the group
* @param {string[]} userIds - users to add to the group
*/
apiCreateCustomUserGroup: typeof apiCreateCustomUserGroup;
}
}
}

View File

@@ -0,0 +1,15 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export function buildQueryString(queryParams = {}) {
let queryString = '';
Object.entries(queryParams).forEach(([k, v], index) => {
if (index > 0) {
queryString += '&';
}
queryString += `${k}=${encodeURIComponent(v)}`;
});
return queryString;
}

View File

@@ -0,0 +1,24 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import './brand';
import './bots';
import './channel';
import './cloud';
import './cluster';
import './common';
import './data_retention';
import './group';
import './keycloak';
import './ldap';
import './preference';
import './plugin';
import './role';
import './saml';
import './scheme';
import './setup';
import './status';
import './system';
import './team';
import './user';
import './webhooks';

View File

@@ -0,0 +1,65 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/// <reference types="cypress" />
// ***************************************************************
// Each command should be properly documented using JSDoc.
// See https://jsdoc.app/index.html for reference.
// Basic requirements for documentation are the following:
// - Meaningful description
// - Specific link to https://api.mattermost.com
// - Each parameter with `@params`
// - Return value with `@returns`
// - Example usage with `@example`
// Custom command should follow naming convention of having `api` prefix, e.g. `apiKeycloakGetAccessToken`.
// ***************************************************************
declare namespace Cypress {
interface Chainable {
/**
* Get access token from Keycloak
* See https://www.keycloak.org/documentation
* @returns {string} token
*
* @example
* cy.apiKeycloakGetAccessToken();
*/
apiKeycloakGetAccessToken(): Chainable<string>;
/**
* Save realm to Keycloak
* See https://www.keycloak.org/documentation
* @param {string} options.accessToken - valid token to authorize a request
* @param {Boolean} options.failOnStatusCode - whether to fail on status code, default is true
* @returns {Response} response: Cypress-chainable response
*
* @example
* cy.apiKeycloakSaveRealm('access-token');
*/
apiKeycloakSaveRealm(accessToken: string, failOnStatusCode: boolean): Chainable<Response>;
/**
* Get realm from Keycloak
* See https://www.keycloak.org/documentation
* @param {string} options.accessToken - valid token to authorize a request
* @param {Boolean} options.failOnStatusCode - whether to fail on status code, default is true
* @returns {Response} response: Cypress-chainable response
*
* @example
* cy.apiKeycloakGetRealm('access-token');
*/
apiKeycloakGetRealm(accessToken: string, failOnStatusCode: boolean): Chainable<Response>;
/**
* Verify Keycloak is reachable and has realm setup
* See https://www.keycloak.org/documentation
* @returns {Response} response: Cypress-chainable response
*
* @example
* cy.apiRequireKeycloak();
*/
apiRequireKeycloak(): Chainable<string>;
}
}

View File

@@ -0,0 +1,89 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// *****************************************************************************
// Keycloak Admin REST API
// https://www.keycloak.org/documentation
// *****************************************************************************
import realmJson from './keycloak_realm.json';
const {
keycloakBaseUrl,
keycloakAppName,
keycloakUsername,
keycloakPassword,
} = Cypress.expose();
Cypress.Commands.add('apiKeycloakGetAccessToken', () => {
return cy.task('keycloakRequest', {
baseUrl: `${keycloakBaseUrl}/auth/realms/master/protocol/openid-connect/token`,
method: 'POST',
headers: {'Content-type': 'application/x-www-form-urlencoded'},
data: `grant_type=password&username=${keycloakUsername}&password=${keycloakPassword}&client_id=admin-cli`,
}).then((response) => {
expect(response.status).to.equal(200);
const token = response.data.access_token;
return cy.wrap(token);
});
});
function getRealmJson() {
const baseUrl = Cypress.config('baseUrl');
const {ldapServer, ldapPort} = Cypress.expose();
const realm = JSON.stringify(realmJson).
replace(/localhost:389/g, `${ldapServer}:${ldapPort}`).
replace(/http:\/\/localhost:8065/g, baseUrl);
return JSON.parse(realm);
}
Cypress.Commands.add('apiKeycloakSaveRealm', (accessToken, failOnStatusCode = true) => {
const realm = getRealmJson();
return cy.task('keycloakRequest', {
baseUrl: `${keycloakBaseUrl}/auth/admin/realms`,
method: 'POST',
data: realm,
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
}).then((response) => {
if (failOnStatusCode) {
expect(response.status).to.equal(201);
}
return cy.wrap(response);
});
});
Cypress.Commands.add('apiKeycloakGetRealm', (accessToken, failOnStatusCode = true) => {
return cy.task('keycloakRequest', {
baseUrl: `${keycloakBaseUrl}/auth/admin/realms/${keycloakAppName}`,
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
failOnStatusCode,
}).then((response) => {
if (failOnStatusCode) {
expect(response.status).to.equal(200);
}
return cy.wrap(response);
});
});
Cypress.Commands.add('apiRequireKeycloak', () => {
cy.apiKeycloakGetAccessToken().then((token) => {
cy.apiKeycloakGetRealm(token, false).then((response) => {
if (response.status !== 200) {
return cy.apiKeycloakSaveRealm(token);
}
return response;
});
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,48 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/// <reference types="cypress" />
// ***************************************************************
// Each command should be properly documented using JSDoc.
// See https://jsdoc.app/index.html for reference.
// Basic requirements for documentation are the following:
// - Meaningful description
// - Specific link to https://api.mattermost.com
// - Each parameter with `@params`
// - Return value with `@returns`
// - Example usage with `@example`
// Custom command should follow naming convention of having `api` prefix, e.g. `apiLDAPSync`.
// ***************************************************************
declare namespace Cypress {
interface Chainable {
/**
* Synchronize any user attribute changes in the configured AD/LDAP server with Mattermost.
* See https://api.mattermost.com/#operation/SyncLdap
*
* @example
* cy.apiLDAPSync();
*/
apiLDAPSync(): Chainable;
/**
* Test the current AD/LDAP configuration to see if the AD/LDAP server can be contacted successfully.
* See https://api.mattermost.com/#operation/TestLdap
*
* @example
* cy.apiLDAPTest();
*/
apiLDAPTest(): Chainable;
/**
* Sync LDAP user
* @returns {UserProfile} user - user object
*
* @example
* cy.apiSyncLDAPUser();
*/
apiSyncLDAPUser({ldapUser = {}, bypassTutorial = true}): Chainable<UserProfile>;
}
}

View File

@@ -0,0 +1,50 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// *****************************************************************************
// LDAP
// https://api.mattermost.com/#tag/LDAP
// *****************************************************************************
Cypress.Commands.add('apiLDAPSync', () => {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: '/api/v4/ldap/sync',
method: 'POST',
}).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap(response);
});
});
Cypress.Commands.add('apiLDAPTest', () => {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: '/api/v4/ldap/test',
method: 'POST',
}).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap(response);
});
});
Cypress.Commands.add('apiSyncLDAPUser', ({
ldapUser = {},
bypassTutorial = true,
}) => {
// # Test LDAP connection and synchronize user
cy.apiLDAPTest();
cy.apiLDAPSync();
// # Login to sync LDAP user
return cy.apiLogin(ldapUser).then(({user}) => {
if (bypassTutorial) {
cy.apiAdminLogin();
}
if (bypassTutorial) {
cy.apiSaveTutorialStep(user.id, '999');
}
return cy.wrap(user);
});
});

View File

@@ -0,0 +1,596 @@
{
"ServiceSettings": {
"SiteURL": "http://localhost:8065",
"WebsocketURL": "",
"LicenseFileLocation": "",
"ListenAddress": ":8065",
"ConnectionSecurity": "",
"TLSCertFile": "",
"TLSKeyFile": "",
"TLSMinVer": "1.2",
"TLSStrictTransport": false,
"TLSStrictTransportMaxAge": 63072000,
"TLSOverwriteCiphers": [],
"UseLetsEncrypt": false,
"LetsEncryptCertificateCacheFile": "./config/letsencrypt.cache",
"Forward80To443": false,
"TrustedProxyIPHeader": [],
"ReadTimeout": 300,
"WriteTimeout": 300,
"IdleTimeout": 300,
"MaximumLoginAttempts": 10,
"GoroutineHealthThreshold": -1,
"EnableOAuthServiceProvider": false,
"EnableIncomingWebhooks": true,
"EnableOutgoingWebhooks": true,
"EnableCommands": true,
"OutgoingIntegrationRequestsTimeout": 30,
"EnablePostUsernameOverride": false,
"EnablePostIconOverride": false,
"GoogleDeveloperKey": "",
"EnableLinkPreviews": false,
"EnablePermalinkPreviews": true,
"RestrictLinkPreviews": "",
"EnableTesting": false,
"EnableDeveloper": false,
"DeveloperFlags": "",
"EnableClientPerformanceDebugging": false,
"EnableSecurityFixAlert": false,
"EnableInsecureOutgoingConnections": false,
"AllowedUntrustedInternalConnections": "localhost",
"EnableMultifactorAuthentication": false,
"EnforceMultifactorAuthentication": false,
"EnableUserAccessTokens": false,
"AllowCorsFrom": "",
"CorsExposedHeaders": "",
"CorsAllowCredentials": false,
"CorsDebug": false,
"AllowCookiesForSubdomains": false,
"ExtendSessionLengthWithActivity": true,
"SessionLengthWebInDays": 30,
"SessionLengthWebInHours": 720,
"SessionLengthMobileInDays": 30,
"SessionLengthMobileInHours": 720,
"SessionLengthSSOInDays": 30,
"SessionLengthSSOInHours": 720,
"SessionCacheInMinutes": 10,
"SessionIdleTimeoutInMinutes": 43200,
"WebsocketSecurePort": 443,
"WebsocketPort": 80,
"WebserverMode": "gzip",
"EnableGifPicker": false,
"GiphySdkKey": "s0glxvzVg9azvPipKxcPLpXV0q1x1fVP",
"EnableCustomEmoji": false,
"EnableEmojiPicker": true,
"PostEditTimeLimit": -1,
"TimeBetweenUserTypingUpdatesMilliseconds": 5000,
"EnableCrossTeamSearch": true,
"EnablePostSearch": true,
"EnableFileSearch": true,
"MinimumHashtagLength": 3,
"EnableUserTypingMessages": true,
"EnableChannelViewedMessages": true,
"EnableUserStatuses": true,
"ExperimentalEnableAuthenticationTransfer": true,
"ClusterLogTimeoutMilliseconds": 2000,
"EnableTutorial": true,
"EnableOnboardingFlow": false,
"ExperimentalEnableDefaultChannelLeaveJoinMessages": true,
"ExperimentalGroupUnreadChannels": "disabled",
"EnableAPITeamDeletion": true,
"EnableAPITriggerAdminNotifications": false,
"EnableAPIUserDeletion": false,
"EnableHardenedMode": false,
"ExperimentalStrictCSRFEnforcement": false,
"EnableEmailInvitations": true,
"DisableBotsWhenOwnerIsDeactivated": true,
"EnableBotAccountCreation": true,
"EnableSVGs": true,
"EnableLatex": false,
"EnableInlineLatex": true,
"PostPriority": true,
"AllowPersistentNotifications": true,
"AllowPersistentNotificationsForGuests": false,
"PersistentNotificationIntervalMinutes": 5,
"PersistentNotificationMaxCount": 6,
"PersistentNotificationMaxRecipients": 5,
"EnableAPIChannelDeletion": false,
"EnableLocalMode": false,
"LocalModeSocketLocation": "/var/tmp/mattermost_local.socket",
"EnableAWSMetering": false,
"AWSMeteringTimeoutSeconds": 30,
"SplitKey": "",
"FeatureFlagSyncIntervalSeconds": 30,
"DebugSplit": false,
"ThreadAutoFollow": true,
"CollapsedThreads": "disabled",
"ManagedResourcePaths": "",
"EnableCustomGroups": true,
"AllowSyncedDrafts": true,
"UniqueEmojiReactionLimitPerPost": 50,
"RefreshPostStatsRunTime": "00:00",
"MaximumPayloadSizeBytes": 100000,
"MaximumURLLength": 2048
},
"TeamSettings": {
"SiteName": "Mattermost",
"MaxUsersPerTeam": 2000,
"EnableJoinLeaveMessageByDefault": true,
"EnableUserCreation": true,
"EnableOpenServer": true,
"EnableUserDeactivation": false,
"RestrictCreationToDomains": "",
"EnableCustomUserStatuses": true,
"EnableCustomBrand": false,
"CustomBrandText": "",
"CustomDescriptionText": "",
"RestrictDirectMessage": "any",
"EnableLastActiveTime": true,
"UserStatusAwayTimeout": 300,
"MaxChannelsPerTeam": 2000,
"MaxNotificationsPerChannel": 1000,
"EnableConfirmNotificationsToChannel": true,
"TeammateNameDisplay": "username",
"ExperimentalEnableAutomaticReplies": false,
"LockTeammateNameDisplay": false,
"ExperimentalPrimaryTeam": "",
"ExperimentalDefaultChannels": []
},
"ClientRequirements": {
"AndroidLatestVersion": "",
"AndroidMinVersion": "",
"IosLatestVersion": "",
"IosMinVersion": ""
},
"SqlSettings": {
"DriverName": "postgres",
"DataSource": "postgres://mmuser:mostest_password@localhost/mattermost_test?sslmode=disable\u0026connect_timeout=10\u0026binary_parameters=yes",
"DataSourceReplicas": [],
"DataSourceSearchReplicas": [],
"ConnMaxLifetimeMilliseconds": 3600000,
"ConnMaxIdleTimeMilliseconds": 300000,
"Trace": false,
"QueryTimeout": 30,
"AnalyticsQueryTimeout": 300,
"DisableDatabaseSearch": false,
"MigrationsStatementTimeoutSeconds": 100000,
"ReplicaLagSettings": [],
"ReplicaMonitorIntervalSeconds": 5
},
"LogSettings": {
"EnableConsole": true,
"ConsoleLevel": "DEBUG",
"ConsoleJson": true,
"EnableColor": false,
"EnableFile": true,
"FileLevel": "DEBUG",
"FileJson": true,
"FileLocation": "",
"EnableWebhookDebugging": true,
"EnableDiagnostics": false,
"EnableSentry": false,
"AdvancedLoggingJSON": {},
"MaxFieldSize": 2048
},
"ExperimentalAuditSettings": {
"FileEnabled": false,
"FileName": "",
"AdvancedLoggingJSON": {}
},
"PasswordSettings": {
"MinimumLength": 14,
"Lowercase": false,
"Number": false,
"Uppercase": false,
"Symbol": false,
"EnableForgotLink": true
},
"FileSettings": {
"EnableFileAttachments": true,
"EnableMobileUpload": true,
"EnableMobileDownload": true,
"MaxFileSize": 104857600,
"MaxImageResolution": 33177600,
"MaxImageDecoderConcurrency": -1,
"DriverName": "local",
"Directory": "./data/",
"EnablePublicLink": false,
"ExtractContent": true,
"ExtractContentTimeout": 10,
"ArchiveRecursion": false,
"PublicLinkSalt": "",
"InitialFont": "nunito-bold.ttf",
"AmazonS3AccessKeyId": "",
"AmazonS3SecretAccessKey": "",
"AmazonS3Bucket": "",
"AmazonS3PathPrefix": "",
"AmazonS3Region": "",
"AmazonS3Endpoint": "s3.amazonaws.com",
"AmazonS3SSL": true,
"AmazonS3SignV2": false,
"AmazonS3SSE": false,
"AmazonS3Trace": false,
"AmazonS3RequestTimeoutMilliseconds": 30000,
"AmazonS3StorageClass": "",
"DedicatedExportStore": false,
"ExportDriverName": "local",
"ExportDirectory": "./data/",
"ExportAmazonS3AccessKeyId": "",
"ExportAmazonS3SecretAccessKey": "",
"ExportAmazonS3Bucket": "",
"ExportAmazonS3PathPrefix": "",
"ExportAmazonS3Region": "",
"ExportAmazonS3Endpoint": "s3.amazonaws.com",
"ExportAmazonS3SSL": true,
"ExportAmazonS3SignV2": false,
"ExportAmazonS3SSE": false,
"ExportAmazonS3Trace": false,
"ExportAmazonS3RequestTimeoutMilliseconds": 30000,
"ExportAmazonS3PresignExpiresSeconds": 21600,
"ExportAmazonS3StorageClass": ""
},
"EmailSettings": {
"EnableSignUpWithEmail": true,
"EnableSignInWithEmail": true,
"EnableSignInWithUsername": true,
"SendEmailNotifications": true,
"UseChannelInEmailNotifications": false,
"RequireEmailVerification": false,
"FeedbackName": "",
"FeedbackEmail": "test@example.com",
"ReplyToAddress": "test@example.com",
"FeedbackOrganization": "",
"EnableSMTPAuth": false,
"SMTPUsername": "",
"SMTPPassword": "",
"SMTPServer": "localhost",
"SMTPPort": "10025",
"SMTPServerTimeout": 10,
"ConnectionSecurity": "",
"SendPushNotifications": true,
"PushNotificationServer": "https://push-test.mattermost.com",
"PushNotificationContents": "generic",
"PushNotificationBuffer": 1000,
"EnableEmailBatching": false,
"EmailBatchingBufferSize": 256,
"EmailBatchingInterval": 30,
"EnablePreviewModeBanner": true,
"SkipServerCertificateVerification": false,
"EmailNotificationContentsType": "full"
},
"RateLimitSettings": {
"Enable": false,
"PerSec": 10,
"MaxBurst": 100,
"MemoryStoreSize": 10000,
"VaryByRemoteAddr": true,
"VaryByUser": false,
"VaryByHeader": ""
},
"PrivacySettings": {
"ShowEmailAddress": true,
"ShowFullName": true
},
"SupportSettings": {
"TermsOfServiceLink": "https://mattermost.com/pl/terms-of-use/",
"PrivacyPolicyLink": "https://mattermost.com/pl/privacy-policy/",
"AboutLink": "https://mattermost.com/pl/about-mattermost",
"HelpLink": "https://mattermost.com/pl/help/",
"ReportAProblemLink": "https://mattermost.com/pl/report-a-bug",
"ReportAProblemType": "link",
"ForgotPasswordLink": "",
"SupportEmail": "",
"CustomTermsOfServiceEnabled": false,
"CustomTermsOfServiceReAcceptancePeriod": 365,
"EnableAskCommunityLink": true
},
"AnnouncementSettings": {
"EnableBanner": false,
"BannerText": "",
"BannerColor": "#f2a93b",
"BannerTextColor": "#333333",
"AllowBannerDismissal": true,
"AdminNoticesEnabled": false,
"UserNoticesEnabled": false,
"NoticesURL": "https://notices.mattermost.com/",
"NoticesFetchFrequency": 3600,
"NoticesSkipCache": false
},
"ThemeSettings": {
"EnableThemeSelection": true,
"DefaultTheme": "default",
"AllowCustomThemes": true,
"AllowedThemes": []
},
"GitLabSettings": {
"Enable": false,
"Secret": "",
"Id": "",
"Scope": "",
"AuthEndpoint": "",
"TokenEndpoint": "",
"UserAPIEndpoint": "",
"DiscoveryEndpoint": "",
"ButtonText": "",
"ButtonColor": ""
},
"GoogleSettings": {
"Enable": false,
"Secret": "",
"Id": "",
"Scope": "profile email",
"AuthEndpoint": "https://accounts.google.com/o/oauth2/v2/auth",
"TokenEndpoint": "https://www.googleapis.com/oauth2/v4/token",
"UserAPIEndpoint": "https://people.googleapis.com/v1/people/me?personFields=names,emailAddresses,nicknames,metadata",
"DiscoveryEndpoint": "",
"ButtonText": "",
"ButtonColor": ""
},
"Office365Settings": {
"Enable": false,
"Secret": "",
"Id": "",
"Scope": "User.Read",
"AuthEndpoint": "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
"TokenEndpoint": "https://login.microsoftonline.com/common/oauth2/v2.0/token",
"UserAPIEndpoint": "https://graph.microsoft.com/v1.0/me",
"DiscoveryEndpoint": "",
"DirectoryId": ""
},
"OpenIdSettings": {
"Enable": false,
"Secret": "",
"Id": "",
"Scope": "profile openid email",
"AuthEndpoint": "",
"TokenEndpoint": "",
"UserAPIEndpoint": "",
"DiscoveryEndpoint": "",
"ButtonText": "",
"ButtonColor": "#145DBF"
},
"LdapSettings": {
"Enable": true,
"EnableSync": false,
"LdapServer": "localhost",
"LdapPort": 389,
"ConnectionSecurity": "",
"BaseDN": "dc=mm,dc=test,dc=com",
"BindUsername": "cn=admin,dc=mm,dc=test,dc=com",
"BindPassword": "mostest_password",
"UserFilter": "",
"GroupFilter": "",
"GuestFilter": "",
"EnableAdminFilter": false,
"AdminFilter": "",
"GroupDisplayNameAttribute": "cn",
"GroupIdAttribute": "entryUUID",
"FirstNameAttribute": "cn",
"LastNameAttribute": "sn",
"EmailAttribute": "mail",
"UsernameAttribute": "uid",
"NicknameAttribute": "cn",
"IdAttribute": "uid",
"PositionAttribute": "sAMAccountType",
"LoginIdAttribute": "uid",
"PictureAttribute": "",
"SyncIntervalMinutes": 10000,
"SkipCertificateVerification": true,
"PublicCertificateFile": "",
"PrivateKeyFile": "",
"QueryTimeout": 60,
"MaxPageSize": 500,
"LoginFieldName": ""
},
"ComplianceSettings": {
"Enable": false,
"Directory": "./data/",
"EnableDaily": false,
"BatchSize": 30000
},
"LocalizationSettings": {
"DefaultServerLocale": "en",
"DefaultClientLocale": "en",
"AvailableLocales": ""
},
"SamlSettings": {
"Enable": false,
"EnableSyncWithLdap": false,
"EnableSyncWithLdapIncludeAuth": false,
"IgnoreGuestsLdapSync": false,
"Verify": true,
"Encrypt": true,
"SignRequest": false,
"IdpURL": "",
"IdpDescriptorURL": "",
"IdpMetadataURL": "",
"ServiceProviderIdentifier": "",
"AssertionConsumerServiceURL": "",
"SignatureAlgorithm": "RSAwithSHA1",
"CanonicalAlgorithm": "Canonical1.0",
"ScopingIDPProviderId": "",
"ScopingIDPName": "",
"IdpCertificateFile": "saml-idp.crt",
"PublicCertificateFile": "saml-public.crt",
"PrivateKeyFile": "saml-private.key",
"IdAttribute": "",
"GuestAttribute": "",
"EnableAdminAttribute": false,
"AdminAttribute": "",
"FirstNameAttribute": "",
"LastNameAttribute": "",
"EmailAttribute": "Email",
"UsernameAttribute": "Username",
"NicknameAttribute": "",
"LocaleAttribute": "",
"PositionAttribute": "",
"LoginButtonText": "SAML"
},
"NativeAppSettings": {
"AppCustomURLSchemes": [
"mmauth://",
"mmauthbeta://"
],
"AppDownloadLink": "https://mattermost.com/pl/download-apps",
"AndroidAppDownloadLink": "https://mattermost.com/pl/android-app/",
"IosAppDownloadLink": "https://mattermost.com/pl/ios-app/"
},
"ClusterSettings": {
"Enable": false,
"ClusterName": "",
"OverrideHostname": "",
"NetworkInterface": "",
"BindAddress": "",
"AdvertiseAddress": "",
"UseIPAddress": true,
"EnableGossipCompression": true,
"EnableGossipEncryption": false,
"ReadOnlyConfig": true,
"GossipPort": 8074
},
"MetricsSettings": {
"Enable": false,
"BlockProfileRate": 0,
"ListenAddress": ":8067"
},
"ExperimentalSettings": {
"LinkMetadataTimeoutMilliseconds": 5000,
"RestrictSystemAdmin": false,
"EnableSharedChannels": false,
"EnableRemoteClusterService": false,
"DisableAppBar": false,
"DisableRefetchingOnBrowserFocus": false,
"DelayChannelAutocomplete": false
},
"AnalyticsSettings": {
"MaxUsersForStatistics": 2500
},
"ElasticsearchSettings": {
"ConnectionURL": "http://localhost:9200",
"Username": "elastic",
"Password": "changeme",
"EnableIndexing": false,
"EnableSearching": false,
"EnableCJKAnalyzers": false,
"EnableAutocomplete": false,
"Sniff": false,
"PostIndexReplicas": 1,
"PostIndexShards": 1,
"ChannelIndexReplicas": 1,
"ChannelIndexShards": 1,
"UserIndexReplicas": 1,
"UserIndexShards": 1,
"AggregatePostsAfterDays": 365,
"PostsAggregatorJobStartTime": "03:00",
"IndexPrefix": "",
"LiveIndexingBatchSize": 10,
"BatchSize": 10000,
"RequestTimeoutSeconds": 30,
"SkipTLSVerification": false,
"CA": "",
"ClientCert": "",
"ClientKey": "",
"Trace": "",
"IgnoredPurgeIndexes": ""
},
"DataRetentionSettings": {
"EnableMessageDeletion": false,
"EnableFileDeletion": false,
"EnableBoardsDeletion": false,
"MessageRetentionDays": 365,
"MessageRetentionHours": 0,
"FileRetentionDays": 365,
"FileRetentionHours": 0,
"BoardsRetentionDays": 365,
"DeletionJobStartTime": "02:00",
"BatchSize": 3000,
"TimeBetweenBatchesMilliseconds": 100,
"RetentionIdsBatchSize": 100
},
"MessageExportSettings": {
"EnableExport": false,
"ExportFormat": "actiance",
"DailyRunTime": "01:00",
"ExportFromTimestamp": 0,
"BatchSize": 10000,
"DownloadExportResults": false,
"GlobalRelaySettings": {
"CustomerType": "A9",
"SMTPUsername": "",
"SMTPPassword": "",
"EmailAddress": "",
"SMTPServerTimeout": 1800,
"CustomSMTPServerName": "",
"CustomSMTPPort": "25",
"CustomHeaderName": "",
"CustomHeaderValue": "",
"SenderAddress": ""
}
},
"JobSettings": {
"RunJobs": true,
"RunScheduler": true,
"CleanupJobsThresholdDays": -1,
"CleanupConfigThresholdDays": -1
},
"PluginSettings": {
"Enable": true,
"EnableUploads": true,
"AllowInsecureDownloadURL": false,
"EnableHealthCheck": true,
"Directory": "./plugins",
"ClientDirectory": "./client/plugins",
"Plugins": {},
"PluginStates": {
"com.mattermost.calls": {
"Enable": false
},
"com.mattermost.nps": {
"Enable": false
},
"playbooks": {
"Enable": true
}
},
"EnableMarketplace": true,
"EnableRemoteMarketplace": true,
"AutomaticPrepackagedPlugins": true,
"RequirePluginSignature": false,
"MarketplaceURL": "https://api.integrations.mattermost.com",
"SignaturePublicKeyFiles": [],
"ChimeraOAuthProxyURL": ""
},
"DisplaySettings": {
"CustomURLSchemes": [],
"MaxMarkdownNodes": 0
},
"GuestAccountsSettings": {
"Enable": true,
"HideTags": false,
"EnforceMultifactorAuthentication": false,
"RestrictCreationToDomains": ""
},
"ImageProxySettings": {
"Enable": true,
"ImageProxyType": "local",
"RemoteImageProxyURL": "",
"RemoteImageProxyOptions": ""
},
"ImportSettings": {
"Directory": "./import",
"RetentionDays": 30
},
"ExportSettings": {
"Directory": "./export",
"RetentionDays": 30
},
"WranglerSettings": {
"PermittedWranglerRoles": [],
"AllowedEmailDomain": [],
"MoveThreadMaxCount": 100,
"MoveThreadToAnotherTeamEnable": false,
"MoveThreadFromPrivateChannelEnable": false,
"MoveThreadFromDirectMessageChannelEnable": false,
"MoveThreadFromGroupMessageChannelEnable": false
}
}

View File

@@ -0,0 +1,152 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/// <reference types="cypress" />
// ***************************************************************
// Each command should be properly documented using JSDoc.
// See https://jsdoc.app/index.html for reference.
// Basic requirements for documentation are the following:
// - Meaningful description
// - Specific link to https://api.mattermost.com
// - Each parameter with `@params`
// - Return value with `@returns`
// - Example usage with `@example`
// Custom command should follow naming convention of having `api` prefix, e.g. `apiLogin`.
// ***************************************************************
interface PluginStatus {
isInstalled: boolean;
isActive: boolean;
}
interface PluginTestInfo {
id: string;
version: string;
url: string;
filename: string;
}
declare namespace Cypress {
interface Chainable {
/**
* Get plugins.
* See https://api.mattermost.com/#tag/plugins/paths/~1plugins/get
* @returns {PluginsResponse} `out.plugins` as `PluginsResponse`
*
* @example
* cy.apiGetAllPlugins().then(({plugins}) => {
* // do something with plugins
* });
*/
apiGetAllPlugins(): Chainable<PluginsResponse>;
/**
* Get plugins.
* @param {string} pluginId - plugin ID
* @param {string} version - plugin version
*
* @returns {PluginStatus} - plugin status if upload and active
*
* @example
* cy.apiGetPluginStatus(pluginId, version).then((status) => {
* // do something with status
* });
*/
apiGetPluginStatus(pluginId: string, version?: string): Chainable<PluginStatus>;
/**
* Upload plugin.
* See https://api.mattermost.com/#tag/plugins/paths/~1plugins/post
* @param {string} filename - name of the plugin to upload
* @returns {Response} response: Cypress-chainable response
*
* @example
* cy.apiUploadPlugin('filename');
*/
apiUploadPlugin(filename: string): Chainable<Response>;
/**
* Upload a plugin and enable.
* - If a plugin is already active, then it will immediately return.
* - If a plugin is inactive, then it will be enabled only.
* - If a plugin is not found in the server, then it will be uploaded
* and the enabled.
* - On plugin upload, if `pluginTestInfo` includes a `url` field, then
* the plugin will be installed via URL. Otherwise if `filename` field
* is present, then it will look at such filename under fixtures folder
* and then use the file to upload.
*
* @param {PluginTestInfo} pluginTestInfo - plugin test info
* @returns {Response} response: Cypress-chainable response
*
* @example
* cy.apiUploadAndEnablePlugin(pluginTestInfo);
*/
apiUploadAndEnablePlugin(pluginTestInfo: PluginTestInfo): Chainable<Response>;
/**
* Install plugin from url.
* See https://api.mattermost.com/#tag/plugins/paths/~1plugins~1install_from_url/post
* @param {string} pluginDownloadUrl - URL used to download the plugin
* @param {string} force - Set to 'true' to overwrite a previously installed plugin with the same ID, if any
* @returns {PluginManifest} `out.plugin` as `PluginManifest`
*
* @example
* cy.apiInstallPluginFromUrl('url', 'true').then(({plugin}) => {
* // do something with plugin
* });
*/
apiInstallPluginFromUrl(pluginDownloadUrl: string, force: string): Chainable<PluginManifest>;
/**
* Enable plugin.
* See https://api.mattermost.com/#tag/plugins/paths/~1plugins~1{plugin_id}~1enable/post
* @param {string} pluginId - Id of the plugin to enable
* @returns {string} `out.status`
*
* @example
* cy.apiEnablePluginById('pluginId');
*/
apiEnablePluginById(pluginId: string): Chainable<Record<string, any>>;
/**
* Disable plugin.
* See https://api.mattermost.com/#tag/plugins/paths/~1plugins~1{plugin_id}~disable/post
* @param {string} pluginId - Id of the plugin to disable
* @returns {string} `out.status`
*
* @example
* cy.apiDisablePluginById('pluginId');
*/
apiDisablePluginById(pluginId: string): Chainable<Record<string, any>>;
/**
* Disable all plugins installed that are not prepackaged.
*
* @example
* cy.apiDisableNonPrepackagedPlugins();
*/
apiDisableNonPrepackagedPlugins(): Chainable<Record<string, any>>;
/**
* Remove plugin.
* See https://api.mattermost.com/#tag/plugins/paths/~1plugins~1{plugin_id}/delete
* @param {string} pluginId - Id of the plugin to uninstall
* @returns {string} `out.status`
*
* @example
* cy.apiRemovePluginById('url');
*/
apiRemovePluginById(pluginId: string, force: string): Chainable<Record<string, any>>;
/**
* Removes all active and inactive plugins.
*
* @example
* cy.apiUninstallAllPlugins();
*/
apiUninstallAllPlugins(): Chainable;
}
}

View File

@@ -0,0 +1,216 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import * as TIMEOUTS from '../../fixtures/timeouts';
// *****************************************************************************
// Plugins
// https://api.mattermost.com/#tag/plugins
// *****************************************************************************
Cypress.Commands.add('apiGetAllPlugins', () => {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: '/api/v4/plugins',
method: 'GET',
failOnStatusCode: false,
}).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap({plugins: response.body});
});
});
function getPlugin(plugins, pluginId, version) {
return Cypress._.find(plugins, (plugin) => {
return version ? plugin.id === pluginId && plugin.version === version : plugin.id === pluginId;
});
}
Cypress.Commands.add('apiGetPluginStatus', (pluginId, version) => {
return cy.apiGetAllPlugins().then(({plugins}) => {
const active = getPlugin(plugins.active, pluginId, version);
const inactive = getPlugin(plugins.inactive, pluginId, version);
if (active) {
return cy.wrap({isInstalled: true, isActive: true});
}
if (inactive) {
return cy.wrap({isInstalled: true, isActive: false});
}
return cy.wrap({isInstalled: false, isActive: false});
});
});
Cypress.Commands.add('apiUploadPlugin', (filename) => {
const options = {
url: '/api/v4/plugins',
method: 'POST',
successStatus: 201,
};
return cy.apiUploadFile('plugin', filename, options).then(() => {
return cy.wait(TIMEOUTS.THREE_SEC);
});
});
Cypress.Commands.add('apiUploadAndEnablePlugin', ({filename, url, id, version}) => {
return cy.apiGetPluginStatus(id, version).then((data) => {
// # Only short-circuit when version is specified, so we know the exact
// installed version matches the desired artifact. Without a version,
// any installed version would match and we'd silently skip the install.
if (version && data.isActive) {
cy.log(`${id}: Plugin is active.`);
return cy.wrap(data);
}
if (version && data.isInstalled) {
cy.log(`${id}: Plugin is inactive. Only going to enable.`);
return cy.apiEnablePluginById(id).then(() => {
cy.wait(TIMEOUTS.ONE_SEC);
return cy.wrap(data);
});
}
// # Remove any old version of the plugin before installing the new one
const removeOldVersion = () => {
return cy.apiGetPluginStatus(id).then((anyVersionData) => {
if (anyVersionData.isInstalled || anyVersionData.isActive) {
cy.log(`${id}: Removing old version before installing new one.`);
return cy.apiRemovePluginById(id);
}
return cy.wrap(null);
});
};
if (url) {
// # Upload plugin by URL then enable
cy.log(`${id}: Plugin is to be uploaded via URL and then enable.`);
return removeOldVersion().then(() => {
return cy.apiInstallPluginFromUrl(url).then(() => {
cy.wait(TIMEOUTS.FIVE_SEC);
return cy.apiEnablePluginById(id).then(() => {
cy.wait(TIMEOUTS.ONE_SEC);
return cy.wrap({isInstalled: true, isActive: true});
});
});
});
}
// # Upload plugin by file then enable
cy.log(`${id}: Plugin is to be uploaded by filename and then enable.`);
return removeOldVersion().then(() => {
return cy.apiUploadPlugin(filename).then(() => {
return cy.apiEnablePluginById(id).then(() => {
return cy.wrap({isInstalled: true, isActive: true});
});
});
});
});
});
Cypress.Commands.add('apiInstallPluginFromUrl', (url, force = true) => {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: `/api/v4/plugins/install_from_url?plugin_download_url=${encodeURIComponent(url)}&force=${force}`,
method: 'POST',
timeout: TIMEOUTS.TWO_MIN,
failOnStatusCode: false,
}).then((response) => {
expect(response.status).to.equal(201);
cy.wait(TIMEOUTS.THREE_SEC);
return cy.wrap({plugin: response.body});
});
});
Cypress.Commands.add('apiEnablePluginById', (pluginId) => {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: `/api/v4/plugins/${encodeURIComponent(pluginId)}/enable`,
method: 'POST',
timeout: TIMEOUTS.TWO_MIN,
failOnStatusCode: false,
}).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap(response);
});
});
Cypress.Commands.add('apiDisablePluginById', (pluginId) => {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: `/api/v4/plugins/${encodeURIComponent(pluginId)}/disable`,
method: 'POST',
timeout: TIMEOUTS.ONE_MIN,
failOnStatusCode: false,
}).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap(response);
});
});
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',
'playbooks',
];
Cypress.Commands.add('apiDisableNonPrepackagedPlugins', () => {
cy.apiGetAllPlugins().then(({plugins}) => {
plugins.active.forEach((plugin) => {
if (!prepackagedPlugins.includes(plugin.id)) {
cy.apiDisablePluginById(plugin.id);
}
});
});
});
Cypress.Commands.add('apiRemovePluginById', (pluginId) => {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: `/api/v4/plugins/${encodeURIComponent(pluginId)}`,
method: 'DELETE',
timeout: TIMEOUTS.TWO_MIN,
failOnStatusCode: false,
}).then((response) => {
return cy.wrap(response);
});
});
Cypress.Commands.add('apiUninstallAllPlugins', () => {
// # Uninstall all plugins
cy.apiGetAllPlugins().then(({plugins}) => {
const {active, inactive} = plugins;
inactive.forEach((plugin) => cy.apiRemovePluginById(plugin.id));
active.forEach((plugin) => cy.apiRemovePluginById(plugin.id));
});
// * Check that all plugins are uninstalled
cy.apiGetAllPlugins().then(({plugins}) => {
const {active, inactive} = plugins;
// # Log all uninstalled plugins for debugging
if (active.length) {
cy.log(JSON.stringify(active));
}
if (inactive.length) {
cy.log(JSON.stringify(active));
}
expect(active.length).to.equal(0);
expect(inactive.length).to.equal(0);
});
});

View File

@@ -0,0 +1,550 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {PreferenceType} from '@mattermost/types/preferences';
import theme from '../../fixtures/theme.json';
import type {ChainableT} from '@/types';
// *****************************************************************************
// Preferences
// https://api.mattermost.com/#tag/preferences
// *****************************************************************************
/**
* Save a list of the user's preferences.
* See https://api.mattermost.com/#tag/preferences/paths/~1users~1{user_id}~1preferences/put
* @param {PreferenceType[]} preferences - List of preference objects
* @param {string} userId - User ID
* @returns {Response} response: Cypress-chainable response which should have successful HTTP status of 200 OK to continue or pass.
*
* @example
* cy.apiSaveUserPreference([{user_id: 'user-id', category: 'display_settings', name: 'channel_display_mode', value: 'full'}], 'user-id');
*/
function apiSaveUserPreference(preferences: PreferenceType[] = [], userId = 'me'): ChainableT<Cypress.Response<unknown>> {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: `/api/v4/users/${userId}/preferences`,
method: 'PUT',
body: preferences,
});
}
Cypress.Commands.add('apiSaveUserPreference', apiSaveUserPreference);
/**
* Save clock display mode to 24-hour preference.
* See https://api.mattermost.com/#tag/preferences/paths/~1users~1{user_id}~1preferences/put
* @param {boolean} is24Hour - true (default) or false for 12-hour
* @returns {Response} response: Cypress-chainable response which should have successful HTTP status of 200 OK to continue or pass.
*
* @example
* cy.apiSaveClockDisplayModeTo24HourPreference(true);
*/
function apiSaveClockDisplayModeTo24HourPreference(is24Hour = true): ChainableT<Cypress.Response<unknown>> {
return cy.getCookie('MMUSERID').then((cookie) => {
const preference = {
user_id: cookie!.value,
category: 'display_settings',
name: 'use_military_time',
value: is24Hour.toString(),
};
return cy.apiSaveUserPreference([preference]);
});
}
Cypress.Commands.add('apiSaveClockDisplayModeTo24HourPreference', apiSaveClockDisplayModeTo24HourPreference);
/**
* Saves channel display mode preference of a user directly via API
* This API assume that the user is logged in and has cookie to access
* @param {String} value - Either "full" (default) or "centered"
*/
function apiSaveChannelDisplayModePreference(value = 'full') {
return cy.getCookie('MMUSERID').then((cookie) => {
const preference = {
user_id: cookie!.value,
category: 'display_settings',
name: 'channel_display_mode',
value,
};
return cy.apiSaveUserPreference([preference]);
});
}
Cypress.Commands.add('apiSaveChannelDisplayModePreference', apiSaveChannelDisplayModePreference);
/**
* Saves message display preference of a user directly via API
* This API assume that the user is logged in and has cookie to access
* @param {String} value - Either "clean" (default) or "compact"
*/
function apiSaveMessageDisplayPreference(value = 'clean') {
return cy.getCookie('MMUSERID').then((cookie) => {
const preference = {
user_id: cookie!.value,
category: 'display_settings',
name: 'message_display',
value,
};
return cy.apiSaveUserPreference([preference]);
});
}
Cypress.Commands.add('apiSaveMessageDisplayPreference', apiSaveMessageDisplayPreference);
/**
* Saves teammate name display preference of a user directly via API
* This API assume that the user is logged in and has cookie to access
* @param {String} value - Either "username" (default), "nickname_full_name" or "full_name"
*/
function apiSaveTeammateNameDisplayPreference(value = 'username') {
return cy.getCookie('MMUSERID').then((cookie) => {
const preference = {
user_id: cookie!.value,
category: 'display_settings',
name: 'name_format',
value,
};
return cy.apiSaveUserPreference([preference]);
});
}
Cypress.Commands.add('apiSaveTeammateNameDisplayPreference', apiSaveTeammateNameDisplayPreference);
/**
* Saves theme preference of a user directly via API
* This API assume that the user is logged in and has cookie to access
* @param {Object} value - theme object. Will pass default value if none is provided.
*/
function apiSaveThemePreference(value = JSON.stringify(theme.default)) {
return cy.getCookie('MMUSERID').then((cookie) => {
const preference = {
user_id: cookie!.value,
category: 'theme',
name: '',
value,
};
return cy.apiSaveUserPreference([preference]);
});
}
Cypress.Commands.add('apiSaveThemePreference', apiSaveThemePreference);
const defaultSidebarSettingPreference = {
grouping: 'by_type',
unreads_at_top: 'true',
favorite_at_top: 'true',
sorting: 'alpha',
};
/**
* Saves theme preference of a user directly via API
* This API assume that the user is logged in and has cookie to access
* @param {Object} value - sidebar settings object. Will pass default value if none is provided.
*/
function apiSaveSidebarSettingPreference(value = {}) {
return cy.getCookie('MMUSERID').then((cookie) => {
const newValue = {
...defaultSidebarSettingPreference,
...value,
};
const preference = {
user_id: cookie!.value,
category: 'sidebar_settings',
name: '',
value: JSON.stringify(newValue),
};
return cy.apiSaveUserPreference([preference]);
});
}
Cypress.Commands.add('apiSaveSidebarSettingPreference', apiSaveSidebarSettingPreference);
/**
* Saves the preference on whether to show link and image previews
* This API assume that the user is logged in and has cookie to access
* @param {boolean} show - Either "true" to show link and images previews (default), or "false"
*/
function apiSaveLinkPreviewsPreference(show = 'true') {
return cy.getCookie('MMUSERID').then((cookie) => {
const preference = {
user_id: cookie!.value,
category: 'display_settings',
name: 'link_previews',
value: show,
};
return cy.apiSaveUserPreference([preference]);
});
}
Cypress.Commands.add('apiSaveLinkPreviewsPreference', apiSaveLinkPreviewsPreference);
/**
* Saves the preference on whether to show link and image previews expanded
* This API assume that the user is logged in and has cookie to access
* @param {boolean} collapse - Either "true" to show previews collapsed (default), or "false"
*/
function apiSaveCollapsePreviewsPreference(collapse = 'true') {
return cy.getCookie('MMUSERID').then((cookie) => {
const preference = {
user_id: cookie!.value,
category: 'display_settings',
name: 'collapse_previews',
value: collapse,
};
return cy.apiSaveUserPreference([preference]);
});
}
Cypress.Commands.add('apiSaveCollapsePreviewsPreference', apiSaveCollapsePreviewsPreference);
/**
* Saves tutorial step of a user
* @param {string} userId - User ID
* @param {string} value - value of tutorial step, e.g. '999' (default, completed tutorial)
*/
function apiSaveTutorialStep(userId: string, value = '999'): ChainableT<Cypress.Response<unknown>> {
const preference = {
user_id: userId,
category: 'tutorial_step',
name: userId,
value,
};
return cy.apiSaveUserPreference([preference], userId);
}
Cypress.Commands.add('apiSaveTutorialStep', apiSaveTutorialStep);
function apiSaveOnboardingPreference(userId: string, name: string, value: string) {
const preference = {
user_id: userId,
category: 'recommended_next_steps',
name,
value,
};
return cy.apiSaveUserPreference([preference], userId);
}
Cypress.Commands.add('apiSaveOnboardingPreference', apiSaveOnboardingPreference);
/**
* Save DM channel show preference.
* See https://api.mattermost.com/#tag/preferences/paths/~1users~1{user_id}~1preferences/put
* @param {string} userId - User ID
* @param {string} otherUserId - Other user in a DM channel
* @param {string} value - options are 'true' or 'false'
* @returns {Response} response: Cypress-chainable response which should have successful HTTP status of 200 OK to continue or pass.
*
* @example
* cy.apiSaveDirectChannelShowPreference('user-id', 'other-user-id', 'false');
*/
function apiSaveDirectChannelShowPreference(userId: string, otherUserId: string, value: string): ChainableT<Cypress.Response<unknown>> {
const preference = {
user_id: userId,
category: 'direct_channel_show',
name: otherUserId,
value,
};
return cy.apiSaveUserPreference([preference], userId);
}
Cypress.Commands.add('apiSaveDirectChannelShowPreference', apiSaveDirectChannelShowPreference);
function apiHideSidebarWhatsNewModalPreference(userId: string, value: string) {
const preference = {
user_id: userId,
category: 'whats_new_modal',
name: 'has_seen_sidebar_whats_new_modal',
value,
};
return cy.apiSaveUserPreference([preference], userId);
}
Cypress.Commands.add('apiHideSidebarWhatsNewModalPreference', apiHideSidebarWhatsNewModalPreference);
/**
* Get the full list of the user's preferences.
* See https://api.mattermost.com/#tag/preferences/paths/~1users~1{user_id}~1preferences/get
* @param {string} userId - User ID
* @returns {Response} response: Cypress-chainable response which should have a list of preference objects
*
* @example
* cy.apiGetUserPreference('user-id');
*/
function apiGetUserPreference(userId: string): ChainableT<PreferenceType[]> {
return cy.request(`/api/v4/users/${userId}/preferences`).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap(response.body as PreferenceType[]);
});
}
Cypress.Commands.add('apiGetUserPreference', apiGetUserPreference);
/**
* Save Collapsed Reply Threads preference.
* See https://api.mattermost.com/#tag/preferences/paths/~1users~1{user_id}~1preferences/put
* @param {string} userId - User ID
* @param {string} value - options are 'on' or 'off'
* @returns {Response} response: Cypress-chainable response which should have successful HTTP status of 200 OK to continue or pass.
*
* @example
* cy.apiSaveCRTPreference('user-id', 'on');
*/
function apiSaveCRTPreference(userId: string, value = 'on'): ChainableT<Cypress.Response<unknown>> {
const preference = {
user_id: userId,
category: 'display_settings',
name: 'collapsed_reply_threads',
value,
};
return cy.apiSaveUserPreference([preference], userId);
}
Cypress.Commands.add('apiSaveCRTPreference', apiSaveCRTPreference);
/**
* Save cloud trial banner preference.
* See https://api.mattermost.com/#tag/preferences/paths/~1users~1{user_id}~1preferences/put
* @param {string} userId - User ID
* @param {string} name - options are trial or hide
* @param {string} value - options are 'max_days_banner' or '3_days_banner' for trial, and 'true' or 'false' for hide
* @returns {Response} response: Cypress-chainable response which should have successful HTTP status of 200 OK to continue or pass.
*
* @example
* cy.apiSaveCloudTrialBannerPreference('user-id', 'hide', 'true');
*/
function apiSaveCloudTrialBannerPreference(userId: string, name: string, value: string): ChainableT<Cypress.Response<unknown>> {
const preference = {
user_id: userId,
category: 'cloud_trial_banner',
name,
value,
};
return cy.apiSaveUserPreference([preference], userId);
}
Cypress.Commands.add('apiSaveCloudTrialBannerPreference', apiSaveCloudTrialBannerPreference);
/**
* Save show trial modal.
* See https://api.mattermost.com/#tag/preferences/paths/~1users~1{user_id}~1preferences/put
* @param {string} userId - User ID
* @param {string} name - trial_modal_auto_shown
* @param {string} value - values are 'true' or 'false'
* @returns {Response} response: Cypress-chainable response which should have successful HTTP status of 200 OK to continue or pass.
*
* @example
* cy.apiSaveStartTrialModal('user-id', 'true');
*/
function apiSaveStartTrialModal(userId: string, value = 'true'): ChainableT<Cypress.Response<unknown>> {
const preference = {
user_id: userId,
category: 'start_trial_modal',
name: 'trial_modal_auto_shown',
value,
};
return cy.apiSaveUserPreference([preference], userId);
}
Cypress.Commands.add('apiSaveStartTrialModal', apiSaveStartTrialModal);
/**
* Save onboarding tasklist preference.
* See https://api.mattermost.com/#tag/preferences/paths/~1users~1{user_id}~1preferences/put
* @param {string} userId - User ID
* @param {string} name - options are complete_profile, team_setup, invite_members or hide
* @param {string} value - options are 'true' or 'false'
* @returns {Response} response: Cypress-chainable response which should have successful HTTP status of 200 OK to continue or pass.
*
* @example
* cy.apiSaveOnboardingTaskListPreference('user-id', 'hide', 'true');
*/
function apiSaveOnboardingTaskListPreference(userId: string, name: string, value: string): ChainableT<Cypress.Response<unknown>> {
const preference = {
user_id: userId,
category: 'onboarding_task_list',
name,
value,
};
return cy.apiSaveUserPreference([preference], userId);
}
Cypress.Commands.add('apiSaveOnboardingTaskListPreference', apiSaveOnboardingTaskListPreference);
/**
* Save skip steps preference.
* @param userId - User ID
* @param {string} value - options are 'true' or 'false'
* @returns {Response} response: Cypress-chainable response which should have successful HTTP status of 200 OK to continue or pass.
*
* @example
* cy.apiSaveSkipStepsPreference('user-id', 'true');
*/
function apiSaveSkipStepsPreference(userId: string, value: string): ChainableT<Cypress.Response<unknown>> {
const preference = {
user_id: userId,
category: 'recommended_next_steps',
name: 'skip',
value,
};
return cy.apiSaveUserPreference([preference], userId);
}
Cypress.Commands.add('apiSaveSkipStepsPreference', apiSaveSkipStepsPreference);
function apiSaveUnreadScrollPositionPreference(userId: string, value: string) {
const preference = {
user_id: userId,
category: 'advanced_settings',
name: 'unread_scroll_position',
value,
};
return cy.apiSaveUserPreference([preference], userId);
}
Cypress.Commands.add('apiSaveUnreadScrollPositionPreference', apiSaveUnreadScrollPositionPreference);
/**
* Mark Boards welcome page as viewed.
* See https://api.mattermost.com/#tag/preferences/paths/~1users~1{user_id}~1preferences/put
* @param {string} userId - User ID
* @returns {Response} response: Cypress-chainable response which should have successful HTTP status of 200 OK to continue or pass.
*
* @example
* cy.apiBoardsWelcomePageViewed('user-id');
*/
function apiBoardsWelcomePageViewed(userId: string): ChainableT<Cypress.Response<unknown>> {
const preferences = [{
user_id: userId,
category: 'boards',
name: 'welcomePageViewed',
value: '1',
},
{
user_id: userId,
category: 'boards',
name: 'version72MessageCanceled',
value: 'true',
}];
return cy.apiSaveUserPreference(preferences, userId);
}
Cypress.Commands.add('apiBoardsWelcomePageViewed', apiBoardsWelcomePageViewed);
/**
* Saves Join/Leave messages preference of a user directly via API
* This API assume that the user is logged in and has cookie to access
* @param {Boolean} enable - Either true (default) or false
*/
function apiSaveJoinLeaveMessagesPreference(userId: string, enable = true) {
const preference = {
user_id: userId,
category: 'advanced_settings',
name: 'join_leave',
value: enable.toString(),
};
return cy.apiSaveUserPreference([preference], userId);
}
Cypress.Commands.add('apiSaveJoinLeaveMessagesPreference', apiSaveJoinLeaveMessagesPreference);
/**
* Disables tutorials for user by marking them finished
*/
function apiDisableTutorials(userId: string) {
const preferences = [
{
user_id: userId,
category: 'playbook_edit',
name: userId,
value: '999',
},
{
user_id: userId,
category: 'tutorial_pb_run_details',
name: userId,
value: '999',
},
{
user_id: userId,
category: 'crt_thread_pane_step',
name: userId,
value: '999',
},
{
user_id: userId,
category: 'playbook_preview',
name: userId,
value: '999',
},
{
user_id: userId,
category: 'tutorial_step',
name: userId,
value: '999',
},
{
user_id: userId,
category: 'app_bar',
name: 'channel_with_board_tip_showed',
value: '{"channel_with_board_tip_showed":true}',
},
];
return cy.apiSaveUserPreference(preferences, userId);
}
Cypress.Commands.add('apiDisableTutorials', apiDisableTutorials);
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
interface Chainable {
apiSaveUserPreference: typeof apiSaveUserPreference;
apiSaveClockDisplayModeTo24HourPreference: typeof apiSaveClockDisplayModeTo24HourPreference;
apiSaveChannelDisplayModePreference: typeof apiSaveChannelDisplayModePreference;
apiSaveMessageDisplayPreference: typeof apiSaveMessageDisplayPreference;
apiSaveTeammateNameDisplayPreference: typeof apiSaveTeammateNameDisplayPreference;
apiSaveThemePreference: typeof apiSaveThemePreference;
apiSaveSidebarSettingPreference: typeof apiSaveSidebarSettingPreference;
apiSaveLinkPreviewsPreference: typeof apiSaveLinkPreviewsPreference;
apiSaveCollapsePreviewsPreference: typeof apiSaveCollapsePreviewsPreference;
apiSaveTutorialStep: typeof apiSaveTutorialStep;
apiSaveOnboardingPreference: typeof apiSaveOnboardingPreference;
apiSaveDirectChannelShowPreference: typeof apiSaveDirectChannelShowPreference;
apiHideSidebarWhatsNewModalPreference: typeof apiHideSidebarWhatsNewModalPreference;
apiGetUserPreference: typeof apiGetUserPreference;
apiSaveCRTPreference: typeof apiSaveCRTPreference;
apiSaveCloudTrialBannerPreference: typeof apiSaveCloudTrialBannerPreference;
apiSaveStartTrialModal: typeof apiSaveStartTrialModal;
apiSaveOnboardingTaskListPreference: typeof apiSaveOnboardingTaskListPreference;
apiSaveSkipStepsPreference: typeof apiSaveSkipStepsPreference;
apiSaveUnreadScrollPositionPreference: typeof apiSaveUnreadScrollPositionPreference;
apiBoardsWelcomePageViewed: typeof apiBoardsWelcomePageViewed;
apiSaveJoinLeaveMessagesPreference: typeof apiSaveJoinLeaveMessagesPreference;
apiDisableTutorials: typeof apiDisableTutorials;
}
}
}

View File

@@ -0,0 +1,69 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/// <reference types="cypress" />
// ***************************************************************
// Each command should be properly documented using JSDoc.
// See https://jsdoc.app/index.html for reference.
// Basic requirements for documentation are the following:
// - Meaningful description
// - Specific link to https://api.mattermost.com
// - Each parameter with `@params`
// - Return value with `@returns`
// - Example usage with `@example`
// Custom command should follow naming convention of having `api` prefix, e.g. `apiLogin`.
// ***************************************************************
declare namespace Cypress {
interface Chainable {
/**
* Get a role from the provided role name.
* See https://api.mattermost.com/#tag/roles/paths/~1roles~1name~1{role_name}/get
* @param {string} name - role name, e.g. 'system_user'
* @returns {Role} `out.role` as `Role`
*
* @example
* cy.getRoleByName('system_user').then(({role}) => {
* // do something with role
* });
*/
getRoleByName(name: string): Chainable<Role>;
/**
* Get a list of roles by name.
* See https://api.mattermost.com/#tag/roles/paths/~1roles~1names/post
* @param {string[]} names - list of role names, e.g. ['system_user']
* @returns {Role[]} `out.roles` as list of `Role` objects
*
* @example
* cy.apiGetRolesByNames(['system_user']).then(({roles}) => {
* // do something with roles
* });
*/
apiGetRolesByNames(names: string[]): Chainable<{roles: Role[]}>;
/**
* Patch a role by ID.
* See https://api.mattermost.com/#tag/roles/paths/~1roles~1{role_id}~1patch/put
* @param {string} id - role ID
* @param {Permissions} patch.permissions - permissions
* @returns {Role} `out.role` as `Role`
*
* @example
* cy.apiPatchRole('role_id', patch).then(({role}) => {
* // do something with role
* });
*/
apiPatchRole(id: string, patch: Record<string, any>): Chainable<Role>;
/**
* Reset roles to default values.
*
* @example
* cy.apiResetRoles();
*/
apiResetRoles();
}
}

View File

@@ -0,0 +1,69 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import xor from 'lodash.xor';
import {defaultRolesPermissions} from './default_roles_permissions';
// *****************************************************************************
// Preferences
// https://api.mattermost.com/#tag/roles
// *****************************************************************************
export {defaultRolesPermissions};
Cypress.Commands.add('getRoleByName', (name) => {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: `/api/v4/roles/name/${name}`,
method: 'GET',
}).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap({name: response.body});
});
});
Cypress.Commands.add('apiGetRolesByNames', (names) => {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: '/api/v4/roles/names',
method: 'POST',
body: names || Object.keys(defaultRolesPermissions),
}).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap({roles: response.body});
});
});
Cypress.Commands.add('apiPatchRole', (roleID, patch) => {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: `/api/v4/roles/${roleID}/patch`,
method: 'PUT',
body: patch,
}).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap({role: response.body});
});
});
Cypress.Commands.add('apiResetRoles', () => {
cy.apiGetRolesByNames().then(({roles}) => {
roles.forEach((role) => {
const permissions = getPermissions(role.name);
const diff = xor(role.permissions, permissions)?.filter((p) => p?.length);
if (diff?.length > 0) {
cy.apiPatchRole(role.id, {permissions});
}
});
});
});
function getPermissions(roleName) {
const permissions = defaultRolesPermissions[roleName];
if (!permissions) {
return [];
}
return permissions.split(' ').map((permission) => permission.trim()).filter((permission) => permission !== '');
}

View File

@@ -0,0 +1,78 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/// <reference types="cypress" />
// ***************************************************************
// Each command should be properly documented using JSDoc.
// See https://jsdoc.app/index.html for reference.
// Basic requirements for documentation are the following:
// - Meaningful description
// - Specific link to https://api.mattermost.com
// - Each parameter with `@params`
// - Return value with `@returns`
// - Example usage with `@example`
// Custom command should follow naming convention of having `api` prefix, e.g. `apiLogin`.
// ***************************************************************
declare namespace Cypress {
interface Chainable {
/**
* Get the status of the uploaded certificates and keys in use by your SAML configuration.
* See https://api.mattermost.com/#tag/SAML/paths/~1saml~1certificate~1status/get
* @returns {Response} response: Cypress-chainable response which should have successful HTTP status of 200 OK to continue or pass.
*
* @example
* cy.apiGetSAMLCertificateStatus();
*/
apiGetSAMLCertificateStatus(): Chainable<Response>;
/**
* Get SAML metadata from the Identity Provider. SAML must be configured properly.
* See https://api.mattermost.com/#tag/SAML/paths/~1saml~1metadatafromidp/post
* @param {String} samlMetadataUrl - SAML metadata URL
* @returns {Response} response: Cypress-chainable response which should have successful HTTP status of 200 OK to continue or pass.
*
* @example
* cy.apiGetMetadataFromIdp(samlMetadataUrl);
*/
apiGetMetadataFromIdp(samlMetadataUrl: string): Chainable<Response>;
/**
* Upload the IDP certificate to be used with your SAML configuration. The server will pick a hard-coded filename for the IdpCertificateFile setting in your config.json.
* See https://api.mattermost.com/#tag/SAML/paths/~1saml~1certificate~1idp/post
* @param {String} filePath - path of the IDP certificate file relative to fixture folder
* @returns {Response} response: Cypress-chainable response which should have successful HTTP status of 200 OK to continue or pass.
*
* @example
* const filePath = 'saml-idp.crt';
* cy.apiUploadSAMLIDPCert(filePath);
*/
apiUploadSAMLIDPCert(filePath: string): Chainable<Response>;
/**
* Upload the public certificate to be used for encryption with your SAML configuration. The server will pick a hard-coded filename for the PublicCertificateFile setting in your config.json.
* See https://api.mattermost.com/#tag/SAML/paths/~1saml~1certificate~1public/post
* @param {String} filePath - path of the public certificate file relative to fixture folder
* @returns {Response} response: Cypress-chainable response which should have successful HTTP status of 200 OK to continue or pass.
*
* @example
* const filePath = 'saml-public.crt';
* cy.apiUploadSAMLPublicCert(filePath);
*/
apiUploadSAMLPublicCert(filePath: string): Chainable<Response>;
/**
* Upload the private key to be used for encryption with your SAML configuration. The server will pick a hard-coded filename for the PrivateKeyFile setting in your config.json.
* See https://api.mattermost.com/#tag/SAML/paths/~1saml~1certificate~1private/post
* @param {String} filePath - path of the private certificate file relative to fixture folder
* @returns {Response} response: Cypress-chainable response which should have successful HTTP status of 200 OK to continue or pass.
*
* @example
* const filePath = 'saml-private.crt';
* cy.apiUploadSAMLPublicCert(filePath);
*/
apiUploadSAMLPrivateKey(filePath: string): Chainable<Response>;
}
}

View File

@@ -0,0 +1,42 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// *****************************************************************************
// SAML
// https://api.mattermost.com/#tag/SAML
// *****************************************************************************
Cypress.Commands.add('apiGetSAMLCertificateStatus', () => {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: '/api/v4/saml/certificate/status',
method: 'GET',
}).then((response) => {
expect(response.status).to.be.oneOf([200, 201]);
return cy.wrap(response);
});
});
Cypress.Commands.add('apiGetMetadataFromIdp', (samlMetadataUrl) => {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: '/api/v4/saml/metadatafromidp',
method: 'POST',
body: {saml_metadata_url: samlMetadataUrl},
}).then((response) => {
expect(response.status, 'Failed to obtain metadata from Identity Provider URL').to.equal(200);
return cy.wrap(response);
});
});
Cypress.Commands.add('apiUploadSAMLIDPCert', (filePath) => {
cy.apiUploadFile('certificate', filePath, {url: '/api/v4/saml/certificate/idp', method: 'POST', successStatus: 200});
});
Cypress.Commands.add('apiUploadSAMLPublicCert', (filePath) => {
cy.apiUploadFile('certificate', filePath, {url: '/api/v4/saml/certificate/public', method: 'POST', successStatus: 200});
});
Cypress.Commands.add('apiUploadSAMLPrivateKey', (filePath) => {
cy.apiUploadFile('certificate', filePath, {url: '/api/v4/saml/certificate/private', method: 'POST', successStatus: 200});
});

View File

@@ -0,0 +1,45 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/// <reference types="cypress" />
// ***************************************************************
// Each command should be properly documented using JSDoc.
// See https://jsdoc.app/index.html for reference.
// Basic requirements for documentation are the following:
// - Meaningful description
// - Specific link to https://api.mattermost.com
// - Each parameter with `@params`
// - Return value with `@returns`
// - Example usage with `@example`
// Custom command should follow naming convention of having `api` prefix, e.g. `apiLogin`.
// ***************************************************************
declare namespace Cypress {
interface Chainable {
/**
* Get the schemes.
* See https://api.mattermost.com/#tag/schemes/paths/~1schemes/get
* @param {string} scope - Limit the results returned to the provided scope, either team or channel.
* @returns {Scheme[]} `out.schemes` as `Scheme[]`
*
* @example
* cy.apiGetSchemes('team').then(({schemes}) => {
* // do something with schemes
* });
*/
apiGetSchemes(scope: string): Chainable<{schemes: Scheme[]}>;
/**
* Delete a scheme.
* See https://api.mattermost.com/#tag/schemes/paths/~1schemes~1{scheme_id}/delete
* @param {string} schemeId - ID of the scheme to delete
* @returns {Response} response: Cypress-chainable response which should have successful HTTP status of 200 OK to continue or pass.
*
* @example
* cy.apiDeleteScheme('scheme_id');
*/
apiDeleteScheme(schemeId: string): Chainable<Response>;
}
}

View File

@@ -0,0 +1,41 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// *****************************************************************************
// Schemes
// https://api.mattermost.com/#tag/schemes
// *****************************************************************************
Cypress.Commands.add('apiGetSchemes', (scope) => {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: `/api/v4/schemes?scope=${scope}`,
method: 'GET',
}).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap({schemes: response.body});
});
});
Cypress.Commands.add('apiCreateScheme', (name, scope, description) => {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: '/api/v4/schemes',
method: 'POST',
body: {display_name: name, scope, description},
}).then((response) => {
expect(response.status).to.equal(201);
return cy.wrap({scheme: response.body});
});
});
Cypress.Commands.add('apiDeleteScheme', (schemeId) => {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: '/api/v4/schemes/' + schemeId,
method: 'DELETE',
}).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap(response);
});
});

View File

@@ -0,0 +1,117 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {ChainableT} from '../../types';
interface SetupResult {
user: Cypress.UserProfile;
team: Cypress.Team;
channel: Cypress.Channel;
channelUrl: string;
offTopicUrl: string;
townSquareUrl: string;
}
interface SetupParam {
loginAfter?: boolean;
promoteNewUserAsAdmin?: boolean;
hideAdminTrialModal?: boolean;
userPrefix?: string;
userCreateAt?: number;
teamPrefix?: {name: string; displayName: string};
channelPrefix?: {name: string; displayName: string};
skipBoardsWelcomePage?: boolean;
}
function apiInitSetup(arg: SetupParam = {}): ChainableT<SetupResult> {
const {
loginAfter = false,
promoteNewUserAsAdmin = false,
hideAdminTrialModal = true,
userPrefix,
userCreateAt,
teamPrefix = {name: 'team', displayName: 'Team'},
channelPrefix = {name: 'channel', displayName: 'Channel'},
skipBoardsWelcomePage = true,
} = arg;
// Cast needed: custom Cypress command chaining not fully typed
return (cy.apiCreateTeam(teamPrefix.name, teamPrefix.displayName) as any).then(({team}: {team: Cypress.Team}) => {
// # Add public channel
return (cy.apiCreateChannel(team.id, channelPrefix.name, channelPrefix.displayName) as any).then(({channel}: {channel: Cypress.Channel}) => { // Cast needed: custom Cypress command chaining not fully typed
return (cy.apiCreateUser({prefix: userPrefix || (promoteNewUserAsAdmin ? 'admin' : 'user'), createAt: userCreateAt}) as any).then(({user}: {user: Cypress.UserProfile}) => { // Cast needed: custom Cypress command chaining not fully typed
if (promoteNewUserAsAdmin) {
(cy as any).apiPatchUserRoles(user.id, ['system_admin', 'system_user']); // Cast needed: custom Cypress command chaining not fully typed
// Only hide start trial modal for admin since it's not applicable to other users
cy.apiSaveStartTrialModal(user.id, hideAdminTrialModal.toString());
}
if (skipBoardsWelcomePage) {
cy.apiBoardsWelcomePageViewed(user.id);
}
return cy.apiAddUserToTeam(team.id, user.id).then(() => {
return cy.apiAddUserToChannel(channel.id, user.id).then(() => {
const getUrl = (channelName: string) => `/${team.name}/channels/${channelName}`;
const data = {
channel,
team,
user,
channelUrl: getUrl(channel.name),
offTopicUrl: getUrl('off-topic'),
townSquareUrl: getUrl('town-square'),
};
if (loginAfter) {
return cy.apiLogin(user).then(() => {
return cy.wrap(data);
});
}
return cy.wrap(data);
});
});
});
});
});
}
Cypress.Commands.add('apiInitSetup', apiInitSetup);
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
interface Chainable {
/**
* Creates a new user and make it a member of the new public team and its channels - one public channel, town-square and off-topic.
* Created user has an option to log in after all are setup.
* Requires sysadmin session to initiate this command.
* @param {boolean} options.loginAfter - false (default) or true if wants to login as the new user after setting up. Note that when true, succeeding API request will be limited to access/permission of a regular system user.
* @param {boolean} options.promoteNewUserAsAdmin - false (default) or true if wants to promote the newly created user as sysadmin.
* @param {boolean} options.hideAdminTrialModal - true (default) or false if wants to hide Start Enterprise Trial modal.
* @param {string} options.userPrefix - 'user' (default) or any prefix to easily identify a user
* @param {string} options.teamPrefix - {name: 'team', displayName: 'Team'} (default) or any prefix to easily identify a team
* @param {string} options.channelPrefix - {name: 'team', displayName: 'Team'} (default) or any prefix to easily identify a channel
* @returns {Object} `out` Cypress-chainable, yielded with element passed into .wrap().
* @returns {Cypress.UserProfile} `out.user` as `UserProfile` object
* @returns {Cypress.Team} `out.team` as `Team` object
* @returns {Cypress.Channel} `out.channel` as `Channel` object
* @returns {string} `out.channelUrl` as channel URL
* @returns {string} `out.offTopicUrl` as off-topic URL
* @returns {string} `out.townSquareUrl` as town-square URL
*
* @example
* let testUser;
* let testTeam;
* let testChannel;
* cy.apiInitSetup(options).then(({team, channel, user}) => {
* testUser = user;
* testTeam = team;
* testChannel = channel;
* });
*/
apiInitSetup: typeof apiInitSetup;
}
}
}

View File

@@ -0,0 +1,67 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/// <reference types="cypress" />
// ***************************************************************
// Each command should be properly documented using JSDoc.
// See https://jsdoc.app/index.html for reference.
// Basic requirements for documentation are the following:
// - Meaningful description
// - Specific link to https://api.mattermost.com
// - Each parameter with `@params`
// - Return value with `@returns`
// - Example usage with `@example`
// Custom command should follow naming convention of having `api` prefix, e.g. `apiLogin`.
// ***************************************************************
declare namespace Cypress {
interface Chainable {
/**
* Update status of a current user.
* See https://api.mattermost.com/#tag/status/paths/~1users~1{user_id}~1status/put
* @param {String} status - "online" (default), "offline", "away" or "dnd"
* @returns {UserStatus} `out.status` as `UserStatus`
*
* @example
* cy.apiUpdateUserStatus('offline').then(({status}) => {
* // do something with status
* });
*/
apiUpdateUserStatus(status: string): Chainable<UserStatus>;
/**
* Get status of a current user.
* See https://api.mattermost.com/#tag/status/paths/~1users~1{user_id}~1status/get
* @param {String} userId - ID of a given user
* @returns {UserStatus} `out.status` as `UserStatus`
*
* @example
* cy.apiGetUserStatus('userId').then(({status}) => {
* // examine the status information of the user
* });
*/
apiGetStatus(userId: string): Chainable<UserStatus>;
/**
* Update custom status of current user.
* See https://api.mattermost.com/#tag/custom_status/paths/~1users~1{user_id}~1status/custom/put
* @param {UserCustomStatus} customStatus - custom status to be updated
*
* @example
* cy.apiUpdateUserCustomStatus({emoji: 'calendar', text: 'In a meeting'});
*/
apiUpdateUserCustomStatus(customStatus: UserCustomStatus);
/**
* Clear custom status of the current user.
* See https://api.mattermost.com/#tag/custom_status/paths/~1users~1{user_id}~1status/custom/delete
* @param {UserCustomStatus} customStatus - custom status to be updated
*
* @example
* cy.apiClearUserCustomStatus();
*/
apiClearUserCustomStatus();
}
}

View File

@@ -0,0 +1,55 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// *****************************************************************************
// Status
// https://api.mattermost.com/#tag/status
// *****************************************************************************
Cypress.Commands.add('apiUpdateUserStatus', (status = 'online') => {
return cy.getCookie('MMUSERID').then((cookie) => {
const data = {user_id: cookie.value, status};
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: '/api/v4/users/me/status',
method: 'PUT',
body: data,
}).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap({status: response.body});
});
});
});
Cypress.Commands.add('apiGetUserStatus', (userId) => {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: `/api/v4/users/${userId}/status`,
method: 'GET',
}).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap({status: response.body});
});
});
Cypress.Commands.add('apiUpdateUserCustomStatus', (customStatus) => {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: '/api/v4/users/me/status/custom',
method: 'PUT',
body: JSON.stringify(customStatus),
}).then((response) => {
expect(response.status).to.equal(200);
});
});
Cypress.Commands.add('apiClearUserCustomStatus', () => {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: '/api/v4/users/me/status/custom',
method: 'DELETE',
}).then((response) => {
expect(response.status).to.equal(200);
});
});

View File

@@ -0,0 +1,211 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/// <reference types="cypress" />
// ***************************************************************
// Each command should be properly documented using JSDoc.
// See https://jsdoc.app/index.html for reference.
// Basic requirements for documentation are the following:
// - Meaningful description
// - Specific link to https://api.mattermost.com
// - Each parameter with `@params`
// - Return value with `@returns`
// - Example usage with `@example`
// Custom command should follow naming convention of having `api` prefix, e.g. `apiLogin`.
// ***************************************************************
declare namespace Cypress {
interface Chainable {
/**
* Get a subset of the server license needed by the client.
* See https://api.mattermost.com/#tag/system/paths/~1license~1client/get
* @returns {ClientLicense} `out.license` as `ClientLicense`
* @returns {Boolean} `out.isLicensed`
* @returns {Boolean} `out.isCloudLicensed`
*
* @example
* cy.apiGetClientLicense().then(({license}) => {
* // do something with license
* });
*/
apiGetClientLicense(): Chainable<ClientLicense>;
/**
* Verify if server has license for a certain feature and fail test if not found.
* Upload a license if it does not exist.
* @param {string[]} ...features - accepts multiple arguments of features to check, e.g. 'LDAP'
* @returns {ClientLicense} `out.license` as `ClientLicense`
*
* @example
* cy.apiRequireLicenseForFeature('LDAP');
* cy.apiRequireLicenseForFeature('LDAP', 'SAML');
*/
apiRequireLicenseForFeature(...features: string[]): Chainable<ClientLicense>;
/**
* Verify if server has license and fail test if not found.
* Upload a license if it does not exist.
* @returns {ClientLicense} `out.license` as `ClientLicense`
*
* @example
* cy.apiRequireLicense();
*/
apiRequireLicense(): Chainable<ClientLicense>;
/**
* Upload a license to enable enterprise features.
* See https://api.mattermost.com/#tag/system/paths/~1license/post
* @param {String} filePath - path of the license file relative to fixtures folder
* @returns {Response} response: Cypress-chainable response which should have successful HTTP status of 200 OK to continue or pass.
*
* @example
* const filePath = 'mattermost-license.txt';
* cy.apiUploadLicense(filePath);
*/
apiUploadLicense(filePath: string): Chainable<Response>;
/**
* Request and install a trial license for your server.
* See https://api.mattermost.com/#tag/system/paths/~1trial-license/post
* @returns {Object} `out.data` as response status
*
* @example
* cy.apiInstallTrialLicense();
*/
apiInstallTrialLicense(): Chainable<Record<string, any>>;
/**
* Remove the license file from the server. This will disable all enterprise features.
* See https://api.mattermost.com/#tag/system/paths/~1license/delete
* @returns {Response} response: Cypress-chainable response which should have successful HTTP status of 200 OK to continue or pass.
*
* @example
* cy.apiDeleteLicense();
*/
apiDeleteLicense(): Chainable<Response>;
/**
* Update configuration.
* See https://api.mattermost.com/#tag/system/paths/~1config/put
* @param {AdminConfig} newConfig - new config
* @returns {AdminConfig} `out.config` as `AdminConfig`
*
* @example
* cy.apiUpdateConfig().then(({config}) => {
* // do something with config
* });
*/
apiUpdateConfig(newConfig: DeepPartial<AdminConfig>): Chainable<{config: AdminConfig}>;
/**
* Reload the configuration file to pick up on any changes made to it.
* See https://api.mattermost.com/#tag/system/paths/~1config~1reload/post
* @returns {AdminConfig} `out.config` as `AdminConfig`
*
* @example
* cy.apiReloadConfig().then(({config}) => {
* // do something with config
* });
*/
apiReloadConfig(): Chainable<AdminConfig>;
/**
* Get configuration.
* See https://api.mattermost.com/#tag/system/paths/~1config/get
* @param {Boolean} old - false (default) or true to return old format of client config
* @returns {AdminConfig} `out.config` as `AdminConfig`
*
* @example
* cy.apiGetConfig().then(({config}) => {
* // do something with config
* });
*/
apiGetConfig(): Chainable<{config: AdminConfig}>;
/**
* Get analytics.
* See https://api.mattermost.com/#tag/system/paths/~1analytics~1old/get
* @returns {AnalyticsRow[]} `out.analytics` as `AnalyticsRow[]`
*
* @example
* cy.apiGetAnalytics().then(({analytics}) => {
* // do something with analytics
* });
*/
apiGetAnalytics(): Chainable<AnalyticsRow[]>;
/**
* Invalidate all the caches.
* See https://api.mattermost.com/#tag/system/paths/~1caches~1invalidate/post
* @returns {Object} `out.data` as response status
*
* @example
* cy.apiInvalidateCache();
*/
apiInvalidateCache(): Chainable<Record<string, any>>;
/**
* Allow test if matches elastic search disabled.
* Otherwise, fail fast.
* @example
* cy.shouldHaveElasticsearchDisabled();
*/
shouldHaveElasticsearchDisabled(): Chainable;
/**
* Allow test for server other than Cloud edition or with Cloud license.
* Otherwise, fail fast.
* @example
* cy.shouldNotRunOnCloudEdition();
*/
shouldNotRunOnCloudEdition(): Chainable;
/**
* Allow test for server on Team edition or without license.
* Otherwise, fail fast.
* @example
* cy.shouldRunOnTeamEdition();
*/
shouldRunOnTeamEdition(): Chainable;
/**
* Allow test for server with Plugin upload enabled.
* Otherwise, fail fast.
* @example
* cy.shouldHavePluginUploadEnabled();
*/
shouldHavePluginUploadEnabled(): Chainable;
/**
* Allow test for server running with subpath.
* Otherwise, fail fast.
* @example
* cy.shouldRunWithSubpath();
*/
shouldRunWithSubpath(): Chainable;
/**
* Allow test if matches feature flag setting
* Otherwise, fail fast.
*
* @param {string} feature - feature name
* @param {string} expectedValue - expected value
*
* @example
* cy.shouldHaveFeatureFlag('feature', 'expected-value');
*/
shouldHaveFeatureFlag(feature: string, expectedValue: any): Chainable;
/**
* Require email service to be reachable by the server
* thru "/api/v4/email/test" if sysadmin account has
* permission to do so. Otherwise, skip email test.
*
* @example
* cy.shouldHaveEmailEnabled();
*/
shouldHaveEmailEnabled(): Chainable;
}
}

View File

@@ -0,0 +1,347 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import merge from 'deepmerge';
import {Constants} from '../../utils';
import onPremDefaultConfig from './on_prem_default_config.json';
import cloudDefaultConfig from './cloud_default_config.json';
// *****************************************************************************
// System
// https://api.mattermost.com/#tag/system
// *****************************************************************************
function hasLicenseForFeature(license, key) {
let hasLicense = false;
for (const [k, v] of Object.entries(license)) {
if (k === key && v === 'true') {
hasLicense = true;
break;
}
}
return hasLicense;
}
Cypress.Commands.add('apiGetClientLicense', () => {
return cy.request('/api/v4/license/client').then((response) => {
expect(response.status).to.equal(200);
const license = response.body;
const isLicensed = license.IsLicensed === 'true';
const isCloudLicensed = hasLicenseForFeature(license, 'Cloud');
return cy.wrap({
license: response.body,
isLicensed,
isCloudLicensed,
});
});
});
Cypress.Commands.add('apiRequireLicenseForFeature', (...keys) => {
Cypress.log({name: 'EE License', message: `Checking if server has license for feature: __${Object.values(keys).join(', ')}__.`});
return uploadLicenseIfNotExist().then((data) => {
const {license, isLicensed} = data;
const hasLicenseMessage = `Server ${isLicensed ? 'has' : 'has no'} EE license.`;
expect(isLicensed, hasLicenseMessage).to.equal(true);
Object.values(keys).forEach((key) => {
const hasLicenseKey = hasLicenseForFeature(license, key);
const hasLicenseKeyMessage = `Server ${hasLicenseKey ? 'has' : 'has no'} EE license for feature: __${key}__`;
expect(hasLicenseKey, hasLicenseKeyMessage).to.equal(true);
});
return cy.wrap(data);
});
});
Cypress.Commands.add('apiRequireLicense', () => {
Cypress.log({name: 'EE License', message: 'Checking if server has license.'});
return uploadLicenseIfNotExist().then((data) => {
const hasLicenseMessage = `Server ${data.isLicensed ? 'has' : 'has no'} EE license.`;
expect(data.isLicensed, hasLicenseMessage).to.equal(true);
return cy.wrap(data);
});
});
Cypress.Commands.add('apiUploadLicense', (filePath) => {
cy.apiUploadFile('license', filePath, {url: '/api/v4/license', method: 'POST', successStatus: 200});
});
Cypress.Commands.add('apiInstallTrialLicense', () => {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: '/api/v4/trial-license',
method: 'POST',
body: {
receive_emails_accepted: true,
terms_accepted: true,
users: Cypress.expose('numberOfTrialUsers'),
// Enriched fields required for trial license as of v10.7
company_country: 'US',
company_name: 'mattermost',
contact_email: 'test@mattermost.com',
company_size: '1-10',
contact_name: 'John Doe',
},
}).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap(response.body);
});
});
Cypress.Commands.add('apiDeleteLicense', () => {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: '/api/v4/license',
method: 'DELETE',
}).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap({response});
});
});
export const getDefaultConfig = () => {
const cypressEnv = Cypress.expose();
const fromCypressEnv = {
ElasticsearchSettings: {
ConnectionURL: cypressEnv.elasticsearchConnectionURL,
},
EmailSettings: {
PushNotificationServer: cypressEnv.pushNotificationServer,
},
LdapSettings: {
LdapServer: cypressEnv.ldapServer,
LdapPort: cypressEnv.ldapPort,
},
ServiceSettings: {
AllowedUntrustedInternalConnections: cypressEnv.allowedUntrustedInternalConnections,
SiteURL: Cypress.config('baseUrl'),
},
};
const isCloud = cypressEnv.serverEdition === Constants.ServerEdition.CLOUD;
if (isCloud) {
fromCypressEnv.CloudSettings = {
CWSURL: cypressEnv.cwsURL,
CWSAPIURL: cypressEnv.cwsAPIURL,
};
}
const defaultConfig = isCloud ? cloudDefaultConfig : onPremDefaultConfig;
return merge(defaultConfig, fromCypressEnv);
};
const expectConfigToBeUpdatable = (currentConfig, newConfig) => {
function errorMessage(name) {
return `${name} is restricted or not available to update. You may check user/sysadmin access, license requirement, server version or edition (on-prem/cloud) compatibility.`;
}
Object.entries(newConfig).forEach(([newMainKey, newSubSetting]) => {
const setting = currentConfig[newMainKey];
if (setting) {
Object.keys(newSubSetting).forEach((newSubKey) => {
const isAvailable = Object.hasOwn(setting, newSubKey);
const name = `${newMainKey}.${newSubKey}`;
expect(isAvailable, isAvailable ? `${name} setting can be updated.` : errorMessage(name)).to.equal(true);
});
} else {
const withSetting = Boolean(setting);
expect(withSetting, withSetting ? `${newMainKey} setting can be updated.` : errorMessage(newMainKey)).to.equal(true);
}
});
};
Cypress.Commands.add('apiUpdateConfig', (newConfig = {}) => {
// # Get current config
return cy.apiGetConfig().then(({config: currentConfig}) => {
// * Check if config can be updated
expectConfigToBeUpdatable(currentConfig, newConfig);
const config = merge.all([currentConfig, getDefaultConfig(), newConfig]);
// # Set the modified config
return cy.getCookie('MMCSRF').then((csrfCookie) => {
let headers;
if (csrfCookie?.value) {
headers = {'X-CSRF-Token': csrfCookie.value};
}
return cy.request({
url: '/api/v4/config',
method: 'PUT',
body: config,
headers,
}).then((updateResponse) => {
expect(updateResponse.status).to.equal(200);
return cy.apiGetConfig();
});
});
});
});
Cypress.Commands.add('apiReloadConfig', () => {
// # Reload the config
return cy.request({
url: '/api/v4/config/reload',
headers: {'X-Requested-With': 'XMLHttpRequest'},
method: 'POST',
}).then((reloadResponse) => {
expect(reloadResponse.status).to.equal(200);
return cy.apiGetConfig();
});
});
Cypress.Commands.add('apiGetConfig', (old = false) => {
// # Get current settings
return cy.request(`/api/v4/config${old ? '/client?format=old' : ''}`).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap({config: response.body});
});
});
Cypress.Commands.add('apiEnsureFeatureFlag', (key, value) => {
cy.apiGetConfig().then(({config}) => {
cy.log(JSON.stringify(config.PluginSettings.Plugins.playbooks));
const currentValue = config.PluginSettings.Plugins.playbooks[key];
if (currentValue !== value) {
cy.apiUpdateConfig({
PluginSettings: {Plugins: {playbooks: {[key]: value}}},
}).then(() => {
return cy.wrap({prevValue: currentValue, value});
});
}
return cy.wrap({prevValue: currentValue, value});
});
});
Cypress.Commands.add('apiGetAnalytics', () => {
cy.apiAdminLogin();
return cy.request('/api/v4/analytics/old').then((response) => {
expect(response.status).to.equal(200);
return cy.wrap({analytics: response.body});
});
});
Cypress.Commands.add('apiInvalidateCache', () => {
return cy.request({
url: '/api/v4/caches/invalidate',
headers: {'X-Requested-With': 'XMLHttpRequest'},
method: 'POST',
}).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap(response);
});
});
function isCloudEdition() {
return cy.apiGetClientLicense().then(({isCloudLicensed}) => {
return cy.wrap(isCloudLicensed);
});
}
Cypress.Commands.add('shouldNotRunOnCloudEdition', () => {
isCloudEdition().then((isCloud) => {
expect(isCloud, isCloud ? 'Should not run on Cloud server' : '').to.equal(false);
});
});
function isTeamEdition() {
return cy.apiGetClientLicense().then(({isLicensed}) => {
return cy.wrap(!isLicensed);
});
}
Cypress.Commands.add('shouldRunOnTeamEdition', () => {
isTeamEdition().then((isTeam) => {
expect(isTeam, isTeam ? '' : 'Should run on Team edition only').to.equal(true);
});
});
function isElasticsearchEnabled() {
return cy.apiGetConfig().then(({config}) => {
let isEnabled = false;
if (config.ElasticsearchSettings) {
const {EnableAutocomplete, EnableIndexing, EnableSearching} = config.ElasticsearchSettings;
isEnabled = EnableAutocomplete && EnableIndexing && EnableSearching;
}
return cy.wrap(isEnabled);
});
}
Cypress.Commands.add('shouldHaveElasticsearchDisabled', () => {
isElasticsearchEnabled().then((data) => {
expect(data, data ? 'Should have Elasticsearch disabled' : '').to.equal(false);
});
});
Cypress.Commands.add('shouldHavePluginUploadEnabled', () => {
return cy.apiGetConfig().then(({config}) => {
const isUploadEnabled = config.PluginSettings.EnableUploads;
expect(isUploadEnabled, isUploadEnabled ? '' : 'Should have Plugin upload enabled').to.equal(true);
});
});
Cypress.Commands.add('shouldHaveClusterEnabled', () => {
return cy.apiGetConfig().then(({config}) => {
const {Enable, ClusterName} = config.ClusterSettings;
expect(Enable, Enable ? '' : 'Should have cluster enabled').to.equal(true);
const sameClusterName = ClusterName === Cypress.expose('serverClusterName');
expect(sameClusterName, sameClusterName ? '' : `Should have cluster name set and as expected. Got "${ClusterName}" but expected "${Cypress.expose('serverClusterName')}"`).to.equal(true);
});
});
Cypress.Commands.add('shouldRunWithSubpath', () => {
return cy.apiGetConfig().then(({config}) => {
const isSubpath = Boolean(config.ServiceSettings.SiteURL.replace(/^https?:\/\//, '').split('/')[1]);
expect(isSubpath, isSubpath ? '' : 'Should run on server running with subpath only').to.equal(true);
});
});
Cypress.Commands.add('shouldHaveFeatureFlag', (key, expectedValue) => {
return cy.apiGetConfig().then(({config}) => {
const actualValue = config.FeatureFlags[key];
const message = actualValue === expectedValue ? `Matches feature flag - "${key}: ${expectedValue}"` : `Expected feature flag "${key}" to be "${expectedValue}", but was "${actualValue}"`;
expect(actualValue, message).to.equal(expectedValue);
});
});
Cypress.Commands.add('shouldHaveEmailEnabled', () => {
return cy.apiGetConfig().then(({config}) => {
if (!config.ExperimentalSettings.RestrictSystemAdmin) {
cy.apiEmailTest();
}
});
});
/**
* Upload a license if it does not exist.
*/
function uploadLicenseIfNotExist() {
return cy.apiGetClientLicense().then((data) => {
if (data.isLicensed) {
return cy.wrap(data);
}
return cy.apiInstallTrialLicense().then(() => {
return cy.apiGetClientLicense();
});
});
}

View File

@@ -0,0 +1,185 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/// <reference types="cypress" />
// ***************************************************************
// Each command should be properly documented using JSDoc.
// See https://jsdoc.app/index.html for reference.
// Basic requirements for documentation are the following:
// - Meaningful description
// - Specific link to https://api.mattermost.com
// - Each parameter with `@params`
// - Return value with `@returns`
// - Example usage with `@example`
// Custom command should follow naming convention of having `api` prefix, e.g. `apiLogin`.
// ***************************************************************
declare namespace Cypress {
interface Chainable {
/**
* Create a team.
* See https://api.mattermost.com/#tag/teams/paths/~1teams/post
* @param {String} name - Unique handler for a team, will be present in the team URL
* @param {String} displayName - Non-unique UI name for the team
* @param {String} type - 'O' for open (default), 'I' for invite only
* @param {Boolean} unique - if true (default), it will create with unique/random team name.
* @param {Partial<Team>} options - other fields of team to include
* @returns {Team} `out.team` as `Team`
*
* @example
* cy.apiCreateTeam('test-team', 'Test Team').then(({team}) => {
* // do something with team
* });
*/
apiCreateTeam(name: string, displayName: string, type?: string, unique?: boolean, options?: Partial<Team>): Chainable<{team: Team}>;
/**
* Delete a team.
* Soft deletes a team, by marking the team as deleted in the database.
* Optionally use the permanent query parameter to hard delete the team.
* See https://api.mattermost.com/#tag/teams/paths/~1teams~1{team_id}/delete
* @param {String} teamId - The team ID to be deleted
* @param {Boolean} permanent - false (default) as soft delete and true as permanent delete
* @returns {Object} `out.data` as response status
*
* @example
* cy.apiDeleteTeam('test-id');
*/
apiDeleteTeam(teamId: string, permanent?: boolean): Chainable<Record<string, any>>;
/**
* Delete the team member object for a user, effectively removing them from a team.
* See https://api.mattermost.com/#tag/teams/paths/~1teams~1{team_id}~1members~1{user_id}/delete
* @param {String} teamId - The team ID which the user is to be removed from
* @param {String} userId - The user ID to be removed from team
* @returns {Object} `out.data` as response status
*
* @example
* cy.apiDeleteUserFromTeam('team-id', 'user-id');
*/
apiDeleteUserFromTeam(teamId: string, userId: string): Chainable<Record<string, any>>;
/**
* Patch a team.
* Partially update a team by providing only the fields you want to update.
* Omitted fields will not be updated.
* The fields that can be updated are defined in the request body, all other provided fields will be ignored.
* See https://api.mattermost.com/#tag/teams/paths/~1teams/post
* @param {String} teamId - The team ID to be patched
* @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
* @returns {Team} `out.team` as `Team`
*
* @example
* cy.apiPatchTeam('test-team', {display_name: 'New Team', group_constrained: true}).then(({team}) => {
* // do something with team
* });
*/
apiPatchTeam(teamId: string, patch: Partial<Team>): Chainable<Team>;
/**
* Get a team based on provided name string.
* See https://api.mattermost.com/#tag/teams/paths/~1teams~1name~1{name}/get
* @param {String} name - Name of a team
* @returns {Team} `out.team` as `Team`
*
* @example
* cy.apiGetTeamByName('team-name').then(({team}) => {
* // do something with team
* });
*/
apiGetTeamByName(name: string): Chainable<Team>;
/**
* Get teams.
* For regular users only returns open teams.
* Users with the "manage_system" permission will return teams regardless of type.
* See https://api.mattermost.com/#tag/teams/paths/~1teams/get
* @param {String} queryParams.page - Page to select, 0 (default)
* @param {String} queryParams.perPage - The number of teams per page, 60 (default)
* @returns {Team[]} `out.teams` as `Team[]`
* @returns {number} `out.totalCount` as `number`
*
* @example
* cy.apiGetAllTeams().then(({teams}) => {
* // do something with teams
* });
*/
apiGetAllTeams(queryParams?: Record<string, any>): Chainable<{teams: Team[]}>;
/**
* Get a list of teams that a user is on.
* See https://api.mattermost.com/#tag/teams/paths/~1users~1{user_id}~1teams/get
* @param {String} userId - User ID to get teams, or 'me' (default)
* @returns {Team[]} `out.teams` as `Team[]`
*
* @example
* cy.apiGetTeamsForUser().then(({teams}) => {
* // do something with teams
* });
*/
apiGetTeamsForUser(userId: string): Chainable<Team[]>;
/**
* Add user to the team by user_id.
* See https://api.mattermost.com/#tag/teams/paths/~1teams~1{team_id}~1members/post
* @param {String} teamId - Team ID
* @param {String} userId - User ID to be added into a team
* @returns {TeamMembership} `out.member` as `TeamMembership`
*
* @example
* cy.apiAddUserToTeam('team-id', 'user-id').then(({member}) => {
* // do something with member
* });
*/
apiAddUserToTeam(teamId: string, userId: string): Chainable<TeamMembership>;
/**
* Get team members.
* See https://api.mattermost.com/#tag/teams/paths/~1teams~1{team_id}~1members/get
* @param {string} teamId - team ID
* @returns {TeamMembership[]} `out.members` as `TeamMembership[]`
*
* @example
* cy.apiGetTeamMembers(teamId).then(({members}) => {
* // do something with members
* });
*/
apiGetTeamMembers(teamId: string): Chainable<TeamMembership[]>;
/**
* Add a number of users to the team.
* See https://api.mattermost.com/#tag/teams/paths/~1teams~1{team_id}~1members~1batch/post
* @param {string} teamId - team ID
* @param {TeamMembership[]} teamMembers - users to add
* @returns {TeamMembership[]} `out.members` as `TeamMembership[]`
*
* @example
* cy.apiAddUsersToTeam(teamId, [{team_id: 'team-id', user_id: 'user-id'}]).then(({members}) => {
* // do something with members
* });
*/
apiAddUsersToTeam(teamId: string, teamMembers: TeamMembership[]): Chainable<TeamMembership[]>;
/**
* Update the scheme-derived roles of a team member.
* Requires sysadmin session to initiate this command.
* See https://api.mattermost.com/#tag/teams/paths/~1teams~1{team_id}~1members~1{user_id}~1schemeRoles/put
* @param {string} teamId - team ID
* @param {string} userId - user ID
* @param {Object} schemeRoles.scheme_admin - false (default) or true to change into team admin
* @param {Object} schemeRoles.scheme_user - true (default) or false to change not to be a team user
* @returns {Object} `out.data` as response status
*
* @example
* cy.apiUpdateTeamMemberSchemeRole(teamId, userId, {scheme_admin: false, scheme_user: true});
*/
apiUpdateTeamMemberSchemeRole(teamId: string, userId: string, schemeRoles: Record<string, any>): Chainable<Record<string, any>>;
}
}

View File

@@ -0,0 +1,163 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {getRandomId} from '../../utils';
// *****************************************************************************
// Teams
// https://api.mattermost.com/#tag/teams
// *****************************************************************************
export function createTeamPatch(name = 'team', displayName = 'Team', type = 'O', unique = true) {
const randomSuffix = getRandomId();
return {
name: unique ? `${name}-${randomSuffix}` : name,
display_name: unique ? `${displayName} ${randomSuffix}` : displayName,
type,
};
}
Cypress.Commands.add('apiCreateTeam', (name, displayName, type, unique, options) => {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: '/api/v4/teams',
method: 'POST',
body: {
...createTeamPatch(name, displayName, type, unique),
...options,
},
}).then((response) => {
expect(response.status).to.equal(201);
return cy.wrap({team: response.body});
});
});
Cypress.Commands.add('apiDeleteTeam', (teamId, permanent = false) => {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: '/api/v4/teams/' + teamId + (permanent ? '?permanent=true' : ''),
method: 'DELETE',
}).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap({data: response.body});
});
});
Cypress.Commands.add('apiDeleteUserFromTeam', (teamId, userId) => {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: '/api/v4/teams/' + teamId + '/members/' + userId,
method: 'DELETE',
}).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap({data: response.body});
});
});
Cypress.Commands.add('apiPatchTeam', (teamId, teamData) => {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: `/api/v4/teams/${teamId}/patch`,
method: 'PUT',
body: teamData,
}).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap({team: response.body});
});
});
Cypress.Commands.add('apiGetTeamByName', (name) => {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: '/api/v4/teams/name/' + name,
method: 'GET',
}).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap({team: response.body});
});
});
Cypress.Commands.add('apiGetAllTeams', ({page = 0, perPage = 60} = {}) => {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: `api/v4/teams?page=${page}&per_page=${perPage}`,
method: 'GET',
}).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap({teams: response.body});
});
});
Cypress.Commands.add('apiGetTeamsForUser', (userId = 'me') => {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: `api/v4/users/${userId}/teams`,
method: 'GET',
}).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap({teams: response.body});
});
});
Cypress.Commands.add('apiAddUserToTeam', (teamId, userId) => {
return cy.request({
method: 'POST',
url: `/api/v4/teams/${teamId}/members`,
headers: {'X-Requested-With': 'XMLHttpRequest'},
body: {team_id: teamId, user_id: userId},
qs: {team_id: teamId},
}).then((response) => {
expect(response.status).to.equal(201);
return cy.wrap({member: response.body});
});
});
Cypress.Commands.add('apiAddUsersToTeam', (teamId, teamMembers) => {
return cy.request({
method: 'POST',
url: `/api/v4/teams/${teamId}/members/batch`,
headers: {'X-Requested-With': 'XMLHttpRequest'},
body: teamMembers,
}).then((response) => {
expect(response.status).to.equal(201);
return cy.wrap({members: response.body});
});
});
Cypress.Commands.add('apiGetTeamMembers', (teamId) => {
return cy.request({
method: 'GET',
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: `/api/v4/teams/${teamId}/members`,
}).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap({members: response.body});
});
});
Cypress.Commands.add('apiUpdateTeamMemberSchemeRole', (teamId, userId, schemeRoles = {}) => {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: `/api/v4/teams/${teamId}/members/${userId}/schemeRoles`,
method: 'PUT',
body: schemeRoles,
}).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap({data: response.body});
});
});
Cypress.Commands.add('apiSetTeamScheme', (teamId, schemeId) => {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: `/api/v4/teams/${teamId}/scheme`,
method: 'PUT',
body: {
scheme_id: schemeId,
},
}).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap({data: response.body});
});
});

View File

@@ -0,0 +1,891 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {UserAccessToken, UserProfile} from '@mattermost/types/users';
import authenticator from 'authenticator';
import {getRandomId, newTestPassword} from '../../utils';
import {getAdminAccount} from '../env';
import {buildQueryString} from './helpers';
import type {ChainableT} from '@/types';
// *****************************************************************************
// Users
// https://api.mattermost.com/#tag/users
// *****************************************************************************
/**
* Login to server via API.
* See https://api.mattermost.com/#tag/users/paths/~1users~1login/post
* @param {string} user.username - username of a user
* @param {string} user.password - password of user
* @returns {UserProfile} out.user: `UserProfile` object
*
* @example
* cy.apiLogin({username: 'sysadmin', password: 'secret'});
*/
function apiLogin(user: Partial<Pick<UserProfile, 'username' | 'email' | 'password'>>, requestOptions: Record<string, unknown> = {}): ChainableT<{user: UserProfile} | {error: unknown}> {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: '/api/v4/users/login',
method: 'POST',
body: {login_id: user.username || user.email, password: user.password},
...requestOptions,
}).then((response) => {
if (requestOptions.failOnStatusCode) {
expect(response.status).to.equal(200);
}
if (response.status === 200) {
return cy.wrap({
user: {
...response.body,
password: user.password,
},
} as {user: UserProfile});
}
return cy.wrap({error: response.body} as {error: unknown});
}) as unknown as ChainableT<{user: UserProfile} | {error: unknown}>;
}
Cypress.Commands.add('apiLogin', apiLogin);
/**
* Login to server via API.
* See https://api.mattermost.com/#tag/users/paths/~1users~1login/post
* @param {string} user.username - username of a user
* @param {string} user.password - password of user
* @param {string} token - MFA token for the session
* @returns {UserProfile} out.user: `UserProfile` object
*
* @example
* cy.apiLoginWithMFA({username: 'sysadmin', password: 'secret', token: '123456'});
*/
function apiLoginWithMFA(user: {username: string; password: string}, token: string): ChainableT<{user: UserProfile}> {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: '/api/v4/users/login',
method: 'POST',
body: {login_id: user.username, password: user.password, token},
}).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap<{user: UserProfile}>({
user: {
...response.body,
password: user.password,
},
});
});
}
Cypress.Commands.add('apiLoginWithMFA', apiLoginWithMFA);
/**
* Login as admin via API.
* See https://api.mattermost.com/#tag/users/paths/~1users~1login/post
* @param {Object} requestOptions - cypress' request options object, see https://docs.cypress.io/api/commands/request#Arguments
* @returns {UserProfile} out.user: `UserProfile` object
*
* @example
* cy.apiAdminLogin();
*/
function apiAdminLogin(requestOptions?: Record<string, unknown>): ChainableT<{user: UserProfile}> {
const admin = getAdminAccount();
// First, login with username
return cy.apiLogin(admin, requestOptions).then((resp) => {
if ((<{error: unknown}>resp).error) {
if (((<{error: unknown}>resp).error as {id: string}).id === 'mfa.validate_token.authenticate.app_error') {
// On fail, try to login via MFA
return cy.dbGetUser({username: admin.username}).then(({user: {mfasecret}}) => {
const token = authenticator.generateToken(mfasecret);
return cy.apiLoginWithMFA(admin, token);
});
}
// Or, try to login via email
// Cast needed: intentionally clearing username to force email-based login
(admin as unknown as {username: string | undefined}).username = undefined;
return cy.apiLogin(admin, requestOptions) as ChainableT<{user: UserProfile}>;
}
return cy.wrap(resp as {user: UserProfile});
});
}
Cypress.Commands.add('apiAdminLogin', apiAdminLogin);
/**
* Login as admin via API.
* See https://api.mattermost.com/#tag/users/paths/~1users~1login/post
* @param {string} token - MFA token for the session
* @returns {UserProfile} out.user: `UserProfile` object
*
* @example
* cy.apiAdminLoginWithMFA(token);
*/
function apiAdminLoginWithMFA(token: string): ChainableT<{user: UserProfile}> {
const admin = getAdminAccount();
return cy.apiLoginWithMFA(admin, token);
}
Cypress.Commands.add('apiAdminLoginWithMFA', apiAdminLoginWithMFA);
/**
* Logout a user's active session from server via API.
* See https://api.mattermost.com/#tag/users/paths/~1users~1logout/post
* Clears all cookies especially `MMAUTHTOKEN`, `MMUSERID` and `MMCSRF`.
*
* @example
* cy.apiLogout();
*/
function apiLogout() {
cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: '/api/v4/users/logout',
method: 'POST',
log: false,
});
// * Verify logged out
cy.visit('/login?extra=expired').url().should('include', '/login');
// # Ensure we clear out these specific cookies
['MMAUTHTOKEN', 'MMUSERID', 'MMCSRF'].forEach((cookie) => {
cy.clearCookie(cookie);
});
// # Clear remainder of cookies
cy.clearCookies();
}
Cypress.Commands.add('apiLogout', apiLogout);
/**
* Get current user.
* See https://api.mattermost.com/#tag/users/paths/~1users~1{user_id}/get
* @returns {user: UserProfile} out.user: `UserProfile` object
*
* @example
* cy.apiGetMe().then(({user}) => {
* // do something with user
* });
*/
function apiGetMe(): ChainableT<{user: UserProfile}> {
return cy.apiGetUserById('me');
}
Cypress.Commands.add('apiGetMe', apiGetMe);
/**
* Get a user by ID.
* See https://api.mattermost.com/#tag/users/paths/~1users~1{user_id}/get
* @param {String} userId - ID of a user to get profile
* @returns {UserProfile} out.user: `UserProfile` object
*
* @example
* cy.apiGetUserById('user-id').then(({user}) => {
* // do something with user
* });
*/
function apiGetUserById(userId: string): ChainableT<{user: UserProfile}> {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: '/api/v4/users/' + userId,
}).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap({user: response.body});
});
}
Cypress.Commands.add('apiGetUserById', apiGetUserById);
/**
* Get a user by email.
* See https://api.mattermost.com/#tag/users/paths/~1users~1email~1{email}/get
* @param {String} email - email address of a user to get profile
* @returns {UserProfile} out.user: `UserProfile` object
*
* @example
* cy.apiGetUserByEmail('email').then(({user}) => {
* // do something with user
* });
*/
function apiGetUserByEmail(email: string, failOnStatusCode = true): ChainableT<{user: UserProfile}> {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: '/api/v4/users/email/' + email,
failOnStatusCode,
}).then((response) => {
const {body, status} = response;
if (failOnStatusCode) {
expect(status).to.equal(200);
return cy.wrap({user: body});
}
return cy.wrap({user: status === 200 ? body : null});
});
}
Cypress.Commands.add('apiGetUserByEmail', apiGetUserByEmail);
/**
* Get users by usernames.
* See https://api.mattermost.com/#tag/users/paths/~1users~1usernames/post
* @param {String[]} usernames - list of usernames to get profiles
* @returns {UserProfile[]} out.users: list of `UserProfile` objects
*
* @example
* cy.apiGetUsersByUsernames().then(({users}) => {
* // do something with users
* });
*/
function apiGetUsersByUsernames(usernames: string[] = []): ChainableT<{users: UserProfile[]}> {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: '/api/v4/users/usernames',
method: 'POST',
body: usernames,
}).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap({users: response.body});
});
}
Cypress.Commands.add('apiGetUsersByUsernames', apiGetUsersByUsernames);
/**
* Patch a user.
* See https://api.mattermost.com/#tag/users/paths/~1users~1{user_id}~1patch/put
* @param {String} userId - ID of user to patch
* @param {UserProfile} userData - user profile to be updated
* @returns {UserProfile} out.user: `UserProfile` object
*
* @example
* cy.apiPatchUser('user-id', {locale: 'en'}).then(({user}) => {
* // do something with user
* });
*/
function apiPatchUser(userId: string, userData: Partial<UserProfile>): ChainableT<{user: UserProfile}> {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
method: 'PUT',
url: `/api/v4/users/${userId}/patch`,
body: userData,
}).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap({user: response.body});
});
}
Cypress.Commands.add('apiPatchUser', apiPatchUser);
/**
* Convenient command to patch a current user.
* See https://api.mattermost.com/#tag/users/paths/~1users~1{user_id}~1patch/put
* @param {UserProfile} userData - user profile to be updated
* @returns {UserProfile} out.user: `UserProfile` object
*
* @example
* cy.apiPatchMe({locale: 'en'}).then(({user}) => {
* // do something with user
* });
*/
function apiPatchMe(data: Partial<UserProfile>): ChainableT<{user: UserProfile}> {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: '/api/v4/users/me/patch',
method: 'PUT',
body: data,
}).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap({user: response.body});
});
}
Cypress.Commands.add('apiPatchMe', apiPatchMe);
/**
* Create a randomly named admin account
*
* @param {boolean} options.loginAfter - false (default) or true if wants to login as the new admin.
* @param {boolean} options.hideAdminTrialModal - true (default) or false if wants to hide Start Enterprise Trial modal.
*
* @returns {UserProfile} `out.sysadmin` as `UserProfile` object
*/
function apiCreateCustomAdmin({loginAfter = false, hideAdminTrialModal = true} = {}): ChainableT<{sysadmin: UserProfile}> {
const sysadminUser = generateRandomUser('other-admin');
return cy.apiCreateUser({user: sysadminUser}).then(({user}) => {
return cy.apiPatchUserRoles(user.id, ['system_admin', 'system_user']).then(() => {
const data = {sysadmin: user};
cy.apiSaveStartTrialModal(user.id, hideAdminTrialModal.toString());
if (loginAfter) {
return cy.apiLogin(user).then(() => {
return cy.wrap(data);
});
}
return cy.wrap(data);
});
});
}
Cypress.Commands.add('apiCreateCustomAdmin', apiCreateCustomAdmin);
/**
* Create an admin account based from the env variables defined in Cypress env.
* @returns {UserProfile} `out.sysadmin` as `UserProfile` object
*
* @example
* cy.apiCreateAdmin();
*/
function apiCreateAdmin() {
const {username, password} = getAdminAccount();
const sysadminUser = {
username,
password,
first_name: 'Kenneth',
last_name: 'Moreno',
email: 'sysadmin@sample.mattermost.com',
};
const options = {
headers: {'X-Requested-With': 'XMLHttpRequest'},
method: 'POST',
url: '/api/v4/users',
body: sysadminUser,
};
// # Create a new user
return cy.request(options).then((res) => {
expect(res.status).to.equal(201);
return cy.wrap({sysadmin: {...res.body, password}});
});
}
Cypress.Commands.add('apiCreateAdmin', apiCreateAdmin);
function generateRandomUser(prefix = 'user', createAt = 0): Partial<UserProfile> {
const randomId = getRandomId();
return {
email: `${prefix}${randomId}@sample.mattermost.com`,
username: `${prefix}${randomId}`,
password: newTestPassword(),
first_name: `First${randomId}`,
last_name: `Last${randomId}`,
nickname: `Nickname${randomId}`,
create_at: createAt,
};
}
/**
* Create a new user with an options to set name prefix and be able to bypass tutorial steps.
* @param {string} options.user - predefined `user` object instead on random user
* @param {string} options.prefix - 'user' (default) or any prefix to easily identify a user
* @param {boolean} options.bypassTutorial - true (default) or false for user to go thru tutorial steps
* @param {boolean} options.hideOnboarding - true (default) to hide or false to show Onboarding steps
* @returns {UserProfile} `out.user` as `UserProfile` object
*
* @example
* cy.apiCreateUser(options);
*/
interface CreateUserOptions {
user: Partial<UserProfile>;
prefix?: string;
createAt?: number;
bypassTutorial?: boolean;
hideOnboarding: boolean;
bypassWhatsNewModal: boolean;
}
function apiCreateUser({
prefix = 'user',
createAt = 0,
bypassTutorial = true,
hideOnboarding = true,
bypassWhatsNewModal = true,
user = null as unknown as Partial<UserProfile>,
}: Partial<CreateUserOptions> = {}): ChainableT<{user: UserProfile}> {
const newUser = user || generateRandomUser(prefix, createAt);
const createUserOption = {
headers: {'X-Requested-With': 'XMLHttpRequest'},
method: 'POST',
url: '/api/v4/users',
body: newUser,
};
return cy.request(createUserOption).then((userRes) => {
expect(userRes.status).to.equal(201);
const createdUser = userRes.body;
// hide the onboarding task list by default so it doesn't block the execution of subsequent tests
cy.apiSaveSkipStepsPreference(createdUser.id, 'true');
cy.apiSaveOnboardingTaskListPreference(createdUser.id, 'onboarding_task_list_open', 'false');
cy.apiSaveOnboardingTaskListPreference(createdUser.id, 'onboarding_task_list_show', 'false');
if (bypassTutorial) {
cy.apiDisableTutorials(createdUser.id);
}
if (hideOnboarding) {
cy.apiSaveOnboardingPreference(createdUser.id, 'hide', 'true');
cy.apiSaveOnboardingPreference(createdUser.id, 'skip', 'true');
}
if (bypassWhatsNewModal) {
cy.apiHideSidebarWhatsNewModalPreference(createdUser.id, 'false');
}
return cy.wrap({user: {...createdUser, password: newUser.password}});
});
}
Cypress.Commands.add('apiCreateUser', apiCreateUser);
/**
* Create a new guest user with an options to set name prefix and be able to bypass tutorial steps.
* @param {string} options.prefix - 'guest' (default) or any prefix to easily identify a guest
* @param {boolean} options.bypassTutorial - true (default) or false for guest to go thru tutorial steps
* @param {boolean} options.showOnboarding - false (default) to hide or true to show Onboarding steps
* @returns {UserProfile} `out.guest` as `UserProfile` object
*
* @example
* cy.apiCreateGuestUser(options);
*/
function apiCreateGuestUser({
prefix = 'guest',
bypassTutorial = true,
}: Partial<CreateUserOptions>): ChainableT<{guest: UserProfile}> {
return cy.apiCreateUser({prefix, bypassTutorial}).then(({user}) => {
cy.apiDemoteUserToGuest(user.id);
return cy.wrap({guest: user});
});
}
Cypress.Commands.add('apiCreateGuestUser', apiCreateGuestUser);
/**
* Revoke all active sessions for a user
* @param {String} userId - ID of user to revoke sessions
*/
/**
* Revoke all active sessions for a user.
* See https://api.mattermost.com/#tag/users/paths/~1users~1{user_id}~1sessions~1revoke~1all/post
* @param {String} userId - ID of a user
* @returns {Object} `out.data` as response status
*
* @example
* cy.apiRevokeUserSessions('user-id');
*/
function apiRevokeUserSessions(userId: string): ChainableT<{data: unknown}> {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: `/api/v4/users/${userId}/sessions/revoke/all`,
method: 'POST',
}).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap({data: response.body});
}) as unknown as ChainableT<{data: unknown}>;
}
Cypress.Commands.add('apiRevokeUserSessions', apiRevokeUserSessions);
/**
* Get list of users based on query parameters
* See https://api.mattermost.com/#tag/users/paths/~1users/get
* @param {String} queryParams - see link on available query parameters
* @returns {UserProfile[]} `out.users` as `UserProfile[]` object
*
* @example
* cy.apiGetUsers().then(({users}) => {
* // do something with users
* });
*/
function apiGetUsers(queryParams: Record<string, string | number | boolean>): ChainableT<{users: UserProfile[]}> {
const queryString = buildQueryString(queryParams);
return cy.request({
method: 'GET',
url: `/api/v4/users?${queryString}`,
headers: {'X-Requested-With': 'XMLHttpRequest'},
}).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap({users: response.body as UserProfile[]});
});
}
Cypress.Commands.add('apiGetUsers', apiGetUsers);
/**
* Get list of users that are not team members.
* See https://api.mattermost.com/#tag/users/paths/~1users/get
* @param {String} queryParams.teamId - Team ID
* @param {String} queryParams.page - Page to select, 0 (default)
* @param {String} queryParams.perPage - The number of users per page, 60 (default)
* @returns {UserProfile[]} `out.users` as `UserProfile[]` object
*
* @example
* cy.apiGetUsersNotInTeam({teamId: 'team-id'}).then(({users}) => {
* // do something with users
* });
*/
function apiGetUsersNotInTeam({teamId, page = 0, perPage = 60}: {teamId: string; page?: number; perPage?: number}): ChainableT<{users: UserProfile[]}> {
return cy.apiGetUsers({not_in_team: teamId, page, per_page: perPage});
}
Cypress.Commands.add('apiGetUsersNotInTeam', apiGetUsersNotInTeam);
/**
* patch user roles
* @param {String} userId - ID of user to patch
* @param {String[]} roleNames - The user roles
* @returns the result of patching the user roles
* @example
* cy.apiPatchUserRoles('user-id', ['system_user']);
*/
function apiPatchUserRoles(userId: string, roleNames: string[] = ['system_user']): ChainableT<{user: unknown}> {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: `/api/v4/users/${userId}/roles`,
method: 'PUT',
body: {roles: roleNames.join(' ')},
}).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap({user: response.body});
});
}
Cypress.Commands.add('apiPatchUserRoles', apiPatchUserRoles);
/**
* Deactivate a user account.
* See https://api.mattermost.com/#tag/users/paths/~1users~1{user_id}/delete
* @param {string} userId - User ID
* @returns {Response} response: Cypress-chainable response which should have successful HTTP status of 200 OK to continue or pass.
*
* @example
* cy.apiDeactivateUser('user-id');
*/
function apiDeactivateUser(userId: string): ChainableT<Cypress.Response<unknown>> {
const options = {
headers: {'X-Requested-With': 'XMLHttpRequest'},
method: 'DELETE',
url: `/api/v4/users/${userId}`,
};
// # Deactivate a user account
return cy.request(options).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap(response);
});
}
Cypress.Commands.add('apiDeactivateUser', apiDeactivateUser);
/**
* Reactivate a user account.
* @param {string} userId - User ID
* @returns {Response} response: Cypress-chainable response which should have successful HTTP status of 200 OK to continue or pass.
*
* @example
* cy.apiActivateUser('user-id');
*/
function apiActivateUser(userId: string): ChainableT<Cypress.Response<unknown>> {
const options = {
headers: {'X-Requested-With': 'XMLHttpRequest'},
method: 'PUT',
url: `/api/v4/users/${userId}/active`,
body: {
active: true,
},
};
// # Activate a user account
return cy.request(options).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap(response);
});
}
Cypress.Commands.add('apiActivateUser', apiActivateUser);
/**
* Convert a regular user into a guest. This will convert the user into a guest for the whole system while retaining their existing team and channel memberships.
* See https://api.mattermost.com/#tag/users/paths/~1users~1{user_id}~1demote/post
* @param {string} userId - User ID
* @returns {UserProfile} out.guest: `UserProfile` object
*
* @example
* cy.apiDemoteUserToGuest('user-id');
*/
function apiDemoteUserToGuest(userId: string): ChainableT<{guest: UserProfile}> {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: `/api/v4/users/${userId}/demote`,
method: 'POST',
}).then((response) => {
expect(response.status).to.equal(200);
return cy.apiGetUserById(userId).then(({user}) => {
return cy.wrap({guest: user});
});
});
}
Cypress.Commands.add('apiDemoteUserToGuest', apiDemoteUserToGuest);
/**
* Convert a guest into a regular user. This will convert the guest into a user for the whole system while retaining any team and channel memberships and automatically joining them to the default channels.
* See https://api.mattermost.com/#tag/users/paths/~1users~1{user_id}~1promote/post
* @param {string} userId - User ID
* @returns {UserProfile} out.user: `UserProfile` object
*
* @example
* cy.apiPromoteGuestToUser('user-id');
*/
function apiPromoteGuestToUser(userId: string): ChainableT<{user: UserProfile}> {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: `/api/v4/users/${userId}/promote`,
method: 'POST',
}).then((response) => {
expect(response.status).to.equal(200);
return cy.apiGetUserById(userId);
});
}
Cypress.Commands.add('apiPromoteGuestToUser', apiPromoteGuestToUser);
/**
* Verifies a user's email via userId without having to go to the user's email inbox.
* See https://api.mattermost.com/#tag/users/paths/~1users~1{user_id}~1email~1verify~1member/post
* @param {string} userId - User ID
* @returns {UserProfile} out.user: `UserProfile` object
*
* @example
* cy.apiVerifyUserEmailById('user-id').then(({user}) => {
* // do something with user
* });
*/
function apiVerifyUserEmailById(userId: string): ChainableT<{user: UserProfile}> {
const options = {
headers: {'X-Requested-With': 'XMLHttpRequest'},
method: 'POST',
url: `/api/v4/users/${userId}/email/verify/member`,
};
return cy.request(options).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap({user: response.body});
});
}
Cypress.Commands.add('apiVerifyUserEmailById', apiVerifyUserEmailById);
/**
* Update a user MFA.
* See https://api.mattermost.com/#tag/users/paths/~1users~1{user_id}~1mfa/put
* @param {String} userId - ID of user to patch
* @param {boolean} activate - Whether MFA is going to be enabled or disabled
* @param {string} token - MFA token/code
* @example
* cy.apiActivateUserMFA('user-id', activate: false);
*/
function apiActivateUserMFA(userId: string, activate: boolean, token: string): ChainableT<Cypress.Response<unknown>> {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: `/api/v4/users/${userId}/mfa`,
method: 'PUT',
body: {
activate,
code: token,
},
}).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap(response);
});
}
Cypress.Commands.add('apiActivateUserMFA', apiActivateUserMFA);
function apiResetPassword(userId: string, currentPass: string, newPass: string) {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
method: 'PUT',
url: `/api/v4/users/${userId}/password`,
body: {
current_password: currentPass,
new_password: newPass,
},
}).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap({user: response.body});
});
}
Cypress.Commands.add('apiResetPassword', apiResetPassword);
function apiGenerateMfaSecret(userId: string) {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
method: 'POST',
url: `/api/v4/users/${userId}/mfa/generate`,
}).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap({code: response.body});
});
}
Cypress.Commands.add('apiGenerateMfaSecret', apiGenerateMfaSecret);
/**
* Create a user access token
* See https://api.mattermost.com/#tag/users/paths/~1users~1{user_id}~1tokens/post
* @param {String} userId - ID of user for whom to generate token
* @param {String} description - The description of the token usage
* @example
* cy.apiAccessToken('user-id', 'token for cypress tests');
*/
function apiAccessToken(userId: string, description: string): ChainableT<UserAccessToken> {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: '/api/v4/users/' + userId + '/tokens',
method: 'POST',
body: {
description,
},
}).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap(response.body as UserAccessToken);
});
}
Cypress.Commands.add('apiAccessToken', apiAccessToken);
/**
* Revoke a user access token
* See https://api.mattermost.com/#tag/users/paths/~1users~1tokens~1revoke/post
* @param {String} tokenId - The id of the token to revoke
* @example
* cy.apiRevokeAccessToken('token-id')
*/
function apiRevokeAccessToken(tokenId: string): ChainableT<Cypress.Response<unknown>> {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
url: '/api/v4/users/tokens/revoke',
method: 'POST',
body: {
token_id: tokenId,
},
}).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap(response);
});
}
Cypress.Commands.add('apiRevokeAccessToken', apiRevokeAccessToken);
/**
* Update a user auth method.
* See https://api.mattermost.com/#tag/users/paths/~1users~1{user_id}~1mfa/put
* @param {String} userId - ID of user to patch
* @param {String} authData
* @param {String} password
* @param {String} authService
* @example
* cy.apiUpdateUserAuth('user-id', 'auth-data', 'password', 'auth-service');
*/
function apiUpdateUserAuth(userId: string, authData: string, password: string, authService: string): ChainableT<Cypress.Response<unknown>> {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
method: 'PUT',
url: `/api/v4/users/${userId}/auth`,
body: {
auth_data: authData,
password,
auth_service: authService,
},
}).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap(response);
});
}
Cypress.Commands.add('apiUpdateUserAuth', apiUpdateUserAuth);
/**
* Get total count of users in the system
* See https://api.mattermost.com/#operation/GetTotalUsersStats
*
* @returns {number} - total count of all users
*
* @example
* cy.apiGetTotalUsers().then(() => {
* // do something with total users
* });
*/
function apiGetTotalUsers(): ChainableT<number> {
return cy.request({
headers: {'X-Requested-With': 'XMLHttpRequest'},
method: 'GET',
url: '/api/v4/users/stats',
}).then((response) => {
expect(response.status).to.equal(200);
return cy.wrap(response.body.total_users_count as number);
});
}
Cypress.Commands.add('apiGetTotalUsers', apiGetTotalUsers);
export {generateRandomUser};
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
interface Chainable {
apiLogin: typeof apiLogin;
apiLoginWithMFA: typeof apiLoginWithMFA;
apiAdminLogin: typeof apiAdminLogin;
apiAdminLoginWithMFA: typeof apiAdminLoginWithMFA;
apiLogout(): ChainableT<void>;
apiGetMe: typeof apiGetMe;
apiGetUserById: typeof apiGetUserById;
apiGetUserByEmail: typeof apiGetUserByEmail;
apiGetUsersByUsernames: typeof apiGetUsersByUsernames;
apiPatchUser: typeof apiPatchUser;
apiPatchMe: typeof apiPatchMe;
apiCreateCustomAdmin: typeof apiCreateCustomAdmin;
apiCreateAdmin: typeof apiCreateAdmin;
apiCreateUser: typeof apiCreateUser;
apiCreateGuestUser: typeof apiCreateGuestUser;
apiRevokeUserSessions: typeof apiRevokeUserSessions;
apiGetUsers: typeof apiGetUsers;
apiGetUsersNotInTeam: typeof apiGetUsersNotInTeam;
apiPatchUserRoles: typeof apiPatchUserRoles;
apiDeactivateUser: typeof apiDeactivateUser;
apiActivateUser: typeof apiActivateUser;
apiDemoteUserToGuest: typeof apiDemoteUserToGuest;
apiPromoteGuestToUser: typeof apiPromoteGuestToUser;
apiVerifyUserEmailById: typeof apiVerifyUserEmailById;
apiActivateUserMFA: typeof apiActivateUserMFA;
apiResetPassword: typeof apiResetPassword;
apiGenerateMfaSecret: typeof apiGenerateMfaSecret;
apiAccessToken: typeof apiAccessToken;
apiRevokeAccessToken: typeof apiRevokeAccessToken;
apiUpdateUserAuth: typeof apiUpdateUserAuth;
apiGetTotalUsers: typeof apiGetTotalUsers;
}
}
}

View File

@@ -0,0 +1,40 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/// <reference types="cypress" />
// ***************************************************************
// Each command should be properly documented using JSDoc.
// See https://jsdoc.app/index.html for reference.
// Basic requirements for documentation are the following:
// - Meaningful description
// - Each parameter with `@params`
// - Return value with `@returns`
// - Example usage with `@example`
// Custom command should follow naming convention of having `api` prefix, e.g. `apiLogin`.
// ***************************************************************
declare namespace Cypress {
interface Chainable {
/**
* Get an incoming webhook given the hook id.
* @param {string} hookId - Incoming Webhook GUID
* @returns {IncomingWebhook} `out.webhook` as `IncomingWebhook`
* @returns {string} `out.status`
* @example
* cy.apiGetIncomingWebhook('hook-id')
*/
apiGetIncomingWebhook(hookId: string): Chainable<Record<string, any>>;
/**
* Get an outgoing webhook given the hook id.
* @param {string} hookId - Outgoing Webhook GUID
* @returns {OutgoingWebhook} `out.webhook` as `OutgoingWebhook`
* @returns {string} `out.status`
* @example
* cy.apiGetOutgoingWebhook('hook-id')
*/
apiGetOutgoingWebhook(hookId: string): Chainable<Record<string, any>>;
}
}

View File

@@ -0,0 +1,35 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// *****************************************************************************
// Webhooks
// https://api.mattermost.com/#tag/webhooks
// *****************************************************************************
Cypress.Commands.add('apiGetIncomingWebhook', (hookId) => {
const options = {
url: `api/v4/hooks/incoming/${hookId}`,
headers: {'X-Requested-With': 'XMLHttpRequest'},
method: 'GET',
failOnStatusCode: false,
};
return cy.request(options).then((response) => {
const {body, status} = response;
return cy.wrap({webhook: body, status});
});
});
Cypress.Commands.add('apiGetOutgoingWebhook', (hookId) => {
const options = {
url: `api/v4/hooks/outgoing/${hookId}`,
headers: {'X-Requested-With': 'XMLHttpRequest'},
method: 'GET',
failOnStatusCode: false,
};
return cy.request(options).then((response) => {
const {body, status} = response;
return cy.wrap({webhook: body, status});
});
});