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
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:
139
e2e-tests/cypress/tests/support/api/bots.ts
Normal file
139
e2e-tests/cypress/tests/support/api/bots.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
32
e2e-tests/cypress/tests/support/api/brand.d.ts
vendored
Normal file
32
e2e-tests/cypress/tests/support/api/brand.d.ts
vendored
Normal 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>>;
|
||||
}
|
||||
}
|
||||
20
e2e-tests/cypress/tests/support/api/brand.js
Normal file
20
e2e-tests/cypress/tests/support/api/brand.js
Normal 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);
|
||||
});
|
||||
});
|
||||
437
e2e-tests/cypress/tests/support/api/channel.ts
Normal file
437
e2e-tests/cypress/tests/support/api/channel.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
41
e2e-tests/cypress/tests/support/api/cloud.d.ts
vendored
Normal file
41
e2e-tests/cypress/tests/support/api/cloud.d.ts
vendored
Normal 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}>;
|
||||
}
|
||||
}
|
||||
24
e2e-tests/cypress/tests/support/api/cloud.js
Normal file
24
e2e-tests/cypress/tests/support/api/cloud.js
Normal 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});
|
||||
});
|
||||
});
|
||||
452
e2e-tests/cypress/tests/support/api/cloud_default_config.json
Normal file
452
e2e-tests/cypress/tests/support/api/cloud_default_config.json
Normal 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
|
||||
}
|
||||
}
|
||||
31
e2e-tests/cypress/tests/support/api/cluster.d.ts
vendored
Normal file
31
e2e-tests/cypress/tests/support/api/cluster.d.ts
vendored
Normal 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[]}>;
|
||||
}
|
||||
}
|
||||
13
e2e-tests/cypress/tests/support/api/cluster.js
Normal file
13
e2e-tests/cypress/tests/support/api/cluster.js
Normal 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});
|
||||
});
|
||||
});
|
||||
42
e2e-tests/cypress/tests/support/api/common.d.ts
vendored
Normal file
42
e2e-tests/cypress/tests/support/api/common.d.ts
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
68
e2e-tests/cypress/tests/support/api/common.js
Normal file
68
e2e-tests/cypress/tests/support/api/common.js
Normal 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
33
e2e-tests/cypress/tests/support/api/data_retention.d.ts
vendored
Normal file
33
e2e-tests/cypress/tests/support/api/data_retention.d.ts
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
157
e2e-tests/cypress/tests/support/api/data_retention.js
Normal file
157
e2e-tests/cypress/tests/support/api/data_retention.js
Normal 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
45
e2e-tests/cypress/tests/support/api/group.ts
Normal file
45
e2e-tests/cypress/tests/support/api/group.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
15
e2e-tests/cypress/tests/support/api/helpers.js
Normal file
15
e2e-tests/cypress/tests/support/api/helpers.js
Normal 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;
|
||||
}
|
||||
24
e2e-tests/cypress/tests/support/api/index.js
Normal file
24
e2e-tests/cypress/tests/support/api/index.js
Normal 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';
|
||||
65
e2e-tests/cypress/tests/support/api/keycloak.d.ts
vendored
Normal file
65
e2e-tests/cypress/tests/support/api/keycloak.d.ts
vendored
Normal 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>;
|
||||
}
|
||||
}
|
||||
89
e2e-tests/cypress/tests/support/api/keycloak.js
Normal file
89
e2e-tests/cypress/tests/support/api/keycloak.js
Normal 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;
|
||||
});
|
||||
});
|
||||
});
|
||||
1966
e2e-tests/cypress/tests/support/api/keycloak_realm.json
Normal file
1966
e2e-tests/cypress/tests/support/api/keycloak_realm.json
Normal file
File diff suppressed because it is too large
Load Diff
48
e2e-tests/cypress/tests/support/api/ldap.d.ts
vendored
Normal file
48
e2e-tests/cypress/tests/support/api/ldap.d.ts
vendored
Normal 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>;
|
||||
}
|
||||
}
|
||||
50
e2e-tests/cypress/tests/support/api/ldap.js
Normal file
50
e2e-tests/cypress/tests/support/api/ldap.js
Normal 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);
|
||||
});
|
||||
});
|
||||
596
e2e-tests/cypress/tests/support/api/on_prem_default_config.json
Normal file
596
e2e-tests/cypress/tests/support/api/on_prem_default_config.json
Normal 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
|
||||
}
|
||||
}
|
||||
152
e2e-tests/cypress/tests/support/api/plugin.d.ts
vendored
Normal file
152
e2e-tests/cypress/tests/support/api/plugin.d.ts
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
216
e2e-tests/cypress/tests/support/api/plugin.js
Normal file
216
e2e-tests/cypress/tests/support/api/plugin.js
Normal 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);
|
||||
});
|
||||
});
|
||||
550
e2e-tests/cypress/tests/support/api/preference.ts
Normal file
550
e2e-tests/cypress/tests/support/api/preference.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
69
e2e-tests/cypress/tests/support/api/role.d.ts
vendored
Normal file
69
e2e-tests/cypress/tests/support/api/role.d.ts
vendored
Normal 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();
|
||||
}
|
||||
}
|
||||
69
e2e-tests/cypress/tests/support/api/role.js
Normal file
69
e2e-tests/cypress/tests/support/api/role.js
Normal 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 !== '');
|
||||
}
|
||||
78
e2e-tests/cypress/tests/support/api/saml.d.ts
vendored
Normal file
78
e2e-tests/cypress/tests/support/api/saml.d.ts
vendored
Normal 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>;
|
||||
}
|
||||
}
|
||||
42
e2e-tests/cypress/tests/support/api/saml.js
Normal file
42
e2e-tests/cypress/tests/support/api/saml.js
Normal 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});
|
||||
});
|
||||
45
e2e-tests/cypress/tests/support/api/scheme.d.ts
vendored
Normal file
45
e2e-tests/cypress/tests/support/api/scheme.d.ts
vendored
Normal 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>;
|
||||
}
|
||||
}
|
||||
41
e2e-tests/cypress/tests/support/api/scheme.js
Normal file
41
e2e-tests/cypress/tests/support/api/scheme.js
Normal 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);
|
||||
});
|
||||
});
|
||||
117
e2e-tests/cypress/tests/support/api/setup.ts
Normal file
117
e2e-tests/cypress/tests/support/api/setup.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
67
e2e-tests/cypress/tests/support/api/status.d.ts
vendored
Normal file
67
e2e-tests/cypress/tests/support/api/status.d.ts
vendored
Normal 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();
|
||||
}
|
||||
}
|
||||
55
e2e-tests/cypress/tests/support/api/status.js
Normal file
55
e2e-tests/cypress/tests/support/api/status.js
Normal 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);
|
||||
});
|
||||
});
|
||||
211
e2e-tests/cypress/tests/support/api/system.d.ts
vendored
Normal file
211
e2e-tests/cypress/tests/support/api/system.d.ts
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
347
e2e-tests/cypress/tests/support/api/system.js
Normal file
347
e2e-tests/cypress/tests/support/api/system.js
Normal 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();
|
||||
});
|
||||
});
|
||||
}
|
||||
185
e2e-tests/cypress/tests/support/api/team.d.ts
vendored
Normal file
185
e2e-tests/cypress/tests/support/api/team.d.ts
vendored
Normal 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>>;
|
||||
}
|
||||
}
|
||||
163
e2e-tests/cypress/tests/support/api/team.js
Normal file
163
e2e-tests/cypress/tests/support/api/team.js
Normal 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});
|
||||
});
|
||||
});
|
||||
891
e2e-tests/cypress/tests/support/api/user.ts
Normal file
891
e2e-tests/cypress/tests/support/api/user.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
40
e2e-tests/cypress/tests/support/api/webhooks.d.ts
vendored
Normal file
40
e2e-tests/cypress/tests/support/api/webhooks.d.ts
vendored
Normal 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>>;
|
||||
}
|
||||
}
|
||||
35
e2e-tests/cypress/tests/support/api/webhooks.js
Normal file
35
e2e-tests/cypress/tests/support/api/webhooks.js
Normal 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});
|
||||
});
|
||||
});
|
||||
585
e2e-tests/cypress/tests/support/api_commands.ts
Normal file
585
e2e-tests/cypress/tests/support/api_commands.ts
Normal file
@@ -0,0 +1,585 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {User} from './env';
|
||||
import {getAdminAccount} from './env';
|
||||
|
||||
import type {ChainableT, ResponseT} from '@/types';
|
||||
|
||||
// *****************************************************************************
|
||||
// Read more:
|
||||
// - https://on.cypress.io/custom-commands on writing Cypress commands
|
||||
// - https://api.mattermost.com/ for Mattermost API reference
|
||||
// *****************************************************************************
|
||||
|
||||
// *****************************************************************************
|
||||
// Commands
|
||||
// https://api.mattermost.com/#tag/commands
|
||||
// *****************************************************************************
|
||||
|
||||
type CypressResponseBody = Cypress.Response<unknown>;
|
||||
function apiCreateCommand(command: Record<string, unknown> = {}): Cypress.Chainable<{data: CypressResponseBody['body']; status: CypressResponseBody['status']}> {
|
||||
const options = {
|
||||
url: '/api/v4/commands',
|
||||
headers: {'X-Requested-With': 'XMLHttpRequest'},
|
||||
method: 'POST',
|
||||
body: command,
|
||||
};
|
||||
|
||||
return cy.request(options).then((response) => {
|
||||
expect(response.status).to.equal(201);
|
||||
return cy.wrap({data: response.body, status: response.status});
|
||||
});
|
||||
}
|
||||
|
||||
Cypress.Commands.add('apiCreateCommand', apiCreateCommand);
|
||||
|
||||
// *****************************************************************************
|
||||
// Email
|
||||
// *****************************************************************************
|
||||
function apiEmailTest(): ResponseT {
|
||||
return cy.request({
|
||||
headers: {'X-Requested-With': 'XMLHttpRequest'},
|
||||
url: '/api/v4/email/test',
|
||||
method: 'POST',
|
||||
}).then((response) => {
|
||||
expect(response.status, 'SMTP not setup at sysadmin config').to.equal(200);
|
||||
return cy.wrap(response);
|
||||
});
|
||||
}
|
||||
Cypress.Commands.add('apiEmailTest', apiEmailTest);
|
||||
|
||||
// *****************************************************************************
|
||||
// Posts
|
||||
// https://api.mattermost.com/#tag/posts
|
||||
// *****************************************************************************
|
||||
|
||||
function apiCreatePost(channelId: string, message: string, rootId: string, props: Record<string, unknown>, token = '', failOnStatusCode = true): ResponseT {
|
||||
const headers: Record<string, string> = {'X-Requested-With': 'XMLHttpRequest'};
|
||||
if (token !== '') {
|
||||
headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return cy.request({
|
||||
headers,
|
||||
failOnStatusCode,
|
||||
url: '/api/v4/posts',
|
||||
method: 'POST',
|
||||
body: {
|
||||
channel_id: channelId,
|
||||
root_id: rootId,
|
||||
message,
|
||||
props,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Cypress.Commands.add('apiCreatePost', apiCreatePost);
|
||||
|
||||
function apiDeletePost(postId: string, user: User = getAdminAccount(), permanent = false): Cypress.Chainable<{status: number}> {
|
||||
return cy.externalRequest({
|
||||
user,
|
||||
method: 'delete',
|
||||
path: `posts/${postId}?permanent=${permanent}`,
|
||||
}).then((response) => {
|
||||
// * Validate that request was successful
|
||||
expect(response.status).to.equal(200);
|
||||
return cy.wrap({status: response.status});
|
||||
});
|
||||
}
|
||||
Cypress.Commands.add('apiDeletePost', apiDeletePost);
|
||||
|
||||
function apiCreateToken(userId: string): Cypress.Chainable<{token: string}> {
|
||||
return cy.request({
|
||||
headers: {'X-Requested-With': 'XMLHttpRequest'},
|
||||
url: `/api/v4/users/${userId}/tokens`,
|
||||
method: 'POST',
|
||||
body: {
|
||||
description: 'some text',
|
||||
},
|
||||
}).then((response) => {
|
||||
// * Validate that request was successful
|
||||
expect(response.status).to.equal(200);
|
||||
return cy.wrap({token: response.body.token});
|
||||
});
|
||||
}
|
||||
Cypress.Commands.add('apiCreateToken', apiCreateToken);
|
||||
|
||||
/**
|
||||
* Unpins pinned posts of given postID directly via API
|
||||
* This API assume that the user is logged in and has cookie to access
|
||||
*/
|
||||
function apiUnpinPosts(postId: string): ResponseT {
|
||||
return cy.request({
|
||||
headers: {'X-Requested-With': 'XMLHttpRequest'},
|
||||
url: '/api/v4/posts/' + postId + '/unpin',
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
Cypress.Commands.add('apiUnpinPosts', apiUnpinPosts);
|
||||
|
||||
// *****************************************************************************
|
||||
// Webhooks
|
||||
// https://api.mattermost.com/#tag/webhooks
|
||||
// *****************************************************************************
|
||||
|
||||
function apiCreateWebhook(hook: Record<string, unknown> = {}, isIncoming = true): ChainableT<{data: Cypress.IncomingWebhook | Cypress.OutgoingWebhook; url: string}> {
|
||||
const hookUrl = isIncoming ? '/api/v4/hooks/incoming' : '/api/v4/hooks/outgoing';
|
||||
const options = {
|
||||
url: hookUrl,
|
||||
headers: {'X-Requested-With': 'XMLHttpRequest'},
|
||||
method: 'POST',
|
||||
body: hook,
|
||||
};
|
||||
|
||||
return cy.request(options).then((response) => {
|
||||
const data = response.body;
|
||||
return cy.wrap(Promise.resolve({...data, url: isIncoming ? `${Cypress.config().baseUrl}/hooks/${data.id}` : ''}));
|
||||
});
|
||||
}
|
||||
|
||||
Cypress.Commands.add('apiCreateWebhook', apiCreateWebhook);
|
||||
|
||||
function apiGetTeam(teamId: string): ChainableT<Cypress.Response<unknown>> {
|
||||
return cy.request({
|
||||
headers: {'X-Requested-With': 'XMLHttpRequest'},
|
||||
url: `api/v4/teams/${teamId}`,
|
||||
method: 'GET',
|
||||
}).then((response) => {
|
||||
expect(response.status).to.equal(200);
|
||||
return cy.wrap(response);
|
||||
});
|
||||
}
|
||||
Cypress.Commands.add('apiGetTeam', apiGetTeam);
|
||||
|
||||
function removeUserFromChannel(channelId: string, userId: string): ReturnType<typeof cy.externalRequest> {
|
||||
const admin = getAdminAccount();
|
||||
|
||||
return cy.externalRequest({user: admin, method: 'delete', path: `channels/${channelId}/members/${userId}`});
|
||||
}
|
||||
Cypress.Commands.add('removeUserFromChannel', removeUserFromChannel);
|
||||
|
||||
function removeUserFromTeam(teamId: string, userId: string): ReturnType<typeof cy.externalRequest> {
|
||||
const admin = getAdminAccount();
|
||||
|
||||
return cy.externalRequest({user: admin, method: 'delete', path: `teams/${teamId}/members/${userId}`});
|
||||
}
|
||||
Cypress.Commands.add('removeUserFromTeam', removeUserFromTeam);
|
||||
|
||||
interface LDAPSyncResponse {
|
||||
status: number;
|
||||
body: Array<{status: string; last_activity_at: number}>;
|
||||
}
|
||||
|
||||
function apiGetLDAPSync(): Cypress.Chainable<LDAPSyncResponse > {
|
||||
return cy.request({
|
||||
headers: {'X-Requested-With': 'XMLHttpRequest'},
|
||||
url: '/api/v4/jobs/type/ldap_sync?page=0&per_page=50',
|
||||
method: 'GET',
|
||||
timeout: 60000,
|
||||
}).then((response) => {
|
||||
expect(response.status).to.equal(200);
|
||||
return cy.wrap(response as unknown as LDAPSyncResponse);
|
||||
});
|
||||
}
|
||||
Cypress.Commands.add('apiGetLDAPSync', apiGetLDAPSync);
|
||||
|
||||
// *****************************************************************************
|
||||
// Groups
|
||||
// https://api.mattermost.com/#tag/groups
|
||||
// *****************************************************************************
|
||||
function apiGetGroups(page = 0, perPage = 100): ResponseT {
|
||||
return cy.request({
|
||||
headers: {'X-Requested-With': 'XMLHttpRequest'},
|
||||
url: `/api/v4/groups?page=${page}&per_page=${perPage}`,
|
||||
method: 'GET',
|
||||
timeout: 60000,
|
||||
}).then((response) => {
|
||||
expect(response.status).to.equal(200);
|
||||
return cy.wrap(response);
|
||||
});
|
||||
}
|
||||
Cypress.Commands.add('apiGetGroups', apiGetGroups);
|
||||
|
||||
function apiPatchGroup(groupID: string, patch: Record<string, unknown>): ResponseT {
|
||||
return cy.request({
|
||||
headers: {'X-Requested-With': 'XMLHttpRequest'},
|
||||
url: `/api/v4/groups/${groupID}/patch`,
|
||||
method: 'PUT',
|
||||
timeout: 60000,
|
||||
body: patch,
|
||||
}).then((response) => {
|
||||
expect(response.status).to.equal(200);
|
||||
return cy.wrap(response);
|
||||
});
|
||||
}
|
||||
Cypress.Commands.add('apiPatchGroup', apiPatchGroup);
|
||||
|
||||
function apiGetLDAPGroups(page = 0, perPage = 100): ResponseT {
|
||||
return cy.request({
|
||||
headers: {'X-Requested-With': 'XMLHttpRequest'},
|
||||
url: `/api/v4/ldap/groups?page=${page}&per_page=${perPage}`,
|
||||
method: 'GET',
|
||||
timeout: 60000,
|
||||
}).then((response) => {
|
||||
expect(response.status).to.equal(200);
|
||||
return cy.wrap(response);
|
||||
});
|
||||
}
|
||||
|
||||
Cypress.Commands.add('apiGetLDAPGroups', apiGetLDAPGroups);
|
||||
|
||||
function apiAddLDAPGroupLink(remoteId: string) {
|
||||
return cy.request({
|
||||
headers: {'X-Requested-With': 'XMLHttpRequest'},
|
||||
url: `/api/v4/ldap/groups/${remoteId}/link`,
|
||||
method: 'POST',
|
||||
timeout: 60000,
|
||||
}).then((response) => {
|
||||
return cy.wrap(response);
|
||||
});
|
||||
}
|
||||
Cypress.Commands.add('apiAddLDAPGroupLink', apiAddLDAPGroupLink);
|
||||
|
||||
function apiGetTeamGroups(teamId: string) {
|
||||
return cy.request({
|
||||
headers: {'X-Requested-With': 'XMLHttpRequest'},
|
||||
url: `/api/v4/teams/${teamId}/groups`,
|
||||
method: 'GET',
|
||||
timeout: 60000,
|
||||
}).then((response) => {
|
||||
expect(response.status).to.equal(200);
|
||||
return cy.wrap(response);
|
||||
});
|
||||
}
|
||||
Cypress.Commands.add('apiGetTeamGroups', apiGetTeamGroups);
|
||||
|
||||
function apiDeleteLinkFromTeamToGroup(groupId: string, teamId: string): ResponseT {
|
||||
return cy.request({
|
||||
headers: {'X-Requested-With': 'XMLHttpRequest'},
|
||||
url: `/api/v4/groups/${groupId}/teams/${teamId}/link`,
|
||||
method: 'DELETE',
|
||||
timeout: 60000,
|
||||
}).then((response) => {
|
||||
expect(response.status).to.equal(200);
|
||||
return cy.wrap(response);
|
||||
});
|
||||
}
|
||||
Cypress.Commands.add('apiDeleteLinkFromTeamToGroup', apiDeleteLinkFromTeamToGroup);
|
||||
|
||||
function apiLinkGroup(groupID: string): ResponseT {
|
||||
return linkUnlinkGroup(groupID, 'POST');
|
||||
}
|
||||
Cypress.Commands.add('apiLinkGroup', apiLinkGroup);
|
||||
|
||||
function apiUnlinkGroup(groupID: string): ResponseT {
|
||||
return linkUnlinkGroup(groupID, 'DELETE');
|
||||
}
|
||||
Cypress.Commands.add('apiUnlinkGroup', apiUnlinkGroup);
|
||||
|
||||
function linkUnlinkGroup(groupID: string, httpMethod: string): ResponseT {
|
||||
return cy.request({
|
||||
headers: {'X-Requested-With': 'XMLHttpRequest'},
|
||||
url: `/api/v4/ldap/groups/${groupID}/link`,
|
||||
method: httpMethod,
|
||||
timeout: 60000,
|
||||
}).then((response) => {
|
||||
expect(response.status).to.be.oneOf([200, 201, 204]);
|
||||
return cy.wrap(response);
|
||||
});
|
||||
}
|
||||
|
||||
function apiGetGroupTeams(groupID: string): ResponseT {
|
||||
return getGroupSyncables(groupID, 'team');
|
||||
}
|
||||
Cypress.Commands.add('apiGetGroupTeams', apiGetGroupTeams);
|
||||
|
||||
function apiGetGroupTeam(groupID: string, teamID: string): ResponseT {
|
||||
return getGroupSyncable(groupID, 'team', teamID);
|
||||
}
|
||||
Cypress.Commands.add('apiGetGroupTeam', apiGetGroupTeam);
|
||||
|
||||
function apiGetGroupChannels(groupID: string): ResponseT {
|
||||
return getGroupSyncables(groupID, 'channel');
|
||||
}
|
||||
Cypress.Commands.add('apiGetGroupChannels', apiGetGroupChannels);
|
||||
|
||||
function apiGetGroupChannel(groupID: string, channelID: string): ResponseT {
|
||||
return getGroupSyncable(groupID, 'channel', channelID);
|
||||
}
|
||||
Cypress.Commands.add('apiGetGroupChannel', apiGetGroupChannel);
|
||||
|
||||
function getGroupSyncable(groupID: string, syncableType: string, syncableID: string): ResponseT {
|
||||
return cy.request({
|
||||
headers: {'X-Requested-With': 'XMLHttpRequest'},
|
||||
url: `/api/v4/groups/${groupID}/${syncableType}s/${syncableID}`,
|
||||
method: 'GET',
|
||||
timeout: 60000,
|
||||
}).then((response) => {
|
||||
expect(response.status).to.equal(200);
|
||||
return cy.wrap(response);
|
||||
});
|
||||
}
|
||||
|
||||
function getGroupSyncables(groupID: string, syncableType: string): ResponseT {
|
||||
return cy.request({
|
||||
headers: {'X-Requested-With': 'XMLHttpRequest'},
|
||||
url: `/api/v4/groups/${groupID}/${syncableType}s?page=0&per_page=100`,
|
||||
method: 'GET',
|
||||
timeout: 60000,
|
||||
}).then((response) => {
|
||||
expect(response.status).to.equal(200);
|
||||
return cy.wrap(response);
|
||||
});
|
||||
}
|
||||
|
||||
function apiUnlinkGroupTeam(groupID: string, teamID: string): ResponseT {
|
||||
return linkUnlinkGroupSyncable(groupID, teamID, 'team', 'DELETE');
|
||||
}
|
||||
Cypress.Commands.add('apiUnlinkGroupTeam', apiUnlinkGroupTeam);
|
||||
|
||||
function apiLinkGroupTeam(groupID: string, teamID: string): ResponseT {
|
||||
return linkUnlinkGroupSyncable(groupID, teamID, 'team', 'POST');
|
||||
}
|
||||
Cypress.Commands.add('apiLinkGroupTeam', apiLinkGroupTeam);
|
||||
|
||||
function apiUnlinkGroupChannel(groupID: string, channelID: string): ResponseT {
|
||||
return linkUnlinkGroupSyncable(groupID, channelID, 'channel', 'DELETE');
|
||||
}
|
||||
Cypress.Commands.add('apiUnlinkGroupChannel', apiUnlinkGroupChannel);
|
||||
|
||||
function apiLinkGroupChannel(groupID: string, channelID: string): ResponseT {
|
||||
return linkUnlinkGroupSyncable(groupID, channelID, 'channel', 'POST');
|
||||
}
|
||||
Cypress.Commands.add('apiLinkGroupChannel', apiLinkGroupChannel);
|
||||
|
||||
function simulateSubscription(subscription: Partial<Cypress.Subscription>, withLimits = true) {
|
||||
cy.intercept('GET', '**/api/v4/cloud/subscription', {
|
||||
statusCode: 200,
|
||||
body: subscription,
|
||||
});
|
||||
|
||||
cy.intercept('GET', '**/api/v4/cloud/products**', {
|
||||
statusCode: 200,
|
||||
body: [
|
||||
{
|
||||
id: 'prod_1',
|
||||
sku: 'cloud-starter',
|
||||
price_per_seat: 0,
|
||||
recurring_interval: 'month',
|
||||
name: 'Cloud Free',
|
||||
cross_sells_to: '',
|
||||
},
|
||||
{
|
||||
id: 'prod_2',
|
||||
sku: 'cloud-professional',
|
||||
price_per_seat: 10,
|
||||
recurring_interval: 'month',
|
||||
name: 'Cloud Professional',
|
||||
cross_sells_to: 'prod_4',
|
||||
},
|
||||
{
|
||||
id: 'prod_3',
|
||||
sku: 'cloud-enterprise',
|
||||
price_per_seat: 30,
|
||||
recurring_interval: 'month',
|
||||
name: 'Cloud Enterprise',
|
||||
cross_sells_to: 'prod_5',
|
||||
},
|
||||
{
|
||||
id: 'prod_4',
|
||||
sku: 'cloud-professional',
|
||||
price_per_seat: 96,
|
||||
recurring_interval: 'year',
|
||||
name: 'Cloud Professional Yearly',
|
||||
cross_sells_to: 'prod_2',
|
||||
},
|
||||
{
|
||||
id: 'prod_5',
|
||||
sku: 'cloud-enterprise',
|
||||
price_per_seat: 96,
|
||||
recurring_interval: 'year',
|
||||
name: 'Cloud Enterprise Yearly',
|
||||
cross_sells_to: 'prod_3',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (withLimits) {
|
||||
cy.intercept('GET', '**/api/v4/cloud/limits', {
|
||||
statusCode: 200,
|
||||
body: {
|
||||
messages: {
|
||||
history: 10000,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Cypress.Commands.add('simulateSubscription', simulateSubscription);
|
||||
|
||||
function linkUnlinkGroupSyncable(groupID: string, syncableID: string, syncableType: string, httpMethod: string) {
|
||||
return cy.request({
|
||||
headers: {'X-Requested-With': 'XMLHttpRequest'},
|
||||
url: `/api/v4/groups/${groupID}/${syncableType}s/${syncableID}/link`,
|
||||
method: httpMethod,
|
||||
body: {auto_add: true},
|
||||
}).then((response) => {
|
||||
expect(response.status).to.be.oneOf([200, 201, 204]);
|
||||
return cy.wrap(response);
|
||||
});
|
||||
}
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
|
||||
/**
|
||||
* Get LDAP Group Sync Job Status
|
||||
*
|
||||
* @example
|
||||
* cy.apiGetLDAPSync().then((response) => {
|
||||
*/
|
||||
apiGetLDAPSync: typeof apiGetLDAPSync;
|
||||
|
||||
/**
|
||||
* Test SMTP setup
|
||||
*/
|
||||
apiEmailTest: typeof apiEmailTest;
|
||||
|
||||
/**
|
||||
* Creates a post directly via API
|
||||
* This API assume that the user is logged in and has cookie to access
|
||||
* @param {String} channelId - Where to post
|
||||
* @param {String} message - What to post
|
||||
* @param {String} rootId - Parent post ID. Set to "" to avoid nesting
|
||||
* @param {Object} props - Post props
|
||||
* @param {String} token - Optional token to use for auth. If not provided - posts as current user
|
||||
*/
|
||||
apiCreatePost: typeof apiCreatePost;
|
||||
|
||||
/**
|
||||
* Deletes a post directly via API
|
||||
* @param {String} postId - Post ID
|
||||
* @param {Object} [user] - the user trying to invoke the API
|
||||
*/
|
||||
apiDeletePost: typeof apiDeletePost;
|
||||
|
||||
/**
|
||||
* Creates a post directly via API
|
||||
* This API assume that the user is logged in as admin
|
||||
* @param {String} userId - user for whom to create the token
|
||||
*/
|
||||
apiCreateToken: typeof apiCreateToken;
|
||||
|
||||
/**
|
||||
* Unpins pinned posts of given postID directly via API
|
||||
* This API assume that the user is logged in and has cookie to access
|
||||
*/
|
||||
apiUnpinPosts: typeof apiUnpinPosts;
|
||||
|
||||
/**
|
||||
* Creates a command directly via API
|
||||
* This API assume that the user is logged in and has required permission to create a command
|
||||
* @param {Object} command - command to be created
|
||||
*/
|
||||
apiCreateCommand: typeof apiCreateCommand;
|
||||
|
||||
apiCreateWebhook: typeof apiCreateWebhook;
|
||||
|
||||
/**
|
||||
* Gets a team on the system
|
||||
* * @param {String} teamId - The team ID to get
|
||||
* All parameter required
|
||||
*/
|
||||
apiGetTeam: typeof apiGetTeam;
|
||||
|
||||
/**
|
||||
* Remove a User from a Channel directly via API
|
||||
* @param {String} channelId - The channel ID
|
||||
* @param {String} userId - The user ID
|
||||
* All parameter required
|
||||
*/
|
||||
removeUserFromChannel: typeof removeUserFromChannel;
|
||||
|
||||
/**
|
||||
* Remove a User from a Team directly via API
|
||||
* @param {String} teamID - The team ID
|
||||
* @param {String} userId - The user ID
|
||||
* All parameter required
|
||||
*/
|
||||
removeUserFromTeam: typeof removeUserFromTeam;
|
||||
|
||||
/**
|
||||
* Get all groups via the API
|
||||
*
|
||||
* @param {Integer} page - The desired page of the paginated list
|
||||
* @param {Integer} perPage - The number of groups per page
|
||||
*
|
||||
*/
|
||||
apiGetGroups: typeof apiGetGroups;
|
||||
|
||||
/**
|
||||
* Patch a group directly via API
|
||||
*
|
||||
* @param {String} name - The new name for the group
|
||||
* @param {Object} patch
|
||||
* {Boolean} allow_reference - Whether to allow reference (group mention) or not - true/false
|
||||
* {String} name - Name for the group, used for group mentions
|
||||
* {String} display_name - Display name for the group
|
||||
* {String} description - Description for the group
|
||||
*
|
||||
*/
|
||||
apiPatchGroup: typeof apiPatchGroup;
|
||||
|
||||
/**
|
||||
* Get all LDAP groups via API
|
||||
* @param {Integer} page - The page to select
|
||||
* @param {Integer} perPage - The number of groups per page
|
||||
*/
|
||||
apiGetLDAPGroups: typeof apiGetLDAPGroups;
|
||||
|
||||
/**
|
||||
* Add a link for LDAP group via API
|
||||
* @param {String} remoteId - remote ID of the group
|
||||
*/
|
||||
apiAddLDAPGroupLink: typeof apiAddLDAPGroupLink;
|
||||
|
||||
/**
|
||||
* Retrieve the list of groups associated with a given team via API
|
||||
* @param {String} teamId - Team GUID
|
||||
*/
|
||||
apiGetTeamGroups: typeof apiGetTeamGroups;
|
||||
|
||||
/**
|
||||
* Delete a link from a team to a group via API
|
||||
* @param {String} groupId - Group GUID
|
||||
* @param {String} teamId - Team GUID
|
||||
*/
|
||||
apiDeleteLinkFromTeamToGroup: typeof apiDeleteLinkFromTeamToGroup;
|
||||
|
||||
apiLinkGroup: typeof apiLinkGroup;
|
||||
|
||||
apiUnlinkGroup: typeof apiUnlinkGroup;
|
||||
|
||||
apiLinkGroupTeam: typeof apiLinkGroupTeam;
|
||||
|
||||
apiUnlinkGroupTeam: typeof apiUnlinkGroupTeam;
|
||||
|
||||
apiUnlinkGroupChannel: typeof apiUnlinkGroupChannel;
|
||||
|
||||
apiLinkGroupChannel: typeof apiLinkGroupChannel;
|
||||
|
||||
apiGetGroupTeams: typeof apiGetGroupTeams;
|
||||
|
||||
apiGetGroupTeam: typeof apiGetGroupTeam;
|
||||
|
||||
apiGetGroupChannels: typeof apiGetGroupChannels;
|
||||
|
||||
apiGetGroupChannel: typeof apiGetGroupChannel;
|
||||
|
||||
simulateSubscription: typeof simulateSubscription;
|
||||
}
|
||||
}
|
||||
}
|
||||
26
e2e-tests/cypress/tests/support/assertions.ts
Normal file
26
e2e-tests/cypress/tests/support/assertions.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// Asserts that an item in the channel sidebar is not unread.
|
||||
export function beRead(items: JQuery<HTMLElement>) {
|
||||
expect(items).to.have.length(1);
|
||||
expect(items[0].className).to.not.match(/unread-title/);
|
||||
}
|
||||
|
||||
// Asserts that an item in the channel sidebar is read.
|
||||
export function beUnread(items: JQuery<HTMLElement>) {
|
||||
expect(items).to.have.length(1);
|
||||
expect(items[0].className).to.match(/unread-title/);
|
||||
}
|
||||
|
||||
// Asserts that an item in the channel sidebar is muted.
|
||||
export function beMuted(items: JQuery<HTMLElement>) {
|
||||
expect(items).to.have.length(1);
|
||||
expect(items[0].className).to.match(/muted/);
|
||||
}
|
||||
|
||||
// Asserts that an item in the channel sidebar is unmuted.
|
||||
export function beUnmuted(items: JQuery<HTMLElement>) {
|
||||
expect(items).to.have.length(1);
|
||||
expect(items[0].className).to.not.match(/muted/);
|
||||
}
|
||||
12
e2e-tests/cypress/tests/support/chai.d.ts
vendored
Normal file
12
e2e-tests/cypress/tests/support/chai.d.ts
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
declare namespace Cypress {
|
||||
interface Chainer<Subject> {
|
||||
(chainer: 'be.focusVisible', {exactStyles}: {exactStyles?: boolean}): Chainable<Subject>;
|
||||
|
||||
(chainer: 'not.be.focusVisible', {exactStyles}: {exactStyles?: boolean}): Chainable<Subject>;
|
||||
}
|
||||
}
|
||||
36
e2e-tests/cypress/tests/support/chai.ts
Normal file
36
e2e-tests/cypress/tests/support/chai.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
chai.use((chai: Chai.ChaiStatic) => {
|
||||
// Chai assertion context has dynamic 'this' binding
|
||||
function assertIsFoo(this: any, {exactStyles = true} = {}) {
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
const obj = this._obj as JQuery<HTMLElement>;
|
||||
|
||||
this.assert(
|
||||
obj.hasClass('a11y--active'),
|
||||
'expected #{this} to have a11y--active class',
|
||||
'expected #{this} to not have a11y--active class',
|
||||
obj,
|
||||
);
|
||||
|
||||
// These should match the styles set on :focus-visible in sass/utils/_modifiers.scss
|
||||
this.assert(
|
||||
exactStyles ? obj.css('box-shadow').includes('0px 0px 1px 3px') : Boolean(obj.css('box-shadow')),
|
||||
'expected #{this} to have focused element style (box-shadow)',
|
||||
'expected #{this} to not have focused element style (box-shadow)',
|
||||
obj.css('box-shadow'),
|
||||
);
|
||||
|
||||
this.assert(
|
||||
exactStyles ? obj.css('border-radius') === '4px' : Boolean(obj.css('border-radius')),
|
||||
'expected #{this} to have focused element style (border-radius)',
|
||||
'expected #{this} to not have focused element style (border-radius)',
|
||||
obj.css('border-radius'),
|
||||
);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
chai.Assertion.addMethod('a11yVisible', assertIsFoo);
|
||||
});
|
||||
35
e2e-tests/cypress/tests/support/client-impl.js
Normal file
35
e2e-tests/cypress/tests/support/client-impl.js
Normal file
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Client4} from '@mattermost/client';
|
||||
|
||||
import clientRequest from '../plugins/client_request';
|
||||
|
||||
export class E2EClient extends Client4 {
|
||||
async doFetchWithResponse(url, options) {
|
||||
const {
|
||||
body,
|
||||
headers,
|
||||
method,
|
||||
} = this.getOptions(options);
|
||||
|
||||
let data;
|
||||
if (body) {
|
||||
data = JSON.parse(body);
|
||||
}
|
||||
|
||||
const response = await clientRequest({
|
||||
headers,
|
||||
url,
|
||||
method,
|
||||
data,
|
||||
});
|
||||
|
||||
if (url.endsWith('/api/v4/users/login')) {
|
||||
this.setToken(response.headers.token);
|
||||
this.setUserId(response.data.id);
|
||||
this.setUserRoles(response.data.roles);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
}
|
||||
22
e2e-tests/cypress/tests/support/client.d.ts
vendored
Normal file
22
e2e-tests/cypress/tests/support/client.d.ts
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
// 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 {
|
||||
makeClient(options?: {user: Pick<UserProfile, 'username' | 'password'>}): Chainable<Client>;
|
||||
}
|
||||
}
|
||||
29
e2e-tests/cypress/tests/support/client.js
Normal file
29
e2e-tests/cypress/tests/support/client.js
Normal file
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {getAdminAccount} from './env';
|
||||
import {E2EClient} from './client-impl';
|
||||
|
||||
const clients = {};
|
||||
|
||||
async function makeClient({user = getAdminAccount(), useCache = true} = {}) {
|
||||
const cacheKey = user.username + user.password;
|
||||
if (useCache && clients[cacheKey] != null) {
|
||||
return clients[cacheKey];
|
||||
}
|
||||
|
||||
const client = new E2EClient();
|
||||
|
||||
const baseUrl = Cypress.config('baseUrl');
|
||||
client.setUrl(baseUrl);
|
||||
|
||||
await client.login(user.username, user.password);
|
||||
|
||||
if (useCache) {
|
||||
clients[cacheKey] = client;
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
Cypress.Commands.add('makeClient', makeClient);
|
||||
33
e2e-tests/cypress/tests/support/common_login_commands.d.ts
vendored
Normal file
33
e2e-tests/cypress/tests/support/common_login_commands.d.ts
vendored
Normal 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 {
|
||||
|
||||
/**
|
||||
* checkForLDAPError verifies that an LDAP error is displayed.
|
||||
* @returns {boolean} - true if error successfully found.
|
||||
*/
|
||||
checkForLDAPError(): Chainable;
|
||||
|
||||
skipOrCreateTeam: typeof skipOrCreateTeam;
|
||||
checkLoginFailed: typeof checkLoginFailed;
|
||||
doMemberLogoutFromSignUp: typeof doMemberLogoutFromSignUp;
|
||||
doLogoutFromSignUp: typeof doLogoutFromSignUp;
|
||||
checkLoginPage: typeof checkLoginPage;
|
||||
checkLeftSideBar: typeof checkLeftSideBar;
|
||||
checkInvitePeoplePage: typeof checkInvitePeoplePage;
|
||||
}
|
||||
}
|
||||
129
e2e-tests/cypress/tests/support/common_login_commands.js
Normal file
129
e2e-tests/cypress/tests/support/common_login_commands.js
Normal file
@@ -0,0 +1,129 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import * as TIMEOUTS from '../fixtures/timeouts';
|
||||
|
||||
Cypress.Commands.add('checkLoginPage', (settings = {}) => {
|
||||
// # Remove autofocus from login input
|
||||
cy.get('.login-body-card-title').click();
|
||||
|
||||
// * Check elements in the body
|
||||
cy.get('#input_loginId', {timeout: TIMEOUTS.ONE_MIN}).should('be.visible').and(($loginTextbox) => {
|
||||
const placeholder = $loginTextbox[0].placeholder;
|
||||
expect(placeholder).to.match(/Email/);
|
||||
expect(placeholder).to.match(/Username/);
|
||||
}).focus();
|
||||
|
||||
cy.get('#input_password-input').should('be.visible').and('have.attr', 'placeholder', 'Password');
|
||||
cy.get('#saveSetting').should('be.visible');
|
||||
|
||||
// * Check the title
|
||||
cy.title().should('include', settings.siteName);
|
||||
});
|
||||
|
||||
Cypress.Commands.add('checkLoginFailed', () => {
|
||||
// * Check the alert banner
|
||||
cy.get('.AlertBanner.danger', {timeout: TIMEOUTS.ONE_MIN}).then(() => {
|
||||
// * Check the login input in error
|
||||
cy.get('.login-body-card-form-input .Input_fieldset').should('have.class', 'Input_fieldset___error');
|
||||
|
||||
// * Check the password input in error
|
||||
cy.get('.login-body-card-form-password-input.Input_fieldset').should('have.class', 'Input_fieldset___error');
|
||||
|
||||
// * Check the Log in button enabled
|
||||
cy.get('#saveSetting').should('not.be.disabled');
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('checkGuestNoChannels', () => {
|
||||
cy.findByText('Your guest account has no channels assigned. Please contact an administrator.').should('be.visible');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('checkMemberNoChannels', () => {
|
||||
cy.findByText('No teams are available to join. Please create a new team or ask your administrator for an invite.').should('be.visible');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('checkLeftSideBar', (settings = {}) => {
|
||||
if (settings.teamName != null && settings.teamName.length > 0) {
|
||||
cy.uiGetLHSHeader().should('contain', settings.teamName);
|
||||
}
|
||||
|
||||
if (settings.user.username.length > 0) {
|
||||
// * Verify username info
|
||||
cy.uiOpenUserMenu().findByText(`@${settings.user.username}`);
|
||||
|
||||
// # Close status menu
|
||||
cy.get('body').type('{esc}');
|
||||
}
|
||||
|
||||
if (settings.user.userType === 'Admin' || settings.user.isAdmin) {
|
||||
// # Check that user is an admin
|
||||
cy.uiOpenProductMenu().findByText('System Console');
|
||||
} else {
|
||||
// # Check that user is not an admin
|
||||
cy.uiOpenProductMenu().findByText('System Console').should('not.exist');
|
||||
}
|
||||
|
||||
// # Close product switch menu
|
||||
cy.uiGetProductMenuButton().click();
|
||||
|
||||
cy.get('#channel_view').should('be.visible');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('checkInvitePeoplePage', (settings = {}) => {
|
||||
cy.findByText('Copy invite link', {timeout: TIMEOUTS.ONE_MIN}).should('be.visible');
|
||||
if (settings.teamName != null && settings.teamName.length > 0) {
|
||||
const inviteRegexp = new RegExp(`Invite .* to ${settings.teamName}`);
|
||||
cy.findByText(inviteRegexp).should('be.visible');
|
||||
}
|
||||
});
|
||||
|
||||
Cypress.Commands.add('checkInvitePeopleAdminPage', (settings = {}) => {
|
||||
cy.findByText('Members', {timeout: TIMEOUTS.ONE_MIN}).should('be.visible');
|
||||
cy.findByText('Guests').should('be.visible');
|
||||
if (settings.teamName != null && settings.teamName.length > 0) {
|
||||
cy.findByText('Invite people to ' + settings.teamName).should('be.visible');
|
||||
}
|
||||
});
|
||||
|
||||
Cypress.Commands.add('doLogoutFromSignUp', () => {
|
||||
cy.checkGuestNoChannels();
|
||||
cy.findByText('Logout').should('be.visible').click();
|
||||
});
|
||||
|
||||
Cypress.Commands.add('doMemberLogoutFromSignUp', () => {
|
||||
cy.checkMemberNoChannels();
|
||||
cy.findByText('Logout').should('be.visible').click();
|
||||
});
|
||||
|
||||
Cypress.Commands.add('skipOrCreateTeam', (settings, userId) => {
|
||||
cy.wait(TIMEOUTS.FIVE_SEC);
|
||||
return cy.get('body').then((body) => {
|
||||
let teamName = '';
|
||||
|
||||
// # Create a team if a user is not member of any team
|
||||
if (body.text().includes('Create a team')) {
|
||||
teamName = 't' + userId.substring(0, 14);
|
||||
|
||||
cy.checkCreateTeamPage(settings);
|
||||
|
||||
cy.get('#createNewTeamLink').scrollIntoView().should('be.visible').click();
|
||||
cy.get('#teamNameInput').should('be.visible').typeWithForce(teamName);
|
||||
cy.findByText('Next').should('be.visible').click();
|
||||
cy.findByText('Finish').should('be.visible').click();
|
||||
}
|
||||
|
||||
return cy.wrap(teamName);
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('checkForLDAPError', () => {
|
||||
cy.wait(TIMEOUTS.FIVE_SEC);
|
||||
return cy.get('body').then((body) => {
|
||||
if (body.text().includes('User not registered on AD/LDAP server.')) {
|
||||
cy.findByText('Back to Mattermost').should('exist').and('be.visible').click().wait(TIMEOUTS.FIVE_SEC);
|
||||
return cy.wrap(true);
|
||||
}
|
||||
return cy.wrap(false);
|
||||
});
|
||||
});
|
||||
7
e2e-tests/cypress/tests/support/constants.js
Normal file
7
e2e-tests/cypress/tests/support/constants.js
Normal file
@@ -0,0 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// Default team is meant for sysadmin's primary team,
|
||||
// selected for compatibility with existing local development.
|
||||
// It should not be used for testing.
|
||||
export const DEFAULT_TEAM = {name: 'ad-1', display_name: 'eligendi', type: 'O'};
|
||||
181
e2e-tests/cypress/tests/support/db_commands.ts
Normal file
181
e2e-tests/cypress/tests/support/db_commands.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {ChainableT} from '../types';
|
||||
|
||||
const dbClient = Cypress.expose('dbClient');
|
||||
const dbConnection = Cypress.expose('dbConnection');
|
||||
const dbConfig = {
|
||||
client: dbClient,
|
||||
connection: dbConnection,
|
||||
};
|
||||
|
||||
const message = `Compare "cypress.json" against "config.json" of mattermost-server. It should match database driver and connection string.
|
||||
|
||||
The value at "cypress.json" is based on default mattermost-server's local database:
|
||||
{"dbClient": "${dbClient}", "dbConnection": "${dbConnection}"}
|
||||
|
||||
If your server is using database other than the default, you may export those as env variables, like:
|
||||
"__CYPRESS_dbClient=[dbClient] CYPRESS_dbConnection=[dbConnection] npm run cypress:open__"
|
||||
`;
|
||||
|
||||
function apiRequireServerDBToMatch(): ChainableT {
|
||||
return cy.apiGetConfig().then(({config}) => {
|
||||
// On Cloud, SqlSettings is not being returned.
|
||||
// With that, checking of server DB will be ignored and will assume it does match with
|
||||
// the one being expected by Cypress.
|
||||
if (config.SqlSettings && config.SqlSettings.DriverName !== dbClient) {
|
||||
expect(config.SqlSettings.DriverName, message).to.equal(dbClient);
|
||||
}
|
||||
});
|
||||
}
|
||||
Cypress.Commands.add('apiRequireServerDBToMatch', apiRequireServerDBToMatch);
|
||||
|
||||
interface GetActiveUserSessionsParam {
|
||||
username: string;
|
||||
userId?: string;
|
||||
limit?: number;
|
||||
}
|
||||
interface GetActiveUserSessionsResult {
|
||||
user: Cypress.UserProfile;
|
||||
// DB session records have dynamic fields
|
||||
sessions: Array<Record<string, any>>;
|
||||
}
|
||||
function dbGetActiveUserSessions(params: GetActiveUserSessionsParam): ChainableT<GetActiveUserSessionsResult> {
|
||||
// cy.task() returns untyped data
|
||||
return cy.task('dbGetActiveUserSessions', {dbConfig, params}).then(({user, sessions, errorMessage}: any) => {
|
||||
expect(errorMessage).to.be.undefined;
|
||||
|
||||
return cy.wrap({user, sessions});
|
||||
});
|
||||
}
|
||||
Cypress.Commands.add('dbGetActiveUserSessions', dbGetActiveUserSessions);
|
||||
|
||||
interface GetUserParam {
|
||||
username: string;
|
||||
}
|
||||
interface GetUserResult {
|
||||
user: Cypress.UserProfile & {mfasecret: string};
|
||||
}
|
||||
function dbGetUser(params: GetUserParam): ChainableT<GetUserResult> {
|
||||
// cy.task() returns untyped data
|
||||
return cy.task('dbGetUser', {dbConfig, params}).then(({user, errorMessage, error}: any) => {
|
||||
verifyError(error, errorMessage);
|
||||
|
||||
return cy.wrap({user});
|
||||
});
|
||||
}
|
||||
Cypress.Commands.add('dbGetUser', dbGetUser);
|
||||
|
||||
interface GetUserSessionParam {
|
||||
sessionId: string;
|
||||
}
|
||||
interface GetUserSessionResult {
|
||||
// DB session records have dynamic fields
|
||||
session: Record<string, any>;
|
||||
}
|
||||
function dbGetUserSession(params: GetUserSessionParam): ChainableT<GetUserSessionResult> {
|
||||
// cy.task() returns untyped data
|
||||
return cy.task('dbGetUserSession', {dbConfig, params}).then(({session, errorMessage}: any) => {
|
||||
expect(errorMessage).to.be.undefined;
|
||||
|
||||
return cy.wrap({session});
|
||||
});
|
||||
}
|
||||
Cypress.Commands.add('dbGetUserSession', dbGetUserSession);
|
||||
|
||||
interface UpdateUserSessionParam {
|
||||
sessionId: string;
|
||||
userId: string;
|
||||
// DB session fields are dynamic
|
||||
fieldsToUpdate: Record<string, any>;
|
||||
}
|
||||
interface UpdateUserSessionResult {
|
||||
// DB session records have dynamic fields
|
||||
session: Record<string, any>;
|
||||
}
|
||||
function dbUpdateUserSession(params: UpdateUserSessionParam): ChainableT<UpdateUserSessionResult> {
|
||||
// cy.task() returns untyped data
|
||||
return cy.task('dbUpdateUserSession', {dbConfig, params}).then(({session, errorMessage}: any) => {
|
||||
expect(errorMessage).to.be.undefined;
|
||||
|
||||
return cy.wrap({session});
|
||||
});
|
||||
}
|
||||
Cypress.Commands.add('dbUpdateUserSession', dbUpdateUserSession);
|
||||
|
||||
function dbRefreshPostStats(): ChainableT<{success?: boolean; skipped?: boolean; message?: string}> {
|
||||
// cy.task() returns untyped data
|
||||
return cy.task('dbRefreshPostStats', {dbConfig}).then(({success, skipped, message, errorMessage, error}: any) => {
|
||||
verifyError(error, errorMessage);
|
||||
return cy.wrap({success, skipped, message} as {success?: boolean; skipped?: boolean; message?: string});
|
||||
}) as ChainableT<{success?: boolean; skipped?: boolean; message?: string}>;
|
||||
}
|
||||
Cypress.Commands.add('dbRefreshPostStats', dbRefreshPostStats);
|
||||
|
||||
function verifyError(error: unknown, errorMessage: string) {
|
||||
if (errorMessage) {
|
||||
expect(errorMessage, `${errorMessage}\n\n${message}\n\n${JSON.stringify(error)}`).to.be.undefined;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
|
||||
/**
|
||||
* Gets server config, and assert if it matches with the database connection being used by Cypress
|
||||
*
|
||||
* @example
|
||||
* cy.apiRequireServerDBToMatch();
|
||||
*/
|
||||
apiRequireServerDBToMatch: typeof apiRequireServerDBToMatch;
|
||||
|
||||
/**
|
||||
* Gets active sessions of a user on a given username or user ID directly from the database
|
||||
* @param {String} username
|
||||
* @param {String} userId
|
||||
* @param {String} limit - maximum number of active sessions to return, e.g. 50 (default)
|
||||
* @returns {Object} user - user object
|
||||
* @returns {[Object]} sessions - an array of active sessions
|
||||
*/
|
||||
dbGetActiveUserSessions: typeof dbGetActiveUserSessions;
|
||||
|
||||
/**
|
||||
* Gets user on a given username directly from the database
|
||||
* @param {Object} options
|
||||
* @param {String} options.username
|
||||
* @returns {UserProfile} user - user object
|
||||
*/
|
||||
dbGetUser: typeof dbGetUser;
|
||||
|
||||
/**
|
||||
* Gets session of a user on a given session ID directly from the database
|
||||
* @param {Object} options
|
||||
* @param {String} options.sessionId
|
||||
* @returns {Session} session
|
||||
*/
|
||||
dbGetUserSession: typeof dbGetUserSession;
|
||||
|
||||
/**
|
||||
* Updates session of a user on a given user ID and session ID with fields to update directly from the database
|
||||
* @param {Object} options
|
||||
* @param {String} options.sessionId
|
||||
* @param {String} options.userId
|
||||
* @param {Object} options.fieldsToUpdate - will update all except session ID and user ID
|
||||
* @returns {Session} session
|
||||
*/
|
||||
dbUpdateUserSession: typeof dbUpdateUserSession;
|
||||
|
||||
/**
|
||||
* Refreshes PostgreSQL materialized views for post statistics
|
||||
* @returns {Object} result
|
||||
* @returns {boolean} result.success - true if refresh was successful
|
||||
* @returns {boolean} result.skipped - true if operation was skipped (non-PostgreSQL)
|
||||
* @returns {string} result.message - message when operation is skipped
|
||||
*/
|
||||
dbRefreshPostStats: typeof dbRefreshPostStats;
|
||||
}
|
||||
}
|
||||
}
|
||||
51
e2e-tests/cypress/tests/support/email.ts
Normal file
51
e2e-tests/cypress/tests/support/email.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {getEmailUrl, splitEmailBodyText} from '../utils';
|
||||
|
||||
/**
|
||||
* getRecentEmail is a task to get email from email service provider
|
||||
* @param {string} username - username of the user
|
||||
* @param {string} username - email of the user
|
||||
*/
|
||||
|
||||
Cypress.Commands.add('getRecentEmail', ({username, email}) => {
|
||||
// cy.task() returns untyped data
|
||||
return cy.task('getRecentEmail', {username, email, mailUrl: getEmailUrl()}).then(({status, data}: any) => {
|
||||
expect(status).to.equal(200);
|
||||
|
||||
const {to, date, body: {text}} = data;
|
||||
|
||||
// * Verify that email is addressed to a user
|
||||
expect(to.length).to.equal(1);
|
||||
expect(to[0]).to.contain(email);
|
||||
|
||||
// * Verify that date is current
|
||||
const isoDate = new Date().toISOString().substring(0, 10);
|
||||
expect(date).to.contain(isoDate);
|
||||
|
||||
const body = splitEmailBodyText(text);
|
||||
return cy.wrap({...data, body});
|
||||
});
|
||||
});
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
|
||||
/**
|
||||
* getRecentEmail is a task to get an email sent to a user
|
||||
* from the email service provider
|
||||
* @param options.username - username of the user
|
||||
* @param options.email - email of the user
|
||||
*
|
||||
* @example
|
||||
* cy.getRecentEmail().then((data) => {
|
||||
* // do something with the email data/content
|
||||
* });
|
||||
*/
|
||||
getRecentEmail(options: Pick<UserProfile, 'username' | 'email'>): Chainable;
|
||||
}
|
||||
}
|
||||
}
|
||||
25
e2e-tests/cypress/tests/support/env.ts
Normal file
25
e2e-tests/cypress/tests/support/env.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
export interface User {
|
||||
username: string;
|
||||
password: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export function getAdminAccount() {
|
||||
return {
|
||||
username: Cypress.expose('adminUsername'),
|
||||
password: Cypress.expose('adminPassword'),
|
||||
email: Cypress.expose('adminEmail'),
|
||||
} as UserProfile;
|
||||
}
|
||||
|
||||
export function getDBConfig() {
|
||||
return {
|
||||
client: Cypress.expose('dbClient'),
|
||||
connection: Cypress.expose('dbConnection'),
|
||||
};
|
||||
}
|
||||
42
e2e-tests/cypress/tests/support/extended_commands.d.ts
vendored
Normal file
42
e2e-tests/cypress/tests/support/extended_commands.d.ts
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
declare namespace Cypress {
|
||||
interface Chainable {
|
||||
|
||||
/**
|
||||
* Reload the page, same as cy.reload but extended with explicit wait to allow page to load freely
|
||||
* @param forceReload — Whether to reload the current page without using the cache. true forces the reload without cache.
|
||||
* @param options — Pass in an options object to change the default behavior of cy.reload()
|
||||
* @param duration — wait duration with 3 seconds by default
|
||||
*
|
||||
* @example
|
||||
* cy.reload();
|
||||
*/
|
||||
reload(forceReload: boolean, options?: Partial<Loggable & Timeoutable>, duration?: number): Chainable;
|
||||
|
||||
/**
|
||||
* Visit the given url, same as cy.visit but extended with explicit wait to allow page to load freely
|
||||
* @param url — The URL to visit. If relative uses baseUrl
|
||||
* @param options — Pass in an options object to change the default behavior of cy.visit()
|
||||
* @param duration — wait duration with 3 seconds by default
|
||||
*
|
||||
* @example
|
||||
* cy.visit('url');
|
||||
*/
|
||||
visit(url: string, options?: Partial<Cypress.VisitOptions>, duration?: number): Chainable;
|
||||
|
||||
/**
|
||||
* types the given string with `TypeOption.force` set to true
|
||||
*
|
||||
* @param text - the string that should be force-typed
|
||||
* @param [options] - optional TypeOptions object (`force` option is omitted because it is manually set on the command)
|
||||
*
|
||||
* @example
|
||||
* cy.get('#emailInput').typeWithForce('john.doe@example.com');
|
||||
*/
|
||||
typeWithForce(text: string, options?: Omit<Partial<TypeOptions>, 'force'>): Chainable;
|
||||
}
|
||||
}
|
||||
20
e2e-tests/cypress/tests/support/extended_commands.js
Normal file
20
e2e-tests/cypress/tests/support/extended_commands.js
Normal file
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import * as TIMEOUTS from '../fixtures/timeouts';
|
||||
|
||||
Cypress.Commands.overwrite('reload', (originalFn, forceReload, options, duration = TIMEOUTS.THREE_SEC) => {
|
||||
localStorage.setItem('__landingPageSeen__', 'true');
|
||||
originalFn(forceReload, options);
|
||||
cy.wait(duration);
|
||||
});
|
||||
|
||||
Cypress.Commands.overwrite('visit', (originalFn, url, options, duration = TIMEOUTS.THREE_SEC) => {
|
||||
localStorage.setItem('__landingPageSeen__', 'true');
|
||||
originalFn(url, options);
|
||||
cy.wait(duration);
|
||||
});
|
||||
|
||||
Cypress.Commands.add('typeWithForce', {prevSubject: true}, (subject, text, options = {}) => {
|
||||
cy.get(subject).type(text, {force: true, ...options});
|
||||
});
|
||||
138
e2e-tests/cypress/tests/support/external_commands.ts
Normal file
138
e2e-tests/cypress/tests/support/external_commands.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {ChannelMembership} from '@mattermost/types/channels';
|
||||
import type {Post} from '@mattermost/types/posts';
|
||||
import type {TeamMembership} from '@mattermost/types/teams';
|
||||
import type {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import {getRandomId} from '../utils';
|
||||
|
||||
import {getAdminAccount} from './env';
|
||||
|
||||
function externalActivateUser(userId: string, active = true) {
|
||||
const admin = getAdminAccount();
|
||||
|
||||
return cy.externalRequest({user: admin, method: 'PUT', path: `users/${userId}/active`, data: {active}});
|
||||
}
|
||||
Cypress.Commands.add('externalActivateUser', externalActivateUser);
|
||||
|
||||
function externalAddUserToChannel(userId: string, channelId: string): Cypress.Chainable<ChannelMembership> {
|
||||
const admin = getAdminAccount();
|
||||
|
||||
return cy.externalRequest({
|
||||
user: admin,
|
||||
method: 'POST',
|
||||
path: `channels/${channelId}/members`,
|
||||
data: {
|
||||
user_id: userId,
|
||||
},
|
||||
}).then((response) => response.data);
|
||||
}
|
||||
Cypress.Commands.add('externalAddUserToChannel', externalAddUserToChannel);
|
||||
|
||||
function externalAddUserToTeam(userId: string, teamId: string): Cypress.Chainable<TeamMembership> {
|
||||
const admin = getAdminAccount();
|
||||
|
||||
return cy.externalRequest({
|
||||
user: admin,
|
||||
method: 'POST',
|
||||
path: `teams/${teamId}/members`,
|
||||
data: {
|
||||
team_id: teamId,
|
||||
user_id: userId,
|
||||
},
|
||||
}).then((response) => response.data);
|
||||
}
|
||||
Cypress.Commands.add('externalAddUserToTeam', externalAddUserToTeam);
|
||||
|
||||
function externalCreatePostAsUser(user: Pick<UserProfile, 'username' | 'password'>, post: Partial<Post>): Cypress.Chainable<Post> {
|
||||
return cy.externalRequest({
|
||||
user,
|
||||
method: 'POST',
|
||||
path: 'posts',
|
||||
data: post,
|
||||
}).then((response) => response.data);
|
||||
}
|
||||
Cypress.Commands.add('externalCreatePostAsUser', externalCreatePostAsUser);
|
||||
|
||||
function externalCreateUser(user: Partial<UserProfile>): Cypress.Chainable<UserProfile> {
|
||||
const admin = getAdminAccount();
|
||||
|
||||
const randomValue = getRandomId();
|
||||
|
||||
return cy.externalRequest({
|
||||
user: admin,
|
||||
method: 'POST',
|
||||
path: 'users',
|
||||
data: {
|
||||
username: 'user' + randomValue,
|
||||
email: 'email' + randomValue + '@example.mattermost.com',
|
||||
password: 'password' + randomValue,
|
||||
...user,
|
||||
},
|
||||
}).then((response) => {
|
||||
// Re-add the password to the result so that we can make requests as that user
|
||||
return {
|
||||
...response.data,
|
||||
password: 'password' + randomValue,
|
||||
};
|
||||
});
|
||||
}
|
||||
Cypress.Commands.add('externalCreateUser', externalCreateUser);
|
||||
|
||||
function externalUpdateUserRoles(userId: string, roles: string) {
|
||||
const admin = getAdminAccount();
|
||||
|
||||
return cy.externalRequest({
|
||||
user: admin,
|
||||
method: 'PUT',
|
||||
path: `users/${userId}/roles`,
|
||||
data: {roles},
|
||||
});
|
||||
}
|
||||
Cypress.Commands.add('externalUpdateUserRoles', externalUpdateUserRoles);
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
|
||||
/**
|
||||
* Makes an external request as a sysadmin and activate/deactivate a user directly via API
|
||||
* @param {String} userId - The user ID
|
||||
* @param {Boolean} active - Whether to activate or deactivate - true/false
|
||||
*
|
||||
* @example
|
||||
* cy.externalActivateUser('user-id', false);
|
||||
*/
|
||||
externalActivateUser: typeof externalActivateUser;
|
||||
|
||||
/**
|
||||
* As the system admin, adds a user to a channel.
|
||||
*/
|
||||
externalAddUserToChannel: typeof externalAddUserToChannel;
|
||||
|
||||
/**
|
||||
* As the system admin, adds a user to a team.
|
||||
*/
|
||||
externalAddUserToTeam: typeof externalAddUserToTeam;
|
||||
|
||||
/**
|
||||
* As the given user, creates a post via the API.
|
||||
*/
|
||||
externalCreatePostAsUser: typeof externalCreatePostAsUser;
|
||||
|
||||
/**
|
||||
* As the system admin, creates a new user via the API. The user is automatically given a username, email,
|
||||
* and password, but the override parameter can be be used to specify any other fields if needed.
|
||||
*/
|
||||
externalCreateUser: typeof externalCreateUser;
|
||||
|
||||
/**
|
||||
* As the system admin, updates a user's roles via the API.
|
||||
*/
|
||||
externalUpdateUserRoles: typeof externalUpdateUserRoles;
|
||||
}
|
||||
}
|
||||
}
|
||||
97
e2e-tests/cypress/tests/support/fetch_commands.ts
Normal file
97
e2e-tests/cypress/tests/support/fetch_commands.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {ChainableT} from '@/types';
|
||||
|
||||
interface MockWebSocket {
|
||||
wrappedSocket: WebSocket | null;
|
||||
onopen: ((ev: Event) => void) | null;
|
||||
onmessage: ((ev: MessageEvent) => void) | null;
|
||||
onerror: ((ev: Event) => void) | null;
|
||||
onclose: ((ev: CloseEvent) => void) | null;
|
||||
send(data: string | ArrayBuffer): void;
|
||||
close(): void;
|
||||
connect(): void;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
mockWebsockets: MockWebSocket[];
|
||||
}
|
||||
}
|
||||
|
||||
function delayRequestToRoutes(routes: string[] = [], delay = 0) {
|
||||
cy.on('window:before:load', (win) => addDelay(win, routes, delay));
|
||||
}
|
||||
|
||||
Cypress.Commands.add('delayRequestToRoutes', delayRequestToRoutes);
|
||||
|
||||
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
const addDelay = (win: Cypress.AUTWindow, routes: string[], delay: number) => {
|
||||
const fetch = win.fetch;
|
||||
cy.stub(win, 'fetch').callsFake((...args: [RequestInfo | URL, RequestInit?]) => {
|
||||
for (let i = 0; i < routes.length; i++) {
|
||||
if (String(args[0]).includes(routes[i])) {
|
||||
return wait(delay).then(() => fetch(...args));
|
||||
}
|
||||
}
|
||||
|
||||
return fetch(...args);
|
||||
});
|
||||
};
|
||||
|
||||
// Websocket list to use with mockWebsockets
|
||||
window.mockWebsockets = [];
|
||||
|
||||
function mockWebsockets() {
|
||||
cy.on('window:before:load', (win) => mockWebsocketsFn(win));
|
||||
}
|
||||
|
||||
// Wrap websocket to be able to connect and close connections on demand
|
||||
Cypress.Commands.add('mockWebsockets', mockWebsockets);
|
||||
|
||||
const mockWebsocketsFn = (win: Cypress.AUTWindow) => {
|
||||
const RealWebSocket = WebSocket;
|
||||
cy.stub(win, 'WebSocket').callsFake((...args: ConstructorParameters<typeof WebSocket>) => {
|
||||
const mockWebSocket: MockWebSocket = {
|
||||
wrappedSocket: null as WebSocket | null,
|
||||
onopen: null as ((ev: Event) => void) | null,
|
||||
onmessage: null as ((ev: MessageEvent) => void) | null,
|
||||
onerror: null as ((ev: Event) => void) | null,
|
||||
onclose: null as ((ev: CloseEvent) => void) | null,
|
||||
send(data: string | ArrayBuffer) {
|
||||
if (this.wrappedSocket) {
|
||||
this.wrappedSocket.send(data);
|
||||
} else if (this.onerror) {
|
||||
this.onerror(new Event('error'));
|
||||
}
|
||||
},
|
||||
close() {
|
||||
if (this.wrappedSocket) {
|
||||
this.wrappedSocket.close(1000);
|
||||
}
|
||||
},
|
||||
connect() {
|
||||
const [param1, restOfParams] = args;
|
||||
this.wrappedSocket = new RealWebSocket(param1, restOfParams);
|
||||
this.wrappedSocket.onopen = this.onopen;
|
||||
this.wrappedSocket.onmessage = this.onmessage;
|
||||
this.wrappedSocket.onerror = this.onerror;
|
||||
this.wrappedSocket.onclose = this.onclose;
|
||||
},
|
||||
};
|
||||
window.mockWebsockets.push(mockWebSocket);
|
||||
return mockWebSocket;
|
||||
});
|
||||
};
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
delayRequestToRoutes(routes: string[], delay: number): ChainableT<void>;
|
||||
mockWebsockets: typeof mockWebsockets;
|
||||
}
|
||||
}
|
||||
}
|
||||
40
e2e-tests/cypress/tests/support/index.d.ts
vendored
Normal file
40
e2e-tests/cypress/tests/support/index.d.ts
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
declare namespace Cypress {
|
||||
type AdminConfig = import('@mattermost/types/config').AdminConfig;
|
||||
type AnalyticsRow = import('@mattermost/types/admin').AnalyticsRow;
|
||||
type Bot = import('@mattermost/types/bots').Bot;
|
||||
type BotPatch = import('@mattermost/types/bots').BotPatch;
|
||||
type Channel = import('@mattermost/types/channels').Channel;
|
||||
type ClusterInfo = import('@mattermost/types/admin').ClusterInfo;
|
||||
type Client = import('./client-impl').E2EClient;
|
||||
type ClientLicense = import('@mattermost/types/config').ClientLicense;
|
||||
type ChannelMembership = import('@mattermost/types/channels').ChannelMembership;
|
||||
type ChannelType = import('@mattermost/types/channels').ChannelType;
|
||||
type IncomingWebhook = import('@mattermost/types/integrations').IncomingWebhook;
|
||||
type OutgoingWebhook = import('@mattermost/types/integrations').OutgoingWebhook;
|
||||
type Permissions = string[];
|
||||
type PluginManifest = import('@mattermost/types/plugins').PluginManifest;
|
||||
type PluginsResponse = import('@mattermost/types/plugins').PluginsResponse;
|
||||
type PreferenceType = import('@mattermost/types/preferences').PreferenceType;
|
||||
type Product = import('@mattermost/types/cloud').Product;
|
||||
type Role = import('@mattermost/types/roles').Role;
|
||||
type Scheme = import('@mattermost/types/schemes').Scheme;
|
||||
type Session = import('@mattermost/types/sessions').Session;
|
||||
type Subscription = import('@mattermost/types/cloud').Subscription;
|
||||
type Team = import('@mattermost/types/teams').Team;
|
||||
type TeamMembership = import('@mattermost/types/teams').TeamMembership;
|
||||
type TermsOfService = import('@mattermost/types/terms_of_service').TermsOfService;
|
||||
type UserProfile = import('@mattermost/types/users').UserProfile;
|
||||
type UserStatus = import('@mattermost/types/users').UserStatus;
|
||||
type UserCustomStatus = import('@mattermost/types/users').UserCustomStatus;
|
||||
type UserAccessToken = import('@mattermost/types/users').UserAccessToken;
|
||||
type DeepPartial = import('@mattermost/types/utilities').DeepPartial;
|
||||
type Group = import('@mattermost/types/groups').Group;
|
||||
interface Chainable {
|
||||
tab: (options?: {shift?: boolean}) => Chainable<JQuery>;
|
||||
}
|
||||
}
|
||||
266
e2e-tests/cypress/tests/support/index.js
Normal file
266
e2e-tests/cypress/tests/support/index.js
Normal file
@@ -0,0 +1,266 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// ***********************************************************
|
||||
// Read more at: https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import localforage from 'localforage';
|
||||
import '@testing-library/cypress/add-commands';
|
||||
import 'cypress-file-upload';
|
||||
import 'cypress-wait-until';
|
||||
import 'cypress-plugin-tab';
|
||||
import 'cypress-real-events';
|
||||
import addContext from 'mochawesome/addContext';
|
||||
|
||||
import './api';
|
||||
import './api_commands'; // soon to deprecate
|
||||
import './client';
|
||||
import './common_login_commands';
|
||||
import './db_commands';
|
||||
import './email';
|
||||
import './external_commands';
|
||||
import './extended_commands';
|
||||
import './fetch_commands';
|
||||
import './keycloak_commands';
|
||||
import './ldap_commands';
|
||||
import './ldap_server_commands';
|
||||
import './notification_commands';
|
||||
import './okta_commands';
|
||||
import './saml_commands';
|
||||
import './shell';
|
||||
import './task_commands';
|
||||
import './ui';
|
||||
import './ui_commands'; // soon to deprecate
|
||||
import {DEFAULT_TEAM} from './constants';
|
||||
import {getDefaultConfig} from './api/system';
|
||||
|
||||
Cypress.dayjs = dayjs;
|
||||
|
||||
Cypress.on('test:after:run', (test, runnable) => {
|
||||
// Only if the test is failed do we want to add
|
||||
// the additional context of the screenshot.
|
||||
if (test.state === 'failed') {
|
||||
let parentNames = '';
|
||||
|
||||
// Define our starting parent
|
||||
let parent = runnable.parent;
|
||||
|
||||
// If the test failed due to a hook, we have to handle
|
||||
// getting our starting parent to form the correct filename.
|
||||
if (test.failedFromHookId) {
|
||||
// Failed from hook Id is always something like 'h2'
|
||||
// We just need the trailing number to match with parent id
|
||||
const hookId = test.failedFromHookId.split('')[1];
|
||||
|
||||
// If the current parentId does not match our hook id
|
||||
// start digging upwards until we get the parent that
|
||||
// has the same hook id, or until we get to a tile of ''
|
||||
// (which means we are at the top level)
|
||||
if (parent.id !== `r${hookId}`) {
|
||||
while (parent.parent && parent.parent.id !== `r${hookId}`) {
|
||||
if (parent.title === '') {
|
||||
// If we have a title of '' we have reached the top parent
|
||||
break;
|
||||
} else {
|
||||
parent = parent.parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now we can go from parent to parent to generate the screenshot filename
|
||||
while (parent) {
|
||||
// Only append parents that have actual content for their titles
|
||||
if (parent.title !== '') {
|
||||
parentNames = parent.title + ' -- ' + parentNames;
|
||||
}
|
||||
|
||||
parent = parent.parent;
|
||||
}
|
||||
|
||||
// Clean up strings of characters that Cypress strips out
|
||||
const charactersToStrip = /[;:"<>/]/g;
|
||||
parentNames = parentNames.replace(charactersToStrip, '');
|
||||
const testTitle = test.title.replace(charactersToStrip, '');
|
||||
|
||||
// If the test has a hook name, that means it failed due to a hook
|
||||
// and consequently Cypress appends some text to the file name
|
||||
const hookName = test.hookName ? ' -- ' + test.hookName + ' hook' : '';
|
||||
|
||||
const filename = encodeURIComponent(`${parentNames}${testTitle}${hookName} (failed).png`);
|
||||
|
||||
// Add context to the mochawesome report which includes the screenshot
|
||||
addContext({test}, {
|
||||
title: 'Failing Screenshot: >> screenshots/' + Cypress.spec.name + '/' + filename,
|
||||
value: 'screenshots/' + Cypress.spec.name + '/' + filename,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Turn off all uncaught exception handling
|
||||
Cypress.on('uncaught:exception', () => {
|
||||
return false;
|
||||
});
|
||||
|
||||
before(() => {
|
||||
// # Clear localforage state
|
||||
localforage.clear();
|
||||
|
||||
// # Try to login using existing sysadmin account
|
||||
cy.apiAdminLogin({failOnStatusCode: false}).then((response) => {
|
||||
if (response.user) {
|
||||
sysadminSetup(response.user);
|
||||
} else {
|
||||
// # Create and login a newly created user as sysadmin
|
||||
cy.apiCreateAdmin().then(({sysadmin}) => {
|
||||
cy.apiAdminLogin().then(() => sysadminSetup(sysadmin));
|
||||
});
|
||||
}
|
||||
|
||||
switch (Cypress.expose('serverEdition')) {
|
||||
case 'Cloud':
|
||||
cy.apiRequireLicenseForFeature('Cloud');
|
||||
break;
|
||||
case 'E20':
|
||||
cy.apiRequireLicense();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (Cypress.expose('serverClusterEnabled')) {
|
||||
cy.log('Checking cluster information...');
|
||||
|
||||
// * Ensure cluster is set up properly when enabled
|
||||
cy.shouldHaveClusterEnabled();
|
||||
cy.apiGetClusterStatus().then(({clusterInfo}) => {
|
||||
const sameCount = clusterInfo?.length === Cypress.expose('serverClusterHostCount');
|
||||
expect(sameCount, sameCount ? '' : `Should match number of hosts in a cluster as expected. Got "${clusterInfo?.length}" but expected "${Cypress.expose('serverClusterHostCount')}"`).to.equal(true);
|
||||
|
||||
clusterInfo.forEach((info) => cy.log(`hostname: ${info.hostname}, version: ${info.version}, config_hash: ${info.config_hash}`));
|
||||
});
|
||||
}
|
||||
|
||||
// Log license status and server details before test
|
||||
printLicenseStatus();
|
||||
printServerDetails();
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// Temporary fix for error related to this.get('prev') being undefined with @testing-library/cypress@9.0.0
|
||||
cy.then(() => null);
|
||||
});
|
||||
|
||||
function printLicenseStatus() {
|
||||
cy.apiGetClientLicense().then(({license}) => {
|
||||
cy.log(`Server License:
|
||||
- IsLicensed = ${license.IsLicensed}
|
||||
- IsTrial = ${license.IsTrial}
|
||||
- SkuName = ${license.SkuName}
|
||||
- SkuShortName = ${license.SkuShortName}
|
||||
- Cloud = ${license.Cloud}
|
||||
- Users = ${license.Users}`);
|
||||
});
|
||||
}
|
||||
|
||||
function printServerDetails() {
|
||||
cy.apiGetConfig(true).then(({config}) => {
|
||||
cy.log(`Build Info:
|
||||
- BuildNumber = ${config.BuildNumber}
|
||||
- BuildDate = ${config.BuildDate}
|
||||
- Version = ${config.Version}
|
||||
- BuildHash = ${config.BuildHash}
|
||||
- BuildHashEnterprise = ${config.BuildHashEnterprise}
|
||||
- BuildEnterpriseReady = ${config.BuildEnterpriseReady}
|
||||
- TelemetryId = ${config.TelemetryId}
|
||||
- ServiceEnvironment = ${config.ServiceEnvironment}`);
|
||||
});
|
||||
cy.apiGetConfig().then(({config}) => {
|
||||
cy.log(`Notable Server Config:
|
||||
- ServiceSettings.EnableSecurityFixAlert = ${config.ServiceSettings.EnableSecurityFixAlert}
|
||||
- LogSettings.EnableDiagnostics = ${config.LogSettings?.EnableDiagnostics}`);
|
||||
});
|
||||
}
|
||||
|
||||
function sysadminSetup(user) {
|
||||
if (Cypress.expose('firstTest')) {
|
||||
// Sends dummy call to update the config to server
|
||||
// Without this, first call to `cy.apiUpdateConfig()` consistently getting time out error in CI against remote server.
|
||||
cy.externalRequest({user, method: 'put', path: 'config', data: getDefaultConfig(), failOnStatusCode: false});
|
||||
}
|
||||
|
||||
// # Reset config to default
|
||||
cy.apiUpdateConfig();
|
||||
|
||||
if (!user.email_verified) {
|
||||
cy.apiVerifyUserEmailById(user.id);
|
||||
}
|
||||
|
||||
// # Reset admin preference, online status and locale
|
||||
resetUserPreference(user.id);
|
||||
cy.apiUpdateUserStatus('online');
|
||||
cy.apiPatchMe({
|
||||
locale: 'en',
|
||||
timezone: {automaticTimezone: '', manualTimezone: 'UTC', useAutomaticTimezone: 'false'},
|
||||
});
|
||||
|
||||
// # Reset roles
|
||||
cy.apiGetClientLicense().then(({isLicensed}) => {
|
||||
if (isLicensed) {
|
||||
cy.apiResetRoles();
|
||||
}
|
||||
});
|
||||
|
||||
// # Disable plugins not included in prepackaged
|
||||
cy.apiDisableNonPrepackagedPlugins();
|
||||
|
||||
// # Deactivate test bots if any
|
||||
cy.apiDeactivateTestBots();
|
||||
|
||||
// # Disable welcome tours if any
|
||||
cy.apiDisableTutorials(user.id);
|
||||
|
||||
// # Check if default team is present; create if not found.
|
||||
cy.apiGetTeamsForUser().then(({teams}) => {
|
||||
const defaultTeam = teams && teams.length > 0 && teams.find((team) => team.name === DEFAULT_TEAM.name);
|
||||
|
||||
if (!defaultTeam) {
|
||||
cy.apiCreateTeam(DEFAULT_TEAM.name, DEFAULT_TEAM.display_name, 'O', false);
|
||||
} else if (defaultTeam && Cypress.expose('resetBeforeTest')) {
|
||||
teams.forEach((team) => {
|
||||
if (team.name !== DEFAULT_TEAM.name) {
|
||||
cy.apiDeleteTeam(team.id);
|
||||
}
|
||||
});
|
||||
|
||||
cy.apiGetChannelsForUser('me', defaultTeam.id).then(({channels}) => {
|
||||
channels.forEach((channel) => {
|
||||
if (
|
||||
(channel.team_id === defaultTeam.id || channel.team_name === defaultTeam.name) &&
|
||||
(channel.name !== 'town-square' && channel.name !== 'off-topic')
|
||||
) {
|
||||
cy.apiDeleteChannel(channel.id);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function resetUserPreference(userId) {
|
||||
cy.apiSaveTeammateNameDisplayPreference('username');
|
||||
cy.apiSaveMessageDisplayPreference('clean');
|
||||
cy.apiSaveLinkPreviewsPreference('true');
|
||||
cy.apiSaveCollapsePreviewsPreference('false');
|
||||
cy.apiSaveClockDisplayModeTo24HourPreference(false);
|
||||
cy.apiSaveTutorialStep(userId, '999');
|
||||
cy.apiSaveOnboardingTaskListPreference(userId, 'onboarding_task_list_open', 'false');
|
||||
cy.apiSaveOnboardingTaskListPreference(userId, 'onboarding_task_list_show', 'false');
|
||||
cy.apiSaveCloudTrialBannerPreference(userId, 'trial', 'max_days_banner');
|
||||
cy.apiSaveSkipStepsPreference(userId, 'true');
|
||||
cy.apiSaveStartTrialModal(userId, 'true');
|
||||
cy.apiSaveUnreadScrollPositionPreference(userId, 'start_from_left_off');
|
||||
}
|
||||
445
e2e-tests/cypress/tests/support/keycloak_commands.ts
Normal file
445
e2e-tests/cypress/tests/support/keycloak_commands.ts
Normal file
@@ -0,0 +1,445 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import * as TIMEOUTS from '../fixtures/timeouts';
|
||||
|
||||
import type {LdapUser} from './ldap_server_commands';
|
||||
|
||||
import type {ChainableT} from '@/types';
|
||||
|
||||
const {
|
||||
keycloakBaseUrl,
|
||||
keycloakAppName,
|
||||
} = Cypress.expose();
|
||||
|
||||
const baseUrl = `${keycloakBaseUrl}/auth/admin/realms/${keycloakAppName}`;
|
||||
const loginUrl = `${keycloakBaseUrl}/auth/realms/master/protocol/openid-connect/token`;
|
||||
|
||||
function buildProfile(user: LdapUser) {
|
||||
return {
|
||||
firstName: user.firstname,
|
||||
lastName: user.lastname,
|
||||
email: user.email,
|
||||
username: user.username,
|
||||
enabled: true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* keycloakGetAccessTokenAPI is a task wrapped as command with post-verification
|
||||
* that an Access Token is successfully retrieved
|
||||
* @returns {string} - access token
|
||||
*/
|
||||
function keycloakGetAccessTokenAPI(): ChainableT<string> {
|
||||
return cy.task('keycloakRequest', {
|
||||
baseUrl: loginUrl,
|
||||
path: '',
|
||||
method: 'post',
|
||||
headers: {'Content-type': 'application/x-www-form-urlencoded'},
|
||||
data: 'grant_type=password&username=mmuser&password=mostest_password&client_id=admin-cli',
|
||||
// cy.task() returns untyped data
|
||||
}).then((response: any) => {
|
||||
expect(response.status).to.equal(200);
|
||||
const token: string = response.data.access_token;
|
||||
return cy.wrap(token);
|
||||
});
|
||||
}
|
||||
|
||||
Cypress.Commands.add('keycloakGetAccessTokenAPI', keycloakGetAccessTokenAPI);
|
||||
|
||||
/**
|
||||
* keycloakCreateUserAPI is a task wrapped as command with post-verification
|
||||
* that a user is successfully created in keycloak
|
||||
* @param {string} accessToken - a valid access token
|
||||
* @param {object} user - a keycloak user object to create
|
||||
*
|
||||
* @example
|
||||
* cy.keycloakCreateUserAPI('abcde', {firstName: 'test', lastName: 'test', email: 'test', username: 'test', enabled: true,});
|
||||
*/
|
||||
function keycloakCreateUserAPI(accessToken: string, user: LdapUser): ChainableT {
|
||||
const profile = buildProfile(user);
|
||||
return cy.task('keycloakRequest', {
|
||||
baseUrl,
|
||||
path: 'users',
|
||||
method: 'post',
|
||||
data: profile,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
// cy.task() returns untyped data
|
||||
}).then((response: any) => {
|
||||
expect(response.status).to.equal(201);
|
||||
});
|
||||
}
|
||||
|
||||
Cypress.Commands.add('keycloakCreateUserAPI', keycloakCreateUserAPI);
|
||||
|
||||
/**
|
||||
* keycloakResetPasswordAPI is a task wrapped as command with post-verification
|
||||
* that a user password is successfully reset in keycloak
|
||||
* @param {string} accessToken - a valid access token
|
||||
* @param {string} userId - a keycloak userId
|
||||
* @param {string} password - new password to set
|
||||
*
|
||||
* @example
|
||||
* cy.keycloakResetPasswordAPI('abcde', '12345', 'password');
|
||||
*/
|
||||
function keycloakResetPasswordAPI(accessToken: string, userId: string, password: string): ChainableT {
|
||||
return cy.task('keycloakRequest', {
|
||||
baseUrl,
|
||||
path: `users/${userId}/reset-password`,
|
||||
method: 'put',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
data: {type: 'password', temporary: false, value: password},
|
||||
// cy.task() returns untyped data
|
||||
}).then((response: any) => {
|
||||
if (response.status === 200 && response.data.length > 0) {
|
||||
return cy.wrap(response.data[0].id);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
Cypress.Commands.add('keycloakResetPasswordAPI', keycloakResetPasswordAPI);
|
||||
|
||||
/**
|
||||
* keycloakGetUserAPI is a task wrapped as command with post-verification
|
||||
* that a user is successfully found in keycloak
|
||||
* @param {string} accessToken - a valid access token
|
||||
* @param {string} email - an email to query
|
||||
* @returns {string} - keycloak userId if found
|
||||
*
|
||||
* @example
|
||||
* cy.keycloakGetUserAPI('abcde', 'test@mm.com');
|
||||
*/
|
||||
function keycloakGetUserAPI(accessToken: string, email: string): ChainableT<string> {
|
||||
return cy.task('keycloakRequest', {
|
||||
baseUrl,
|
||||
path: 'users?email=' + email,
|
||||
method: 'get',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
// cy.task() returns untyped data
|
||||
}).then((response: any) => {
|
||||
if (response.status === 200 && response.data.length > 0) {
|
||||
return cy.wrap<string>(response.data[0].id);
|
||||
}
|
||||
return cy.wrap(null as unknown as string);
|
||||
});
|
||||
}
|
||||
|
||||
Cypress.Commands.add('keycloakGetUserAPI', keycloakGetUserAPI);
|
||||
|
||||
/**
|
||||
* keycloakDeleteUserAPI is a task wrapped as command with post-verification
|
||||
* that a user is successfully deleted in keycloak
|
||||
* @param {string} accessToken - a valid access token
|
||||
* @param {string} userId - keycloak user id to delete
|
||||
*
|
||||
* @example
|
||||
* cy.keycloakDeleteUserAPI('abcde', '12345');
|
||||
*/
|
||||
function keycloakDeleteUserAPI(accessToken: string, userId: string): ChainableT {
|
||||
return cy.task('keycloakRequest', {
|
||||
baseUrl,
|
||||
path: `users/${userId}`,
|
||||
method: 'delete',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
// cy.task() returns untyped data
|
||||
}).then((response: any) => {
|
||||
expect(response.status).to.equal(204);
|
||||
});
|
||||
}
|
||||
Cypress.Commands.add('keycloakDeleteUserAPI', keycloakDeleteUserAPI);
|
||||
|
||||
/**
|
||||
* keycloakUpdateUserAPI is a task wrapped as command with post-verification
|
||||
* that a user is successfully updated in keycloak
|
||||
* @param {string} accessToken - a valid access token
|
||||
* @param {string} userId - keycloak user id to delete
|
||||
* @param {object} data - keycloak user object
|
||||
*
|
||||
* @example
|
||||
* cy.keycloakUpdateUserAPI('abcde', '12345', {'enabled': false}});
|
||||
*/
|
||||
function keycloakUpdateUserAPI(accessToken: string, userId: string, data: Record<string, unknown>): ChainableT {
|
||||
return cy.task('keycloakRequest', {
|
||||
baseUrl,
|
||||
path: 'users/' + userId,
|
||||
method: 'put',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
data,
|
||||
// cy.task() returns untyped data
|
||||
}).then((response: any) => {
|
||||
expect(response.status).to.equal(204);
|
||||
});
|
||||
}
|
||||
Cypress.Commands.add('keycloakUpdateUserAPI', keycloakUpdateUserAPI);
|
||||
|
||||
/**
|
||||
* keycloakDeleteSessionAPI is a task wrapped as command with post-verification
|
||||
* that a users session is successfully deleted in keycloak
|
||||
* @param {string} accessToken - a valid access token
|
||||
* @param {string} sessionId- keycloak session id to delete
|
||||
*
|
||||
* @example
|
||||
* cy.keycloakDeleteSessionAPI('abcde', '12345');
|
||||
*/
|
||||
function keycloakDeleteSessionAPI(accessToken: string, sessionId: string): ChainableT {
|
||||
return cy.task('keycloakRequest', {
|
||||
baseUrl,
|
||||
path: `sessions/${sessionId}`,
|
||||
method: 'delete',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
// cy.task() returns untyped data
|
||||
}).then((delResponse: any) => {
|
||||
expect(delResponse.status).to.equal(204);
|
||||
});
|
||||
}
|
||||
|
||||
Cypress.Commands.add('keycloakDeleteSessionAPI', keycloakDeleteSessionAPI);
|
||||
|
||||
/**
|
||||
* keycloakGetUserSessionsAPI is a task wrapped as command with post-verification
|
||||
* that a users sessions are successfully found
|
||||
* @param {string} accessToken - a valid access token
|
||||
* @param {string} userId - keycloak user id to find sessions
|
||||
* @returns {string[]} - array of keycloak session ids
|
||||
*
|
||||
* @example
|
||||
* cy.keycloakGetUserSessionsAPI('abcde', '12345');
|
||||
*/
|
||||
function keycloakGetUserSessionsAPI(accessToken: string, userId: string): ChainableT<string[]> {
|
||||
return cy.task('keycloakRequest', {
|
||||
baseUrl,
|
||||
path: `users/${userId}/sessions`,
|
||||
method: 'get',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
// cy.task() returns untyped data
|
||||
}).then((response: any) => {
|
||||
expect(response.status).to.equal(200);
|
||||
expect(response.data);
|
||||
return cy.wrap<string[]>(response.data);
|
||||
});
|
||||
}
|
||||
|
||||
Cypress.Commands.add('keycloakGetUserSessionsAPI', keycloakGetUserSessionsAPI);
|
||||
|
||||
/**
|
||||
* keycloakDeleteUserSessions is a command that finds a user's sessions
|
||||
* and deletes them.
|
||||
* @param {string} accessToken - a valid access token
|
||||
* @param {string} userId- keycloak user id to delete sessions
|
||||
*
|
||||
* @example
|
||||
* cy.keycloakDeleteUserSessions('abcde', '12345');
|
||||
*/
|
||||
function keycloakDeleteUserSessions(accessToken: string, userId: string): ChainableT {
|
||||
return cy.keycloakGetUserSessionsAPI(accessToken, userId).then((responseData) => {
|
||||
if (responseData.length > 0) {
|
||||
responseData.forEach((sessionId) => {
|
||||
cy.keycloakDeleteSessionAPI(accessToken, sessionId);
|
||||
});
|
||||
|
||||
// Ensure we clear out these specific cookies
|
||||
['JSESSIONID'].forEach((cookie) => {
|
||||
cy.clearCookie(cookie);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Cypress.Commands.add('keycloakDeleteUserSessions', keycloakDeleteUserSessions);
|
||||
|
||||
/**
|
||||
* keycloakResetUsers is a command that "resets" (deletes and re-creates) the users.
|
||||
* @param {object[]} users - an array of user objects
|
||||
*
|
||||
* @example
|
||||
* cy.keycloakResetUsers([{firstName: 'test', lastName: 'test', email: 'test', username: 'test', enabled: true}]);
|
||||
*/
|
||||
function keycloakResetUsers(users: LdapUser[]): ChainableT {
|
||||
return cy.keycloakGetAccessTokenAPI().then((accessToken) => {
|
||||
users.forEach((_user) => {
|
||||
cy.keycloakGetUserAPI(accessToken, _user.email).then((userId) => {
|
||||
if (userId) {
|
||||
cy.keycloakDeleteUserAPI(accessToken, userId);
|
||||
}
|
||||
}).then(() => {
|
||||
cy.keycloakCreateUser(accessToken, _user).then((_id) => {
|
||||
_user.keycloakId = _id;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Cypress.Commands.add('keycloakResetUsers', keycloakResetUsers);
|
||||
|
||||
/**
|
||||
* keycloakCreateUser is a command that creates a keycloak user.
|
||||
* @param {User} user - a user object
|
||||
*
|
||||
* @example
|
||||
* cy.keycloakCreateUser({firstName: 'test', lastName: 'test', email: 'test', username: 'test', enabled: true});
|
||||
*/
|
||||
function keycloakCreateUser(accessToken: string, user: LdapUser): ChainableT {
|
||||
return cy.keycloakCreateUserAPI(accessToken, user).then(() => {
|
||||
cy.keycloakGetUserAPI(accessToken, user.email).then((newId) => {
|
||||
cy.keycloakResetPasswordAPI(accessToken, newId, user.password).then(() => {
|
||||
cy.keycloakDeleteUserSessions(accessToken, newId).then(() => {
|
||||
return cy.wrap(newId);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
Cypress.Commands.add('keycloakCreateUser', keycloakCreateUser);
|
||||
|
||||
/**
|
||||
* keycloakCreateUsers is a command that creates keycloak users.
|
||||
* @param {User[]} users - an array of users
|
||||
*
|
||||
* @example
|
||||
* cy.keycloakCreateUsers(users);
|
||||
*/
|
||||
function keycloakCreateUsers(users: LdapUser[] = []) {
|
||||
return cy.keycloakGetAccessTokenAPI().then((accessToken) => {
|
||||
return users.forEach((user) => {
|
||||
return cy.keycloakCreateUser(accessToken, user);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Cypress.Commands.add('keycloakCreateUsers', keycloakCreateUsers);
|
||||
|
||||
/**
|
||||
* keycloakUpdateUser is a command that updates a keycloak user data.
|
||||
* @param {string} userEmail - the user email
|
||||
* @param {Record<string, unknown>} data - the user data to update
|
||||
*
|
||||
* @example
|
||||
* cy.keycloakUpdateUser('user@example.com', {firstName: 'test', lastName: 'test'});
|
||||
*/
|
||||
function keycloakUpdateUser(userEmail: string, data: Record<string, unknown>) {
|
||||
return cy.keycloakGetAccessTokenAPI().then((accessToken) => {
|
||||
return cy.keycloakGetUserAPI(accessToken, userEmail).then((userId) => {
|
||||
return cy.keycloakUpdateUserAPI(accessToken, userId, data);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Cypress.Commands.add('keycloakUpdateUser', keycloakUpdateUser);
|
||||
|
||||
/**
|
||||
* keycloakSuspendUser is a command that suspends a user (enabled=false)
|
||||
* @param {string} userEmail - email of keycloak user
|
||||
*
|
||||
* @example
|
||||
* cy.keycloakSuspendUser('user@test.com');
|
||||
*/
|
||||
function keycloakSuspendUser(userEmail: string) {
|
||||
const data = {enabled: false};
|
||||
cy.keycloakUpdateUser(userEmail, data);
|
||||
}
|
||||
|
||||
Cypress.Commands.add('keycloakSuspendUser', keycloakSuspendUser);
|
||||
|
||||
/**
|
||||
* keycloakUnsuspendUser is a command that re-activates a user (enabled=true)
|
||||
* @param {string} userEmail - email of keycloak user
|
||||
*
|
||||
* @example
|
||||
* cy.keycloakUnsuspendUser('user@test.com');
|
||||
*/
|
||||
function keycloakUnsuspendUser(userEmail: string): ChainableT {
|
||||
const data = {enabled: true};
|
||||
return cy.keycloakUpdateUser(userEmail, data);
|
||||
}
|
||||
|
||||
Cypress.Commands.add('keycloakUnsuspendUser', keycloakUnsuspendUser);
|
||||
|
||||
/**
|
||||
* checkKeycloakLoginPage is a command that verifies the keycloak login page is displayed
|
||||
*
|
||||
* @example
|
||||
* cy.checkKeycloakLoginPage();
|
||||
*/
|
||||
function checkKeycloakLoginPage() {
|
||||
cy.findByText('Username or email', {timeout: TIMEOUTS.ONE_SEC}).should('be.visible');
|
||||
cy.findByText('Password').should('be.visible');
|
||||
cy.findAllByText('Log In').should('be.visible');
|
||||
}
|
||||
|
||||
Cypress.Commands.add('checkKeycloakLoginPage', checkKeycloakLoginPage);
|
||||
|
||||
/**
|
||||
* doKeycloakLogin is a command that attempts to log a user into keycloak.
|
||||
*
|
||||
* @example
|
||||
* cy.doKeycloakLogin();
|
||||
*/
|
||||
function doKeycloakLogin(user: LdapUser) {
|
||||
cy.apiLogout();
|
||||
cy.visit('/login');
|
||||
cy.findByText('SAML').click();
|
||||
cy.findByText('Username or email').type(user.email);
|
||||
cy.findByText('Password').type(user.password);
|
||||
cy.findAllByText('Log In').last().click();
|
||||
}
|
||||
|
||||
Cypress.Commands.add('doKeycloakLogin', doKeycloakLogin);
|
||||
|
||||
/**
|
||||
* verifyKeycloakLoginFailed is a command that verifies a keycloak login failed.
|
||||
*
|
||||
* @example
|
||||
* cy.verifyKeycloakLoginFailed();
|
||||
*/
|
||||
function verifyKeycloakLoginFailed() {
|
||||
cy.findAllByText('Account is disabled, contact your administrator.').should('be.visible');
|
||||
}
|
||||
|
||||
Cypress.Commands.add('verifyKeycloakLoginFailed', verifyKeycloakLoginFailed);
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
keycloakCreateUsers: typeof keycloakCreateUsers;
|
||||
keycloakUpdateUser: typeof keycloakUpdateUser;
|
||||
keycloakGetAccessTokenAPI: typeof keycloakGetAccessTokenAPI;
|
||||
keycloakCreateUserAPI: typeof keycloakCreateUserAPI;
|
||||
keycloakResetPasswordAPI: typeof keycloakResetPasswordAPI;
|
||||
keycloakGetUserAPI: typeof keycloakGetUserAPI;
|
||||
keycloakDeleteUserAPI: typeof keycloakDeleteUserAPI;
|
||||
keycloakUpdateUserAPI: typeof keycloakUpdateUserAPI;
|
||||
keycloakDeleteSessionAPI: typeof keycloakDeleteSessionAPI;
|
||||
keycloakGetUserSessionsAPI: typeof keycloakGetUserSessionsAPI;
|
||||
keycloakDeleteUserSessions: typeof keycloakDeleteUserSessions;
|
||||
keycloakResetUsers: typeof keycloakResetUsers;
|
||||
keycloakCreateUser: typeof keycloakCreateUser;
|
||||
keycloakSuspendUser(userEmail: string): ChainableT<void>;
|
||||
keycloakUnsuspendUser: typeof keycloakUnsuspendUser;
|
||||
checkKeycloakLoginPage: typeof checkKeycloakLoginPage;
|
||||
doKeycloakLogin(user: LdapUser): ChainableT<void>;
|
||||
verifyKeycloakLoginFailed: typeof verifyKeycloakLoginFailed;
|
||||
}
|
||||
}
|
||||
}
|
||||
60
e2e-tests/cypress/tests/support/ldap_commands.d.ts
vendored
Normal file
60
e2e-tests/cypress/tests/support/ldap_commands.d.ts
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
// 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 {
|
||||
|
||||
/**
|
||||
* runLdapSync is a task that runs an external request to run an ldap sync job.
|
||||
* it then waits for the ldap sync job to complete.
|
||||
* @param {UserProfile} admin - an admin user
|
||||
* @returns {boolean} - true if sync run successfully
|
||||
*/
|
||||
runLdapSync(admin: UserProfile): boolean;
|
||||
|
||||
/**
|
||||
* getLdapSyncJobStatus is a task that runs an external request for ldap_sync job status
|
||||
* @param {number} start - start time of the job.
|
||||
* @returns {string} - current status of job
|
||||
*/
|
||||
getLdapSyncJobStatus(start: number): string;
|
||||
|
||||
/**
|
||||
* doLDAPLogin is a task that runs LDAP login
|
||||
* @param {object} settings - login settings
|
||||
* @param {boolean} useEmail - true if uses email
|
||||
*/
|
||||
doLDAPLogin(settings: object = {}, useEmail = false): Chainable<void>;
|
||||
|
||||
/**
|
||||
* doLDAPLogout is a task that runs LDAP logout
|
||||
* @param {Object} settings - logout settings
|
||||
*/
|
||||
doLDAPLogout(settings: object = {}): Chainable<void>;
|
||||
|
||||
/**
|
||||
* visitLDAPSettings is a task that navigates to LDAP settings Page
|
||||
*/
|
||||
visitLDAPSettings(): Chainable<void>;
|
||||
|
||||
/**
|
||||
* waitForLdapSyncCompletion is a task that runs recursively
|
||||
* until getLdapSyncJobStatus completes or timeouts.
|
||||
* @param {number} start - start time of the job.
|
||||
* @param {number} timeout - the maxmimum time to wait for the job to complete
|
||||
*/
|
||||
waitForLdapSyncCompletion(start: number, timeout: number): void;
|
||||
}
|
||||
}
|
||||
94
e2e-tests/cypress/tests/support/ldap_commands.js
Normal file
94
e2e-tests/cypress/tests/support/ldap_commands.js
Normal file
@@ -0,0 +1,94 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import * as TIMEOUTS from '../fixtures/timeouts';
|
||||
|
||||
import {getAdminAccount} from './env';
|
||||
|
||||
Cypress.Commands.add('visitLDAPSettings', () => {
|
||||
// # Go to LDAP settings Page
|
||||
cy.visit('/admin_console/authentication/ldap');
|
||||
cy.get('.admin-console__header').should('be.visible').and('have.text', 'AD/LDAP Wizard');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('doLDAPLogin', (settings = {}, useEmail = false) => {
|
||||
// # Go to login page
|
||||
cy.apiLogout();
|
||||
cy.visit('/login');
|
||||
cy.wait(TIMEOUTS.FIVE_SEC);
|
||||
cy.checkLoginPage(settings);
|
||||
cy.performLDAPLogin(settings, useEmail);
|
||||
});
|
||||
|
||||
Cypress.Commands.add('performLDAPLogin', (settings = {}, useEmail = false) => {
|
||||
const loginId = useEmail ? settings.user.email : settings.user.username;
|
||||
cy.get('#input_loginId').type(loginId);
|
||||
cy.get('#input_password-input').type(settings.user.password);
|
||||
|
||||
//click the login button
|
||||
cy.get('#saveSetting').should('not.be.disabled').click();
|
||||
});
|
||||
|
||||
Cypress.Commands.add('doLDAPLogout', (settings = {}) => {
|
||||
cy.checkLeftSideBar(settings);
|
||||
|
||||
// # Logout then check login page
|
||||
cy.uiLogout();
|
||||
cy.checkLoginPage(settings);
|
||||
});
|
||||
|
||||
Cypress.Commands.add('doSkipTutorial', () => {
|
||||
cy.wait(TIMEOUTS.FIVE_SEC);
|
||||
cy.get('body').then((body) => {
|
||||
if (body.find('#tutorialSkipLink').length > 0) {
|
||||
cy.get('#tutorialSkipLink').click().wait(TIMEOUTS.HALF_SEC);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('runLdapSync', (admin) => {
|
||||
cy.externalRequest({user: admin, method: 'post', path: 'ldap/sync'}).then(() => {
|
||||
cy.waitForLdapSyncCompletion(Date.now(), TIMEOUTS.THREE_MIN).then(() => {
|
||||
return cy.wrap(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('getLdapSyncJobStatus', (start) => {
|
||||
const admin = getAdminAccount();
|
||||
cy.externalRequest({user: admin, method: 'get', path: 'jobs/type/ldap_sync'}).then((result) => {
|
||||
const jobs = result.data;
|
||||
if (jobs && jobs[0]) {
|
||||
if (Math.abs(jobs[0].create_at - start) < TIMEOUTS.TWO_SEC) {
|
||||
switch (jobs[0].status) {
|
||||
case 'success':
|
||||
return cy.wrap('success');
|
||||
case 'pending':
|
||||
case 'in_progress':
|
||||
return cy.wrap('pending');
|
||||
default:
|
||||
return cy.wrap('unsuccessful');
|
||||
}
|
||||
}
|
||||
}
|
||||
return cy.wrap('not found');
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('waitForLdapSyncCompletion', (start, timeout) => {
|
||||
if (Date.now() - start > timeout) {
|
||||
throw new Error('Timeout Waiting for LdapSync');
|
||||
}
|
||||
|
||||
cy.getLdapSyncJobStatus(start).then((status) => {
|
||||
if (status === 'success') {
|
||||
return;
|
||||
}
|
||||
if (status === 'unsuccessful') {
|
||||
throw new Error('LdapSync Unsuccessful');
|
||||
}
|
||||
|
||||
cy.wait(TIMEOUTS.FIVE_SEC);
|
||||
cy.waitForLdapSyncCompletion(start, timeout);
|
||||
});
|
||||
});
|
||||
151
e2e-tests/cypress/tests/support/ldap_server_commands.ts
Normal file
151
e2e-tests/cypress/tests/support/ldap_server_commands.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {getRandomId} from '../utils';
|
||||
|
||||
import type {ChainableT} from '@/types';
|
||||
|
||||
const ldapTmpFolder = 'ldap_tmp';
|
||||
|
||||
export interface LdapUser {
|
||||
username: string;
|
||||
password: string;
|
||||
email: string;
|
||||
firstname: string;
|
||||
lastname: string;
|
||||
ldapfirstname: string;
|
||||
ldaplastname: string;
|
||||
keycloakId: string;
|
||||
}
|
||||
|
||||
function modifyLDAPUsers(filename: string) {
|
||||
cy.exec(`ldapmodify -x -D "cn=admin,dc=mm,dc=test,dc=com" -w mostest_password -H ldap://${Cypress.expose('ldapServer')}:${Cypress.expose('ldapPort')} -f tests/fixtures/${filename} -c`, {failOnNonZeroExit: false});
|
||||
}
|
||||
|
||||
Cypress.Commands.add('modifyLDAPUsers', modifyLDAPUsers);
|
||||
|
||||
function resetLDAPUsers() {
|
||||
cy.modifyLDAPUsers('ldap-reset-data.ldif');
|
||||
}
|
||||
|
||||
Cypress.Commands.add('resetLDAPUsers', resetLDAPUsers);
|
||||
|
||||
function createLDAPUser({prefix = 'ldap', user = null as LdapUser | null} = {}): ChainableT<LdapUser> {
|
||||
const ldapUser = user || generateLDAPUser(prefix);
|
||||
const data = generateContent(ldapUser);
|
||||
const filename = `new_user_${Date.now()}.ldif`;
|
||||
const filePath = `tests/fixtures/${ldapTmpFolder}/${filename}`;
|
||||
|
||||
cy.task('writeToFile', ({filename, fixturesFolder: ldapTmpFolder, data}));
|
||||
|
||||
return cy.ldapAdd(filePath).then(() => {
|
||||
return cy.wrap<LdapUser>(ldapUser);
|
||||
});
|
||||
}
|
||||
|
||||
Cypress.Commands.add('createLDAPUser', createLDAPUser);
|
||||
|
||||
function updateLDAPUser(user: Partial<LdapUser>) {
|
||||
const data = generateContent(user, true);
|
||||
const filename = `update_user_${Date.now()}.ldif`;
|
||||
const filePath = `tests/fixtures/${ldapTmpFolder}/${filename}`;
|
||||
|
||||
cy.task('writeToFile', ({filename, fixturesFolder: ldapTmpFolder, data}));
|
||||
|
||||
return cy.ldapModify(filePath).then(() => {
|
||||
return cy.wrap(user);
|
||||
});
|
||||
}
|
||||
|
||||
Cypress.Commands.add('updateLDAPUser', updateLDAPUser);
|
||||
|
||||
function ldapAdd(filePath: string) {
|
||||
const {host, bindDn, password} = getLDAPCredentials();
|
||||
|
||||
return cy.exec(
|
||||
`ldapadd -x -D "${bindDn}" -w ${password} -H ${host} -f ${filePath} -c`,
|
||||
{failOnNonZeroExit: false},
|
||||
).then(({exitCode, stdout, stderr}) => {
|
||||
cy.log(`ldapadd code: ${exitCode}, stdout: ${stdout}, stderr: ${stderr}`);
|
||||
});
|
||||
}
|
||||
|
||||
Cypress.Commands.add('ldapAdd', ldapAdd);
|
||||
|
||||
function ldapModify(filePath: string) {
|
||||
const {host, bindDn, password} = getLDAPCredentials();
|
||||
|
||||
return cy.exec(
|
||||
`ldapmodify -x -D "${bindDn}" -w ${password} -H ${host} -f ${filePath} -c`,
|
||||
{failOnNonZeroExit: false},
|
||||
).then(({exitCode, stdout, stderr}) => {
|
||||
cy.log(`ldapmodify code: ${exitCode}, stdout: ${stdout}, stderr: ${stderr}`);
|
||||
});
|
||||
}
|
||||
|
||||
Cypress.Commands.add('ldapModify', ldapModify);
|
||||
|
||||
function getLDAPCredentials() {
|
||||
const host = `ldap://${Cypress.expose('ldapServer')}:${Cypress.expose('ldapPort')}`;
|
||||
const bindDn = 'cn=admin,dc=mm,dc=test,dc=com';
|
||||
const password = 'mostest_password';
|
||||
|
||||
return {host, bindDn, password};
|
||||
}
|
||||
|
||||
export function generateLDAPUser(prefix = 'ldap'): LdapUser {
|
||||
const randomId = getRandomId();
|
||||
const username = `${prefix}user${randomId}`;
|
||||
|
||||
return <LdapUser>{
|
||||
username,
|
||||
password: 'Password1',
|
||||
email: `${username}@mmtest.com`,
|
||||
firstname: `Firstname-${randomId}`,
|
||||
lastname: `Lastname-${randomId}`,
|
||||
ldapfirstname: `${prefix.toUpperCase()}Firstname-${randomId}`,
|
||||
ldaplastname: `${prefix.toUpperCase()}Lastname-${randomId}`,
|
||||
keycloakId: '',
|
||||
};
|
||||
}
|
||||
|
||||
function generateContent(user: Partial<LdapUser> = {}, isUpdate = false) {
|
||||
let deleteContent = '';
|
||||
if (isUpdate) {
|
||||
deleteContent = `dn: uid=${user.username},ou=e2etest,dc=mm,dc=test,dc=com
|
||||
changetype: delete
|
||||
`;
|
||||
}
|
||||
|
||||
return `
|
||||
${deleteContent}
|
||||
|
||||
dn: ou=e2etest,dc=mm,dc=test,dc=com
|
||||
changetype: add
|
||||
objectclass: organizationalunit
|
||||
|
||||
# generic test users
|
||||
dn: uid=${user.username},ou=e2etest,dc=mm,dc=test,dc=com
|
||||
changetype: add
|
||||
objectclass: iNetOrgPerson
|
||||
cn: ${user.firstname}
|
||||
sn: ${user.lastname}
|
||||
uid: ${user.username}
|
||||
mail: ${user.email}
|
||||
userPassword: Password1
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
modifyLDAPUsers: typeof modifyLDAPUsers;
|
||||
resetLDAPUsers: typeof resetLDAPUsers;
|
||||
createLDAPUser: typeof createLDAPUser;
|
||||
updateLDAPUser: typeof updateLDAPUser;
|
||||
ldapAdd: typeof ldapAdd;
|
||||
ldapModify: typeof ldapModify;
|
||||
}
|
||||
}
|
||||
}
|
||||
19
e2e-tests/cypress/tests/support/notification.ts
Normal file
19
e2e-tests/cypress/tests/support/notification.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// Stub the browser notification API with the given name and permission
|
||||
export function spyNotificationAs(name: string, permission: NotificationPermission) {
|
||||
cy.window().then((win) => {
|
||||
const stub = cy.stub().as(name);
|
||||
Object.assign(stub, {
|
||||
permission,
|
||||
requestPermission: () => Promise.resolve(permission),
|
||||
});
|
||||
win.Notification = stub as unknown as typeof Notification;
|
||||
});
|
||||
|
||||
cy.window().should((win) => {
|
||||
expect(win.Notification).to.exist;
|
||||
expect(win.Notification.permission).to.equal(permission);
|
||||
});
|
||||
}
|
||||
48
e2e-tests/cypress/tests/support/notification_commands.ts
Normal file
48
e2e-tests/cypress/tests/support/notification_commands.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import * as TIMEOUTS from '../fixtures/timeouts';
|
||||
|
||||
/**
|
||||
* permission can be 'granted', 'denied', or 'default'
|
||||
*/
|
||||
function stubNotificationPermission(permission: string) {
|
||||
cy.window().then((win) => {
|
||||
cy.stub(win.Notification, 'permission').value(permission);
|
||||
cy.stub(win.Notification, 'requestPermission').resolves(permission);
|
||||
cy.stub(win, 'Notification').as('notificationStub').callsFake(() => {
|
||||
return {
|
||||
onclick: cy.stub().as('notificationOnClick'),
|
||||
onerror: cy.stub().as('notificationOnError'),
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the system bot message was received
|
||||
*/
|
||||
function notificationMessage(notificationMessage: string) {
|
||||
// * Assert the unread count is correct
|
||||
cy.get('.SidebarLink:contains(system-bot)').find('#unreadMentions').as('unreadCount').should('be.visible').should('have.text', '1');
|
||||
cy.get('.SidebarLink:contains(system-bot)').find('.Avatar').should('exist').click().wait(TIMEOUTS.HALF_SEC);
|
||||
cy.get('@unreadCount').should('not.exist');
|
||||
|
||||
// * Assert the notification message
|
||||
cy.getLastPostId().then((postId) => {
|
||||
cy.get(`#postMessageText_${postId}`).scrollIntoView().should('be.visible').should('have.text', notificationMessage);
|
||||
});
|
||||
}
|
||||
|
||||
Cypress.Commands.add('stubNotificationPermission', stubNotificationPermission);
|
||||
Cypress.Commands.add('verifySystemBotMessageRecieved', notificationMessage);
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
stubNotificationPermission: typeof stubNotificationPermission;
|
||||
verifySystemBotMessageRecieved: typeof notificationMessage;
|
||||
}
|
||||
}
|
||||
}
|
||||
367
e2e-tests/cypress/tests/support/okta_commands.ts
Normal file
367
e2e-tests/cypress/tests/support/okta_commands.ts
Normal file
@@ -0,0 +1,367 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import * as TIMEOUTS from '../fixtures/timeouts';
|
||||
|
||||
import type {ChainableT} from '@/types';
|
||||
|
||||
const token = 'SSWS ' + Cypress.expose('oktaMMAppToken');
|
||||
|
||||
type UserId = string | undefined;
|
||||
|
||||
interface Profile {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
login: string;
|
||||
userType: string;
|
||||
isAdmin: string;
|
||||
isGuest: string;
|
||||
}
|
||||
|
||||
interface User {
|
||||
firstname: string;
|
||||
lastname: string;
|
||||
email: string;
|
||||
login: string;
|
||||
userType: string;
|
||||
isAdmin: string;
|
||||
isGuest: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface UserCollection {
|
||||
admins: Record<string, Partial<User>>;
|
||||
guests: Record<string, Partial<User>>;
|
||||
regulars: Record<string, Partial<User>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a user profile object
|
||||
* @param {Object} user - the user data
|
||||
* @returns {Object} profile: the user profile
|
||||
*/
|
||||
function buildProfile(user: User): Profile {
|
||||
const profile: Profile = {
|
||||
firstName: user.firstname,
|
||||
lastName: user.lastname,
|
||||
email: user.email,
|
||||
login: user.email,
|
||||
userType: user.userType,
|
||||
isAdmin: user.isAdmin,
|
||||
isGuest: user.isGuest,
|
||||
};
|
||||
return profile;
|
||||
}
|
||||
|
||||
/**
|
||||
* creates a user
|
||||
* @param {Object} user - the user to create
|
||||
* @returns {String} userId: the user id
|
||||
*/
|
||||
function oktaCreateUser(user: Partial<User> = {}): ChainableT<UserId> {
|
||||
const profile = buildProfile(user as User);
|
||||
return cy.task('oktaRequest', {
|
||||
baseUrl: Cypress.expose('oktaApiUrl'),
|
||||
urlSuffix: '/users/',
|
||||
method: 'post',
|
||||
token,
|
||||
data: {
|
||||
profile,
|
||||
credentials: {
|
||||
password: {value: user.password},
|
||||
recovery_question: {
|
||||
question: 'What is the best open source messaging platform for developers?',
|
||||
answer: 'Mattermost',
|
||||
},
|
||||
},
|
||||
},
|
||||
// cy.task() returns untyped data
|
||||
}).then((response: any) => {
|
||||
expect(response.status).to.equal(200);
|
||||
const userId = response.data.id;
|
||||
return cy.wrap(userId as UserId);
|
||||
});
|
||||
}
|
||||
|
||||
Cypress.Commands.add('oktaCreateUser', oktaCreateUser);
|
||||
|
||||
/**
|
||||
* gets a user by user id
|
||||
* @param {String} userId - the user id
|
||||
* @returns {String} userId: the user id or null if not found
|
||||
*/
|
||||
function oktaGetUser(userId: string = ''): ChainableT<UserId> {
|
||||
return cy.task('oktaRequest', {
|
||||
baseUrl: Cypress.expose('oktaApiUrl'),
|
||||
urlSuffix: '/users?q=' + userId,
|
||||
method: 'get',
|
||||
token,
|
||||
// cy.task() returns untyped data
|
||||
}).then((response: any) => {
|
||||
expect(response.status).to.be.equal(200);
|
||||
if (response.data.length > 0) {
|
||||
return cy.wrap(response.data[0].id as UserId);
|
||||
}
|
||||
return cy.wrap(undefined as UserId);
|
||||
});
|
||||
}
|
||||
|
||||
Cypress.Commands.add('oktaGetUser', oktaGetUser);
|
||||
|
||||
/**
|
||||
* Updates the user data
|
||||
* @param {String} userId - the user id
|
||||
* @param {Object} user - the user data
|
||||
* @returns {Object} data: the user data as a response
|
||||
*/
|
||||
function oktaUpdateUser(userId: string = '', user: Partial<User> = {}) {
|
||||
const profile = buildProfile(user as User);
|
||||
|
||||
return cy.task('oktaRequest', {
|
||||
baseUrl: Cypress.expose('oktaApiUrl'),
|
||||
urlSuffix: '/users/' + userId,
|
||||
method: 'post',
|
||||
token,
|
||||
data: {
|
||||
profile,
|
||||
},
|
||||
// cy.task() returns untyped data
|
||||
}).then((response: any) => {
|
||||
expect(response.status).to.equal(201);
|
||||
return cy.wrap(response.data);
|
||||
});
|
||||
}
|
||||
|
||||
Cypress.Commands.add('oktaUpdateUser', oktaUpdateUser);
|
||||
|
||||
/**
|
||||
* deletes a user by user id
|
||||
* @param {String} userId - the user id
|
||||
*/
|
||||
function oktaDeleteUser(userId: string = '') {
|
||||
// cy.task() returns untyped data
|
||||
cy.task('oktaRequest', {
|
||||
baseUrl: Cypress.expose('oktaApiUrl'),
|
||||
urlSuffix: '/users/' + userId,
|
||||
method: 'delete',
|
||||
token,
|
||||
}).then((response: any) => {
|
||||
expect(response.status).to.equal(204);
|
||||
cy.task('oktaRequest', {
|
||||
baseUrl: Cypress.expose('oktaApiUrl'),
|
||||
urlSuffix: '/users/' + userId,
|
||||
method: 'delete',
|
||||
token,
|
||||
}).then((_response: any) => {
|
||||
expect(_response.status).to.equal(204);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
//first we deactivate the user, then we actually delete it
|
||||
Cypress.Commands.add('oktaDeleteUser', oktaDeleteUser);
|
||||
|
||||
/**
|
||||
* deletes a user's session
|
||||
* @param {String} userId - the user id
|
||||
*/
|
||||
function oktaDeleteSession(userId: string = '') {
|
||||
// cy.task() returns untyped data
|
||||
cy.task('oktaRequest', {
|
||||
baseUrl: Cypress.expose('oktaApiUrl'),
|
||||
urlSuffix: '/users/' + userId + '/sessions',
|
||||
method: 'delete',
|
||||
token,
|
||||
}).then((response: any) => {
|
||||
expect(response.status).to.equal(204);
|
||||
|
||||
// Ensure we clear out these specific cookies
|
||||
['JSESSIONID'].forEach((cookie) => {
|
||||
cy.clearCookie(cookie);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Cypress.Commands.add('oktaDeleteSession', oktaDeleteSession);
|
||||
|
||||
/**
|
||||
* Assigns a user to an application
|
||||
* @param {String} userId - the user id
|
||||
* @param {Object} user - the user data
|
||||
* @returns {Object} data: the user data as response
|
||||
*/
|
||||
function oktaAssignUserToApplication(userId: string = '', user: Partial<User> = {}) {
|
||||
return cy.task('oktaRequest', {
|
||||
baseUrl: Cypress.expose('oktaApiUrl'),
|
||||
urlSuffix: '/apps/' + Cypress.expose('oktaMMAppId') + '/users',
|
||||
method: 'post',
|
||||
token,
|
||||
data: {
|
||||
id: userId,
|
||||
scope: 'USER',
|
||||
profile: {
|
||||
firstName: user.firstname,
|
||||
lastName: user.lastname,
|
||||
email: user.email,
|
||||
},
|
||||
},
|
||||
// cy.task() returns untyped data
|
||||
}).then((response: any) => {
|
||||
expect(response.status).to.be.equal(200);
|
||||
return cy.wrap(response.data);
|
||||
});
|
||||
}
|
||||
|
||||
Cypress.Commands.add('oktaAssignUserToApplication', oktaAssignUserToApplication);
|
||||
|
||||
/**
|
||||
* Gets the user data if exists or create one if does not exists
|
||||
* @param {Object} user - the user data
|
||||
* @returns {String} userId: the user id
|
||||
*/
|
||||
function oktaGetOrCreateUser(user: User): ChainableT<UserId> {
|
||||
let userId: UserId;
|
||||
return cy.oktaGetUser(user.email).then((uId) => {
|
||||
userId = uId;
|
||||
if (userId == null) {
|
||||
cy.oktaCreateUser(user).then((_uId) => {
|
||||
userId = _uId;
|
||||
cy.oktaAssignUserToApplication(userId as string, user);
|
||||
});
|
||||
} else {
|
||||
cy.oktaAssignUserToApplication(userId as string, user);
|
||||
}
|
||||
return cy.wrap(userId as UserId);
|
||||
});
|
||||
}
|
||||
|
||||
Cypress.Commands.add('oktaGetOrCreateUser', oktaGetOrCreateUser);
|
||||
|
||||
/**
|
||||
* Add users given a collection of users
|
||||
* @param {Object} users - a collection of users
|
||||
*/
|
||||
function oktaAddUsers(users: UserCollection) {
|
||||
let userId: UserId;
|
||||
Object.values(users.regulars).forEach((_user) => {
|
||||
cy.oktaGetUser(_user.email).then((uId) => {
|
||||
userId = uId;
|
||||
if (userId == null) {
|
||||
cy.oktaCreateUser(_user).then((_uId) => {
|
||||
userId = _uId;
|
||||
cy.oktaAssignUserToApplication(userId as string, _user);
|
||||
cy.oktaDeleteSession(userId as string);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Object.values(users.guests).forEach((_user) => {
|
||||
cy.oktaGetUser(_user.email).then((uId) => {
|
||||
userId = uId;
|
||||
if (userId == null) {
|
||||
cy.oktaCreateUser(_user).then((_uId) => {
|
||||
userId = _uId;
|
||||
cy.oktaAssignUserToApplication(userId as string, _user);
|
||||
cy.oktaDeleteSession(userId as string);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Object.values(users.admins).forEach((_user) => {
|
||||
cy.oktaGetUser(_user.email).then((uId) => {
|
||||
userId = uId;
|
||||
if (userId == null) {
|
||||
cy.oktaCreateUser(_user).then((_uId) => {
|
||||
userId = _uId;
|
||||
cy.oktaAssignUserToApplication(userId as string, _user);
|
||||
cy.oktaDeleteSession(userId as string);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Cypress.Commands.add('oktaAddUsers', oktaAddUsers);
|
||||
|
||||
/**
|
||||
* Remove users given a collection of users
|
||||
* @param {Object} users - a collection of users
|
||||
*/
|
||||
function oktaRemoveUsers(users: UserCollection) {
|
||||
let userId: UserId;
|
||||
Object.values(users.regulars).forEach((_user) => {
|
||||
cy.oktaGetUser(_user.email).then((_uId) => {
|
||||
userId = _uId;
|
||||
if (userId != null) {
|
||||
cy.oktaDeleteUser(userId as string);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Object.values(users.guests).forEach((_user) => {
|
||||
cy.oktaGetUser(_user.email).then((_uId) => {
|
||||
userId = _uId;
|
||||
if (userId != null) {
|
||||
cy.oktaDeleteUser(userId as string);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Object.values(users.admins).forEach((_user) => {
|
||||
cy.oktaGetUser(_user.email).then((_uId) => {
|
||||
userId = _uId;
|
||||
if (userId != null) {
|
||||
cy.oktaDeleteUser(userId as string);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Cypress.Commands.add('oktaRemoveUsers', oktaRemoveUsers);
|
||||
|
||||
/**
|
||||
* check if okta login page is visible
|
||||
*/
|
||||
function checkOktaLoginPage() {
|
||||
cy.findByText('Powered by').should('be.visible');
|
||||
cy.findAllByText('Sign In').should('be.visible');
|
||||
cy.get('#okta-signin-password').should('be.visible');
|
||||
cy.get('#okta-signin-submit').should('be.visible');
|
||||
}
|
||||
|
||||
Cypress.Commands.add('checkOktaLoginPage', checkOktaLoginPage);
|
||||
|
||||
/**
|
||||
* performs an okta login
|
||||
*/
|
||||
function doOktaLogin(user: User) {
|
||||
cy.checkOktaLoginPage();
|
||||
|
||||
cy.get('#okta-signin-username').type(user.email);
|
||||
cy.get('#okta-signin-password').type(user.password);
|
||||
cy.findAllByText('Sign In').last().click().wait(TIMEOUTS.FIVE_SEC);
|
||||
}
|
||||
|
||||
Cypress.Commands.add('doOktaLogin', doOktaLogin);
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
oktaCreateUser: typeof oktaCreateUser;
|
||||
oktaGetUser: typeof oktaGetUser;
|
||||
oktaUpdateUser: typeof oktaUpdateUser;
|
||||
oktaDeleteUser: typeof oktaDeleteUser;
|
||||
oktaDeleteSession: typeof oktaDeleteSession;
|
||||
oktaAssignUserToApplication: typeof oktaAssignUserToApplication;
|
||||
oktaGetOrCreateUser: typeof oktaGetOrCreateUser;
|
||||
oktaAddUsers: typeof oktaAddUsers;
|
||||
oktaRemoveUsers: typeof oktaRemoveUsers;
|
||||
checkOktaLoginPage: typeof checkOktaLoginPage;
|
||||
doOktaLogin(user: User): ChainableT<void>;
|
||||
}
|
||||
}
|
||||
}
|
||||
126
e2e-tests/cypress/tests/support/saml_commands.ts
Normal file
126
e2e-tests/cypress/tests/support/saml_commands.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {AdminConfig} from '@mattermost/types/config';
|
||||
|
||||
import * as TIMEOUTS from '../fixtures/timeouts';
|
||||
import type {ChainableT} from '../types';
|
||||
import {stubClipboard} from '../utils';
|
||||
|
||||
// SAMLUser interface is based on cypress/tests/fixtures/saml_users.json
|
||||
interface SAMLUser {
|
||||
username: string;
|
||||
password: string;
|
||||
email: string;
|
||||
firstname: string;
|
||||
lastname: string;
|
||||
userType: string;
|
||||
isGuest?: boolean;
|
||||
}
|
||||
|
||||
interface TestSettings {
|
||||
loginButtonText: string;
|
||||
siteName: string;
|
||||
siteUrl: string;
|
||||
teamName: string;
|
||||
user: SAMLUser | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* checkCreateTeamPage checks that the "create a team" element is visible in the page if user is not a Guest.
|
||||
* Otherwise it should not exist.
|
||||
* @param {TestSettings} settings - Settings object
|
||||
*/
|
||||
function checkCreateTeamPage(settings: TestSettings) {
|
||||
if (settings.user!.userType === 'Guest' || settings.user!.isGuest) {
|
||||
cy.findByText('Create a team').scrollIntoView().should('not.exist');
|
||||
} else {
|
||||
cy.findByText('Create a team').scrollIntoView().should('be.visible');
|
||||
}
|
||||
}
|
||||
|
||||
Cypress.Commands.add('checkCreateTeamPage', checkCreateTeamPage);
|
||||
|
||||
/**
|
||||
* doSamlLogin check that the login page is loaded with the correct settings (siteName) and logs in using SAML.
|
||||
* @param {TestSettings} settings - Settings object to perform SAML tests.
|
||||
*/
|
||||
function doSamlLogin(settings: TestSettings) {
|
||||
// # Go to login page
|
||||
cy.apiLogout();
|
||||
cy.visit('/login');
|
||||
cy.checkLoginPage(settings);
|
||||
|
||||
//click the login button
|
||||
return cy.findByText(settings.loginButtonText).should('be.visible').click().wait(TIMEOUTS.ONE_SEC);
|
||||
}
|
||||
|
||||
Cypress.Commands.add('doSamlLogin', doSamlLogin);
|
||||
|
||||
/**
|
||||
* doSamlLogout logs out and checks that it reloads the login page with the correct settings (siteName).
|
||||
* @param {TestSettings} settings - Settings object to perform SAML tests.
|
||||
*/
|
||||
function doSamlLogout(settings: TestSettings) {
|
||||
cy.checkLeftSideBar(settings);
|
||||
|
||||
// # Logout then check login page
|
||||
cy.uiLogout();
|
||||
return cy.checkLoginPage(settings);
|
||||
}
|
||||
|
||||
Cypress.Commands.add('doSamlLogout', doSamlLogout);
|
||||
|
||||
/**
|
||||
* getInvitePeopleLink gets the invite people link from the invite people modal
|
||||
* @param {TestSettings} settings - Settings object to perform SAML tests.
|
||||
* @returns {ChainableT<string>} - the invite people link wrapped in a cypress chainable
|
||||
*/
|
||||
function getInvitePeopleLink(settings: TestSettings) {
|
||||
cy.checkLeftSideBar(settings);
|
||||
|
||||
// # Open team menu and click 'Invite People'
|
||||
cy.uiOpenTeamMenu('Invite people');
|
||||
|
||||
stubClipboard().as('clipboard');
|
||||
cy.checkInvitePeoplePage();
|
||||
cy.findByTestId('InviteView__copyInviteLink').click();
|
||||
return cy.get('@clipboard').its('contents').then((text) => {
|
||||
// # Close Invite People modal
|
||||
cy.uiClose();
|
||||
return cy.wrap(text);
|
||||
});
|
||||
}
|
||||
|
||||
Cypress.Commands.add('getInvitePeopleLink', getInvitePeopleLink);
|
||||
|
||||
/**
|
||||
* setTestSettings sets the test settings object based on the AdminConfig
|
||||
* @param {string} loginButtonText - The text of the login button
|
||||
* @param {AdminConfig} config - The config object
|
||||
* @returns {TestSettings} - The settings to use for SAML tests
|
||||
*/
|
||||
function setTestSettings(loginButtonText: string, config: AdminConfig): ChainableT<TestSettings> {
|
||||
return cy.wrap<TestSettings>({
|
||||
loginButtonText,
|
||||
siteName: config.TeamSettings.SiteName,
|
||||
siteUrl: config.ServiceSettings.SiteURL,
|
||||
teamName: '',
|
||||
user: null,
|
||||
});
|
||||
}
|
||||
|
||||
Cypress.Commands.add('setTestSettings', setTestSettings);
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
checkCreateTeamPage: typeof checkCreateTeamPage;
|
||||
doSamlLogin: typeof doSamlLogin;
|
||||
doSamlLogout: typeof doSamlLogout;
|
||||
getInvitePeopleLink: typeof getInvitePeopleLink;
|
||||
setTestSettings: typeof setTestSettings;
|
||||
}
|
||||
}
|
||||
}
|
||||
56
e2e-tests/cypress/tests/support/shell.d.ts
vendored
Normal file
56
e2e-tests/cypress/tests/support/shell.d.ts
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
// 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 {
|
||||
|
||||
/**
|
||||
* Find file/s similar to "find" shell command
|
||||
* Extends find of shelljs, https://github.com/shelljs/shelljs#findpath--path-
|
||||
*
|
||||
* @param {string} path - file path
|
||||
* @param {RegExp} pattern - pattern to match with
|
||||
*
|
||||
* @example
|
||||
* cy.shellFind('path', '/file.xml/').then((files) => {
|
||||
* // do something with files
|
||||
* });
|
||||
*/
|
||||
shellFind(path: string, pattern: RegExp): Chainable;
|
||||
|
||||
/**
|
||||
* Remove file/s similar to "rm" shell command
|
||||
* Extends rm of shelljs, https://github.com/shelljs/shelljs#rmoptions-file--file-
|
||||
*
|
||||
* @param {string} option - ex. -rf
|
||||
* @param {string} file - file/pattern to remove
|
||||
*
|
||||
* @example
|
||||
* cy.shellRm('-rf', 'file.png');
|
||||
*/
|
||||
shellRm(option: string, file: string): Chainable;
|
||||
|
||||
/**
|
||||
* Unzip source file into a target folder
|
||||
*
|
||||
* @param {string} source - source file
|
||||
* @param {string} target - target folder
|
||||
*
|
||||
* @example
|
||||
* cy.shellUnzip('source.zip', 'target-folder');
|
||||
*/
|
||||
shellUnzip(source: string, target: string): Chainable;
|
||||
}
|
||||
}
|
||||
14
e2e-tests/cypress/tests/support/shell.js
Normal file
14
e2e-tests/cypress/tests/support/shell.js
Normal file
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
Cypress.Commands.add('shellFind', (path, pattern) => {
|
||||
return cy.task('shellFind', {path, pattern});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('shellRm', (option, file) => {
|
||||
return cy.task('shellRm', {option, file});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('shellUnzip', (source, target) => {
|
||||
return cy.task('shellUnzip', {source, target});
|
||||
});
|
||||
317
e2e-tests/cypress/tests/support/task_commands.ts
Normal file
317
e2e-tests/cypress/tests/support/task_commands.ts
Normal file
@@ -0,0 +1,317 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {type AxiosResponse} from 'axios';
|
||||
|
||||
import type {ChainableT} from '../types';
|
||||
|
||||
/**
|
||||
* postMessageAs is a task which is wrapped as command with post-verification
|
||||
* that a message is successfully posted by the user/sender
|
||||
* @param {Object} sender - a user object who will post a message
|
||||
* @param {String} message - message in a post
|
||||
* @param {Object} channelId - where a post will be posted
|
||||
*/
|
||||
|
||||
export interface PostMessageResp {
|
||||
id: string;
|
||||
status: number;
|
||||
// Post data from the API response
|
||||
data: Record<string, any>;
|
||||
}
|
||||
|
||||
interface PostMessageArg {
|
||||
sender: {
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
message: string;
|
||||
channelId: string;
|
||||
rootId?: string;
|
||||
createAt?: number;
|
||||
}
|
||||
|
||||
interface PostMessageRequest {
|
||||
token: string;
|
||||
message: string;
|
||||
props?: Record<string, unknown>;
|
||||
channelId: string;
|
||||
rootId?: string;
|
||||
createAt?: number;
|
||||
failOnStatus?: boolean;
|
||||
}
|
||||
|
||||
function postMessageAs(arg: PostMessageArg): ChainableT<PostMessageResp> {
|
||||
const {sender, message, channelId, rootId, createAt} = arg;
|
||||
const baseUrl = Cypress.config('baseUrl');
|
||||
|
||||
// cy.task() returns untyped data
|
||||
return cy.task('postMessageAs', {sender, message, channelId, rootId, createAt, baseUrl}).then((response: any) => {
|
||||
const {status, data} = response;
|
||||
expect(status).to.equal(201);
|
||||
|
||||
// # Return the data so it can be interacted in a test
|
||||
return cy.wrap({id: data.id, status, data});
|
||||
});
|
||||
}
|
||||
Cypress.Commands.add('postMessageAs', postMessageAs);
|
||||
|
||||
/**
|
||||
* @param {string} [numberOfMessages = 30] - Number of messages
|
||||
* @param {Object} sender - a user object who will post a message
|
||||
* @param {String} message - message in a post
|
||||
* @param {Object} channelId - where a post will be posted
|
||||
*/
|
||||
|
||||
function postListOfMessages({numberOfMessages = 30, ...rest}): ChainableT<unknown> {
|
||||
const baseUrl = Cypress.config('baseUrl');
|
||||
|
||||
// Cast needed: Cypress.Commands.add requires cast for overloaded commands
|
||||
return (cy as any).
|
||||
task('postListOfMessages', {numberOfMessages, baseUrl, ...rest}, {timeout: numberOfMessages * 200}).
|
||||
each((message: {status: number}) => expect(message.status).to.equal(201));
|
||||
}
|
||||
|
||||
Cypress.Commands.add('postListOfMessages', postListOfMessages);
|
||||
|
||||
/**
|
||||
* reactToMessageAs is a task wrapped as command with post-verification
|
||||
* that a reaction is added successfully to a message by a user/sender
|
||||
* @param {Object} sender - a user object who will post a message
|
||||
* @param {String} postId - post on which reaction is intended
|
||||
* @param {String} reaction - emoji text eg. smile
|
||||
*/
|
||||
Cypress.Commands.add('reactToMessageAs', ({sender, postId, reaction}) => {
|
||||
const baseUrl = Cypress.config('baseUrl');
|
||||
|
||||
// cy.task() returns untyped data
|
||||
return cy.task('reactToMessageAs', {sender, postId, reaction, baseUrl}).then(({status, data}: any) => {
|
||||
expect(status).to.equal(200);
|
||||
|
||||
// # Return the response after reaction is added
|
||||
return cy.wrap({status, data});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* postIncomingWebhook is a task which is wrapped as command with post-verification
|
||||
* that the incoming webhook is successfully posted
|
||||
* @param {String} url - incoming webhook URL
|
||||
* @param {Object} data - payload on incoming webhook
|
||||
*/
|
||||
|
||||
interface WebhookData {
|
||||
text?: string;
|
||||
attachments?: Array<Record<string, unknown> & {pretext?: string}>;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
function postIncomingWebhook({url, data, waitFor}: {
|
||||
url: string;
|
||||
data: WebhookData;
|
||||
waitFor?: string;
|
||||
}): ChainableT {
|
||||
cy.task('postIncomingWebhook', {url, data}).its('status').should('be.equal', 200);
|
||||
|
||||
if (!waitFor) {
|
||||
return cy.wrap(undefined) as ChainableT;
|
||||
}
|
||||
|
||||
return cy.waitUntil(() => cy.getLastPost().then((el) => {
|
||||
switch (waitFor) {
|
||||
case 'text': {
|
||||
const textEl = el.find('.post-message__text > p')[0];
|
||||
return Boolean(textEl && data.text && textEl.textContent.includes(data.text));
|
||||
}
|
||||
case 'attachment-pretext': {
|
||||
const pretext = data.attachments?.[0]?.pretext;
|
||||
if (!pretext) {
|
||||
return false;
|
||||
}
|
||||
const mmBlocksText = el.find('.mm-blocks-text');
|
||||
if (mmBlocksText.length) {
|
||||
return Array.from(mmBlocksText).some((node) => node.textContent?.includes(pretext));
|
||||
}
|
||||
const attachmentPretextEl = el.find('.attachment__thumb-pretext > p')[0];
|
||||
return Boolean(attachmentPretextEl && attachmentPretextEl.textContent.includes(pretext));
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
Cypress.Commands.add('postIncomingWebhook', postIncomingWebhook);
|
||||
|
||||
interface ExternalRequestArg<T> {
|
||||
user: Record<string, unknown>;
|
||||
method: string;
|
||||
path: string;
|
||||
data?: T;
|
||||
failOnStatusCode?: boolean;
|
||||
}
|
||||
function externalRequest<T=unknown, U=unknown>(arg: ExternalRequestArg<U>): ChainableT<Pick<AxiosResponse<T>, 'data' | 'status'>> {
|
||||
const {user, method, path, data, failOnStatusCode = true} = arg;
|
||||
const baseUrl = Cypress.config('baseUrl');
|
||||
|
||||
// cy.task() returns untyped data
|
||||
return cy.task('externalRequest', {baseUrl, user, method, path, data}).then((response: any) => {
|
||||
// Temporarily ignore error related to Cloud
|
||||
const cloudErrorId = [
|
||||
'ent.cloud.request_error',
|
||||
'api.cloud.get_subscription.error',
|
||||
];
|
||||
|
||||
if (response.data && !cloudErrorId.includes(response.data.id) && failOnStatusCode) {
|
||||
expect(response.status).to.be.oneOf([200, 201, 204]);
|
||||
}
|
||||
|
||||
return cy.wrap(response as Pick<AxiosResponse<T>, 'data' | 'status'>);
|
||||
}) as ChainableT<Pick<AxiosResponse<T>, 'data' | 'status'>>;
|
||||
}
|
||||
// Cypress.Commands.add requires cast for overloaded commands
|
||||
Cypress.Commands.add('externalRequest', externalRequest as any);
|
||||
|
||||
/**
|
||||
* postMessageAs is a task which is wrapped as command with post-verification
|
||||
* that a message is successfully posted by the bot
|
||||
* @param {String} message - message in a post
|
||||
* @param {Object} channelId - where a post will be posted
|
||||
*/
|
||||
|
||||
function postBotMessage({token, message, props, channelId, rootId, createAt, failOnStatus = true}: PostMessageRequest): ChainableT<PostMessageResp> {
|
||||
const baseUrl = Cypress.config('baseUrl');
|
||||
|
||||
// cy.task() returns untyped data
|
||||
return cy.task('postBotMessage', {token, message, props, channelId, rootId, createAt, baseUrl}).then(({status, data}: any) => {
|
||||
if (failOnStatus) {
|
||||
expect(status).to.equal(201);
|
||||
}
|
||||
|
||||
// # Return the data so it can be interacted in a test
|
||||
return cy.wrap({id: data.id, status, data});
|
||||
});
|
||||
}
|
||||
|
||||
Cypress.Commands.add('postBotMessage', postBotMessage);
|
||||
|
||||
/**
|
||||
* urlHealthCheck is a task wrapped as command that checks whether
|
||||
* a URL is healthy and reachable.
|
||||
* @param {String} name - name of service to check
|
||||
* @param {String} url - URL to check
|
||||
* @param {String} helperMessage - a message to display on error to help resolve the issue
|
||||
* @param {String} method - a request using a specific method
|
||||
* @param {String} httpStatus - expected HTTP status
|
||||
*/
|
||||
|
||||
function urlHealthCheck({name, url, helperMessage, method, httpStatus}: {name: string; url: string; helperMessage: string; method: string; httpStatus: number}): ChainableT {
|
||||
Cypress.log({name, message: `Checking URL health at ${url}`});
|
||||
|
||||
// cy.task() returns untyped data
|
||||
return cy.task('urlHealthCheck', {url, method}).then(({data, errorCode, status, success}: any) => {
|
||||
const urlService = `__${name}__ at ${url}`;
|
||||
|
||||
const successMessage = success ? `${urlService}: reachable` : `${errorCode}: The test you're running requires ${urlService} to be reachable. \n${helperMessage}`;
|
||||
expect(success, successMessage).to.equal(true);
|
||||
|
||||
const statusMessage = status === httpStatus ? `${urlService}: responded with ${status} HTTP status` : `${urlService}: expected to respond with ${httpStatus} but got ${status} HTTP status`;
|
||||
expect(status, statusMessage).to.equal(httpStatus);
|
||||
|
||||
return cy.wrap({data, status});
|
||||
});
|
||||
}
|
||||
|
||||
Cypress.Commands.add('urlHealthCheck', urlHealthCheck);
|
||||
|
||||
Cypress.Commands.add('requireWebhookServer', () => {
|
||||
const baseUrl = Cypress.config('baseUrl');
|
||||
const webhookBaseUrl = Cypress.expose('webhookBaseUrl');
|
||||
const adminUsername = Cypress.expose('adminUsername');
|
||||
const adminPassword = Cypress.expose('adminPassword');
|
||||
const helperMessage = `
|
||||
__Tips:__
|
||||
1. In local development, you may run "__npm run start:webhook__" at "/e2e" folder.
|
||||
2. If reachable from remote host, you may export it as env variable, like "__CYPRESS_webhookBaseUrl=[url] npm run cypress:open__".
|
||||
`;
|
||||
|
||||
cy.urlHealthCheck({
|
||||
name: 'Webhook Server',
|
||||
url: webhookBaseUrl,
|
||||
helperMessage,
|
||||
method: 'get',
|
||||
httpStatus: 200,
|
||||
});
|
||||
|
||||
cy.task('postIncomingWebhook', {
|
||||
url: `${webhookBaseUrl}/setup`,
|
||||
data: {
|
||||
baseUrl,
|
||||
webhookBaseUrl,
|
||||
adminUsername,
|
||||
adminPassword,
|
||||
}}).
|
||||
its('status').should('be.equal', 201);
|
||||
});
|
||||
|
||||
Cypress.Commands.overwrite('log', (subject, message) => cy.task('log', message));
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
|
||||
/**
|
||||
* externalRequest is a task which is wrapped as command with post-verification
|
||||
* that the external request is successfully completed
|
||||
* @param {Object} options
|
||||
* @param {<UserProfile, 'username' | 'password'>} options.user - a user initiating external request
|
||||
* @param {String} options.method - an HTTP method (e.g. get, post, etc)
|
||||
* @param {String} options.path - API path that is relative to Cypress.config().baseUrl
|
||||
* @param {Object} options.data - payload
|
||||
* @param {Boolean} options.failOnStatusCode - whether to fail on status code, default is true
|
||||
*
|
||||
* @example
|
||||
* cy.externalRequest({user: sysadmin, method: 'POST', path: 'config', data});
|
||||
*/
|
||||
externalRequest(options?: {
|
||||
user: Pick<UserProfile, 'username' | 'password'>;
|
||||
method: string;
|
||||
path: string;
|
||||
data?: Record<string, unknown> | unknown[];
|
||||
failOnStatusCode?: boolean;
|
||||
}): Chainable<Pick<AxiosResponse, 'data' | 'status'>>;
|
||||
|
||||
/**
|
||||
* Adds a given reaction to a specific post from a user
|
||||
* @param {Object} reactToMessageObject - Information on person and post to which a reaction needs to be added
|
||||
* @param {Object} reactToMessageObject.sender - a user object who will post a message
|
||||
* @param {string} reactToMessageObject.postId - post on which reaction is intended
|
||||
* @param {string} reactToMessageObject.reaction - emoji text eg. smile
|
||||
* @returns {Response} response: Cypress-chainable response
|
||||
*
|
||||
* @example
|
||||
* cy.reactToMessageAs({sender:user2, postId:"ABC123", reaction: 'smile'});
|
||||
*/
|
||||
reactToMessageAs({sender, postId, reaction}: {sender: Record<string, unknown>; postId: string; reaction: string}): Chainable<{status: number; data: unknown}>;
|
||||
|
||||
/**
|
||||
* Verify that the webhook server is accessible, and then sets up base URLs and credential.
|
||||
*
|
||||
* @example
|
||||
* cy.requireWebhookServer();
|
||||
*/
|
||||
requireWebhookServer(): Chainable;
|
||||
|
||||
postMessageAs: typeof postMessageAs;
|
||||
|
||||
postListOfMessages: typeof postListOfMessages;
|
||||
|
||||
postIncomingWebhook: typeof postIncomingWebhook;
|
||||
|
||||
postBotMessage: typeof postBotMessage;
|
||||
|
||||
urlHealthCheck: typeof urlHealthCheck;
|
||||
}
|
||||
}
|
||||
}
|
||||
59
e2e-tests/cypress/tests/support/ui/account_settings_modal.d.ts
vendored
Normal file
59
e2e-tests/cypress/tests/support/ui/account_settings_modal.d.ts
vendored
Normal file
@@ -0,0 +1,59 @@
|
||||
// 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 `ui` prefix, e.g. `uiOpenProfileModal`.
|
||||
// ***************************************************************
|
||||
|
||||
declare namespace Cypress {
|
||||
interface Chainable {
|
||||
|
||||
/**
|
||||
* Open the profile settings modal
|
||||
* @param {string} section - such as `'General'`, `'Security'`, `'Notifications'`, `'Display'`, `'Sidebar'` and `'Advanced'`
|
||||
* @return the "#accountSettingsModal"
|
||||
*
|
||||
* @example
|
||||
* cy.uiOpenProfileModal('Profile Settings').within(() => {
|
||||
* // Do something here
|
||||
* });
|
||||
*/
|
||||
uiOpenProfileModal(section: string): Chainable<JQuery<HTMLElement>>;
|
||||
|
||||
/**
|
||||
* Close the profile settings modal given that the modal itself is opened.
|
||||
*
|
||||
* @example
|
||||
* cy.uiCloseAccountSettingsModal();
|
||||
*/
|
||||
uiCloseAccountSettingsModal(): Chainable;
|
||||
|
||||
/**
|
||||
* Navigate to profile settings and verify the user's first, last name
|
||||
* @param {String} firstname - expected user firstname
|
||||
* @param {String} lastname - expected user lastname
|
||||
*/
|
||||
verifyAccountNameSettings(firstname: string, lastname: string): Chainable;
|
||||
|
||||
/**
|
||||
* Navigate to account display settings and change collapsed reply threads setting
|
||||
* @param {String} setting - ON or OFF
|
||||
*/
|
||||
uiChangeCRTDisplaySetting(setting: string): Chainable;
|
||||
|
||||
/**
|
||||
* Navigate to account display settings and change message display setting
|
||||
* @param {String} setting - COMPACT or STANDARD
|
||||
*/
|
||||
uiChangeMessageDisplaySetting(setting: string): Chainable;
|
||||
}
|
||||
}
|
||||
60
e2e-tests/cypress/tests/support/ui/account_settings_modal.js
Normal file
60
e2e-tests/cypress/tests/support/ui/account_settings_modal.js
Normal file
@@ -0,0 +1,60 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
Cypress.Commands.add('uiOpenProfileModal', (section = '') => {
|
||||
// # Open profile settings modal
|
||||
cy.uiOpenUserMenu('Profile');
|
||||
|
||||
const profileSettingsModal = () => cy.findByRole('dialog', {name: 'Profile'}).should('be.visible');
|
||||
|
||||
if (!section) {
|
||||
return profileSettingsModal();
|
||||
}
|
||||
|
||||
// # Click on a particular section
|
||||
cy.findByRoleExtended('tab', {name: section}).should('be.visible').click();
|
||||
|
||||
return profileSettingsModal();
|
||||
});
|
||||
|
||||
Cypress.Commands.add('verifyAccountNameSettings', (firstname, lastname) => {
|
||||
// # Go to Profile
|
||||
cy.uiOpenProfileModal('Profile Settings');
|
||||
|
||||
// * Check name value
|
||||
cy.get('#nameDesc').should('have.text', `${firstname} ${lastname}`);
|
||||
cy.uiClose();
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiChangeGenericDisplaySetting', (setting, option) => {
|
||||
cy.uiOpenSettingsModal('Display');
|
||||
cy.get(setting).scrollIntoView();
|
||||
cy.get(setting).click();
|
||||
cy.get('.section-max').scrollIntoView();
|
||||
|
||||
cy.get(option).check().should('be.checked');
|
||||
|
||||
cy.uiSaveAndClose();
|
||||
});
|
||||
|
||||
/*
|
||||
* Change the message display setting
|
||||
* @param {String} setting - as 'STANDARD' or 'COMPACT'
|
||||
*/
|
||||
Cypress.Commands.add('uiChangeMessageDisplaySetting', (setting = 'STANDARD') => {
|
||||
const SETTINGS = {STANDARD: '#message_displayFormatA', COMPACT: '#message_displayFormatB'};
|
||||
cy.uiChangeGenericDisplaySetting('#message_displayTitle', SETTINGS[setting]);
|
||||
});
|
||||
|
||||
/*
|
||||
* Change the collapsed reply threads display setting
|
||||
* @param {String} setting - as 'OFF' or 'ON'
|
||||
*/
|
||||
Cypress.Commands.add('uiChangeCRTDisplaySetting', (setting = 'OFF') => {
|
||||
const SETTINGS = {
|
||||
ON: '#collapsed_reply_threadsFormatA',
|
||||
OFF: '#collapsed_reply_threadsFormatB',
|
||||
};
|
||||
|
||||
cy.uiChangeGenericDisplaySetting('#collapsed_reply_threadsTitle', SETTINGS[setting]);
|
||||
});
|
||||
28
e2e-tests/cypress/tests/support/ui/announcement_bar.d.ts
vendored
Normal file
28
e2e-tests/cypress/tests/support/ui/announcement_bar.d.ts
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
// 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 `ui` prefix, e.g. `uiCloseAnnouncementBar`.
|
||||
// ***************************************************************
|
||||
|
||||
declare namespace Cypress {
|
||||
interface Chainable {
|
||||
|
||||
/**
|
||||
* Close the announcement bar if shown in the UI
|
||||
*
|
||||
* @example
|
||||
* cy.uiCloseAnnouncementBar();
|
||||
*/
|
||||
uiCloseAnnouncementBar(): Chainable;
|
||||
}
|
||||
}
|
||||
11
e2e-tests/cypress/tests/support/ui/announcement_bar.js
Normal file
11
e2e-tests/cypress/tests/support/ui/announcement_bar.js
Normal file
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
Cypress.Commands.add('uiCloseAnnouncementBar', () => {
|
||||
cy.document().then((doc) => {
|
||||
const announcementBar = doc.getElementsByClassName('announcement-bar')[0];
|
||||
if (announcementBar) {
|
||||
cy.get('.announcement-bar__close').click();
|
||||
}
|
||||
});
|
||||
});
|
||||
56
e2e-tests/cypress/tests/support/ui/boards.d.ts
vendored
Normal file
56
e2e-tests/cypress/tests/support/ui/boards.d.ts
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
// 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 `ui` prefix, e.g. `uiCreateEmptyBoard`.
|
||||
// ***************************************************************
|
||||
|
||||
declare namespace Cypress {
|
||||
interface Chainable {
|
||||
|
||||
/**
|
||||
* Create a board on a given menu item.
|
||||
*
|
||||
* @param {string} item - one of the template menu options, ex. 'Empty board'
|
||||
*/
|
||||
uiCreateBoard(item: string): Chainable;
|
||||
|
||||
/**
|
||||
* Create an empty board.
|
||||
* @example
|
||||
* cy.uiCreateEmptyBoard();
|
||||
*/
|
||||
uiCreateEmptyBoard(): Chainable;
|
||||
|
||||
/**
|
||||
* Create a board with the given title
|
||||
*
|
||||
* @param {string} title - title of the new board
|
||||
*/
|
||||
uiCreateNewBoard: (title?: string) => Chainable;
|
||||
|
||||
/**
|
||||
* Create a new group with the given name
|
||||
*
|
||||
* @param {string} name - name of the new group
|
||||
*/
|
||||
uiAddNewGroup: (name?: string) => Chainable;
|
||||
|
||||
/**
|
||||
* Create a card with the given title
|
||||
*
|
||||
* @param {string} title - title of the new card
|
||||
* @param {string} columnIndex - the column index to create the card
|
||||
*/
|
||||
uiAddNewCard: (title?: string, columnIndex?: number) => Chainable;
|
||||
}
|
||||
}
|
||||
65
e2e-tests/cypress/tests/support/ui/boards.js
Normal file
65
e2e-tests/cypress/tests/support/ui/boards.js
Normal file
@@ -0,0 +1,65 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import timeouts from '../../fixtures/timeouts';
|
||||
|
||||
Cypress.Commands.add('uiCreateBoard', (item) => {
|
||||
cy.log(`Create new board: ${item}`);
|
||||
|
||||
cy.uiAddBoard('Create new board');
|
||||
cy.contains(item).click();
|
||||
cy.contains('Use this template').click({force: true}).wait(timeouts.ONE_SEC);
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiCreateEmptyBoard', () => {
|
||||
cy.log('Create new empty board');
|
||||
|
||||
cy.contains('Create an empty board').click({force: true}).wait(timeouts.ONE_SEC);
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiAddBoard', (item) => {
|
||||
cy.get('.add-board-icon').should('be.visible').click();
|
||||
cy.get('.menu-contents').should('be.visible');
|
||||
|
||||
if (item) {
|
||||
cy.findByRole('button', {name: item}).click();
|
||||
}
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiCreateNewBoard', (title) => {
|
||||
cy.log('**Create new empty board**');
|
||||
cy.uiCreateEmptyBoard();
|
||||
|
||||
cy.findByPlaceholderText('Untitled board').should('be.visible');
|
||||
cy.wait(timeouts.QUARTER_SEC);
|
||||
if (title) {
|
||||
cy.log('**Rename board**');
|
||||
cy.findByPlaceholderText('Untitled board').type(`${title}{enter}`);
|
||||
cy.findByRole('textbox', {name: title}).should('exist');
|
||||
}
|
||||
cy.wait(timeouts.HALF_SEC);
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiAddNewGroup', (name) => {
|
||||
cy.log('**Add a new group**');
|
||||
cy.findByRole('button', {name: '+ Add a group'}).click();
|
||||
cy.findByRole('textbox', {name: 'New group'}).should('exist');
|
||||
|
||||
if (name) {
|
||||
cy.log('**Rename group**');
|
||||
cy.findByRole('textbox', {name: 'New group'}).type(`{selectall}${name}{enter}`);
|
||||
cy.findByRole('textbox', {name}).should('exist');
|
||||
}
|
||||
cy.wait(timeouts.HALF_SEC);
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiAddNewCard', (title, columnIndex) => {
|
||||
cy.log('**Add a new card**');
|
||||
cy.findByRole('button', {name: '+ New'}).eq(columnIndex || 0).click();
|
||||
cy.findByRole('dialog').should('exist');
|
||||
|
||||
if (title) {
|
||||
cy.log('**Change card title**');
|
||||
cy.findByPlaceholderText('Untitled').type(title);
|
||||
}
|
||||
});
|
||||
77
e2e-tests/cypress/tests/support/ui/channel.d.ts
vendored
Normal file
77
e2e-tests/cypress/tests/support/ui/channel.d.ts
vendored
Normal file
@@ -0,0 +1,77 @@
|
||||
// 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 `ui` prefix, e.g. `uiCreateChannel`.
|
||||
// ***************************************************************
|
||||
|
||||
declare namespace Cypress {
|
||||
interface Chainable {
|
||||
|
||||
/**
|
||||
* Create a new channel in the current team.
|
||||
* @param {string} options.prefix - Prefix for the name of the channel, it will be added a random string ot it.
|
||||
* @param {boolean} options.isPrivate - is the channel private or public (default)?
|
||||
* @param {string} options.purpose - Channel's purpose
|
||||
* @param {string} options.header - Channel's header
|
||||
* @param {boolean} options.isNewSidebar) - the new sidebar has a different ui flow, set this setting to true to use that. Defaults to false.
|
||||
* @param {string} options.createBoard) - Board template to create
|
||||
*
|
||||
* @example
|
||||
* cy.uiCreateChannel({prefix: 'private-channel-', isPrivate: true, purpose: 'my private channel', header: 'my private header', isNewSidebar: false});
|
||||
*/
|
||||
uiCreateChannel(options: Record<string, unknown>): Chainable;
|
||||
|
||||
/**
|
||||
* Add users to the current channel.
|
||||
* @param {string[]} usernameList - list of userids to add to the channel
|
||||
*
|
||||
* @example
|
||||
* cy.uiAddUsersToCurrentChannel(['user1', 'user2']);
|
||||
*/
|
||||
uiAddUsersToCurrentChannel(usernameList: string[]);
|
||||
|
||||
/**
|
||||
* Invite users to the current channel.
|
||||
* @param {string[]} usernameList - list of userids to be invited to the channel
|
||||
*
|
||||
* @example
|
||||
* cy.uiInviteUsersToCurrentChannel(['user1', 'user2']);
|
||||
*/
|
||||
uiInviteUsersToCurrentChannel(usernameList: string[]);
|
||||
|
||||
/**
|
||||
* Archive the current channel.
|
||||
*
|
||||
* @example
|
||||
* cy.uiArchiveChannel();
|
||||
*/
|
||||
uiArchiveChannel();
|
||||
|
||||
/**
|
||||
* Unarchive the current channel.
|
||||
*
|
||||
* @example
|
||||
* cy.uiUnarchiveChannel();
|
||||
*/
|
||||
uiUnarchiveChannel();
|
||||
|
||||
/**
|
||||
* Leave the current channel.
|
||||
* @param {boolean} isPrivate - is the channel private or public (default)?
|
||||
*
|
||||
* @example
|
||||
* cy.uiLeaveChannel(true);
|
||||
*/
|
||||
uiLeaveChannel(isPrivate?: boolean);
|
||||
}
|
||||
}
|
||||
108
e2e-tests/cypress/tests/support/ui/channel.js
Normal file
108
e2e-tests/cypress/tests/support/ui/channel.js
Normal file
@@ -0,0 +1,108 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {getRandomId} from '../../utils';
|
||||
import * as TIMEOUTS from '../../fixtures/timeouts';
|
||||
|
||||
Cypress.Commands.add('uiCreateChannel', ({
|
||||
prefix = 'channel-',
|
||||
isPrivate = false,
|
||||
purpose = '',
|
||||
name = '',
|
||||
createBoard = '',
|
||||
}) => {
|
||||
cy.uiBrowseOrCreateChannel('Create new channel');
|
||||
|
||||
cy.get('#new-channel-modal').should('be.visible');
|
||||
if (isPrivate) {
|
||||
cy.get('#public-private-selector-button-P').click().wait(TIMEOUTS.HALF_SEC);
|
||||
} else {
|
||||
cy.get('#public-private-selector-button-O').click().wait(TIMEOUTS.HALF_SEC);
|
||||
}
|
||||
const channelName = name || `${prefix}${getRandomId()}`;
|
||||
cy.get('#input_new-channel-modal-name').should('be.visible').clear().type(channelName);
|
||||
if (purpose) {
|
||||
cy.get('#new-channel-modal-purpose').clear().type(purpose);
|
||||
}
|
||||
|
||||
if (createBoard) {
|
||||
cy.get('#add-board-to-channel').should('be.visible');
|
||||
cy.findByTestId('add-board-to-channel-check').then((el) => {
|
||||
if (el && !el.hasClass('checked')) {
|
||||
el.click();
|
||||
cy.get('.templates-selector').find('input').click({force: true});
|
||||
cy.findByText(createBoard).scrollIntoView().should('be.visible').click({force: true});
|
||||
}
|
||||
});
|
||||
}
|
||||
cy.findByText('Create channel').click();
|
||||
cy.get('#new-channel-modal').should('not.exist');
|
||||
cy.get('#channelIntro').should('be.visible');
|
||||
return cy.wrap({name: channelName});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiAddUsersToCurrentChannel', (usernameList) => {
|
||||
if (usernameList.length) {
|
||||
cy.get('#channelHeaderTitle').click();
|
||||
cy.get('#channelMembers').click();
|
||||
cy.uiGetButton('Add').click();
|
||||
cy.get('#addUsersToChannelModal').should('be.visible');
|
||||
usernameList.forEach((username) => {
|
||||
cy.get('#selectItems input').typeWithForce(`@${username}{enter}`);
|
||||
});
|
||||
cy.get('#saveItems').click();
|
||||
cy.get('#addUsersToChannelModal').should('not.exist');
|
||||
}
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiInviteUsersToCurrentChannel', (usernameList) => {
|
||||
if (usernameList.length) {
|
||||
cy.get('#channelHeaderTitle').click();
|
||||
cy.get('#channelMembers').click();
|
||||
cy.uiGetButton('Add').click();
|
||||
cy.get('#addUsersToChannelModal').should('be.visible');
|
||||
usernameList.forEach((username) => {
|
||||
cy.get('#selectItems input').typeWithForce(`@${username}{enter}`);
|
||||
});
|
||||
cy.get('#saveItems').click();
|
||||
cy.get('#addUsersToChannelModal').should('not.exist');
|
||||
}
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiArchiveChannel', () => {
|
||||
cy.get('#channelHeaderTitle').click();
|
||||
cy.get('#channelArchiveChannel').click();
|
||||
return cy.get('#deleteChannelModalDeleteButton').click();
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiUnarchiveChannel', () => {
|
||||
cy.get('#channelHeaderTitle').should('be.visible').click();
|
||||
cy.get('#channelUnarchiveChannel').should('be.visible').click();
|
||||
return cy.get('#unarchiveChannelModalDeleteButton').should('be.visible').click();
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiLeaveChannel', (isPrivate = false) => {
|
||||
cy.get('#channelHeaderTitle').click();
|
||||
|
||||
if (isPrivate) {
|
||||
cy.get('#channelLeaveChannel').click();
|
||||
return cy.get('#confirmModalButton').click();
|
||||
}
|
||||
|
||||
return cy.get('#channelLeaveChannel').click();
|
||||
});
|
||||
|
||||
Cypress.Commands.add('goToDm', (username) => {
|
||||
cy.uiAddDirectMessage().click({force: true});
|
||||
|
||||
// # Start typing part of a username that matches previously created users
|
||||
cy.get('#selectItems input').typeWithForce(username);
|
||||
cy.findByRole('dialog', {name: 'Direct Messages'}).should('be.visible').wait(TIMEOUTS.ONE_SEC);
|
||||
cy.findByRole('combobox', {name: 'Search for people'}).
|
||||
typeWithForce(username).
|
||||
wait(TIMEOUTS.ONE_SEC).
|
||||
typeWithForce('{enter}');
|
||||
|
||||
// # Save the selected item
|
||||
return cy.get('#saveItems').click().wait(TIMEOUTS.HALF_SEC);
|
||||
});
|
||||
86
e2e-tests/cypress/tests/support/ui/channel_header.d.ts
vendored
Normal file
86
e2e-tests/cypress/tests/support/ui/channel_header.d.ts
vendored
Normal file
@@ -0,0 +1,86 @@
|
||||
// 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 `ui` prefix, e.g. `uiGetChannelFavoriteButton`.
|
||||
// ***************************************************************
|
||||
|
||||
declare namespace Cypress {
|
||||
interface Chainable {
|
||||
|
||||
/**
|
||||
* Get channel header button.
|
||||
*
|
||||
* @example
|
||||
* cy.uiGetChannelHeaderButton().click();
|
||||
*/
|
||||
uiGetChannelHeaderButton(): Chainable;
|
||||
|
||||
/**
|
||||
* Get favorite button from channel header.
|
||||
*
|
||||
* @example
|
||||
* cy.uiGetChannelFavoriteButton().click();
|
||||
*/
|
||||
uiGetChannelFavoriteButton(): Chainable;
|
||||
|
||||
/**
|
||||
* Get mute button from channel header.
|
||||
*
|
||||
* @example
|
||||
* cy.uiGetMuteButton().click();
|
||||
*/
|
||||
uiGetMuteButton(): Chainable;
|
||||
|
||||
/**
|
||||
* Get member button from channel header.
|
||||
*
|
||||
* @example
|
||||
* cy.uiGetChannelMemberButton().click();
|
||||
*/
|
||||
uiGetChannelMemberButton(): Chainable;
|
||||
|
||||
/**
|
||||
* Get pin button from channel header.
|
||||
*
|
||||
* @example
|
||||
* cy.uiGetChannelPinButton().click();
|
||||
*/
|
||||
uiGetChannelPinButton(): Chainable;
|
||||
|
||||
/**
|
||||
* Get files button from channel header.
|
||||
*
|
||||
* @example
|
||||
* cy.uiGetChannelFileButton().click();
|
||||
*/
|
||||
uiGetChannelFileButton(): Chainable;
|
||||
|
||||
/**
|
||||
* Get channel menu
|
||||
*
|
||||
* @example
|
||||
* cy.uiGetChannelMenu();
|
||||
*/
|
||||
uiGetChannelMenu(): Chainable;
|
||||
|
||||
/**
|
||||
* Open channel menu
|
||||
* @param {string} [menu] - such as `'View Info'`, `'Notification Preferences'`, `'Team Settings'` and other items in the main menu.
|
||||
* @return the channel menu
|
||||
*
|
||||
* @example
|
||||
* cy.uiOpenChannelMenu();
|
||||
*/
|
||||
uiOpenChannelMenu(menu?: string): Chainable;
|
||||
}
|
||||
}
|
||||
56
e2e-tests/cypress/tests/support/ui/channel_header.js
Normal file
56
e2e-tests/cypress/tests/support/ui/channel_header.js
Normal file
@@ -0,0 +1,56 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// Buttons
|
||||
|
||||
Cypress.Commands.add('uiGetChannelHeaderButton', () => {
|
||||
return cy.get('#channelHeaderDropdownButton').should('be.visible');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiGetChannelFavoriteButton', () => {
|
||||
return cy.get('#toggleFavorite').should('be.visible');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiGetMuteButton', () => {
|
||||
return cy.get('#toggleMute').should('be.visible');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiGetChannelMemberButton', () => {
|
||||
return cy.get('#member_rhs').should('be.visible');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiGetChannelPinButton', () => {
|
||||
return cy.get('#channelHeaderPinButton').should('be.visible');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiGetChannelFileButton', () => {
|
||||
return cy.get('#channelHeaderFilesButton').should('be.visible');
|
||||
});
|
||||
|
||||
// Menus
|
||||
|
||||
Cypress.Commands.add('uiGetChannelMenu', (options = {exist: true}) => {
|
||||
if (options.exist) {
|
||||
return cy.get('#channelHeaderDropdownMenu').
|
||||
should('be.visible');
|
||||
}
|
||||
|
||||
return cy.get('#channelHeaderDropdownMenu').should('not.exist');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiOpenChannelMenu', (item = '') => {
|
||||
// # Click on channel header button
|
||||
cy.uiGetChannelHeaderButton().click();
|
||||
|
||||
if (!item) {
|
||||
// # Return the menu if no item is passed
|
||||
return cy.uiGetChannelMenu();
|
||||
}
|
||||
|
||||
// # Click on a particular item
|
||||
return cy.uiGetChannelMenu().
|
||||
findByText(item).
|
||||
scrollIntoView().
|
||||
should('be.visible').
|
||||
click();
|
||||
});
|
||||
84
e2e-tests/cypress/tests/support/ui/channel_sidebar.ts
Normal file
84
e2e-tests/cypress/tests/support/ui/channel_sidebar.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {getRandomId} from '../../utils';
|
||||
|
||||
import type {ChainableT} from '@/types';
|
||||
|
||||
/**
|
||||
* Create a new category
|
||||
*
|
||||
* @param [categoryName] category's name
|
||||
*
|
||||
* @example
|
||||
* cy.uiCreateSidebarCategory();
|
||||
*/
|
||||
function uiCreateSidebarCategory(categoryName: string = `category-${getRandomId()}`): ChainableT<any> {
|
||||
// # Click on the sidebar menu dropdown and select Create Category
|
||||
cy.uiBrowseOrCreateChannel('Create new category');
|
||||
|
||||
cy.findByRole('dialog', {name: 'Create New Category'}).should('be.visible').within(() => {
|
||||
// # Fill in the category name and click 'Create'
|
||||
cy.findByRole('textbox').should('be.visible').typeWithForce(categoryName).
|
||||
invoke('val').should('equal', categoryName);
|
||||
cy.findByRole('button', {name: 'Create'}).should('be.enabled').click();
|
||||
});
|
||||
|
||||
// * Wait for the category to appear in the sidebar
|
||||
cy.contains('.SidebarChannelGroup', categoryName, {matchCase: false});
|
||||
|
||||
return cy.wrap({displayName: categoryName});
|
||||
}
|
||||
|
||||
Cypress.Commands.add('uiCreateSidebarCategory', uiCreateSidebarCategory);
|
||||
|
||||
/**
|
||||
* Move a channel to a category.
|
||||
* Open the channel menu, select Move to, and click either New Category or on the category.
|
||||
*
|
||||
* @param channelName channel's name
|
||||
* @param categoryName category's name
|
||||
* @param [newCategory=false] create a new category to move into
|
||||
* @param [isChannelId=false] whether channelName is a channel ID
|
||||
*
|
||||
* @example
|
||||
* cy.uiMoveChannelToCategory('Town Square', 'Favorites');
|
||||
*/
|
||||
function uiMoveChannelToCategory(channelName: string, categoryName: string, newCategory = false, isChannelId = false): ChainableT<any> {
|
||||
// # Open the channel menu, select Move to
|
||||
cy.uiGetChannelSidebarMenu(channelName, isChannelId).within(() => {
|
||||
cy.findByText('Move to...').should('be.visible').trigger('mouseover');
|
||||
});
|
||||
|
||||
// # Select the move to category
|
||||
cy.findAllByRole('menu', {name: 'Move to submenu'}).should('be.visible').within(() => {
|
||||
if (newCategory) {
|
||||
cy.findByText('New Category').should('be.visible').click({force: true});
|
||||
} else {
|
||||
cy.findByText(categoryName).should('be.visible').click({force: true});
|
||||
}
|
||||
});
|
||||
|
||||
if (newCategory) {
|
||||
cy.findByRole('dialog', {name: 'Create New Category'}).should('be.visible').within(() => {
|
||||
// # Fill in the category name and click 'Create'
|
||||
cy.findByRole('textbox').should('be.visible').typeWithForce(categoryName).
|
||||
invoke('val').should('equal', categoryName);
|
||||
cy.findByRole('button', {name: 'Create'}).should('be.enabled').click();
|
||||
});
|
||||
}
|
||||
|
||||
return cy.wrap({displayName: categoryName});
|
||||
}
|
||||
|
||||
Cypress.Commands.add('uiMoveChannelToCategory', uiMoveChannelToCategory);
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
uiCreateSidebarCategory: typeof uiCreateSidebarCategory;
|
||||
uiMoveChannelToCategory: typeof uiMoveChannelToCategory;
|
||||
}
|
||||
}
|
||||
}
|
||||
27
e2e-tests/cypress/tests/support/ui/cloud_billing.d.ts
vendored
Normal file
27
e2e-tests/cypress/tests/support/ui/cloud_billing.d.ts
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
// 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 {
|
||||
|
||||
/**
|
||||
* Iframe element in Stripe
|
||||
*
|
||||
* @example
|
||||
* cy.getIframeBody();
|
||||
*/
|
||||
uiGetPaymentCardInput(): Chainable;
|
||||
}
|
||||
}
|
||||
9
e2e-tests/cypress/tests/support/ui/cloud_billing.js
Normal file
9
e2e-tests/cypress/tests/support/ui/cloud_billing.js
Normal file
@@ -0,0 +1,9 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
Cypress.Commands.add('uiGetPaymentCardInput', () => {
|
||||
return cy.
|
||||
get('.__PrivateStripeElement > iframe').
|
||||
its('0.contentDocument.body').should('not.be.empty').
|
||||
then(cy.wrap);
|
||||
});
|
||||
114
e2e-tests/cypress/tests/support/ui/common.d.ts
vendored
Normal file
114
e2e-tests/cypress/tests/support/ui/common.d.ts
vendored
Normal file
@@ -0,0 +1,114 @@
|
||||
// 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 `ui` prefix, e.g. `uiSave`.
|
||||
// ***************************************************************
|
||||
|
||||
declare namespace Cypress {
|
||||
interface Chainable {
|
||||
|
||||
/**
|
||||
* Click 'Save' button
|
||||
*
|
||||
* @example
|
||||
* cy.uiSave();
|
||||
*/
|
||||
uiSave(): Chainable;
|
||||
|
||||
/**
|
||||
* Click 'Cancel' button
|
||||
*
|
||||
* @example
|
||||
* cy.uiCancel();
|
||||
*/
|
||||
uiCancel(): Chainable;
|
||||
|
||||
/**
|
||||
* Click 'Close' button
|
||||
*
|
||||
* @example
|
||||
* cy.uiClose();
|
||||
*/
|
||||
uiClose(): Chainable;
|
||||
|
||||
/**
|
||||
* Click Save then Close buttons
|
||||
*
|
||||
* @example
|
||||
* cy.uiSaveAndClose();
|
||||
*/
|
||||
uiSaveAndClose(): Chainable;
|
||||
|
||||
/**
|
||||
* Get a button by its text using "cy.findByRole"
|
||||
*
|
||||
* @param {String} label - Button text
|
||||
*
|
||||
* @example
|
||||
* cy.uiGetButton('Save');
|
||||
*/
|
||||
uiGetButton(label: string): Chainable;
|
||||
|
||||
/**
|
||||
* Get save button
|
||||
*
|
||||
* @example
|
||||
* cy.uiSaveButton();
|
||||
*/
|
||||
uiSaveButton(): Chainable;
|
||||
|
||||
/**
|
||||
* Get cancel button
|
||||
*
|
||||
* @example
|
||||
* cy.uiCancelButton();
|
||||
*/
|
||||
uiCancelButton(): Chainable;
|
||||
|
||||
/**
|
||||
* Get close button
|
||||
*
|
||||
* @example
|
||||
* cy.uiCloseButton();
|
||||
*/
|
||||
uiCloseButton(): Chainable;
|
||||
|
||||
/**
|
||||
* Get a radio button by its text using "cy.findByRole"
|
||||
*
|
||||
* @example
|
||||
* cy.uiGetRadioButton('Custom Theme');
|
||||
*/
|
||||
uiGetRadioButton(): Chainable;
|
||||
|
||||
/**
|
||||
* Get a heading by its text using "cy.findByRole"
|
||||
*
|
||||
* @param {string} headingText - Heading text
|
||||
*
|
||||
* @example
|
||||
* cy.uiGetHeading('General Settings');
|
||||
*/
|
||||
uiGetHeading(headingText: string): Chainable;
|
||||
|
||||
/**
|
||||
* Get a textbox by its text using "cy.findByRole"
|
||||
*
|
||||
* @param {string} text - Textbox label
|
||||
*
|
||||
* @example
|
||||
* cy.uiGetTextbox('Nickname');
|
||||
*/
|
||||
uiGetTextbox(text: string): Chainable;
|
||||
}
|
||||
}
|
||||
55
e2e-tests/cypress/tests/support/ui/common.js
Normal file
55
e2e-tests/cypress/tests/support/ui/common.js
Normal file
@@ -0,0 +1,55 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
Cypress.Commands.add('uiSave', () => {
|
||||
return cy.findByRole('button', {name: 'Save'}).scrollIntoView().click();
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiCancel', () => {
|
||||
return cy.findByRole('button', {name: 'Cancel'}).click();
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiClose', () => {
|
||||
return cy.findAllByRole('button', {name: 'Close'}).eq(0).click();
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiSaveAndClose', () => {
|
||||
cy.uiSave();
|
||||
cy.uiClose();
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiGetButton', (name) => {
|
||||
return cy.findByRole('button', {name});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiSaveButton', () => {
|
||||
return cy.uiGetButton('Save');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiCancelButton', () => {
|
||||
return cy.uiGetButton('Cancel');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiCloseButton', () => {
|
||||
return cy.uiGetButton('Close');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiGetRadioButton', (name) => {
|
||||
return cy.findByRole('radio', {name}).should('be.visible');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiGetHeading', (name) => {
|
||||
return cy.findByRole('heading', {name}).should('be.visible');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiGetTextbox', (name) => {
|
||||
return cy.findByRole('textbox', {name}).should('be.visible');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiCloseOnboardingTaskList', () => {
|
||||
cy.get('[data-cy=onboarding-task-list-action-button]').then(($btn) => {
|
||||
if ($btn.find('i.icon-close').length) {
|
||||
$btn.trigger('click');
|
||||
}
|
||||
});
|
||||
});
|
||||
39
e2e-tests/cypress/tests/support/ui/compliance_export.d.ts
vendored
Normal file
39
e2e-tests/cypress/tests/support/ui/compliance_export.d.ts
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
// 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 {
|
||||
|
||||
/**
|
||||
* Select compliance export format
|
||||
* @param {string} exportFormat - compliance export format
|
||||
*
|
||||
* @example
|
||||
* const EXPORTFORMAT = "Actiance XML";
|
||||
* cy.uiEnableComplianceExport(Compliance Export Format);
|
||||
*/
|
||||
uiEnableComplianceExport(exportFormat: string): Chainable;
|
||||
|
||||
/**
|
||||
* Go to Compliance Page
|
||||
*/
|
||||
uiGoToCompliancePage(): Chainable;
|
||||
|
||||
/**
|
||||
* Click Run Export Compliance and wait for Success status
|
||||
*/
|
||||
uiExportCompliance(): Chainable;
|
||||
}
|
||||
}
|
||||
53
e2e-tests/cypress/tests/support/ui/compliance_export.js
Normal file
53
e2e-tests/cypress/tests/support/ui/compliance_export.js
Normal file
@@ -0,0 +1,53 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import * as TIMEOUTS from '../../fixtures/timeouts';
|
||||
|
||||
Cypress.Commands.add('uiEnableComplianceExport', (exportFormat = 'csv') => {
|
||||
// * Verify that the page is loaded
|
||||
cy.findByText('Enable Compliance Export:').should('be.visible');
|
||||
cy.findByText('Compliance Export Time:').should('be.visible');
|
||||
cy.findByText('Export Format:').should('be.visible');
|
||||
|
||||
// # Enable compliance export
|
||||
cy.findByRole('radio', {name: /false/i}).click();
|
||||
cy.findByRole('radio', {name: /true/i}).click();
|
||||
|
||||
// # Change export format
|
||||
cy.findByTestId('exportFormatdropdown').should('be.visible').select(exportFormat);
|
||||
|
||||
// # Save settings
|
||||
cy.uiSaveConfig({confirm: true});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiGoToCompliancePage', () => {
|
||||
cy.visit('/admin_console/compliance/export');
|
||||
cy.get('.admin-console__header', {timeout: TIMEOUTS.TWO_MIN}).should('be.visible').invoke('text').should('include', 'Compliance Export');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiExportCompliance', () => {
|
||||
// # Click the export job button
|
||||
cy.findByRole('button', {name: /run compliance export job now/i}).click();
|
||||
|
||||
// # Small wait to ensure new row is add
|
||||
cy.wait(TIMEOUTS.THREE_SEC);
|
||||
|
||||
// # Get the first row
|
||||
cy.get('.job-table__table').find('tbody > tr').eq(0).as('firstRow');
|
||||
|
||||
// # Get the first table header
|
||||
cy.get('.job-table__table').find('thead > tr').as('firstheader');
|
||||
|
||||
// # Wait until export is finished
|
||||
cy.waitUntil(() => {
|
||||
return cy.get('@firstRow').find('td:eq(0)').then((el) => {
|
||||
return el[0].innerText.trim() === 'Success';
|
||||
});
|
||||
},
|
||||
{
|
||||
timeout: TIMEOUTS.FIVE_MIN,
|
||||
interval: TIMEOUTS.ONE_SEC,
|
||||
errorMsg: 'Compliance export did not finish in time',
|
||||
});
|
||||
});
|
||||
|
||||
86
e2e-tests/cypress/tests/support/ui/data_retention.d.ts
vendored
Normal file
86
e2e-tests/cypress/tests/support/ui/data_retention.d.ts
vendored
Normal file
@@ -0,0 +1,86 @@
|
||||
// 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 {
|
||||
|
||||
/**
|
||||
* Go to Data Retention page
|
||||
*/
|
||||
uiGoToDataRetentionPage(): Chainable;
|
||||
|
||||
/**
|
||||
* Click create policy button
|
||||
*/
|
||||
uiClickCreatePolicy(): Chainable;
|
||||
|
||||
/**
|
||||
* Fill out custom policy form fields
|
||||
* @param {string} name - policy name
|
||||
* @param {string} durationDropdown - duration dropdown value (days, years, forever)
|
||||
* @param {string?} durationText - duration text
|
||||
*/
|
||||
uiFillOutCustomPolicyFields(name: string, durationDropdown: string, durationText?: string): Chainable;
|
||||
|
||||
/**
|
||||
* Search and add teams to custom policy
|
||||
* @param {string[]} teamNames - array of team names
|
||||
*/
|
||||
uiAddTeamsToCustomPolicy(teamNames: string[]): Chainable;
|
||||
|
||||
/**
|
||||
* Search and add channels to custom policy
|
||||
* @param {string[]} channelNames - array of channel names
|
||||
*/
|
||||
uiAddChannelsToCustomPolicy(channelNames: string[]): Chainable;
|
||||
|
||||
/**
|
||||
* Add teams to a custom policy
|
||||
* @param {number} numberOfTeams - number of teams to add to the policy
|
||||
*/
|
||||
uiAddRandomTeamToCustomPolicy(numberOfTeams?: number): Chainable;
|
||||
|
||||
/**
|
||||
* Add channels to a custom policy
|
||||
* @param {number} numberOfTeams - number of teams to add to the policy
|
||||
*/
|
||||
uiAddRandomChannelToCustomPolicy(numberOfChannels?: number): Chainable;
|
||||
|
||||
/**
|
||||
* Verify custom policy UI information
|
||||
* @param {string} policyId - Custom Policy ID
|
||||
* @param {string} description - The name of the policy
|
||||
* @param {string} duration - How long messages last in the policy
|
||||
* @param {string} appliedTo - Teams and channels the policy apples to
|
||||
*/
|
||||
uiVerifyCustomPolicyRow(policyId: string, description: string, duration: string, appliedTo: string): Chainable;
|
||||
|
||||
/**
|
||||
* Click edit custom policy
|
||||
* @param {string} policyId - Custom Policy ID
|
||||
*/
|
||||
uiClickEditCustomPolicyRow(policyId: string): Chainable;
|
||||
|
||||
/**
|
||||
* Verify custom create policy response
|
||||
* @param body - Response body
|
||||
* @param {number} teamCount - Number of teams the policy applies to
|
||||
* @param {number} channelCount - Number of channels the policy applies to
|
||||
* @param {number} duration - How long messages last in the policy
|
||||
* @param {string} displayName - The name of the policy
|
||||
*/
|
||||
uiVerifyPolicyResponse(body, teamCount: number, channelCount: number, duration: number, displayName: string): Chainable;
|
||||
}
|
||||
}
|
||||
105
e2e-tests/cypress/tests/support/ui/data_retention.js
Normal file
105
e2e-tests/cypress/tests/support/ui/data_retention.js
Normal file
@@ -0,0 +1,105 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import * as TIMEOUTS from '../../fixtures/timeouts';
|
||||
|
||||
Cypress.Commands.add('uiGoToDataRetentionPage', () => {
|
||||
cy.visit('/admin_console/compliance/data_retention_settings');
|
||||
cy.get('.DataRetentionSettings .admin-console__header', {timeout: TIMEOUTS.TWO_MIN}).should('be.visible').invoke('text').should('include', 'Data Retention Policies');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiClickCreatePolicy', () => {
|
||||
cy.uiGetButton('Add policy').click();
|
||||
cy.get('.DataRetentionSettings .admin-console__header', {timeout: TIMEOUTS.TWO_MIN}).should('be.visible').invoke('text').should('include', 'Custom Retention Policy');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiFillOutCustomPolicyFields', (name, durationDropdown, durationText = '') => {
|
||||
// # Type policy name
|
||||
cy.uiGetTextbox('Policy name').clear().type(name);
|
||||
|
||||
// # Add message retention values
|
||||
cy.get('.CustomPolicy__fields #DropdownInput_message_retention').should('be.visible').click();
|
||||
cy.get(`.message_retention__menu .message_retention__option span.option_${durationDropdown}`).should('be.visible').click();
|
||||
if (durationText) {
|
||||
cy.get('.CustomPolicy__fields input#message_retention_input').clear().type(durationText);
|
||||
}
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiAddTeamsToCustomPolicy', (teamNames) => {
|
||||
cy.uiGetButton('Add teams').click();
|
||||
teamNames.forEach((teamName) => {
|
||||
cy.findByRole('combobox', {name: 'Search and add teams'}).typeWithForce(teamName);
|
||||
cy.get('.team-info-block').then((el) => {
|
||||
el.click();
|
||||
});
|
||||
});
|
||||
cy.uiGetButton('Add').click();
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiAddChannelsToCustomPolicy', (channelNames) => {
|
||||
cy.uiGetButton('Add channels').click();
|
||||
channelNames.forEach((channelName) => {
|
||||
cy.findByRole('combobox', {name: 'Search and add channels'}).typeWithForce(channelName);
|
||||
cy.wait(TIMEOUTS.ONE_SEC);
|
||||
cy.get('.channel-info-block').then((el) => {
|
||||
el.click();
|
||||
});
|
||||
});
|
||||
cy.uiGetButton('Add').click();
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiAddRandomTeamToCustomPolicy', (numberOfTeams = 1) => {
|
||||
cy.uiGetButton('Add teams').click();
|
||||
for (let i = 0; i < numberOfTeams; i++) {
|
||||
cy.get('.team-info-block').first().then((el) => {
|
||||
el.click();
|
||||
});
|
||||
}
|
||||
cy.uiGetButton('Add').click();
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiAddRandomChannelToCustomPolicy', (numberOfChannels = 1) => {
|
||||
cy.uiGetButton('Add channels').click();
|
||||
for (let i = 0; i < numberOfChannels; i++) {
|
||||
cy.get('.channel-info-block').first().then((el) => {
|
||||
el.click();
|
||||
});
|
||||
}
|
||||
cy.uiGetButton('Add').click();
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiVerifyCustomPolicyRow', (policyId, description, duration, appliedTo) => {
|
||||
// * Assert row has correct description
|
||||
cy.get(`#customDescription-${policyId}`).should('include.text', description);
|
||||
|
||||
// * Assert row has correct duration
|
||||
cy.get(`#customDuration-${policyId}`).should('include.text', duration);
|
||||
|
||||
// * Assert row has correct team/channel counts
|
||||
cy.get(`#customAppliedTo-${policyId}`).should('include.text', appliedTo);
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiClickEditCustomPolicyRow', (policyId) => {
|
||||
cy.get(`#customWrapper-${policyId}`).trigger('mouseover').click();
|
||||
cy.findByRole('button', {name: /edit/i}).should('be.visible').click();
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiVerifyPolicyResponse', (body, teamCount, channelCount, duration, displayName) => {
|
||||
// * Assert response body exists
|
||||
assert.isNotNull(body);
|
||||
|
||||
// * Assert response body contains an ID
|
||||
assert.isNotNull(body.id);
|
||||
|
||||
// * Assert response body team_count matches supplied value
|
||||
expect(body.team_count).to.equal(teamCount);
|
||||
|
||||
// * Assert response body channel_count matches supplied value
|
||||
expect(body.channel_count).to.equal(channelCount);
|
||||
|
||||
// * Assert response body duration matches supplied value
|
||||
expect(body.post_duration).to.equal(duration);
|
||||
|
||||
// * Assert response body display_name matches supplied value
|
||||
expect(body.display_name).to.equal(displayName);
|
||||
});
|
||||
59
e2e-tests/cypress/tests/support/ui/emoji.ts
Normal file
59
e2e-tests/cypress/tests/support/ui/emoji.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {ChainableT} from '@/types';
|
||||
|
||||
Cypress.Commands.add('uiGetEmojiPicker', (): ChainableT<JQuery> => {
|
||||
return cy.get('#emojiPicker').should('be.visible');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiOpenEmojiPicker', (): ChainableT<JQuery> => {
|
||||
cy.get('body').then(($body) => {
|
||||
if ($body.find('#emojiPicker:visible').length) {
|
||||
cy.get('body').type('{esc}');
|
||||
}
|
||||
});
|
||||
cy.get('#emojiPicker').should('not.exist');
|
||||
cy.findByRole('button', {name: 'select an emoji'}).should('be.visible').click();
|
||||
return cy.get('#emojiPicker').should('be.visible');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiOpenCustomEmoji', () => {
|
||||
cy.uiOpenEmojiPicker();
|
||||
cy.findByText('Custom Emoji').should('be.visible').click();
|
||||
|
||||
cy.url().should('include', '/emoji');
|
||||
cy.get('.backstage-header').should('be.visible').and('contain', 'Custom Emoji');
|
||||
});
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
|
||||
/**
|
||||
* Open custom emoji
|
||||
*
|
||||
* @example
|
||||
* cy.uiOpenCustomEmoji();
|
||||
*/
|
||||
uiGetEmojiPicker(): Chainable;
|
||||
|
||||
/**
|
||||
* Open custom emoji
|
||||
*
|
||||
* @example
|
||||
* cy.uiOpenCustomEmoji();
|
||||
*/
|
||||
uiOpenCustomEmoji(): Chainable;
|
||||
|
||||
/**
|
||||
* Open emoji picker
|
||||
*
|
||||
* @example
|
||||
* cy.uiOpenEmojiPicker();
|
||||
*/
|
||||
uiOpenEmojiPicker(): Chainable;
|
||||
}
|
||||
}
|
||||
}
|
||||
30
e2e-tests/cypress/tests/support/ui/extend_testing_library.d.ts
vendored
Normal file
30
e2e-tests/cypress/tests/support/ui/extend_testing_library.d.ts
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
// 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 the Testing Library commands
|
||||
// ***************************************************************
|
||||
|
||||
declare namespace Cypress {
|
||||
interface Chainable {
|
||||
|
||||
/**
|
||||
* Extends `findByRole` by matching case to `name` as insensitive but sensitive to `text` value
|
||||
* @param {string} role - button, input, textbox, etc.
|
||||
* @param {Object} option - text value of the target element
|
||||
*
|
||||
* @example
|
||||
* cy.findByRoleExtended('button', {name: 'Advanced'}).should('be.visible').click();
|
||||
*/
|
||||
findByRoleExtended(role: string, option: {name: string}): Chainable;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
Cypress.Commands.add('findByRoleExtended', (role, {name}) => {
|
||||
const re = RegExp(name, 'i');
|
||||
return cy.findByRole(role, {name: re}).should('have.text', name);
|
||||
});
|
||||
124
e2e-tests/cypress/tests/support/ui/file_preview.d.ts
vendored
Normal file
124
e2e-tests/cypress/tests/support/ui/file_preview.d.ts
vendored
Normal file
@@ -0,0 +1,124 @@
|
||||
// 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 `ui` prefix, e.g. `uiOpenFilePreviewModal`.
|
||||
// ***************************************************************
|
||||
|
||||
declare namespace Cypress {
|
||||
interface Chainable {
|
||||
|
||||
/**
|
||||
* Get file thumbnail from a post
|
||||
*
|
||||
* @param {string} filename
|
||||
*
|
||||
* @example
|
||||
* cy.uiGetFileThumbnail('image.png');
|
||||
*/
|
||||
uiGetFileThumbnail(filename: string): Chainable;
|
||||
|
||||
/**
|
||||
* Get file upload preview located below post textbox
|
||||
*
|
||||
* @example
|
||||
* cy.uiGetFileUploadPreview();
|
||||
*/
|
||||
uiGetFileUploadPreview(): Chainable;
|
||||
|
||||
/**
|
||||
* Wait for file upload preview located below post textbox
|
||||
*
|
||||
* @example
|
||||
* cy.uiGetFileUploadPreview();
|
||||
*/
|
||||
uiGetFileUploadPreview(): Chainable;
|
||||
|
||||
/**
|
||||
* Get file preview modal
|
||||
*
|
||||
* @param {bool} option.exist - Set to false to not verify if the element exists. Otherwise, true (default) to check existence.
|
||||
*
|
||||
* @example
|
||||
* cy.uiGetFilePreviewModal();
|
||||
*/
|
||||
uiGetFilePreviewModal(option: Record<string, boolean>): Chainable;
|
||||
|
||||
/**
|
||||
* Get Public Link
|
||||
*
|
||||
* @param {bool} option.exist - Set to false to not verify if the element exists. Otherwise, true (default) to check existence.
|
||||
*
|
||||
* @example
|
||||
* cy.uiGetPublicLink();
|
||||
*/
|
||||
uiGetPublicLink(option: Record<string, boolean>): Chainable;
|
||||
|
||||
/**
|
||||
* Open file preview modal
|
||||
*
|
||||
* @param {string} filename
|
||||
*
|
||||
* @example
|
||||
* cy.uiOpenFilePreviewModal('image.png');
|
||||
*/
|
||||
uiOpenFilePreviewModal(filename: string): Chainable;
|
||||
|
||||
/**
|
||||
* Close file preview modal
|
||||
*
|
||||
* @example
|
||||
* cy.uiCloseFilePreviewModal();
|
||||
*/
|
||||
uiCloseFilePreviewModal(): Chainable;
|
||||
|
||||
/**
|
||||
* Get main content of file preview modal
|
||||
*
|
||||
* @example
|
||||
* cy.uiGetContentFilePreviewModal();
|
||||
*/
|
||||
uiGetContentFilePreviewModal(): Chainable;
|
||||
|
||||
/**
|
||||
* Get download link button from file preview modal
|
||||
*
|
||||
* @example
|
||||
* cy.uiGetDownloadLinkFilePreviewModal();
|
||||
*/
|
||||
uiGetDownloadLinkFilePreviewModal(): Chainable;
|
||||
|
||||
/**
|
||||
* Get download button from file preview modal
|
||||
*
|
||||
* @example
|
||||
* cy.uiGetDownloadFilePreviewModal();
|
||||
*/
|
||||
uiGetDownloadFilePreviewModal(): Chainable;
|
||||
|
||||
/**
|
||||
* Get arrow left button from file preview modal
|
||||
*
|
||||
* @example
|
||||
* cy.uiGetArrowLeftFilePreviewModal();
|
||||
*/
|
||||
uiGetArrowLeftFilePreviewModal(): Chainable;
|
||||
|
||||
/**
|
||||
* Get arrow right button from file preview modal
|
||||
*
|
||||
* @example
|
||||
* cy.uiGetArrowRightFilePreviewModal();
|
||||
*/
|
||||
uiGetArrowRightFilePreviewModal(): Chainable;
|
||||
}
|
||||
}
|
||||
80
e2e-tests/cypress/tests/support/ui/file_preview.js
Normal file
80
e2e-tests/cypress/tests/support/ui/file_preview.js
Normal file
@@ -0,0 +1,80 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
Cypress.Commands.add('uiGetFileThumbnail', (filename) => {
|
||||
// Gallery tiles preserve original filename casing on data-file-name,
|
||||
// while the legacy thumbnail aria-label is always lowercased
|
||||
// (see file_preview.tsx). Match each accordingly.
|
||||
const tileMatches = Cypress.$('[data-testid="media-gallery-tile"]').filter((_, el) => {
|
||||
return el.getAttribute('data-file-name')?.toLowerCase() === filename.toLowerCase();
|
||||
});
|
||||
if (tileMatches.length) {
|
||||
return cy.wrap(tileMatches);
|
||||
}
|
||||
return cy.findByLabelText(`file thumbnail ${filename.toLowerCase()}`);
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiGetFileUploadPreview', () => {
|
||||
return cy.get('.file-preview__container');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiWaitForFileUploadPreview', () => {
|
||||
cy.waitUntil(() => cy.uiGetFileUploadPreview().then((el) => {
|
||||
return el.find('.post-image__thumbnail').length > 0;
|
||||
}));
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiGetFilePreviewModal', (options = {exist: true}) => {
|
||||
if (options.exist) {
|
||||
return cy.get('.file-preview-modal').should('be.visible');
|
||||
}
|
||||
|
||||
return cy.get('.file-preview-modal').should('not.exist');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiGetPublicLink', (options = {exist: true}) => {
|
||||
if (options.exist) {
|
||||
return cy.get('.icon-link-variant').should('be.visible');
|
||||
}
|
||||
return cy.get('.icon-link-variant').should('not.exist');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiGetHeaderFilePreviewModal', () => {
|
||||
return cy.uiGetFilePreviewModal().find('.file-preview-modal-header').should('be.visible');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiOpenFilePreviewModal', (filename) => {
|
||||
if (filename) {
|
||||
cy.uiGetFileThumbnail(filename).click();
|
||||
return;
|
||||
}
|
||||
if (Cypress.$('[data-testid="media-gallery-tile"]').length) {
|
||||
cy.get('[data-testid="media-gallery-tile"]').first().click();
|
||||
} else {
|
||||
cy.findByTestId('fileAttachmentList').children().first().click();
|
||||
}
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiCloseFilePreviewModal', () => {
|
||||
return cy.uiGetFilePreviewModal().find('.icon-close').click();
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiGetContentFilePreviewModal', () => {
|
||||
return cy.uiGetFilePreviewModal().find('.file-preview-modal__content');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiGetDownloadLinkFilePreviewModal', () => {
|
||||
return cy.uiGetFilePreviewModal().find('.icon-link-variant');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiGetDownloadFilePreviewModal', () => {
|
||||
return cy.uiGetFilePreviewModal().find('.icon-download-outline');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiGetArrowLeftFilePreviewModal', () => {
|
||||
return cy.uiGetFilePreviewModal().find('.icon-chevron-left');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiGetArrowRightFilePreviewModal', () => {
|
||||
return cy.uiGetFilePreviewModal().find('.icon-chevron-right');
|
||||
});
|
||||
189
e2e-tests/cypress/tests/support/ui/global_header.d.ts
vendored
Normal file
189
e2e-tests/cypress/tests/support/ui/global_header.d.ts
vendored
Normal file
@@ -0,0 +1,189 @@
|
||||
// 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 `ui` prefix, e.g. `uiGetProductMenuButton`.
|
||||
// ***************************************************************
|
||||
|
||||
declare namespace Cypress {
|
||||
interface Chainable {
|
||||
|
||||
/**
|
||||
* Get product switch button
|
||||
*
|
||||
* @example
|
||||
* cy.uiGetProductMenuButton().click();
|
||||
*/
|
||||
uiGetProductMenuButton(): Chainable;
|
||||
|
||||
/**
|
||||
* Get product switch menu
|
||||
*
|
||||
* @example
|
||||
* cy.uiGetProductMenu().click();
|
||||
*/
|
||||
uiGetProductMenu(): Chainable;
|
||||
|
||||
/**
|
||||
* Open product switch menu
|
||||
*
|
||||
* @param {string} item - menu item ex. System Console, Integrations, etc.
|
||||
*
|
||||
* @example
|
||||
* cy.uiOpenProductMenu().click();
|
||||
*/
|
||||
uiOpenProductMenu(item: string = ''): Chainable;
|
||||
|
||||
/**
|
||||
* Get set status button
|
||||
*
|
||||
* @example
|
||||
* cy.uiGetSetStatusButton().click();
|
||||
*/
|
||||
uiGetSetStatusButton(): Chainable;
|
||||
|
||||
/**
|
||||
* Get profile header
|
||||
*
|
||||
* @example
|
||||
* cy.uiGetProfileHeader();
|
||||
*/
|
||||
uiGetProfileHeader(): Chainable;
|
||||
|
||||
/**
|
||||
* Get status menu container
|
||||
*
|
||||
* @param {bool} option.exist - Set to false to not verify if the element exists. Otherwise, true (default) to check existence.
|
||||
* @example
|
||||
* cy.uiGetStatusMenuContainer({exist: false});
|
||||
*/
|
||||
uiGetStatusMenuContainer(option?: Record<string, boolean>): Chainable;
|
||||
|
||||
/**
|
||||
* Get user menu
|
||||
*
|
||||
* @example
|
||||
* cy.uiGetStatusMenu();
|
||||
*/
|
||||
uiGetStatusMenu(): Chainable;
|
||||
|
||||
/**
|
||||
* Open help menu
|
||||
*
|
||||
* @param {string} item - menu item ex. Ask the community, Help resources, etc.
|
||||
*
|
||||
* @example
|
||||
* cy.uiOpenHelpMenu();
|
||||
*/
|
||||
uiOpenHelpMenu(item: string): Chainable;
|
||||
|
||||
/**
|
||||
* Get help button
|
||||
*
|
||||
* @example
|
||||
* cy.uiGetHelpButton();
|
||||
*/
|
||||
uiGetHelpButton(): Chainable;
|
||||
|
||||
/**
|
||||
* Get help menu
|
||||
*
|
||||
* @example
|
||||
* cy.uiGetHelpMenu();
|
||||
*/
|
||||
uiGetHelpMenu(): Chainable;
|
||||
|
||||
/**
|
||||
* Open user menu
|
||||
*
|
||||
* @param {string} [item] - menu item ex. Profile, Logout, etc.
|
||||
*
|
||||
* @example
|
||||
* cy.uiOpenUserMenu();
|
||||
*/
|
||||
uiOpenUserMenu(item?: string): Chainable;
|
||||
|
||||
/**
|
||||
* Get search form container
|
||||
*
|
||||
* @example
|
||||
* cy.uiGetSearchContainer();
|
||||
*/
|
||||
uiGetSearchContainer(): Chainable;
|
||||
|
||||
/**
|
||||
* Get search box
|
||||
*
|
||||
* @example
|
||||
* cy.uiGetSearchBox();
|
||||
*/
|
||||
uiGetSearchBox(): Chainable;
|
||||
|
||||
/**
|
||||
* Get at-mention button
|
||||
*
|
||||
* @example
|
||||
* cy.uiGetRecentMentionButton();
|
||||
*/
|
||||
uiGetRecentMentionButton(): Chainable;
|
||||
|
||||
/**
|
||||
* Get saved posts button
|
||||
*
|
||||
* @example
|
||||
* cy.uiGetSavedPostButton();
|
||||
*/
|
||||
uiGetSavedPostButton(): Chainable;
|
||||
|
||||
/**
|
||||
* Get settings button
|
||||
*
|
||||
* @example
|
||||
* cy.uiGetSettingsButton();
|
||||
*/
|
||||
uiGetSettingsButton(): Chainable;
|
||||
|
||||
/**
|
||||
* Get settings modal
|
||||
*
|
||||
* @example
|
||||
* cy.uiGetSettingsModal();
|
||||
*/
|
||||
uiGetSettingsModal(): Chainable;
|
||||
|
||||
/**
|
||||
* Get channel info button
|
||||
*
|
||||
* @example
|
||||
* cy.uiGetChannelInfoButton();
|
||||
*/
|
||||
uiGetChannelInfoButton(): Chainable;
|
||||
|
||||
/**
|
||||
* Open settings modal
|
||||
*
|
||||
* @param {string} section - ex. Display, Sidebar, etc.
|
||||
*
|
||||
* @example
|
||||
* cy.uiOpenSettingsModal();
|
||||
*/
|
||||
uiOpenSettingsModal(section: string): Chainable;
|
||||
|
||||
/**
|
||||
* User log out via user menu
|
||||
*
|
||||
* @example
|
||||
* cy.uiLogout();
|
||||
*/
|
||||
uiLogout(): Chainable;
|
||||
}
|
||||
}
|
||||
152
e2e-tests/cypress/tests/support/ui/global_header.js
Normal file
152
e2e-tests/cypress/tests/support/ui/global_header.js
Normal file
@@ -0,0 +1,152 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
Cypress.Commands.add('uiGetProductMenuButton', () => {
|
||||
return cy.findByRole('button', {name: 'Product switch menu'}).should('be.visible');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiGetProductMenu', () => {
|
||||
return cy.get('.product-switcher-menu').should('be.visible');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiOpenProductMenu', (item = '') => {
|
||||
// # Click on product switch button
|
||||
cy.uiGetProductMenuButton().click();
|
||||
|
||||
if (!item) {
|
||||
// # Return the menu if no item is passed
|
||||
return cy.uiGetProductMenu();
|
||||
}
|
||||
|
||||
// # Click on a particular item
|
||||
return cy.uiGetProductMenu().
|
||||
findByText(item).
|
||||
scrollIntoView().
|
||||
should('be.visible').
|
||||
click();
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiGetSetStatusButton', () => {
|
||||
// # Get set status button
|
||||
return cy.get('#userAccountMenuButton').should('be.visible');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiGetProfileHeader', () => {
|
||||
return cy.uiGetSetStatusButton().parent();
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiGetStatusMenuContainer', (options = {exist: true}) => {
|
||||
if (options.exist) {
|
||||
return cy.get('#userAccountMenu').should('exist').and('be.visible');
|
||||
}
|
||||
|
||||
return cy.get('#userAccountMenu').should('not.exist');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiGetStatusMenu', (options = {visible: true}) => {
|
||||
if (options.visible) {
|
||||
return cy.get('#userAccountMenu').should('exist').and('be.visible');
|
||||
}
|
||||
|
||||
return cy.get('#userAccountMenu').should('not.exist');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiOpenHelpMenu', (item = '') => {
|
||||
// # Click on help status button
|
||||
cy.uiGetHelpButton().click();
|
||||
|
||||
if (!item) {
|
||||
// # Return the menu if no item is passed
|
||||
return cy.uiGetHelpMenu();
|
||||
}
|
||||
|
||||
// # Click on a particular item
|
||||
return cy.uiGetHelpMenu().
|
||||
findByText(item).
|
||||
scrollIntoView().
|
||||
should('be.visible').
|
||||
click();
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiGetHelpButton', () => {
|
||||
return cy.findByRole('button', {name: 'Help'}).should('be.visible');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiGetHelpMenu', (options = {visible: true}) => {
|
||||
const dropdown = () => cy.get('#helpMenuPortal').find('.dropdown-menu');
|
||||
|
||||
if (options.visible) {
|
||||
return dropdown().should('be.visible');
|
||||
}
|
||||
|
||||
return dropdown().should('not.be.visible');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiOpenUserMenu', (item = '') => {
|
||||
// # Click on user status button
|
||||
cy.uiGetSetStatusButton().click();
|
||||
|
||||
if (!item) {
|
||||
// # Return the menu if no item is passed
|
||||
return cy.uiGetStatusMenu();
|
||||
}
|
||||
|
||||
// # Click on a particular item
|
||||
return cy.uiGetStatusMenu().
|
||||
findByText(item).
|
||||
scrollIntoView().
|
||||
should('be.visible').
|
||||
click();
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiGetSearchContainer', () => {
|
||||
return cy.get('#searchFormContainer').should('be.visible');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiGetSearchBox', () => {
|
||||
return cy.findByRole('searchbox').should('be.visible');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiGetRecentMentionButton', () => {
|
||||
return cy.findByRole('button', {name: 'Recent mentions'}).should('be.visible');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiGetSavedPostButton', () => {
|
||||
return cy.findByRole('button', {name: 'Saved messages'}).should('be.visible');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiGetSettingsButton', () => {
|
||||
return cy.findByRole('button', {name: 'Settings'}).should('be.visible');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiGetChannelInfoButton', () => {
|
||||
return cy.findByRole('button', {name: 'View Info'}).should('be.visible');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiGetSettingsModal', () => {
|
||||
// # Get settings modal
|
||||
return cy.findByRole('dialog', {name: 'Settings'});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiOpenSettingsModal', (section = '') => {
|
||||
// # Open settings modal
|
||||
cy.uiGetSettingsButton().click();
|
||||
|
||||
if (!section) {
|
||||
return cy.uiGetSettingsModal();
|
||||
}
|
||||
|
||||
// # Click on a particular section
|
||||
cy.findByRoleExtended('tab', {name: section}).should('be.visible').click();
|
||||
|
||||
return cy.uiGetSettingsModal();
|
||||
});
|
||||
|
||||
Cypress.Commands.add('uiLogout', () => {
|
||||
// # Click logout via user menu
|
||||
cy.uiOpenUserMenu('Log out');
|
||||
|
||||
cy.url().should('include', '/login');
|
||||
cy.get('.login-body-message').should('be.visible');
|
||||
cy.get('.login-body-card').should('be.visible');
|
||||
});
|
||||
30
e2e-tests/cypress/tests/support/ui/index.js
Normal file
30
e2e-tests/cypress/tests/support/ui/index.js
Normal file
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import './account_settings_modal';
|
||||
import './announcement_bar';
|
||||
import './boards';
|
||||
import './channel';
|
||||
import './channel_header';
|
||||
import './channel_sidebar';
|
||||
import './cloud_billing';
|
||||
import './common';
|
||||
import './compliance_export';
|
||||
import './data_retention';
|
||||
import './extend_testing_library';
|
||||
import './global_header';
|
||||
import './emoji';
|
||||
import './file_preview';
|
||||
import './login';
|
||||
import './menu';
|
||||
import './mfa';
|
||||
import './modal';
|
||||
import './post';
|
||||
import './post_dropdown_menu';
|
||||
import './search';
|
||||
import './sidebar_left';
|
||||
import './sidebar_right';
|
||||
import './suggestion_list';
|
||||
import './system';
|
||||
import './team';
|
||||
import './tooltip';
|
||||
30
e2e-tests/cypress/tests/support/ui/login.d.ts
vendored
Normal file
30
e2e-tests/cypress/tests/support/ui/login.d.ts
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
// 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 `ui` prefix, e.g. `uiLogin`.
|
||||
// ***************************************************************
|
||||
|
||||
declare namespace Cypress {
|
||||
interface Chainable {
|
||||
|
||||
/**
|
||||
* Login vi UI at login page
|
||||
*
|
||||
* @param {UserProfile} user - user with username and password
|
||||
*
|
||||
* @example
|
||||
* cy.uiLogin(user);
|
||||
*/
|
||||
uiLogin(user: UserProfile): Chainable;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user