From a358bef3cd91d4a7e76b0d3cdae036d507f16f2a Mon Sep 17 00:00:00 2001 From: Angel Mendez Date: Mon, 28 Oct 2024 03:03:21 -0600 Subject: [PATCH] MM-47287 Refactor/migrate bot accounts to typescript (#28347) * refactor: convert bot_api_1_spec.js to typescript - convert file bot_accounts/bot_api_1_spec.js to typescript - update related data types in order to fix argument issues * refactor: convert bot_api_not_cloud_spec.js to ts - convert file bot_accounts/bot_api_not_cloud_spec.js to typescript - update related data types in order to fix argument issues * refactor: convert bot_channel_intro_spec.js to ts - convert file bot_accounts/bot_channel_intro_spec.js to typescript - convert support/api/bots.js to typescript - remove data type bot.d.ts and include docs and definitions in support/api/bots.ts * refactor: convert bot_accounts files to ts - convert files from js to typescript - move declaration files to ts - update js docs - fix lint and type errors Related to #21303 * fix: replace params undefined on client.createUser - replace undefined params with empty strings as suggested in review * fix: add types to variables on spec tags_spec.ts - add types to variables * fix: update args for apiCreateBot on api/bots.ts - update args so that lint issue is fixed * Apply suggestions from code review update assertion to 401 --------- Co-authored-by: Saturnino Abril <5334504+saturninoabril@users.noreply.github.com> --- .../{bot_api_1_spec.js => bot_api_1_spec.ts} | 13 +- .../{bot_api_2_spec.js => bot_api_2_spec.ts} | 23 +- ...loud_spec.js => bot_api_not_cloud_spec.ts} | 3 +- ...ntro_spec.js => bot_channel_intro_spec.ts} | 10 +- ...{create_bot_spec.js => create_bot_spec.ts} | 0 ...t_cloud_spec.js => crud_not_cloud_spec.ts} | 4 +- .../{crud_spec.js => crud_spec.ts} | 7 +- ...play_name_spec.js => display_name_spec.ts} | 10 +- .../{edit_bot_spec.js => edit_bot_spec.ts} | 3 +- ...name_spec.js => edit_bot_username_spec.ts} | 8 +- .../bot_accounts/{helpers.js => helpers.ts} | 3 +- ...{in_lists_1_spec.js => in_lists_1_spec.ts} | 20 +- ...{in_lists_2_spec.js => in_lists_2_spec.ts} | 18 +- ..._spec.js => in_teams_and_channels_spec.ts} | 10 +- ...> managing_bot_accounts_not_cloud_spec.ts} | 3 +- ..._spec.js => managing_bot_accounts_spec.ts} | 3 +- ...t_message_spec.js => post_message_spec.ts} | 4 +- ..._demote_spec.js => promote_demote_spec.ts} | 3 +- ...isplay_spec.js => sidebar_display_spec.ts} | 24 +- .../{tags_spec.js => tags_spec.ts} | 13 +- .../channel_settings/channel_header_spec.ts | 3 +- .../group_mentions_posts_spec.ts | 8 +- .../guest_identification_spec.ts | 3 +- .../enterprise/ldap/ldap_login_spec.ts | 5 +- .../self-hosted/self_hosted_purchase_spec.ts | 7 +- e2e-tests/cypress/tests/support/api/bots.d.ts | 67 -- e2e-tests/cypress/tests/support/api/bots.js | 73 -- e2e-tests/cypress/tests/support/api/bots.ts | 137 +++ .../cypress/tests/support/api/channel.d.ts | 225 ----- .../cypress/tests/support/api/channel.js | 204 ---- .../cypress/tests/support/api/channel.ts | 435 +++++++++ .../cypress/tests/support/api/preference.d.ts | 165 ---- .../cypress/tests/support/api/preference.js | 409 -------- .../cypress/tests/support/api/preference.ts | 590 ++++++++++++ e2e-tests/cypress/tests/support/api/user.d.ts | 379 -------- e2e-tests/cypress/tests/support/api/user.js | 503 ---------- e2e-tests/cypress/tests/support/api/user.ts | 892 ++++++++++++++++++ .../cypress/tests/support/db_commands.ts | 2 +- .../cypress/tests/support/task_commands.ts | 12 +- .../tests/support/ui/global_header.d.ts | 2 +- e2e-tests/cypress/tests/support/ui/search.ts | 4 +- 41 files changed, 2197 insertions(+), 2110 deletions(-) rename e2e-tests/cypress/tests/integration/channels/bot_accounts/{bot_api_1_spec.js => bot_api_1_spec.ts} (96%) rename e2e-tests/cypress/tests/integration/channels/bot_accounts/{bot_api_2_spec.js => bot_api_2_spec.ts} (96%) rename e2e-tests/cypress/tests/integration/channels/bot_accounts/{bot_api_not_cloud_spec.js => bot_api_not_cloud_spec.ts} (97%) rename e2e-tests/cypress/tests/integration/channels/bot_accounts/{bot_channel_intro_spec.js => bot_channel_intro_spec.ts} (85%) rename e2e-tests/cypress/tests/integration/channels/bot_accounts/{create_bot_spec.js => create_bot_spec.ts} (100%) rename e2e-tests/cypress/tests/integration/channels/bot_accounts/{crud_not_cloud_spec.js => crud_not_cloud_spec.ts} (97%) rename e2e-tests/cypress/tests/integration/channels/bot_accounts/{crud_spec.js => crud_spec.ts} (97%) rename e2e-tests/cypress/tests/integration/channels/bot_accounts/{display_name_spec.js => display_name_spec.ts} (93%) rename e2e-tests/cypress/tests/integration/channels/bot_accounts/{edit_bot_spec.js => edit_bot_spec.ts} (97%) rename e2e-tests/cypress/tests/integration/channels/bot_accounts/{edit_bot_username_spec.js => edit_bot_username_spec.ts} (95%) rename e2e-tests/cypress/tests/integration/channels/bot_accounts/{helpers.js => helpers.ts} (88%) rename e2e-tests/cypress/tests/integration/channels/bot_accounts/{in_lists_1_spec.js => in_lists_1_spec.ts} (76%) rename e2e-tests/cypress/tests/integration/channels/bot_accounts/{in_lists_2_spec.js => in_lists_2_spec.ts} (84%) rename e2e-tests/cypress/tests/integration/channels/bot_accounts/{in_teams_and_channels_spec.js => in_teams_and_channels_spec.ts} (92%) rename e2e-tests/cypress/tests/integration/channels/bot_accounts/{managing_bot_accounts_not_cloud_spec.js => managing_bot_accounts_not_cloud_spec.ts} (98%) rename e2e-tests/cypress/tests/integration/channels/bot_accounts/{managing_bot_accounts_spec.js => managing_bot_accounts_spec.ts} (99%) rename e2e-tests/cypress/tests/integration/channels/bot_accounts/{post_message_spec.js => post_message_spec.ts} (95%) rename e2e-tests/cypress/tests/integration/channels/bot_accounts/{promote_demote_spec.js => promote_demote_spec.ts} (97%) rename e2e-tests/cypress/tests/integration/channels/bot_accounts/{sidebar_display_spec.js => sidebar_display_spec.ts} (80%) rename e2e-tests/cypress/tests/integration/channels/bot_accounts/{tags_spec.js => tags_spec.ts} (91%) delete mode 100644 e2e-tests/cypress/tests/support/api/bots.d.ts delete mode 100644 e2e-tests/cypress/tests/support/api/bots.js create mode 100644 e2e-tests/cypress/tests/support/api/bots.ts delete mode 100644 e2e-tests/cypress/tests/support/api/channel.d.ts delete mode 100644 e2e-tests/cypress/tests/support/api/channel.js create mode 100644 e2e-tests/cypress/tests/support/api/channel.ts delete mode 100644 e2e-tests/cypress/tests/support/api/preference.d.ts delete mode 100644 e2e-tests/cypress/tests/support/api/preference.js create mode 100644 e2e-tests/cypress/tests/support/api/preference.ts delete mode 100644 e2e-tests/cypress/tests/support/api/user.d.ts delete mode 100644 e2e-tests/cypress/tests/support/api/user.js create mode 100644 e2e-tests/cypress/tests/support/api/user.ts diff --git a/e2e-tests/cypress/tests/integration/channels/bot_accounts/bot_api_1_spec.js b/e2e-tests/cypress/tests/integration/channels/bot_accounts/bot_api_1_spec.ts similarity index 96% rename from e2e-tests/cypress/tests/integration/channels/bot_accounts/bot_api_1_spec.js rename to e2e-tests/cypress/tests/integration/channels/bot_accounts/bot_api_1_spec.ts index 7f683d1968..021635cf58 100644 --- a/e2e-tests/cypress/tests/integration/channels/bot_accounts/bot_api_1_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/bot_accounts/bot_api_1_spec.ts @@ -10,14 +10,17 @@ // Stage: @prod // Group: @channels @bot_accounts @mfa +import {Channel} from '@mattermost/types/channels'; +import {Team} from '@mattermost/types/teams'; +import {UserProfile} from '@mattermost/types/users'; import * as TIMEOUTS from '../../../fixtures/timeouts'; describe('Bot accounts ownership and API', () => { - let newTeam; - let newUser; - let newChannel; - let botId; - let botName; + let newTeam: Team; + let newUser: UserProfile; + let newChannel: Channel; + let botId: string; + let botName: string; beforeEach(() => { cy.apiAdminLogin(); diff --git a/e2e-tests/cypress/tests/integration/channels/bot_accounts/bot_api_2_spec.js b/e2e-tests/cypress/tests/integration/channels/bot_accounts/bot_api_2_spec.ts similarity index 96% rename from e2e-tests/cypress/tests/integration/channels/bot_accounts/bot_api_2_spec.js rename to e2e-tests/cypress/tests/integration/channels/bot_accounts/bot_api_2_spec.ts index 857157f010..b9cde746ec 100644 --- a/e2e-tests/cypress/tests/integration/channels/bot_accounts/bot_api_2_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/bot_accounts/bot_api_2_spec.ts @@ -10,16 +10,19 @@ // Stage: @prod // Group: @channels @bot_accounts @mfa +import {Channel} from '@mattermost/types/channels'; +import {Team} from '@mattermost/types/teams'; +import {UserProfile} from '@mattermost/types/users'; import * as TIMEOUTS from '../../../fixtures/timeouts'; describe('Bot accounts ownership and API', () => { - let newTeam; - let newUser; - let newChannel; - let botId; - let botUsername; - let botName; - let adminUser; + let newTeam: Team; + let newUser: UserProfile; + let newChannel: Channel; + let botId: string; + let botUsername: string; + let botName: string; + let adminUser: UserProfile; beforeEach(() => { cy.apiAdminLogin().then(({user}) => { @@ -180,7 +183,7 @@ describe('Bot accounts ownership and API', () => { // # Create a post cy.postBotMessage({channelId: channel.id, message: msg2, token, failOnStatus: false}).then(({status}) => { // * Validate that posting failed - expect(status, 403); + expect(status).to.equal(401); }); cy.apiAdminLogin(); @@ -238,7 +241,7 @@ describe('Bot accounts ownership and API', () => { // # Create a post cy.postBotMessage({channelId: channel.id, message: msg2, token, failOnStatus: false}).then(({status}) => { // * Validate that posting failed - expect(status, 403); + expect(status).to.equal(401); }); // # Enable the bot token again @@ -301,7 +304,7 @@ describe('Bot accounts ownership and API', () => { // # Create a post cy.postBotMessage({channelId: channel.id, message: msg2, token, failOnStatus: false}).then(({status}) => { // * Validate that posting failed - expect(status, 403); + expect(status).to.equal(401); }); }); }); diff --git a/e2e-tests/cypress/tests/integration/channels/bot_accounts/bot_api_not_cloud_spec.js b/e2e-tests/cypress/tests/integration/channels/bot_accounts/bot_api_not_cloud_spec.ts similarity index 97% rename from e2e-tests/cypress/tests/integration/channels/bot_accounts/bot_api_not_cloud_spec.js rename to e2e-tests/cypress/tests/integration/channels/bot_accounts/bot_api_not_cloud_spec.ts index 2ffe42033e..15759f8813 100644 --- a/e2e-tests/cypress/tests/integration/channels/bot_accounts/bot_api_not_cloud_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/bot_accounts/bot_api_not_cloud_spec.ts @@ -10,10 +10,11 @@ // Stage: @prod // Group: @channels @not_cloud @bot_accounts +import {Team} from '@mattermost/types/teams'; import * as TIMEOUTS from '../../../fixtures/timeouts'; describe('Bot accounts ownership and API', () => { - let newTeam; + let newTeam: Team; before(() => { cy.shouldNotRunOnCloudEdition(); diff --git a/e2e-tests/cypress/tests/integration/channels/bot_accounts/bot_channel_intro_spec.js b/e2e-tests/cypress/tests/integration/channels/bot_accounts/bot_channel_intro_spec.ts similarity index 85% rename from e2e-tests/cypress/tests/integration/channels/bot_accounts/bot_channel_intro_spec.js rename to e2e-tests/cypress/tests/integration/channels/bot_accounts/bot_channel_intro_spec.ts index cf315bc8fc..4eb46d34ae 100644 --- a/e2e-tests/cypress/tests/integration/channels/bot_accounts/bot_channel_intro_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/bot_accounts/bot_channel_intro_spec.ts @@ -10,11 +10,13 @@ // Stage: @prod // Group: @channels @bot_accounts +import {Bot} from '@mattermost/types/bots'; +import {Team} from '@mattermost/types/teams'; import {createBotPatch} from '../../../support/api/bots'; describe('Bot channel intro and avatar', () => { - let team; - let bot; + let team: Team; + let bot: Bot; before(() => { cy.apiInitSetup().then((out) => { @@ -33,7 +35,7 @@ describe('Bot channel intro and avatar', () => { cy.visit(`/${team.name}/messages/@${bot.username}`); // # Get channel intro and bot-post Avatars - cy.get(`#channelIntro .profile-icon > img.Avatar, img.Avatar[alt="${bot.username} profile image"]`). + cy.get(`#channelIntro .profile-icon > img.Avatar, img.Avatar[alt="${bot.username} profile image"]`). should(($imgs) => { // * Verify imgs downloaded expect($imgs[0].naturalWidth).to.be.greaterThan(0); @@ -44,7 +46,7 @@ describe('Bot channel intro and avatar', () => { cy.wrap($img). should('be.visible'). and('have.attr', 'src'). - then((url) => cy.request({url, encoding: 'binary'})). + then((url) => cy.request({url, encoding: 'binary'} as unknown as Partial)). then(({body}) => { // * Verify matches expected default bot avatar cy.fixture('bot-default-avatar.png', 'binary').should('deep.equal', body); diff --git a/e2e-tests/cypress/tests/integration/channels/bot_accounts/create_bot_spec.js b/e2e-tests/cypress/tests/integration/channels/bot_accounts/create_bot_spec.ts similarity index 100% rename from e2e-tests/cypress/tests/integration/channels/bot_accounts/create_bot_spec.js rename to e2e-tests/cypress/tests/integration/channels/bot_accounts/create_bot_spec.ts diff --git a/e2e-tests/cypress/tests/integration/channels/bot_accounts/crud_not_cloud_spec.js b/e2e-tests/cypress/tests/integration/channels/bot_accounts/crud_not_cloud_spec.ts similarity index 97% rename from e2e-tests/cypress/tests/integration/channels/bot_accounts/crud_not_cloud_spec.js rename to e2e-tests/cypress/tests/integration/channels/bot_accounts/crud_not_cloud_spec.ts index 281c07fd83..3138a3dbdd 100644 --- a/e2e-tests/cypress/tests/integration/channels/bot_accounts/crud_not_cloud_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/bot_accounts/crud_not_cloud_spec.ts @@ -10,12 +10,14 @@ // Stage: @prod // Group: @channels @not_cloud @bot_accounts +import {Team} from '@mattermost/types/teams'; + import {getRandomId} from '../../../utils'; import {createBotInteractive} from './helpers'; describe('Bot accounts - CRUD Testing', () => { - let newTeam; + let newTeam: Team; before(() => { cy.shouldNotRunOnCloudEdition(); diff --git a/e2e-tests/cypress/tests/integration/channels/bot_accounts/crud_spec.js b/e2e-tests/cypress/tests/integration/channels/bot_accounts/crud_spec.ts similarity index 97% rename from e2e-tests/cypress/tests/integration/channels/bot_accounts/crud_spec.js rename to e2e-tests/cypress/tests/integration/channels/bot_accounts/crud_spec.ts index 44328a4c17..9a4685e5b7 100644 --- a/e2e-tests/cypress/tests/integration/channels/bot_accounts/crud_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/bot_accounts/crud_spec.ts @@ -10,13 +10,16 @@ // Stage: @prod // Group: @channels @bot_accounts +import {Bot} from '@mattermost/types/bots'; +import {Team} from '@mattermost/types/teams'; + import {getRandomId} from '../../../utils'; import {createBotInteractive} from './helpers'; describe('Bot accounts - CRUD Testing', () => { - let newTeam; - let testBot; + let newTeam: Team; + let testBot: Bot & {fullDisplayName: string}; before(() => { // # Create and visit new channel diff --git a/e2e-tests/cypress/tests/integration/channels/bot_accounts/display_name_spec.js b/e2e-tests/cypress/tests/integration/channels/bot_accounts/display_name_spec.ts similarity index 93% rename from e2e-tests/cypress/tests/integration/channels/bot_accounts/display_name_spec.js rename to e2e-tests/cypress/tests/integration/channels/bot_accounts/display_name_spec.ts index fac426ab3c..cb7fe9b2f2 100644 --- a/e2e-tests/cypress/tests/integration/channels/bot_accounts/display_name_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/bot_accounts/display_name_spec.ts @@ -10,9 +10,13 @@ // Stage: @prod // Group: @channels @bot_accounts +import {Bot} from '@mattermost/types/bots'; +import {Channel} from '@mattermost/types/channels'; +import {UserProfile} from '@mattermost/types/users'; + describe('Bot display name', () => { - let offTopicChannel; - let otherSysadmin; + let offTopicChannel: Channel; + let otherSysadmin: UserProfile; before(() => { cy.intercept('**/api/v4/**').as('resources'); @@ -77,7 +81,7 @@ describe('Bot display name', () => { should('have.text', bot.display_name); }).then(() => { // # Change display name after prior verification - cy.wrap(client.patchBot(bot.user_id, {display_name: `NEW ${bot.display_name}`})).then((newBot) => { + cy.wrap(client.patchBot(bot.user_id, {display_name: `NEW ${bot.display_name}`})).then((newBot: Bot) => { cy.postBotMessage({token, message: secondMessage, props, channelId: offTopicChannel.id}). its('id'). should('exist'). diff --git a/e2e-tests/cypress/tests/integration/channels/bot_accounts/edit_bot_spec.js b/e2e-tests/cypress/tests/integration/channels/bot_accounts/edit_bot_spec.ts similarity index 97% rename from e2e-tests/cypress/tests/integration/channels/bot_accounts/edit_bot_spec.js rename to e2e-tests/cypress/tests/integration/channels/bot_accounts/edit_bot_spec.ts index b18a8a9da8..f935649b41 100644 --- a/e2e-tests/cypress/tests/integration/channels/bot_accounts/edit_bot_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/bot_accounts/edit_bot_spec.ts @@ -10,11 +10,12 @@ // Stage: @prod // Group: @channels @bot_accounts +import {Team} from '@mattermost/types/teams'; import * as MESSAGES from '../../../fixtures/messages'; import {getRandomId} from '../../../utils'; describe('Edit bot', () => { - let testTeam; + let testTeam: Team; before(() => { cy.apiInitSetup().then(({team, townSquareUrl}) => { diff --git a/e2e-tests/cypress/tests/integration/channels/bot_accounts/edit_bot_username_spec.js b/e2e-tests/cypress/tests/integration/channels/bot_accounts/edit_bot_username_spec.ts similarity index 95% rename from e2e-tests/cypress/tests/integration/channels/bot_accounts/edit_bot_username_spec.js rename to e2e-tests/cypress/tests/integration/channels/bot_accounts/edit_bot_username_spec.ts index 5876284c1c..164eb6e539 100644 --- a/e2e-tests/cypress/tests/integration/channels/bot_accounts/edit_bot_username_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/bot_accounts/edit_bot_username_spec.ts @@ -10,11 +10,12 @@ // Stage: @prod // Group: @channels @bot_accounts +import {Team} from '@mattermost/types/teams'; import * as TIMEOUTS from '../../../fixtures/timeouts'; import {getRandomId} from '../../../utils'; describe('Edit bot username', () => { - let team; + let team: Team; before(() => { cy.apiInitSetup().then((out) => { @@ -68,8 +69,9 @@ describe('Edit bot username', () => { // # Click update button cy.get('#saveBot').click(); - cy.wrap(newBotName); + return cy.wrap(newBotName); } + return cy.wrap(null); }).then((newBotName) => { // * Set alias for bot entry in bot list, this also checks that the bot entry exists cy.get('.backstage-list__item').contains('.backstage-list__item', newBotName).as('newbotEntry'); @@ -97,7 +99,7 @@ describe('Edit bot username', () => { const NAMING_WARNING_STANDARD = 'Usernames have to begin with a lowercase letter and be 3-22 characters long. You can use lowercase letters, numbers, periods, dashes, and underscores.'; const NAMING_WARNING_ENDING_PERIOD = 'Bot usernames cannot have a period as the last character'; - function tryUsername(name, warningMessage) { + function tryUsername(name: string, warningMessage?: string) { cy.get('#username').clear().type(name); cy.get('#saveBot').click(); diff --git a/e2e-tests/cypress/tests/integration/channels/bot_accounts/helpers.js b/e2e-tests/cypress/tests/integration/channels/bot_accounts/helpers.ts similarity index 88% rename from e2e-tests/cypress/tests/integration/channels/bot_accounts/helpers.js rename to e2e-tests/cypress/tests/integration/channels/bot_accounts/helpers.ts index e46a8df897..ffbd8b665b 100644 --- a/e2e-tests/cypress/tests/integration/channels/bot_accounts/helpers.js +++ b/e2e-tests/cypress/tests/integration/channels/bot_accounts/helpers.ts @@ -1,10 +1,11 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {Team} from '@mattermost/types/teams'; import {getRandomId} from '../../../utils'; import * as TIMEOUTS from '../../../fixtures/timeouts'; -export function createBotInteractive(team, username = `bot-${getRandomId()}`) { +export function createBotInteractive(team: Team, username = `bot-${getRandomId()}`) { // # Visit the Integrations > Bot Accounts page cy.visit(`/${team.name}/integrations/bots`); diff --git a/e2e-tests/cypress/tests/integration/channels/bot_accounts/in_lists_1_spec.js b/e2e-tests/cypress/tests/integration/channels/bot_accounts/in_lists_1_spec.ts similarity index 76% rename from e2e-tests/cypress/tests/integration/channels/bot_accounts/in_lists_1_spec.js rename to e2e-tests/cypress/tests/integration/channels/bot_accounts/in_lists_1_spec.ts index 532f80808b..e79c939beb 100644 --- a/e2e-tests/cypress/tests/integration/channels/bot_accounts/in_lists_1_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/bot_accounts/in_lists_1_spec.ts @@ -10,15 +10,19 @@ // Stage: @prod // Group: @channels @bot_accounts +import {Bot} from '@mattermost/types/bots'; +import {Channel} from '@mattermost/types/channels'; +import {Team} from '@mattermost/types/teams'; +import {UserProfile} from '@mattermost/types/users'; import {createBotPatch} from '../../../support/api/bots'; import {generateRandomUser} from '../../../support/api/user'; import * as TIMEOUTS from '../../../fixtures/timeouts'; describe('Bots in lists', () => { - let team; - let channel; - let bots; - let createdUsers; + let team: Team; + let channel: Channel; + let bots: Bot[]; + let createdUsers: UserProfile[]; before(() => { cy.apiInitSetup().then((out) => { @@ -36,8 +40,8 @@ describe('Bots in lists', () => { // # Create users createdUsers = await Promise.all([ - client.createUser(generateRandomUser()), - client.createUser(generateRandomUser()), + client.createUser(generateRandomUser() as UserProfile, '', ''), + client.createUser(generateRandomUser() as UserProfile, '', ''), ]); await Promise.all([ @@ -48,8 +52,8 @@ describe('Bots in lists', () => { cy.wrap(user).its('username'); // # Add to team and channel - await client.addToTeam(team.id, user.user_id ?? user.id); - await client.addToChannel(user.user_id ?? user.id, channel.id); + await client.addToTeam(team.id, (user as Bot).user_id ?? (user as UserProfile).id); + await client.addToChannel((user as Bot).user_id ?? (user as UserProfile).id, channel.id); })); }); }); diff --git a/e2e-tests/cypress/tests/integration/channels/bot_accounts/in_lists_2_spec.js b/e2e-tests/cypress/tests/integration/channels/bot_accounts/in_lists_2_spec.ts similarity index 84% rename from e2e-tests/cypress/tests/integration/channels/bot_accounts/in_lists_2_spec.js rename to e2e-tests/cypress/tests/integration/channels/bot_accounts/in_lists_2_spec.ts index 98bfee2ec4..e2e7af4339 100644 --- a/e2e-tests/cypress/tests/integration/channels/bot_accounts/in_lists_2_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/bot_accounts/in_lists_2_spec.ts @@ -9,13 +9,17 @@ // Group: @channels @bot_accounts +import {Bot} from '@mattermost/types/bots'; +import {Channel} from '@mattermost/types/channels'; +import {Team} from '@mattermost/types/teams'; +import {UserProfile} from '@mattermost/types/users'; import {createBotPatch} from '../../../support/api/bots'; import {generateRandomUser} from '../../../support/api/user'; describe('Bots in lists', () => { - let team; - let channel; - let testUser; + let team: Team; + let channel: Channel; + let testUser: UserProfile; const STATUS_PRIORITY = { online: 0, @@ -42,8 +46,8 @@ describe('Bots in lists', () => { // # Create users const createdUsers = await Promise.all([ - client.createUser(generateRandomUser()), - client.createUser(generateRandomUser()), + client.createUser(generateRandomUser() as UserProfile, '', ''), + client.createUser(generateRandomUser() as UserProfile, '', ''), ]); await Promise.all([ @@ -54,8 +58,8 @@ describe('Bots in lists', () => { cy.wrap(user).its('username'); // # Add to team and channel - await client.addToTeam(team.id, user.user_id ?? user.id); - await client.addToChannel(user.user_id ?? user.id, channel.id); + await client.addToTeam(team.id, (user as Bot).user_id ?? (user as UserProfile).id); + await client.addToChannel((user as Bot).user_id ?? (user as UserProfile).id, channel.id); })); }); }); diff --git a/e2e-tests/cypress/tests/integration/channels/bot_accounts/in_teams_and_channels_spec.js b/e2e-tests/cypress/tests/integration/channels/bot_accounts/in_teams_and_channels_spec.ts similarity index 92% rename from e2e-tests/cypress/tests/integration/channels/bot_accounts/in_teams_and_channels_spec.js rename to e2e-tests/cypress/tests/integration/channels/bot_accounts/in_teams_and_channels_spec.ts index ec12c7f73e..eb5d8781bc 100644 --- a/e2e-tests/cypress/tests/integration/channels/bot_accounts/in_teams_and_channels_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/bot_accounts/in_teams_and_channels_spec.ts @@ -10,11 +10,13 @@ // Stage: @prod // Group: @channels @bot_accounts +import {Team} from '@mattermost/types/teams'; import {createBotPatch} from '../../../support/api/bots'; import {createChannelPatch} from '../../../support/api/channel'; +import {Channel} from '@mattermost/types/channels'; describe('Managing bots in Teams and Channels', () => { - let team; + let team: Team; before(() => { cy.apiUpdateConfig({ @@ -45,7 +47,7 @@ describe('Managing bots in Teams and Channels', () => { it('MM-T1816 Add a BOT to a channel', () => { cy.makeClient().then(async (client) => { // # Go to channel - const channel = await client.createChannel(createChannelPatch(team.id, 'a-chan', 'A Channel')); + const channel = await client.createChannel(createChannelPatch(team.id, 'a-chan', 'A Channel') as Channel); cy.visit(`/${team.name}/channels/${channel.name}`); // # Add bot to team @@ -63,7 +65,7 @@ describe('Managing bots in Teams and Channels', () => { it('MM-T1817 Add a BOT to a channel that is not on the Team', () => { cy.makeClient().then(async (client) => { // # Go to channel - const channel = await client.createChannel(createChannelPatch(team.id, 'a-chan', 'A Channel')); + const channel = await client.createChannel(createChannelPatch(team.id, 'a-chan', 'A Channel') as Channel); cy.visit(`/${team.name}/channels/${channel.name}`); // # Invite bot to team @@ -78,7 +80,7 @@ describe('Managing bots in Teams and Channels', () => { it('MM-T1818 No ephemeral post about Adding a bot to a channel When Bot is mentioned', () => { cy.makeClient().then(async (client) => { // # Go to channel - const channel = await client.createChannel(createChannelPatch(team.id, 'a-chan', 'A Channel')); + const channel = await client.createChannel(createChannelPatch(team.id, 'a-chan', 'A Channel') as Channel); cy.visit(`/${team.name}/channels/${channel.name}`); // # And bot to team diff --git a/e2e-tests/cypress/tests/integration/channels/bot_accounts/managing_bot_accounts_not_cloud_spec.js b/e2e-tests/cypress/tests/integration/channels/bot_accounts/managing_bot_accounts_not_cloud_spec.ts similarity index 98% rename from e2e-tests/cypress/tests/integration/channels/bot_accounts/managing_bot_accounts_not_cloud_spec.js rename to e2e-tests/cypress/tests/integration/channels/bot_accounts/managing_bot_accounts_not_cloud_spec.ts index 873e8caf60..0d8a30bce7 100644 --- a/e2e-tests/cypress/tests/integration/channels/bot_accounts/managing_bot_accounts_not_cloud_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/bot_accounts/managing_bot_accounts_not_cloud_spec.ts @@ -10,11 +10,12 @@ // Stage: @prod // Group: @channels @bot_accounts @plugin @not_cloud +import {Team} from '@mattermost/types/teams'; import * as TIMEOUTS from '../../../fixtures/timeouts'; import {matterpollPlugin} from '../../../utils/plugins'; describe('Managing bot accounts', () => { - let newTeam; + let newTeam: Team; before(() => { cy.shouldNotRunOnCloudEdition(); diff --git a/e2e-tests/cypress/tests/integration/channels/bot_accounts/managing_bot_accounts_spec.js b/e2e-tests/cypress/tests/integration/channels/bot_accounts/managing_bot_accounts_spec.ts similarity index 99% rename from e2e-tests/cypress/tests/integration/channels/bot_accounts/managing_bot_accounts_spec.js rename to e2e-tests/cypress/tests/integration/channels/bot_accounts/managing_bot_accounts_spec.ts index 71861d1353..988fbb35d8 100644 --- a/e2e-tests/cypress/tests/integration/channels/bot_accounts/managing_bot_accounts_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/bot_accounts/managing_bot_accounts_spec.ts @@ -10,11 +10,12 @@ // Stage: @prod // Group: @channels @bot_accounts +import {Team} from '@mattermost/types/teams'; import * as TIMEOUTS from '../../../fixtures/timeouts'; import {getRandomId} from '../../../utils'; describe('Managing bot accounts', () => { - let newTeam; + let newTeam: Team; before(() => { // # Create and visit new channel diff --git a/e2e-tests/cypress/tests/integration/channels/bot_accounts/post_message_spec.js b/e2e-tests/cypress/tests/integration/channels/bot_accounts/post_message_spec.ts similarity index 95% rename from e2e-tests/cypress/tests/integration/channels/bot_accounts/post_message_spec.js rename to e2e-tests/cypress/tests/integration/channels/bot_accounts/post_message_spec.ts index 66546829c5..9906a3a4a8 100644 --- a/e2e-tests/cypress/tests/integration/channels/bot_accounts/post_message_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/bot_accounts/post_message_spec.ts @@ -1,6 +1,8 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {Channel} from '@mattermost/types/channels'; + // *************************************************************** // - [#] indicates a test step (e.g. # Go to a page) // - [*] indicates an assertion (e.g. * Check the title) @@ -11,7 +13,7 @@ // Group: @channels @bot_accounts describe('Bot post message', () => { - let offTopicChannel; + let offTopicChannel: Channel; before(() => { cy.apiInitSetup().then(({team}) => { diff --git a/e2e-tests/cypress/tests/integration/channels/bot_accounts/promote_demote_spec.js b/e2e-tests/cypress/tests/integration/channels/bot_accounts/promote_demote_spec.ts similarity index 97% rename from e2e-tests/cypress/tests/integration/channels/bot_accounts/promote_demote_spec.js rename to e2e-tests/cypress/tests/integration/channels/bot_accounts/promote_demote_spec.ts index 52d6ada67e..c29b304d8c 100644 --- a/e2e-tests/cypress/tests/integration/channels/bot_accounts/promote_demote_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/bot_accounts/promote_demote_spec.ts @@ -10,10 +10,11 @@ // Stage: @prod // Group: @channels @bot_accounts +import {Team} from '@mattermost/types/teams'; import {createBotPatch} from '../../../support/api/bots'; describe('Managing bots in Teams and Channels', () => { - let team; + let team: Team; before(() => { cy.apiUpdateConfig({ diff --git a/e2e-tests/cypress/tests/integration/channels/bot_accounts/sidebar_display_spec.js b/e2e-tests/cypress/tests/integration/channels/bot_accounts/sidebar_display_spec.ts similarity index 80% rename from e2e-tests/cypress/tests/integration/channels/bot_accounts/sidebar_display_spec.js rename to e2e-tests/cypress/tests/integration/channels/bot_accounts/sidebar_display_spec.ts index c9fda340c6..0fd97c5a71 100644 --- a/e2e-tests/cypress/tests/integration/channels/bot_accounts/sidebar_display_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/bot_accounts/sidebar_display_spec.ts @@ -10,15 +10,19 @@ // Stage: @prod // Group: @channels @bot_accounts @not_cloud +import {Bot} from '@mattermost/types/bots'; +import {Channel} from '@mattermost/types/channels'; +import {Team} from '@mattermost/types/teams'; +import {UserProfile} from '@mattermost/types/users'; import {createBotPatch} from '../../../support/api/bots'; import {generateRandomUser} from '../../../support/api/user'; describe('Bot accounts', () => { - let team; - let channel; - let testUser; - let bots; - let createdUsers; + let team: Team; + let channel: Channel; + let testUser: UserProfile; + let bots: Bot[]; + let createdUsers: UserProfile[]; before(() => { cy.shouldNotRunOnCloudEdition(); @@ -39,8 +43,8 @@ describe('Bot accounts', () => { // # Create users createdUsers = await Promise.all([ - client.createUser(generateRandomUser()), - client.createUser(generateRandomUser()), + client.createUser(generateRandomUser() as UserProfile, '', ''), + client.createUser(generateRandomUser() as UserProfile, '', ''), ]); await Promise.all([ @@ -51,8 +55,8 @@ describe('Bot accounts', () => { cy.wrap(user).its('username'); // # Add to team and channel - await client.addToTeam(team.id, user.user_id ?? user.id); - await client.addToChannel(user.user_id ?? user.id, channel.id); + await client.addToTeam(team.id, (user as Bot).user_id ?? (user as UserProfile).id); + await client.addToChannel((user as Bot).user_id ?? (user as UserProfile).id, channel.id); })); }); }); @@ -72,7 +76,7 @@ describe('Bot accounts', () => { // * Verify bot icon exists cy.wrap($link).find('.Avatar').should('exist'). - and('have.attr', 'src'). + invoke('attr', 'src'). then((url) => cy.request({url, encoding: 'binary'})). then(({body}) => { // * Verify it matches default bot avatar diff --git a/e2e-tests/cypress/tests/integration/channels/bot_accounts/tags_spec.js b/e2e-tests/cypress/tests/integration/channels/bot_accounts/tags_spec.ts similarity index 91% rename from e2e-tests/cypress/tests/integration/channels/bot_accounts/tags_spec.js rename to e2e-tests/cypress/tests/integration/channels/bot_accounts/tags_spec.ts index cf891f67fc..04496d8ec4 100644 --- a/e2e-tests/cypress/tests/integration/channels/bot_accounts/tags_spec.js +++ b/e2e-tests/cypress/tests/integration/channels/bot_accounts/tags_spec.ts @@ -10,14 +10,17 @@ // Stage: @prod // Group: @channels @bot_accounts +import {Channel} from '@mattermost/types/channels'; +import {Team} from '@mattermost/types/teams'; +import {UserProfile} from '@mattermost/types/users'; import {createBotPatch} from '../../../support/api/bots'; import * as TIMEOUTS from '../../../fixtures/timeouts'; describe('Bot tags', () => { - let me; - let team; - let channel; - let postId; + let me: UserProfile; + let team: Team; + let channel: Channel; + let postId: string; before(() => { cy.apiInitSetup().then((out) => { @@ -92,6 +95,6 @@ describe('Bot tags', () => { }); }); -function rhsPostHasBotBadge(postId) { +function rhsPostHasBotBadge(postId: string) { cy.get(`.post#searchResult_${postId} .Tag`).should('be.visible').and('have.text', 'BOT'); } diff --git a/e2e-tests/cypress/tests/integration/channels/channel_settings/channel_header_spec.ts b/e2e-tests/cypress/tests/integration/channels/channel_settings/channel_header_spec.ts index 9936471ecc..9153b55646 100644 --- a/e2e-tests/cypress/tests/integration/channels/channel_settings/channel_header_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/channel_settings/channel_header_spec.ts @@ -11,6 +11,7 @@ // Group: @channels @channel_settings // node run_tests.js --group='@channel_settings' +import {ChannelType} from '@mattermost/types/channels'; import {getRandomId} from '../../../utils'; import * as TIMEOUTS from '../../../fixtures/timeouts'; @@ -35,7 +36,7 @@ describe('Channel Settings', () => { it('MM-T1808 Hover effect exists to add a channel description / header (when not already present)', () => { // # Create a new public channel and then private channel ['O', 'P'].forEach((channelType) => { - cy.apiCreateChannel(testTeam.id, `chan${getRandomId()}`, 'chan', channelType).then(({channel}) => { + cy.apiCreateChannel(testTeam.id, `chan${getRandomId()}`, 'chan', channelType as ChannelType).then(({channel}) => { // # Go to new channel cy.visit(`/${testTeam.name}/channels/${channel.name}`); diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/group_mentions/group_mentions_posts_spec.ts b/e2e-tests/cypress/tests/integration/channels/enterprise/group_mentions/group_mentions_posts_spec.ts index 1753c65014..a5daa72be5 100644 --- a/e2e-tests/cypress/tests/integration/channels/enterprise/group_mentions/group_mentions_posts_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/enterprise/group_mentions/group_mentions_posts_spec.ts @@ -260,17 +260,17 @@ describe('Group Mentions', () => { // # Link the group and the channel. cy.apiLinkGroupChannel(groupID1, channel.id); - cy.apiLogin({username: 'board.one', password: 'Password1'} as any).then((boardOne) => { - cy.apiAddUserToChannel(channel.id, boardOne.id); + cy.apiLogin({username: 'board.one', password: 'Password1'} as any).then(({user}: {user: UserProfile}) => { + cy.apiAddUserToChannel(channel.id, user.id); // # Make the channel private and group-synced. cy.apiPatchChannel(channel.id, {group_constrained: true, type: 'P'}); // # Login to create the dev user - cy.apiLogin({username: 'dev.one', password: 'Password1'} as any).then((devOne) => { + cy.apiLogin({username: 'dev.one', password: 'Password1'} as any).then(({user}: {user: UserProfile}) => { cy.apiAdminLogin(); - cy.apiAddUserToTeam(testTeam.id, devOne.id); + cy.apiAddUserToTeam(testTeam.id, user.id); cy.apiLogin({username: 'board.one', password: 'Password1'} as any); diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/guest_accounts/guest_identification_spec.ts b/e2e-tests/cypress/tests/integration/channels/enterprise/guest_accounts/guest_identification_spec.ts index 2057a66a55..71d326c2b3 100644 --- a/e2e-tests/cypress/tests/integration/channels/enterprise/guest_accounts/guest_identification_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/enterprise/guest_accounts/guest_identification_spec.ts @@ -22,6 +22,7 @@ import { reUrl, verifyEmailBody, } from '../../../../utils'; +import {UserProfile} from '@mattermost/types/users'; describe('Guest Accounts', () => { let sysadmin: Cypress.UserProfile; @@ -40,7 +41,7 @@ describe('Guest Accounts', () => { }); // # Log in as a team admin. - cy.apiAdminLogin().then((user) => { + cy.apiAdminLogin().then(({user}: {user: UserProfile}) => { sysadmin = user; }); }); diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/ldap/ldap_login_spec.ts b/e2e-tests/cypress/tests/integration/channels/enterprise/ldap/ldap_login_spec.ts index 04f405d7c8..c86b605ea7 100644 --- a/e2e-tests/cypress/tests/integration/channels/enterprise/ldap/ldap_login_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/enterprise/ldap/ldap_login_spec.ts @@ -10,6 +10,7 @@ // Stage: @prod // Group: @channels @enterprise @ldap +import {UserProfile} from '@mattermost/types/users'; import ldapUsers from '../../../../fixtures/ldap_users.json'; import {getRandomId} from '../../../../utils'; @@ -220,7 +221,7 @@ function setLDAPTestSettings(config) { } function disableOnboardingTaskList(ldapLogin) { - cy.apiLogin(ldapLogin).then((user) => { + cy.apiLogin(ldapLogin).then(({user}: {user: UserProfile}) => { cy.apiSaveOnboardingTaskListPreference(user.id, 'onboarding_task_list_open', 'false'); cy.apiSaveOnboardingTaskListPreference(user.id, 'onboarding_task_list_show', 'false'); cy.apiSaveSkipStepsPreference(user.id, 'true'); @@ -228,7 +229,7 @@ function disableOnboardingTaskList(ldapLogin) { } function removeUserFromAllTeams(testUser) { - cy.apiGetUsersByUsernames([testUser.username]).then((users) => { + cy.apiGetUsersByUsernames([testUser.username]).then(({users}) => { if (users.length > 0) { users.forEach((user) => { cy.apiGetTeamsForUser(user.id).then((teams) => { diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/self-hosted/self_hosted_purchase_spec.ts b/e2e-tests/cypress/tests/integration/channels/enterprise/self-hosted/self_hosted_purchase_spec.ts index a21f5994b1..7bc516c53f 100644 --- a/e2e-tests/cypress/tests/integration/channels/enterprise/self-hosted/self_hosted_purchase_spec.ts +++ b/e2e-tests/cypress/tests/integration/channels/enterprise/self-hosted/self_hosted_purchase_spec.ts @@ -20,6 +20,7 @@ // * Change mattermost-server utils/license.go to test public key // * e.g. see (https://github.com/mattermost/mattermost-server/pull/16778/files) +import {UserProfile} from '@mattermost/types/users'; import * as TIMEOUTS from '../../../../fixtures/timeouts'; function verifyPurchaseModal() { @@ -136,7 +137,7 @@ function getCurrentUsers(): Cypress.Chainable { } describe('Self hosted Purchase', () => { - let adminUser: Cypress.UserProfile | undefined; + let adminUser: UserProfile; beforeEach(() => { // prevent failed tests from bleeding over @@ -145,9 +146,9 @@ describe('Self hosted Purchase', () => { before(() => { cy.apiInitSetup().then(() => { - cy.apiAdminLogin().then((result) => { + cy.apiAdminLogin().then(({user}) => { // assertion because current typings are wrong. - adminUser = (result as unknown as {user: Cypress.UserProfile}).user; + adminUser = user; cy.apiDeleteLicense(); cy.visit('/'); diff --git a/e2e-tests/cypress/tests/support/api/bots.d.ts b/e2e-tests/cypress/tests/support/api/bots.d.ts deleted file mode 100644 index 7fc8da314b..0000000000 --- a/e2e-tests/cypress/tests/support/api/bots.d.ts +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -/// - -// *************************************************************** -// 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 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 - * }); - */ - apiCreateBot({bot: BotPatch, prefix: string}?): Chainable<{bot: Bot & {fullDisplayName: string}}>; - - /** - * 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(); - */ - apiGetBots(page?: number, perPage?: number, includeDeleted?: boolean): Chainable<{bots: Bot[]}>; - - /** - * 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); - */ - apiDisableBot(userId: string): Chainable; - - /** - * Deactivate test bots. - * - * @example - * cy.apiDeactivateTestBots(); - */ - apiDeactivateTestBots(): Chainable<>; - } -} diff --git a/e2e-tests/cypress/tests/support/api/bots.js b/e2e-tests/cypress/tests/support/api/bots.js deleted file mode 100644 index 7e72f9483f..0000000000 --- a/e2e-tests/cypress/tests/support/api/bots.js +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import {getRandomId} from '../../utils'; - -// ***************************************************************************** -// Bots -// https://api.mattermost.com/#tag/bots -// ***************************************************************************** - -Cypress.Commands.add('apiCreateBot', ({prefix, bot = createBotPatch(prefix)} = {}) => { - return cy.request({ - headers: {'X-Requested-With': 'XMLHttpRequest'}, - url: '/api/v4/bots', - method: 'POST', - body: bot, - }).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('apiGetBots', (page = 0, perPage = 200, includeDeleted = false) => { - 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('apiDisableBot', (userId) => { - 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); - }); -}); - -export function createBotPatch(prefix = 'bot') { - const randomId = getRandomId(); - - return { - username: `${prefix}-${randomId}`, - display_name: `Test Bot ${randomId}`, - description: `Test bot description ${randomId}`, - }; -} - -Cypress.Commands.add('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}"`); - } - }); - }); -}); diff --git a/e2e-tests/cypress/tests/support/api/bots.ts b/e2e-tests/cypress/tests/support/api/bots.ts new file mode 100644 index 0000000000..12c8710723 --- /dev/null +++ b/e2e-tests/cypress/tests/support/api/bots.ts @@ -0,0 +1,137 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Bot, BotPatch} from '@mattermost/types/bots'; +import {getRandomId} from '../../utils'; +import {ChainableT} from 'tests/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) { + 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; + } + } +} diff --git a/e2e-tests/cypress/tests/support/api/channel.d.ts b/e2e-tests/cypress/tests/support/api/channel.d.ts deleted file mode 100644 index a137ab31d4..0000000000 --- a/e2e-tests/cypress/tests/support/api/channel.d.ts +++ /dev/null @@ -1,225 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -/// - -// *************************************************************** -// 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 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 - * }); - */ - apiCreateChannel( - teamId: string, - name: string, - displayName: string, - type?: string, - purpose?: string, - header?: string, - unique: boolean = true - ): Chainable<{channel: Channel}>; - - /** - * 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 - * }); - */ - apiCreateDirectChannel(userIds: string[]): Chainable<{channel: Channel}>; - - /** - * 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 - * }); - */ - apiCreateGroupChannel(userIds: string[]): Chainable<{channel: Channel}>; - - /** - * 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'}); - */ - apiUpdateChannel(channelId: string, channel: Channel): Chainable<{channel: Channel}>; - - /** - * 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'}); - */ - apiPatchChannel(channelId: string, channel: Partial): Chainable<{channel: Channel}>; - - /** - * 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'); - */ - apiPatchChannelPrivacy(channelId: string, privacy: string): Chainable<{channel: Channel}>; - - /** - * 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 - * }); - */ - apiGetChannel(channelId: string): Chainable<{channel: Channel}>; - - /** - * 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 - * }); - */ - apiGetChannelByName(teamName: string, channelName: string): Chainable<{channel: Channel}>; - - /** - * 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 - * }); - */ - apiGetAllChannels(): Chainable<{channels: Channel[]}>; - - /** - * 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 - * }); - */ - apiGetChannelsForUser(): Chainable<{channels: Channel[]}>; - - /** - * 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'); - */ - apiDeleteChannel(channelId: string): Chainable; - - /** - * 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 - * }); - */ - apiAddUserToChannel(channelId: string, userId: string): Chainable; - - /** - * 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 - * }); - */ - apiCreateArchivedChannel(name: string, displayName: string, type: string, teamId: string, messages?: string[], user?: UserProfile): Chainable; - - /** - * 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 - */ - apiConvertGMToPrivateChannel(channelId: string, teamId: string, displayName: string, name: string): Chainable; - } -} diff --git a/e2e-tests/cypress/tests/support/api/channel.js b/e2e-tests/cypress/tests/support/api/channel.js deleted file mode 100644 index 37fcc24e17..0000000000 --- a/e2e-tests/cypress/tests/support/api/channel.js +++ /dev/null @@ -1,204 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import {getRandomId} from '../../utils'; - -// ***************************************************************************** -// Channels -// https://api.mattermost.com/#tag/channels -// ***************************************************************************** - -export function createChannelPatch(teamId, name, displayName, type = 'O', purpose = '', header = '', unique = true) { - const randomSuffix = getRandomId(); - - return { - team_id: teamId, - name: unique ? `${name}-${randomSuffix}` : name, - display_name: unique ? `${displayName} ${randomSuffix}` : displayName, - type, - purpose, - header, - }; -} - -Cypress.Commands.add('apiCreateChannel', (...args) => { - return cy.request({ - headers: {'X-Requested-With': 'XMLHttpRequest'}, - url: '/api/v4/channels', - method: 'POST', - body: createChannelPatch(...args), - }).then((response) => { - expect(response.status).to.equal(201); - return cy.wrap({channel: response.body}); - }); -}); - -Cypress.Commands.add('apiCreateDirectChannel', (userIds = []) => { - 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('apiCreateGroupChannel', (userIds = []) => { - 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('apiUpdateChannel', (channelId, channelData) => { - return cy.request({ - headers: {'X-Requested-With': 'XMLHttpRequest'}, - url: '/api/v4/channels/' + channelId, - method: 'PUT', - body: { - id: channelId, - ...channelData, - }, - }).then((response) => { - expect(response.status).to.equal(200); - return cy.wrap({channel: response.body}); - }); -}); - -Cypress.Commands.add('apiPatchChannel', (channelId, channelData) => { - return cy.request({ - headers: {'X-Requested-With': 'XMLHttpRequest'}, - method: 'PUT', - url: `/api/v4/channels/${channelId}/patch`, - body: channelData, - }).then((response) => { - expect(response.status).to.equal(200); - return cy.wrap({channel: response.body}); - }); -}); - -Cypress.Commands.add('apiPatchChannelPrivacy', (channelId, privacy = 'O') => { - 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('apiGetChannel', (channelId) => { - 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('apiGetChannelByName', (teamName, channelName) => { - 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('apiGetAllChannels', () => { - 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('apiGetChannelsForUser', (userId, teamId) => { - 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('apiDeleteChannel', (channelId) => { - 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('apiAddUserToChannel', (channelId, userId) => { - return cy.request({ - 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('apiRemoveUserFromChannel', (channelId, userId) => { - 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('apiCreateArchivedChannel', (name, displayName, type = 'O', teamId, messages = [], user) => { - return cy.apiCreateChannel(teamId, name, displayName, type).then(({channel}) => { - Cypress._.forEach(messages, (message) => { - cy.postMessageAs({ - sender: user, - message, - channelId: channel.id, - }); - }); - - cy.apiDeleteChannel(channel.id); - return cy.wrap(channel); - }); -}); - -Cypress.Commands.add('apiConvertGMToPrivateChannel', (channelId, teamId, displayName, name) => { - const body = { - channel_id: channelId, - team_id: teamId, - display_name: displayName, - name, - }; - - return cy.request({ - headers: {'X-Requested-With': 'XMLHttpRequest'}, - url: `/api/v4/channels/${channelId}/convert_to_channel?team_id=${teamId}`, - method: 'POST', - body, - }); -}); diff --git a/e2e-tests/cypress/tests/support/api/channel.ts b/e2e-tests/cypress/tests/support/api/channel.ts new file mode 100644 index 0000000000..ee125fd169 --- /dev/null +++ b/e2e-tests/cypress/tests/support/api/channel.ts @@ -0,0 +1,435 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Channel, ChannelMembership, ChannelType} from '@mattermost/types/channels'; +import {UserProfile} from '@mattermost/types/users'; +import {ChainableT} from 'tests/types'; +import {getRandomId} from '../../utils'; + +// ***************************************************************************** +// Channels +// https://api.mattermost.com/#tag/channels +// ***************************************************************************** + +export function createChannelPatch(teamId: string, name: string, displayName: string, type: ChannelType = 'O', purpose = '', header = '', unique = true): Partial { + 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: { + id: channelId, + ...channel, + }, + }).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): 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 { + 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({ + 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, userId): ChainableT { + 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 { + return cy.apiCreateChannel(teamId, name, displayName, type).then(({channel}) => { + Cypress._.forEach(messages, (message) => { + cy.postMessageAs({ + sender: user, + 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 { + const body = { + channel_id: channelId, + team_id: teamId, + display_name: displayName, + name, + }; + + return cy.request({ + 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; + } + } +} diff --git a/e2e-tests/cypress/tests/support/api/preference.d.ts b/e2e-tests/cypress/tests/support/api/preference.d.ts deleted file mode 100644 index d26c5fe447..0000000000 --- a/e2e-tests/cypress/tests/support/api/preference.d.ts +++ /dev/null @@ -1,165 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -/// - -// *************************************************************** -// 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 { - - // ******************************************************************************* - // 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'); - */ - apiSaveUserPreference(preferences: PreferenceType[], userId: string): Chainable; - - /** - * 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'); - */ - apiGetUserPreference(userId: string): Chainable; - - /** - * 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); - */ - apiSaveClockDisplayModeTo24HourPreference(is24Hour: boolean): Chainable; - - /** - * 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'); - */ - apiSaveOnboardingTaskListPreference(userId: string, name: string, value: string): Chainable; - - /** - * 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'); - */ - apiSaveSkipStepsPreference(userId: string, value: string): Chainable; - - /** - * 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'); - */ - apiSaveDirectChannelShowPreference(userId: string, otherUserId: string, value: string): Chainable; - - /** - * 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'); - */ - apiSaveCRTPreference(userId: string, value: string): Chainable; - - /** - * Saves tutorial step of a user - * @param {string} userId - User ID - * @param {string} value - value of tutorial step, e.g. '999' (default, completed tutorial) - */ - apiSaveTutorialStep(userId: string, value: string): Chainable; - - /** - * 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'); - */ - apiSaveCloudTrialBannerPreference(userId: string, name: string, value: string): Chainable; - - /** - * 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'); - */ - apiSaveStartTrialModal(userId: string, value: string): Chainable; - - /** - * Save drafts tour tip preference. - * See https://api.mattermost.com/#tag/preferences/paths/~1users~1{user_id}~1preferences/put - * @param {string} userId - User ID - * @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.apiSaveDraftsTourTipPreference('user-id', 'true'); - */ - apiSaveDraftsTourTipPreference(userId: string, value: boolean): Chainable; - - /** - * 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'); - */ - apiBoardsWelcomePageViewed(userId: string): Chainable; - } -} diff --git a/e2e-tests/cypress/tests/support/api/preference.js b/e2e-tests/cypress/tests/support/api/preference.js deleted file mode 100644 index ba517bea35..0000000000 --- a/e2e-tests/cypress/tests/support/api/preference.js +++ /dev/null @@ -1,409 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import theme from '../../fixtures/theme.json'; - -// ***************************************************************************** -// Preferences -// https://api.mattermost.com/#tag/preferences -// ***************************************************************************** - -/** - * Saves user's preference directly via API - * This API assume that the user is logged in and has cookie to access - * @param {Array} preference - a list of user's preferences - */ -Cypress.Commands.add('apiSaveUserPreference', (preferences = [], userId = 'me') => { - return cy.request({ - headers: {'X-Requested-With': 'XMLHttpRequest'}, - url: `/api/v4/users/${userId}/preferences`, - method: 'PUT', - body: preferences, - }); -}); - -/** - * Saves clock display mode 24-hour preference of a user directly via API - * This API assume that the user is logged in and has cookie to access - * @param {Boolean} is24Hour - Either true (default) or false - */ -Cypress.Commands.add('apiSaveClockDisplayModeTo24HourPreference', (is24Hour = true) => { - 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]); - }); -}); - -/** - * 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" - */ -Cypress.Commands.add('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]); - }); -}); - -/** - * 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" - */ -Cypress.Commands.add('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]); - }); -}); - -/** - * 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" - */ -Cypress.Commands.add('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]); - }); -}); - -/** - * 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. - */ -Cypress.Commands.add('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]); - }); -}); - -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. - */ -Cypress.Commands.add('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]); - }); -}); - -/** - * 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" - */ -Cypress.Commands.add('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]); - }); -}); - -/** - * 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" - */ -Cypress.Commands.add('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]); - }); -}); - -/** - * Saves tutorial step of a user - * This API assume that the user is logged in and has cookie to access - * @param {string} value - value of tutorial step, e.g. '999' (default, completed tutorial) - */ -Cypress.Commands.add('apiSaveTutorialStep', (userId, value = '999') => { - const preference = { - user_id: userId, - category: 'tutorial_step', - name: userId, - value, - }; - - return cy.apiSaveUserPreference([preference], userId); -}); - -Cypress.Commands.add('apiSaveOnboardingPreference', (userId, name, value) => { - const preference = { - user_id: userId, - category: 'recommended_next_steps', - name, - value, - }; - - return cy.apiSaveUserPreference([preference], userId); -}); - -Cypress.Commands.add('apiSaveDirectChannelShowPreference', (userId, otherUserId, value) => { - const preference = { - user_id: userId, - category: 'direct_channel_show', - name: otherUserId, - value, - }; - - return cy.apiSaveUserPreference([preference], userId); -}); - -Cypress.Commands.add('apiHideSidebarWhatsNewModalPreference', (userId, value) => { - 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('apiGetUserPreference', (userId) => { - return cy.request(`/api/v4/users/${userId}/preferences`).then((response) => { - expect(response.status).to.equal(200); - return cy.wrap(response.body); - }); -}); - -Cypress.Commands.add('apiSaveCRTPreference', (userId, value = 'on') => { - const preference = { - user_id: userId, - category: 'display_settings', - name: 'collapsed_reply_threads', - value, - }; - - return cy.apiSaveUserPreference([preference], userId); -}); - -Cypress.Commands.add('apiSaveCloudTrialBannerPreference', (userId, name, value) => { - const preference = { - user_id: userId, - category: 'cloud_trial_banner', - name, - value, - }; - - return cy.apiSaveUserPreference([preference], userId); -}); - -Cypress.Commands.add('apiSaveStartTrialModal', (userId, value = 'true') => { - const preference = { - user_id: userId, - category: 'start_trial_modal', - name: 'trial_modal_auto_shown', - value, - }; - - return cy.apiSaveUserPreference([preference], userId); -}); - -Cypress.Commands.add('apiSaveOnboardingTaskListPreference', (userId, name, value) => { - const preference = { - user_id: userId, - category: 'onboarding_task_list', - name, - value, - }; - - return cy.apiSaveUserPreference([preference], userId); -}); - -Cypress.Commands.add('apiSaveSkipStepsPreference', (userId, value) => { - const preference = { - user_id: userId, - category: 'recommended_next_steps', - name: 'skip', - value, - }; - - return cy.apiSaveUserPreference([preference], userId); -}); - -Cypress.Commands.add('apiSaveUnreadScrollPositionPreference', (userId, value) => { - const preference = { - user_id: userId, - category: 'advanced_settings', - name: 'unread_scroll_position', - value, - }; - - return cy.apiSaveUserPreference([preference], userId); -}); - -Cypress.Commands.add('apiSaveDraftsTourTipPreference', (userId, value) => { - const preference = { - user_id: userId, - category: 'drafts', - name: 'drafts_tour_tip_showed', - value: JSON.stringify({drafts_tour_tip_showed: value}), - }; - - return cy.apiSaveUserPreference([preference], userId); -}); - -Cypress.Commands.add('apiBoardsWelcomePageViewed', (userId) => { - 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); -}); - -/** - * 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 - */ -Cypress.Commands.add('apiSaveJoinLeaveMessagesPreference', (userId, enable = true) => { - const preference = { - user_id: userId, - category: 'advanced_settings', - name: 'join_leave', - value: enable.toString(), - }; - - return cy.apiSaveUserPreference([preference], userId); -}); - -/** - * Disables tutorials for user by marking them finished - */ -Cypress.Commands.add('apiDisableTutorials', (userId) => { - 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: 'crt_tutorial_triggered', - name: userId, - value: '999', - }, - { - user_id: userId, - category: 'crt_thread_pane_step', - name: userId, - value: '999', - }, - { - user_id: userId, - category: 'drafts', - name: 'drafts_tour_tip_showed', - value: '{"drafts_tour_tip_showed":true}', - }, - { - user_id: userId, - category: 'app_bar', - name: 'channel_with_board_tip_showed', - value: '{"channel_with_board_tip_showed":true}', - }, - ]; - - return cy.apiSaveUserPreference(preferences, userId); -}); diff --git a/e2e-tests/cypress/tests/support/api/preference.ts b/e2e-tests/cypress/tests/support/api/preference.ts new file mode 100644 index 0000000000..8588d4c123 --- /dev/null +++ b/e2e-tests/cypress/tests/support/api/preference.ts @@ -0,0 +1,590 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {ChainableT} from 'tests/types'; +import theme from '../../fixtures/theme.json'; +import {PreferenceType} from '@mattermost/types/preferences'; + +// ***************************************************************************** +// 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 { + 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 { + 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 { + const preference = { + user_id: userId, + category: 'tutorial_step', + name: userId, + value, + }; + + return cy.apiSaveUserPreference([preference], userId); +} + +Cypress.Commands.add('apiSaveTutorialStep', apiSaveTutorialStep); + +function apiSaveOnboardingPreference(userId, name, value) { + 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 { + 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, value) { + 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 { + return cy.request(`/api/v4/users/${userId}/preferences`).then((response) => { + expect(response.status).to.equal(200); + return cy.wrap(response.body); + }); +} + +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 { + 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 { + 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 { + 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 { + 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 { + 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, value) { + const preference = { + user_id: userId, + category: 'advanced_settings', + name: 'unread_scroll_position', + value, + }; + + return cy.apiSaveUserPreference([preference], userId); +} + +Cypress.Commands.add('apiSaveUnreadScrollPositionPreference', apiSaveUnreadScrollPositionPreference); + +/** + * Save drafts tour tip preference. + * See https://api.mattermost.com/#tag/preferences/paths/~1users~1{user_id}~1preferences/put + * @param {string} userId - User ID + * @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.apiSaveDraftsTourTipPreference('user-id', 'true'); + */ +function apiSaveDraftsTourTipPreference(userId: string, value: boolean): ChainableT { + const preference = { + user_id: userId, + category: 'drafts', + name: 'drafts_tour_tip_showed', + value: JSON.stringify({drafts_tour_tip_showed: value}), + }; + + return cy.apiSaveUserPreference([preference], userId); +} + +Cypress.Commands.add('apiSaveDraftsTourTipPreference', apiSaveDraftsTourTipPreference); + +/** + * 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 { + 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, 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) { + 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: 'crt_tutorial_triggered', + name: userId, + value: '999', + }, + { + user_id: userId, + category: 'crt_thread_pane_step', + name: userId, + value: '999', + }, + { + user_id: userId, + category: 'drafts', + name: 'drafts_tour_tip_showed', + value: '{"drafts_tour_tip_showed":true}', + }, + { + 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; + apiSaveDraftsTourTipPreference: typeof apiSaveDraftsTourTipPreference; + apiBoardsWelcomePageViewed: typeof apiBoardsWelcomePageViewed; + apiSaveJoinLeaveMessagesPreference: typeof apiSaveJoinLeaveMessagesPreference; + apiDisableTutorials: typeof apiDisableTutorials; + } + } +} diff --git a/e2e-tests/cypress/tests/support/api/user.d.ts b/e2e-tests/cypress/tests/support/api/user.d.ts deleted file mode 100644 index 81f1965a27..0000000000 --- a/e2e-tests/cypress/tests/support/api/user.d.ts +++ /dev/null @@ -1,379 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -/// - -// *************************************************************** -// 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 { - - /** - * 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'}); - */ - apiLogin(user: UserProfile): Chainable; - - /** - * 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'}); - */ - apiLoginWithMFA(user: UserProfile, token: string): Chainable<{user: UserProfile}>; - - /** - * 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(); - */ - apiAdminLogin(requestOptions?: Record): Chainable; - - /** - * 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); - */ - apiAdminLoginWithMFA(token: string): Chainable<{user: UserProfile}>; - - /** - * 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(); - */ - 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 - * }); - */ - apiGetMe(): Chainable<{user: UserProfile}>; - - /** - * 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 - * }); - */ - apiGetUserById(userId: string): Chainable; - - /** - * 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 - * }); - */ - apiGetUserByEmail(email: string): Chainable<{user: UserProfile}>; - - /** - * 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 - * }); - */ - apiGetUsersByUsernames(usernames: string[]): Chainable; - - /** - * 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 - * @param {string} userData.email - * @param {string} userData.username - * @param {string} userData.first_name - * @param {string} userData.last_name - * @param {string} userData.nickname - * @param {string} userData.locale - * @param {Object} userData.timezone - * @param {string} userData.position - * @param {Object} userData.props - * @param {Object} userData.notify_props - * @returns {UserProfile} out.user: `UserProfile` object - * - * @example - * cy.apiPatchUser('user-id', {locale: 'en'}).then(({user}) => { - * // do something with user - * }); - */ - apiPatchUser(userId: string, userData: UserProfile): Chainable<{user: UserProfile}>; - - /** - * 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 - * @param {string} userData.email - * @param {string} userData.username - * @param {string} userData.first_name - * @param {string} userData.last_name - * @param {string} userData.nickname - * @param {string} userData.locale - * @param {Object} userData.timezone - * @param {string} userData.position - * @param {Object} userData.props - * @param {Object} userData.notify_props - * @returns {UserProfile} out.user: `UserProfile` object - * - * @example - * cy.apiPatchMe({locale: 'en'}).then(({user}) => { - * // do something with user - * }); - */ - apiPatchMe(userData: UserProfile): Chainable; - - /** - * Create an admin account based from the env variables defined in Cypress env. - * @param {string} options.namePrefix - '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.showOnboarding - false (default) to hide or true to show Onboarding steps - * @returns {UserProfile} `out.sysadmin` as `UserProfile` object - * - * @example - * cy.apiCreateAdmin(options); - */ - apiCreateAdmin(options: Record): Chainable; - - /** - * 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 - */ - apiCreateCustomAdmin({loginAfter = false, hideAdminTrialModal = true} = {}): Chainable<{sysadmin: UserProfile}>; - - /** - * 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.showOnboarding - false (default) to hide or true to show Onboarding steps - * @returns {UserProfile} `out.user` as `UserProfile` object - * - * @example - * cy.apiCreateUser(options); - */ - apiCreateUser(options?: { - user?: Partial; - prefix?: string; - createAt?: number; - bypassTutorial?: boolean; - showOnboarding?: boolean; - }): Chainable<{user: UserProfile}>; - - /** - * 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); - */ - apiCreateGuestUser(options: Record): Chainable<{guest: UserProfile}>; - - /** - * 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'); - */ - apiRevokeUserSessions(userId: string): Chainable>; - - /** - * 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 - * }); - */ - apiGetUsers(queryParams: Record): Chainable<{users: UserProfile[]}>; - - /** - * 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 - * }); - */ - apiGetUsersNotInTeam(queryParams: Record): Chainable; - - /** - * 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'); - */ - apiActivateUser(userId: string): Chainable; - - /** - * 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'); - */ - apiDeactivateUser(userId: string): Chainable; - - /** - * 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.user: `UserProfile` object - * - * @example - * cy.apiDemoteUserToGuest('user-id'); - */ - apiDemoteUserToGuest(userId: string): Chainable; - - /** - * 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'); - */ - apiPromoteGuestToUser(userId: string): Chainable; - - /** - * 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 - * }); - */ - apiVerifyUserEmailById(userId: string): Chainable; - - /** - * 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); - */ - apiActivateUserMFA(userId: string, activate: boolean, token: string): Chainable; - - /** - * 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'); - */ - apiAccessToken(userId: string, description: string): Chainable; - - /** - * 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') - */ - apiRevokeAccessToken(tokenId: string): Chainable; - - /** - * 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'); - */ - apiUpdateUserAuth(userId: string, authData: string, password: string, authService: string): Chainable; - - /** - * 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 - * }); - */ - apiGetTotalUsers(): Chainable; - } -} diff --git a/e2e-tests/cypress/tests/support/api/user.js b/e2e-tests/cypress/tests/support/api/user.js deleted file mode 100644 index 155339e58f..0000000000 --- a/e2e-tests/cypress/tests/support/api/user.js +++ /dev/null @@ -1,503 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import authenticator from 'authenticator'; - -import {getRandomId} from '../../utils'; -import {getAdminAccount} from '../env'; - -import {buildQueryString} from './helpers'; - -// ***************************************************************************** -// Users -// https://api.mattermost.com/#tag/users -// ***************************************************************************** - -Cypress.Commands.add('apiLogin', (user, requestOptions = {}) => { - 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, - }, - }); - } - - return cy.wrap({error: response.body}); - }); -}); - -Cypress.Commands.add('apiLoginWithMFA', (user, token) => { - 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: { - ...response.body, - password: user.password, - }, - }); - }); -}); - -Cypress.Commands.add('apiAdminLogin', (requestOptions = {}) => { - const admin = getAdminAccount(); - - // First, login with username - cy.apiLogin(admin, requestOptions).then((resp) => { - if (resp.error) { - if (resp.error.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 - delete admin.username; - return cy.apiLogin(admin, requestOptions); - } - - return resp; - }); -}); - -Cypress.Commands.add('apiAdminLoginWithMFA', (token) => { - const admin = getAdminAccount(); - - return cy.apiLoginWithMFA(admin, token); -}); - -Cypress.Commands.add('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('apiGetMe', () => { - return cy.apiGetUserById('me'); -}); - -Cypress.Commands.add('apiGetUserById', (userId) => { - 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('apiGetUserByEmail', (email, failOnStatusCode = true) => { - 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('apiGetUsersByUsernames', (usernames = []) => { - 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('apiPatchUser', (userId, userData) => { - 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('apiPatchMe', (data) => { - 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('apiCreateCustomAdmin', ({loginAfter = false, hideAdminTrialModal = true} = {}) => { - 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('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}}); - }); -}); - -function generateRandomUser(prefix = 'user', createAt = 0) { - const randomId = getRandomId(); - - return { - email: `${prefix}${randomId}@sample.mattermost.com`, - username: `${prefix}${randomId}`, - password: 'passwd', - first_name: `First${randomId}`, - last_name: `Last${randomId}`, - nickname: `Nickname${randomId}`, - create_at: createAt, - }; -} - -Cypress.Commands.add('apiCreateUser', ({ - prefix = 'user', - createAt = 0, - bypassTutorial = true, - hideOnboarding = true, - bypassWhatsNewModal = true, - user = null, -} = {}) => { - 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'); - - // hide drafts tour tip so it doesn't block the execution of subsequent tests - cy.apiSaveDraftsTourTipPreference(createdUser.id, true); - - 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('apiCreateGuestUser', ({ - prefix = 'guest', - bypassTutorial = true, -} = {}) => { - return cy.apiCreateUser({prefix, bypassTutorial}).then(({user}) => { - cy.apiDemoteUserToGuest(user.id); - - return cy.wrap({guest: user}); - }); -}); - -/** - * Revoke all active sessions for a user - * @param {String} userId - ID of user to revoke sessions - */ -Cypress.Commands.add('apiRevokeUserSessions', (userId) => { - 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}); - }); -}); - -Cypress.Commands.add('apiGetUsers', (queryParams = {}) => { - 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}); - }); -}); - -Cypress.Commands.add('apiGetUsersNotInTeam', ({teamId, page = 0, perPage = 60} = {}) => { - return cy.apiGetUsers({not_in_team: teamId, page, per_page: perPage}); -}); - -Cypress.Commands.add('apiPatchUserRoles', (userId, roleNames = ['system_user']) => { - 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('apiDeactivateUser', (userId) => { - 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('apiActivateUser', (userId) => { - 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('apiDemoteUserToGuest', (userId) => { - 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('apiPromoteGuestToUser', (userId) => { - 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); - }); -}); - -/** - * Verify a user email via API - * @param {String} userId - ID of user of email to verify - */ -Cypress.Commands.add('apiVerifyUserEmailById', (userId) => { - 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('apiActivateUserMFA', (userId, activate, token) => { - 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('apiResetPassword', (userId, currentPass, newPass) => { - 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('apiGenerateMfaSecret', (userId) => { - 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('apiAccessToken', (userId, description) => { - 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); - }); -}); - -Cypress.Commands.add('apiRevokeAccessToken', (tokenId) => { - 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('apiUpdateUserAuth', (userId, authData, password, authService) => { - 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('apiGetTotalUsers', () => { - 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); - }); -}); - -export {generateRandomUser}; diff --git a/e2e-tests/cypress/tests/support/api/user.ts b/e2e-tests/cypress/tests/support/api/user.ts new file mode 100644 index 0000000000..13ecc7f335 --- /dev/null +++ b/e2e-tests/cypress/tests/support/api/user.ts @@ -0,0 +1,892 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {UserAccessToken, UserProfile} from '@mattermost/types/users'; +import authenticator from 'authenticator'; +import {ChainableT} from 'tests/types'; + +import {getRandomId} from '../../utils'; +import {getAdminAccount} from '../env'; + +import {buildQueryString} from './helpers'; + +// ***************************************************************************** +// 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>, requestOptions: Record = {}): ChainableT<{user: UserProfile} | {error: any}> { + 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: UserProfile}>({ + user: { + ...response.body, + password: user.password, + }, + }); + } + + return cy.wrap({error: response.body}); + }); +} + +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): ChainableT<{user: UserProfile}> { + const admin = getAdminAccount(); + + // First, login with username + return cy.apiLogin(admin, requestOptions).then((resp) => { + if ((<{error: any}>resp).error) { + if ((<{error: any}>resp).error.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 + delete admin.username; + 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): 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): 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): 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 { + const randomId = getRandomId(); + + return { + email: `${prefix}${randomId}@sample.mattermost.com`, + username: `${prefix}${randomId}`, + password: 'passwd', + 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; + prefix?: string; + createAt?: number; + bypassTutorial?: boolean; + hideOnboarding: boolean; + bypassWhatsNewModal: boolean; +} + +function apiCreateUser({ + prefix = 'user', + createAt = 0, + bypassTutorial = true, + hideOnboarding = true, + bypassWhatsNewModal = true, + user = null, +}: Partial = {}): 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'); + + // hide drafts tour tip so it doesn't block the execution of subsequent tests + cy.apiSaveDraftsTourTipPreference(createdUser.id, true); + + 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): 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> { + 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}); + }); +} + +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): 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}: Record): 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 {any} - the result of patching the user roles + * @example + * cy.apiPatchUserRoles('user-id', ['system_user']); + */ +function apiPatchUserRoles(userId: string, roleNames: string[] = ['system_user']): any { + 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 { + 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 { + 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 { + 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, currentPass, newPass) { + 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) { + 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 { + 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 { + 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 { + 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 { + 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; + 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; + } + } +} diff --git a/e2e-tests/cypress/tests/support/db_commands.ts b/e2e-tests/cypress/tests/support/db_commands.ts index 05ddac6f46..472a45c920 100644 --- a/e2e-tests/cypress/tests/support/db_commands.ts +++ b/e2e-tests/cypress/tests/support/db_commands.ts @@ -53,7 +53,7 @@ interface GetUserParam { username: string; } interface GetUserResult { - user: Cypress.UserProfile; + user: Cypress.UserProfile & {mfasecret: string}; } function dbGetUser(params: GetUserParam): ChainableT { return cy.task('dbGetUser', {dbConfig, params}).then(({user, errorMessage, error}) => { diff --git a/e2e-tests/cypress/tests/support/task_commands.ts b/e2e-tests/cypress/tests/support/task_commands.ts index 579821fc57..69aafd4e1d 100644 --- a/e2e-tests/cypress/tests/support/task_commands.ts +++ b/e2e-tests/cypress/tests/support/task_commands.ts @@ -30,6 +30,16 @@ interface PostMessageArg { createAt?: number; } +interface PostMessageRequest { + token: string; + message: string; + props?; + channelId: string; + rootId?; + createAt?; + failOnStatus?: boolean; +} + function postMessageAs(arg: PostMessageArg): ChainableT { const {sender, message, channelId, rootId, createAt} = arg; const baseUrl = Cypress.config('baseUrl'); @@ -149,7 +159,7 @@ Cypress.Commands.add('externalRequest', externalRequest); * @param {Object} channelId - where a post will be posted */ -function postBotMessage({token, message, props, channelId, rootId, createAt, failOnStatus = true}): ChainableT { +function postBotMessage({token, message, props, channelId, rootId, createAt, failOnStatus = true}: PostMessageRequest): ChainableT { const baseUrl = Cypress.config('baseUrl'); return cy.task('postBotMessage', {token, message, props, channelId, rootId, createAt, baseUrl}).then(({status, data}) => { diff --git a/e2e-tests/cypress/tests/support/ui/global_header.d.ts b/e2e-tests/cypress/tests/support/ui/global_header.d.ts index e634814982..cc74e0e181 100644 --- a/e2e-tests/cypress/tests/support/ui/global_header.d.ts +++ b/e2e-tests/cypress/tests/support/ui/global_header.d.ts @@ -41,7 +41,7 @@ declare namespace Cypress { * @example * cy.uiOpenProductMenu().click(); */ - uiOpenProductMenu(item: string): Chainable; + uiOpenProductMenu(item: string = ''): Chainable; /** * Get set status button diff --git a/e2e-tests/cypress/tests/support/ui/search.ts b/e2e-tests/cypress/tests/support/ui/search.ts index 56ea4f7697..fe1786a451 100644 --- a/e2e-tests/cypress/tests/support/ui/search.ts +++ b/e2e-tests/cypress/tests/support/ui/search.ts @@ -32,8 +32,8 @@ declare global { // eslint-disable-next-line @typescript-eslint/no-namespace namespace Cypress { interface Chainable { - uiSearchPosts(searchTerm: string): ChainableT; - uiJumpToSearchResult(postId: string): ChainableT; + uiSearchPosts(searchTerm: string): ChainableT; + uiJumpToSearchResult(postId: string): ChainableT; } } }