diff --git a/e2e-tests/cypress/tests/integration/channels/autocomplete/helpers.ts b/e2e-tests/cypress/tests/integration/channels/autocomplete/helpers.ts
index 3a467d6a08..840d51320f 100644
--- a/e2e-tests/cypress/tests/integration/channels/autocomplete/helpers.ts
+++ b/e2e-tests/cypress/tests/integration/channels/autocomplete/helpers.ts
@@ -243,13 +243,8 @@ function createChannel(channelType: string, teamId: string, userToAdd: Cypress.U
if (userToAdd) {
// # Get user profile by email
return cy.apiGetUserByEmail(userToAdd.email).then(({user}) => {
- // # Add user to team
- cy.externalRequest({
- user: getAdminAccount(),
- method: 'post',
- path: `channels/${channel.id}/members`,
- data: {user_id: user.id},
- }).then(() => {
+ // # Add user to channel
+ cy.externalAddUserToChannel(user.id, channel.id).then(() => {
// # Explicitly wait to give some time to index before searching
cy.wait(TIMEOUTS.TWO_SEC);
return cy.wrap(channel);
diff --git a/e2e-tests/cypress/tests/integration/channels/channel/user_to_admin_updates_manage_channel_members_rhs_spec.js b/e2e-tests/cypress/tests/integration/channels/channel/user_to_admin_updates_manage_channel_members_rhs_spec.js
index 09d3c8db24..d3e0a65e2e 100644
--- a/e2e-tests/cypress/tests/integration/channels/channel/user_to_admin_updates_manage_channel_members_rhs_spec.js
+++ b/e2e-tests/cypress/tests/integration/channels/channel/user_to_admin_updates_manage_channel_members_rhs_spec.js
@@ -10,11 +10,7 @@
// Stage: @prod
// Group: @channels @channel
-import {getAdminAccount} from '../../../support/env';
-
describe('View Members modal', () => {
- const sysadmin = getAdminAccount();
-
it('MM-20164 - Going from a Member to an Admin should update the modal', () => {
cy.apiInitSetup().then(({team, user}) => {
cy.apiCreateUser().then(({user: user1}) => {
@@ -24,27 +20,19 @@ describe('View Members modal', () => {
// # Promote user as a system admin
// # Visit default channel and verify members modal
cy.apiLogin(user);
- promoteToSysAdmin(user, sysadmin);
+ cy.externalUpdateUserRoles(user.id, 'system_user system_admin');
cy.visit(`/${team.name}/channels/town-square`);
verifyMemberDropdownAction(true);
// # Make user a regular member
// # Reload and verify members modal
- demoteToMember(user, sysadmin);
+ cy.externalUpdateUserRoles(user.id, 'system_user');
cy.reload();
verifyMemberDropdownAction(false);
});
});
});
-const demoteToMember = (user, sysadmin) => {
- cy.externalRequest({user: sysadmin, method: 'put', path: `users/${user.id}/roles`, data: {roles: 'system_user'}});
-};
-
-const promoteToSysAdmin = (user, sysadmin) => {
- cy.externalRequest({user: sysadmin, method: 'put', path: `users/${user.id}/roles`, data: {roles: 'system_user system_admin'}});
-};
-
function verifyMemberDropdownAction(hasActionItem) {
// # Click member count to open member rhs
cy.get('#member_rhs').click();
diff --git a/e2e-tests/cypress/tests/integration/channels/channel/user_to_channel_admin_member_updates_manage_channel_members_rhs_spec.js b/e2e-tests/cypress/tests/integration/channels/channel/user_to_channel_admin_member_updates_manage_channel_members_rhs_spec.js
index d53bc4c443..2ca0eedac2 100644
--- a/e2e-tests/cypress/tests/integration/channels/channel/user_to_channel_admin_member_updates_manage_channel_members_rhs_spec.js
+++ b/e2e-tests/cypress/tests/integration/channels/channel/user_to_channel_admin_member_updates_manage_channel_members_rhs_spec.js
@@ -11,10 +11,6 @@
import {getAdminAccount} from '../../../support/env';
-const demoteToMember = (user, admin) => {
- cy.externalRequest({user: admin, method: 'put', path: `users/${user.id}/roles`, data: {roles: 'system_user'}});
-};
-
const demoteToChannelMember = (user, channelId, admin) => {
cy.externalRequest({
user: admin,
@@ -70,7 +66,7 @@ describe('Change Roles', () => {
cy.visit(`/${team.name}/channels/${channel.name}`);
// # Make user a regular member for channel and system
- demoteToMember(testUser, admin);
+ cy.externalUpdateUserRoles(user.id, 'system_user');
demoteToChannelMember(testUser, testChannelId, admin);
// # Reload page to ensure no cache or saved information
diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/elasticsearch_autocomplete/helpers/index.js b/e2e-tests/cypress/tests/integration/channels/enterprise/elasticsearch_autocomplete/helpers/index.js
index f271035b2a..6aab3c45f3 100644
--- a/e2e-tests/cypress/tests/integration/channels/enterprise/elasticsearch_autocomplete/helpers/index.js
+++ b/e2e-tests/cypress/tests/integration/channels/enterprise/elasticsearch_autocomplete/helpers/index.js
@@ -53,13 +53,8 @@ function createChannel(channelType, teamId, userToAdd = null) {
if (userToAdd) {
// # Get user profile by email
return cy.apiGetUserByEmail(userToAdd.email).then(({user}) => {
- // # Add user to team
- cy.externalRequest({
- user: admin,
- method: 'post',
- path: `channels/${channel.id}/members`,
- data: {user_id: user.id},
- }).then(() => {
+ // # Add user to channel
+ cy.externalAddUserToChannel(user.id, channel.id).then(() => {
// # Explicitly wait to give some time to index before searching
cy.wait(TIMEOUTS.TWO_SEC);
return cy.wrap(channel);
diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/reporting/site_statistics_spec.js b/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/reporting/site_statistics_spec.js
index a6103329b3..ae32a30ed6 100644
--- a/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/reporting/site_statistics_spec.js
+++ b/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/reporting/site_statistics_spec.js
@@ -11,7 +11,6 @@
// Group: @channels @enterprise @system_console
import * as TIMEOUTS from '../../../../../fixtures/timeouts';
-import {getAdminAccount} from '../../../../../support/env';
describe('System Console > Site Statistics', () => {
let testTeam;
@@ -102,8 +101,6 @@ describe('System Console > Site Statistics', () => {
it('MM-T902 - Reporting ➜ Site statistics line graphs show same date', () => {
cy.intercept('**/api/v4/**').as('resources');
- const sysadmin = getAdminAccount();
-
let newChannel;
// # Create and visit new channel
@@ -114,7 +111,7 @@ describe('System Console > Site Statistics', () => {
// # Create a bot and get userID
cy.apiCreateBot().then(({bot}) => {
const botUserId = bot.user_id;
- cy.externalRequest({user: sysadmin, method: 'put', path: `users/${botUserId}/roles`, data: {roles: 'system_user system_post_all system_admin'}});
+ cy.externalUpdateUserRoles(botUserId, 'system_user system_post_all system_admin');
// # Get token from bots id
cy.apiAccessToken(botUserId, 'Create token').then(({token}) => {
diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/system_scheme_permission_spec.js b/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/system_scheme_permission_spec.js
index afbbed7ed6..16e43211e2 100644
--- a/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/system_scheme_permission_spec.js
+++ b/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/system_scheme_permission_spec.js
@@ -60,7 +60,7 @@ const setUserTeamAndChannelMemberships = (user, team, channel, channelAdmin = fa
const admin = getAdminAccount();
// # Set user as regular system user
- cy.externalRequest({user: admin, method: 'put', path: `users/${user.id}/roles`, data: {roles: 'system_user'}});
+ cy.externalUpdateUserRoles(user.id, 'system_user');
// # Get team membership
cy.externalRequest({user: admin, method: 'put', path: `teams/${team.id}/members/${user.id}/schemeRoles`, data: {scheme_user: true, scheme_admin: teamAdmin}});
diff --git a/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/team_scheme_permission_spec.js b/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/team_scheme_permission_spec.js
index 2c4e44959c..e3dd7d4f82 100644
--- a/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/team_scheme_permission_spec.js
+++ b/e2e-tests/cypress/tests/integration/channels/enterprise/system_console/team_scheme_permission_spec.js
@@ -91,7 +91,7 @@ const setUserTeamAndChannelMemberships = (user, team, channel, channelAdmin = fa
const admin = getAdminAccount();
// # Set user as regular system user
- cy.externalRequest({user: admin, method: 'put', path: `users/${user.id}/roles`, data: {roles: 'system_user'}});
+ cy.externalUpdateUserRoles(user.id, 'system_user');
// # Get team membership
cy.externalRequest({user: admin, method: 'put', path: `teams/${team.id}/members/${user.id}/schemeRoles`, data: {scheme_user: true, scheme_admin: teamAdmin}});
diff --git a/e2e-tests/cypress/tests/integration/channels/messaging/at_mention_loading_spec.ts b/e2e-tests/cypress/tests/integration/channels/messaging/at_mention_loading_spec.ts
new file mode 100644
index 0000000000..4b7d6ce4cb
--- /dev/null
+++ b/e2e-tests/cypress/tests/integration/channels/messaging/at_mention_loading_spec.ts
@@ -0,0 +1,201 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {getAdminAccount} from '../../../../tests/support/env';
+
+// ***************************************************************
+// - [#] indicates a test step (e.g. # Go to a page)
+// - [*] indicates an assertion (e.g. * Check the title)
+// - Use element ID when selecting an element. Create one if none.
+// ***************************************************************
+
+// Group: @channels @messaging
+
+describe('loading of at-mentioned users', () => {
+ const admin = getAdminAccount();
+
+ let testChannel;
+
+ before(() => {
+ // # Login as test user and visit off-topic
+ cy.apiInitSetup({loginAfter: true}).then(({channel, channelUrl}) => {
+ testChannel = channel;
+
+ cy.visit(channelUrl);
+
+ // # Wait for the channel to visibly load
+ cy.findByText('Write to ' + testChannel.display_name);
+ });
+ });
+
+ it('should load a user who joins the channel', () => {
+ cy.externalCreateUser({}).then((otherUser) => {
+ // * The new user shouldn't be loaded because the current user hasn't seen them yet
+ assertUserNotLoaded(otherUser.id);
+
+ // # Have the admin add them to the team and channel
+ cy.externalAddUserToTeam(otherUser.id, testChannel.team_id);
+ cy.externalAddUserToChannel(otherUser.id, testChannel.id);
+
+ // * Wait for the system message at-mentioning that user to know that they've been loaded
+ cy.contains('a', '@' + otherUser.username).should('be.visible');
+ });
+ });
+
+ it('should load a user who posts in the channel', () => {
+ cy.externalCreateUser({}).then((otherUser) => {
+ // # Make the new user into an admin so that they can post without joining the channel to simulate
+ // someone posting in the channel for the first time in a long time
+ cy.externalUpdateUserRoles(otherUser.id, 'system_user system_admin');
+
+ // * The new user shouldn't be loaded because the current user hasn't seen them yet
+ assertUserNotLoaded(otherUser.id);
+
+ // # Make a post as the other user
+ cy.externalCreatePostAsUser(otherUser, {
+ channel_id: testChannel.id,
+ message: 'This is a post',
+ }).then((post) => {
+ cy.findByText(post.message).should('be.visible');
+ cy.get(`#${post.id}_message`).should('be.visible');
+ });
+
+ // * Wait for the user's name to know that they've been loaded
+ cy.contains('button.user-popover', otherUser.username).should('be.visible');
+ });
+ });
+
+ it("should load a user who's been at-mentioned in a post", () => {
+ cy.externalCreateUser({}).then((otherUser) => {
+ // * The new user shouldn't be loaded because the current user hasn't seen them yet
+ assertUserNotLoaded(otherUser.id);
+
+ // # Have the admin at-mention the new user
+ cy.externalCreatePostAsUser(admin, {
+ channel_id: testChannel.id,
+ message: `Created @${otherUser.username}`,
+ });
+
+ // * Confirm that the at-mention renders as a link
+ cy.contains('a', '@' + otherUser.username).should('be.visible');
+ });
+ });
+
+ it("should load a user who's been at-mentioned in a message attachment's text", () => {
+ cy.externalCreateUser({}).then((otherUser) => {
+ // * The new user shouldn't be loaded because the current user hasn't seen them yet
+ assertUserNotLoaded(otherUser.id);
+
+ // # Have the admin at-mention the new user
+ cy.externalCreatePostAsUser(admin, {
+ channel_id: testChannel.id,
+ props: {
+ attachments: [
+ {text: `Ticket updated by @${otherUser.username}`},
+ ],
+ },
+ });
+
+ // * Confirm that the at-mention renders as a link
+ cy.contains('a', '@' + otherUser.username).should('be.visible');
+ });
+ });
+
+ it("should load a user who's been at-mentioned in a message attachment's pretext", () => {
+ cy.externalCreateUser({}).then((otherUser) => {
+ // * The new user shouldn't be loaded because the current user hasn't seen them yet
+ assertUserNotLoaded(otherUser.id);
+
+ // # Have the admin at-mention the new user
+ cy.externalCreatePostAsUser(admin, {
+ channel_id: testChannel.id,
+ props: {
+ attachments: [
+ {pretext: `@${otherUser.username} created a ticket`, text: 'Ticket #123 - Fix some bug'},
+ ],
+ },
+ });
+
+ // * Confirm that the at-mention renders as a link
+ cy.contains('a', '@' + otherUser.username).should('be.visible');
+ });
+ });
+
+ it("should not load a user who's been at-mentioned in a message attachment's title", () => {
+ cy.externalCreateUser({}).then((otherUser) => {
+ // * The new user shouldn't be loaded because the current user hasn't seen them yet
+ assertUserNotLoaded(otherUser.id);
+
+ // # Have the admin at-mention the new user
+ cy.externalCreatePostAsUser(admin, {
+ channel_id: testChannel.id,
+ props: {
+ attachments: [
+ {title: `@${otherUser.username}'s ticket`, text: 'TODO'},
+ ],
+ },
+ });
+
+ // * Confirm that the attachment title doesn't render as a link
+ cy.contains('h1', `@${otherUser.username}'s ticket`).should('be.visible');
+ });
+ });
+
+ it("should not load a user who's been at-mentioned in a message attachment's field's title", () => {
+ cy.externalCreateUser({}).then((otherUser) => {
+ // * The new user shouldn't be loaded because the current user hasn't seen them yet
+ assertUserNotLoaded(otherUser.id);
+
+ // # Have the admin at-mention the new user
+ cy.externalCreatePostAsUser(admin, {
+ channel_id: testChannel.id,
+ props: {
+ attachments: [
+ {
+ title: 'Ticket created',
+ fields: [
+ {title: `Note from @${otherUser.username}`, value: 'Something happened'},
+ ],
+ },
+ ],
+ },
+ });
+
+ // * Confirm that the field title doesn't render as a link
+ cy.contains('th', `Note from @${otherUser.username}`).should('be.visible');
+ });
+ });
+
+ it("should load a user who's been at-mentioned in a message attachment's field's value", () => {
+ cy.externalCreateUser({}).then((otherUser) => {
+ // * The new user shouldn't be loaded because the current user hasn't seen them yet
+ assertUserNotLoaded(otherUser.id);
+
+ // # Have the admin at-mention the new user
+ cy.externalCreatePostAsUser(admin, {
+ channel_id: testChannel.id,
+ props: {
+ attachments: [
+ {
+ title: 'Ticket created',
+ fields: [
+ {title: 'Assignee', value: `Created @${otherUser.username}`},
+ ],
+ },
+ ],
+ },
+ });
+
+ // * Confirm that the at-mention renders as a link
+ cy.contains('a', '@' + otherUser.username).should('be.visible');
+ });
+ });
+});
+
+function assertUserNotLoaded(userId: string) {
+ cy.window().then((win) => {
+ const state = (win as any).store.getState();
+
+ cy.wrap(state.entities.users.profiles[userId]).should('be.undefined');
+ });
+}
diff --git a/e2e-tests/cypress/tests/integration/channels/messaging/message_reply_bot_post_spec.js b/e2e-tests/cypress/tests/integration/channels/messaging/message_reply_bot_post_spec.js
index dc386bc568..7311cbba3d 100644
--- a/e2e-tests/cypress/tests/integration/channels/messaging/message_reply_bot_post_spec.js
+++ b/e2e-tests/cypress/tests/integration/channels/messaging/message_reply_bot_post_spec.js
@@ -10,10 +10,7 @@
// Stage: @prod
// Group: @channels @messaging
-import {getAdminAccount} from '../../../support/env';
-
describe('Messaging', () => {
- const sysadmin = getAdminAccount();
let newChannel;
before(() => {
@@ -31,7 +28,7 @@ describe('Messaging', () => {
// # Create a bot and get userID
cy.apiCreateBot().then(({bot}) => {
const botUserId = bot.user_id;
- cy.externalRequest({user: sysadmin, method: 'put', path: `users/${botUserId}/roles`, data: {roles: 'system_user system_post_all system_admin'}});
+ cy.externalUpdateUserRoles(botUserId, 'system_user system_post_all system_admin');
// # Get token from bots id
cy.apiAccessToken(botUserId, 'Create token').then(({token}) => {
@@ -98,7 +95,7 @@ describe('Messaging', () => {
// # Create a bot and get userID
cy.apiCreateBot().then(({bot}) => {
const botUserId = bot.user_id;
- cy.externalRequest({user: sysadmin, method: 'put', path: `users/${botUserId}/roles`, data: {roles: 'system_user system_post_all system_admin'}});
+ cy.externalUpdateUserRoles(botUserId, 'system_user system_post_all system_admin');
// # Get token from bots id
cy.apiAccessToken(botUserId, 'Create token').then(({token}) => {
diff --git a/e2e-tests/cypress/tests/integration/channels/multi_team_and_dm/town_square_not_marked_as_unread_spec.js b/e2e-tests/cypress/tests/integration/channels/multi_team_and_dm/town_square_not_marked_as_unread_spec.js
index 21d588473c..70dbc4c8de 100644
--- a/e2e-tests/cypress/tests/integration/channels/multi_team_and_dm/town_square_not_marked_as_unread_spec.js
+++ b/e2e-tests/cypress/tests/integration/channels/multi_team_and_dm/town_square_not_marked_as_unread_spec.js
@@ -10,14 +10,11 @@
// Stage: @prod
// Group: @channels @multi_team_and_dm
-import {getAdminAccount} from '../../../support/env';
-
describe('Multi Team and DM', () => {
let testChannel;
let testTeam;
let testUser;
let otherUser;
- const sysadmin = getAdminAccount();
before(() => {
// # Setup with the new team, channel and user
@@ -55,7 +52,7 @@ describe('Multi Team and DM', () => {
cy.findByLabelText('off-topic public channel').click();
// # Add second user to team in external session
- cy.externalRequest({user: sysadmin, method: 'post', path: `teams/${testTeam.id}/members`, data: {team_id: testTeam.id, user_id: otherUser.id}});
+ cy.externalAddUserToTeam(otherUser.id, testTeam.id);
// * Assert that Town Square is still marked as read after second user added to team
cy.findByLabelText('town square public channel').should('be.visible');
diff --git a/e2e-tests/cypress/tests/integration/channels/search_filter/helpers.js b/e2e-tests/cypress/tests/integration/channels/search_filter/helpers.js
index bfa2e22c31..a52159c616 100644
--- a/e2e-tests/cypress/tests/integration/channels/search_filter/helpers.js
+++ b/e2e-tests/cypress/tests/integration/channels/search_filter/helpers.js
@@ -98,8 +98,7 @@ export function setupTestData(data, {team, channel, admin, anotherAdmin}) {
} = data;
// # Create another admin user so we can create override create_at of posts
- const baseUrl = Cypress.config('baseUrl');
- cy.externalRequest({user: admin, method: 'put', baseUrl, path: `users/${anotherAdmin.id}/roles`, data: {roles: 'system_user system_admin'}});
+ cy.externalUpdateUserRoles(anotherAdmin.id, 'system_user system_admin');
// # Create a post from today
cy.get('#postListContent', {timeout: TIMEOUTS.HALF_MIN}).should('be.visible');
diff --git a/e2e-tests/cypress/tests/integration/channels/system_console/demoted_user_spec.js b/e2e-tests/cypress/tests/integration/channels/system_console/demoted_user_spec.js
index 18dcd7e4c1..602efdb2f1 100644
--- a/e2e-tests/cypress/tests/integration/channels/system_console/demoted_user_spec.js
+++ b/e2e-tests/cypress/tests/integration/channels/system_console/demoted_user_spec.js
@@ -11,10 +11,8 @@
// Group: @channels @system_console @smoke
import * as TIMEOUTS from '../../../fixtures/timeouts';
-import {getAdminAccount} from '../../../support/env';
describe('System Console', () => {
- const sysadmin = getAdminAccount();
let testUser;
before(() => {
@@ -25,10 +23,8 @@ describe('System Console', () => {
});
it('MM-T922 Demoted user cannot continue to view System Console', () => {
- const baseUrl = Cypress.config('baseUrl');
-
// # Set user to be a sysadmin, so it can access the system console
- cy.externalRequest({user: sysadmin, method: 'put', baseUrl, path: `users/${testUser.id}/roles`, data: {roles: 'system_user system_admin'}});
+ cy.externalUpdateUserRoles(testUser.id, 'system_user system_admin');
// # Visit a page on the system console
cy.visit('/admin_console/reporting/system_analytics');
@@ -36,7 +32,7 @@ describe('System Console', () => {
cy.url().should('include', '/admin_console/reporting/system_analytics');
// # Change the role of the user back to user
- cy.externalRequest({user: sysadmin, method: 'put', baseUrl, path: `users/${testUser.id}/roles`, data: {roles: 'system_user'}});
+ cy.externalUpdateUserRoles(testUser.id, 'system_user');
// # User should get redirected to town square
cy.get('#adminConsoleWrapper').should('not.exist');
diff --git a/e2e-tests/cypress/tests/integration/channels/websocket/handle_new_post_spec.js b/e2e-tests/cypress/tests/integration/channels/websocket/handle_new_post_spec.js
index fe28103ae4..ad0d796115 100644
--- a/e2e-tests/cypress/tests/integration/channels/websocket/handle_new_post_spec.js
+++ b/e2e-tests/cypress/tests/integration/channels/websocket/handle_new_post_spec.js
@@ -102,17 +102,7 @@ describe('Handle new post', () => {
const channel = response.data;
// # And then invite the current user
- cy.externalRequest({
- user: admin,
- baseUrl,
- method: 'post',
- path: `channels/${channel.id}/members`,
- data: {
- user_id: user1.id,
- },
- }).then((addResponse) => {
- expect(addResponse.status).to.equal(201);
- });
+ cy.externalAddUserToChannel(user1.id, channel.id);
});
// * Verify that the channel is in the current user's sidebar and is unread with one mention
@@ -145,17 +135,7 @@ describe('Handle new post', () => {
cy.delayRequestToRoutes([`channels/${channel.id}`], 100);
// # And then invite the current user
- cy.externalRequest({
- user: admin,
- baseUrl,
- method: 'post',
- path: `channels/${channel.id}/members`,
- data: {
- user_id: user1.id,
- },
- }).then((addResponse) => {
- expect(addResponse.status).to.equal(201);
- });
+ cy.externalAddUserToChannel(user1.id, channel.id);
});
// * Verify that the channel is in the current user's sidebar and is unread with one mention
diff --git a/e2e-tests/cypress/tests/support/external_commands.d.ts b/e2e-tests/cypress/tests/support/external_commands.d.ts
deleted file mode 100644
index 5b5389ada3..0000000000
--- a/e2e-tests/cypress/tests/support/external_commands.d.ts
+++ /dev/null
@@ -1,30 +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
-// - Each parameter with `@params`
-// - Return value with `@returns`
-// - Example usage with `@example`
-// Custom command should follow naming convention of having `external` prefix, e.g. `externalActivateUser`.
-// ***************************************************************
-
-declare namespace Cypress {
- interface Chainable {
-
- /**
- * Makes an external request as a sysadmin and activate/deactivate a user directly via API
- * @param {String} userId - The user ID
- * @param {Boolean} active - Whether to activate or deactivate - true/false
- *
- * @example
- * cy.externalActivateUser('user-id', false);
- */
- externalActivateUser(userId: string, activate: boolean): Chainable;
- }
-}
diff --git a/e2e-tests/cypress/tests/support/external_commands.js b/e2e-tests/cypress/tests/support/external_commands.js
deleted file mode 100644
index a8b68df64a..0000000000
--- a/e2e-tests/cypress/tests/support/external_commands.js
+++ /dev/null
@@ -1,11 +0,0 @@
-// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
-// See LICENSE.txt for license information.
-
-import {getAdminAccount} from './env';
-
-Cypress.Commands.add('externalActivateUser', (userId, active = true) => {
- const baseUrl = Cypress.config('baseUrl');
- const admin = getAdminAccount();
-
- cy.externalRequest({user: admin, method: 'put', baseUrl, path: `users/${userId}/active`, data: {active}});
-});
diff --git a/e2e-tests/cypress/tests/support/external_commands.ts b/e2e-tests/cypress/tests/support/external_commands.ts
new file mode 100644
index 0000000000..303f0346b6
--- /dev/null
+++ b/e2e-tests/cypress/tests/support/external_commands.ts
@@ -0,0 +1,138 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import type {ChannelMembership} from '@mattermost/types/channels';
+import type {Post} from '@mattermost/types/posts';
+import type {TeamMembership} from '@mattermost/types/teams';
+import type {UserProfile} from '@mattermost/types/users';
+
+import {getAdminAccount} from './env';
+
+import {getRandomId} from '../utils';
+
+function externalActivateUser(userId: string, active = true) {
+ const admin = getAdminAccount();
+
+ cy.externalRequest({user: admin, method: 'PUT', path: `users/${userId}/active`, data: {active}});
+}
+Cypress.Commands.add('externalActivateUser', externalActivateUser);
+
+function externalAddUserToChannel(userId: string, channelId: string): Cypress.Chainable {
+ const admin = getAdminAccount();
+
+ return cy.externalRequest({
+ user: admin,
+ method: 'POST',
+ path: `channels/${channelId}/members`,
+ data: {
+ user_id: userId,
+ },
+ }).then((response) => response.data);
+}
+Cypress.Commands.add('externalAddUserToChannel', externalAddUserToChannel);
+
+function externalAddUserToTeam(userId: string, teamId: string): Cypress.Chainable {
+ const admin = getAdminAccount();
+
+ return cy.externalRequest({
+ user: admin,
+ method: 'POST',
+ path: `teams/${teamId}/members`,
+ data: {
+ team_id: teamId,
+ user_id: userId,
+ },
+ }).then((response) => response.data);
+}
+Cypress.Commands.add('externalAddUserToTeam', externalAddUserToTeam);
+
+function externalCreatePostAsUser(user: Pick, post: Partial): Cypress.Chainable {
+ return cy.externalRequest({
+ user,
+ method: 'POST',
+ path: 'posts',
+ data: post,
+ }).then((response) => response.data);
+}
+Cypress.Commands.add('externalCreatePostAsUser', externalCreatePostAsUser);
+
+function externalCreateUser(user: Partial): Cypress.Chainable {
+ const admin = getAdminAccount();
+
+ const randomValue = getRandomId();
+
+ return cy.externalRequest({
+ user: admin,
+ method: 'POST',
+ path: 'users',
+ data: {
+ username: 'user' + randomValue,
+ email: 'email' + randomValue + '@example.mattermost.com',
+ password: 'password' + randomValue,
+ ...user,
+ },
+ }).then((response) => {
+ // Re-add the password to the result so that we can make requests as that user
+ return {
+ ...response.data,
+ password: 'password' + randomValue,
+ };
+ });
+}
+Cypress.Commands.add('externalCreateUser', externalCreateUser);
+
+function externalUpdateUserRoles(userId: string, roles: string): Cypress.Chainable {
+ const admin = getAdminAccount();
+
+ return cy.externalRequest({
+ user: admin,
+ method: 'PUT',
+ path: `users/${userId}/roles`,
+ data: {roles},
+ });
+}
+Cypress.Commands.add('externalUpdateUserRoles', externalUpdateUserRoles);
+
+declare global {
+ // eslint-disable-next-line @typescript-eslint/no-namespace
+ namespace Cypress {
+ interface Chainable {
+
+ /**
+ * Makes an external request as a sysadmin and activate/deactivate a user directly via API
+ * @param {String} userId - The user ID
+ * @param {Boolean} active - Whether to activate or deactivate - true/false
+ *
+ * @example
+ * cy.externalActivateUser('user-id', false);
+ */
+ externalActivateUser: typeof externalActivateUser;
+
+ /**
+ * As the system admin, adds a user to a channel.
+ */
+ externalAddUserToChannel: typeof externalAddUserToChannel;
+
+ /**
+ * As the system admin, adds a user to a team.
+ */
+ externalAddUserToTeam: typeof externalAddUserToTeam;
+
+ /**
+ * As the given user, creates a post via the API.
+ */
+ externalCreatePostAsUser: typeof externalCreatePostAsUser;
+
+ /**
+ * As the system admin, creates a new user via the API. The user is automatically given a username, email,
+ * and password, but the override parameter can be be used to specify any other fields if needed.
+ */
+ externalCreateUser: typeof externalCreateUser;
+
+ /**
+ * As the system admin, updates a user's roles via the API.
+ */
+ externalUpdateUserRoles: typeof externalUpdateUserRoles;
+ }
+ }
+}
diff --git a/server/channels/app/notification.go b/server/channels/app/notification.go
index bc571ff454..fe6bf5eeae 100644
--- a/server/channels/app/notification.go
+++ b/server/channels/app/notification.go
@@ -1410,6 +1410,12 @@ func getMentionsEnabledFields(post *model.Post) model.StringArray {
if attachment.Text != "" {
ret = append(ret, attachment.Text)
}
+
+ for _, field := range attachment.Fields {
+ if valueString, ok := field.Value.(string); ok && valueString != "" {
+ ret = append(ret, valueString)
+ }
+ }
}
return ret
}
diff --git a/server/channels/app/notification_test.go b/server/channels/app/notification_test.go
index 08662d3892..76335833d1 100644
--- a/server/channels/app/notification_test.go
+++ b/server/channels/app/notification_test.go
@@ -1337,6 +1337,25 @@ func TestGetExplicitMentions(t *testing.T) {
HereMentioned: true,
},
},
+ "should include the mentions from attachment field values (but not field titles)": {
+ Message: "this is a message",
+ Attachments: []*model.SlackAttachment{
+ {
+ Fields: []*model.SlackAttachmentField{
+ {
+ Title: "@user1",
+ Value: "@user2",
+ },
+ },
+ },
+ },
+ Keywords: map[string][]string{"@user1": {id1}, "@user2": {id2}},
+ Expected: &MentionResults{
+ Mentions: map[string]MentionType{
+ id2: KeywordMention,
+ },
+ },
+ },
"Name on keywords is a prefix of a mention": {
Message: "@other @test-two",
Keywords: map[string][]string{"@test": {model.NewId()}},
@@ -2167,6 +2186,12 @@ func TestGetMentionsEnabledFields(t *testing.T) {
attachmentWithOutPreText := model.SlackAttachment{
Text: "some text",
+ Fields: []*model.SlackAttachmentField{
+ {
+ Title: "field title",
+ Value: "field value",
+ },
+ },
}
attachments := []*model.SlackAttachment{
&attachmentWithTextAndPreText,
@@ -2183,11 +2208,12 @@ func TestGetMentionsEnabledFields(t *testing.T) {
"This is the message",
"@Channel some comment for the channel",
"@here with mentions",
- "some text"}
+ "some text",
+ "field value",
+ }
mentionEnabledFields := getMentionsEnabledFields(post)
- assert.EqualValues(t, 4, len(mentionEnabledFields))
assert.EqualValues(t, expectedFields, mentionEnabledFields)
}
diff --git a/webapp/channels/src/components/post_view/message_attachments/message_attachment/__snapshots__/message_attachment.test.tsx.snap b/webapp/channels/src/components/post_view/message_attachments/message_attachment/__snapshots__/message_attachment.test.tsx.snap
index 911d04a1a4..2c5e8e5566 100644
--- a/webapp/channels/src/components/post_view/message_attachments/message_attachment/__snapshots__/message_attachment.test.tsx.snap
+++ b/webapp/channels/src/components/post_view/message_attachments/message_attachment/__snapshots__/message_attachment.test.tsx.snap
@@ -424,6 +424,7 @@ exports[`components/post_view/MessageAttachment should match snapshot when the a
message="Do you like https://mattermost.com?"
options={
Object {
+ "atMentions": false,
"autolinkedUrlSchemes": Array [],
"mentionHighlight": false,
"renderer": LinkOnlyRenderer {
@@ -469,6 +470,7 @@ exports[`components/post_view/MessageAttachment should match snapshot when the a
message="Do you like :pizza:?"
options={
Object {
+ "atMentions": false,
"autolinkedUrlSchemes": Array [],
"mentionHighlight": false,
"renderer": LinkOnlyRenderer {
@@ -514,6 +516,7 @@ exports[`components/post_view/MessageAttachment should match snapshot when the a
message="Don't you like emojis?"
options={
Object {
+ "atMentions": false,
"autolinkedUrlSchemes": Array [],
"mentionHighlight": false,
"renderer": LinkOnlyRenderer {
@@ -559,6 +562,7 @@ exports[`components/post_view/MessageAttachment should match snapshot when the f
message="footer"
options={
Object {
+ "atMentions": false,
"autolinkedUrlSchemes": Array [],
"mentionHighlight": false,
"renderer": LinkOnlyRenderer {
@@ -620,6 +624,7 @@ exports[`components/post_view/MessageAttachment should match value on getFieldsT
message="title_1"
options={
Object {
+ "atMentions": false,
"markdown": false,
"mentionHighlight": false,
}
@@ -654,6 +659,7 @@ exports[`components/post_view/MessageAttachment should match value on getFieldsT
message="title_2"
options={
Object {
+ "atMentions": false,
"markdown": false,
"mentionHighlight": false,
}
diff --git a/webapp/channels/src/components/post_view/message_attachments/message_attachment/message_attachment.tsx b/webapp/channels/src/components/post_view/message_attachments/message_attachment/message_attachment.tsx
index bb5041083b..dc992a93d3 100644
--- a/webapp/channels/src/components/post_view/message_attachments/message_attachment/message_attachment.tsx
+++ b/webapp/channels/src/components/post_view/message_attachments/message_attachment/message_attachment.tsx
@@ -232,7 +232,7 @@ export default class MessageAttachment extends React.PureComponent
let rowPos = 0;
let lastWasLong = false;
let nrTables = 0;
- const markdown = {markdown: false, mentionHighlight: false};
+ const markdown = {markdown: false, mentionHighlight: false, atMentions: false};
fields.forEach((field: MessageAttachmentField, i: number) => {
if (rowPos === 2 || !(field.short === true) || lastWasLong) {
@@ -421,6 +421,7 @@ export default class MessageAttachment extends React.PureComponent
{
});
});
- it('getNeededAtMentionedUsernames', async () => {
+ describe('getNeededAtMentionedUsernames', () => {
const state = {
entities: {
users: {
@@ -592,65 +592,55 @@ describe('Actions.Posts', () => {
},
} as unknown as GlobalState;
- expect(
- Actions.getNeededAtMentionedUsernamesAndGroups(state, [
- TestHelper.getPostMock({message: 'aaa'}),
- ])).toEqual(
- new Set(),
- );
-
- expect(
- Actions.getNeededAtMentionedUsernamesAndGroups(state, [
- TestHelper.getPostMock({message: '@aaa'}),
- ])).toEqual(
- new Set(),
- );
-
- expect(
- Actions.getNeededAtMentionedUsernamesAndGroups(state, [
- TestHelper.getPostMock({message: '@zzz'}),
- ])).toEqual(
- new Set(),
- );
-
- expect(
- Actions.getNeededAtMentionedUsernamesAndGroups(state, [
- TestHelper.getPostMock({message: '@aaa @bbb @ccc @zzz'}),
- ])).toEqual(
- new Set(['bbb', 'ccc']),
- );
-
- expect(
- Actions.getNeededAtMentionedUsernamesAndGroups(state, [
- TestHelper.getPostMock({message: '@bbb. @ccc.ddd'}),
- ])).toEqual(
- new Set(['bbb.', 'bbb', 'ccc.ddd']),
- );
-
- expect(
- Actions.getNeededAtMentionedUsernamesAndGroups(state, [
- TestHelper.getPostMock({message: '@bbb- @ccc-ddd'}),
- ])).toEqual(
- new Set(['bbb-', 'bbb', 'ccc-ddd']),
- );
-
- expect(
- Actions.getNeededAtMentionedUsernamesAndGroups(state, [
- TestHelper.getPostMock({message: '@bbb_ @ccc_ddd'}),
- ])).toEqual(
- new Set(['bbb_', 'ccc_ddd']),
- );
-
- expect(
- Actions.getNeededAtMentionedUsernamesAndGroups(state, [
- TestHelper.getPostMock({message: '(@bbb/@ccc) ddd@eee'}),
- ])).toEqual(
- new Set(['bbb', 'ccc']),
- );
-
- expect(
- Actions.getNeededAtMentionedUsernamesAndGroups(state, [
- TestHelper.getPostMock({
+ const testCases = [
+ {
+ name: "shouldn't return anything when no users are at-mentioned",
+ input: TestHelper.getPostMock({message: 'aaa'}),
+ expected: new Set(),
+ },
+ {
+ name: "shouldn't return anything for a user that's already loaded",
+ input: TestHelper.getPostMock({message: '@aaa'}),
+ expected: new Set(),
+ },
+ {
+ name: "shouldn't return anything for a group that's already loaded",
+ input: TestHelper.getPostMock({message: '@zzz'}),
+ expected: new Set(),
+ },
+ {
+ name: 'should return any unrecognized at-mentions',
+ input: TestHelper.getPostMock({message: '@aaa @bbb @ccc @zzz'}),
+ expected: new Set(['bbb', 'ccc']),
+ },
+ {
+ name: 'should return at-mentions followed by period both with and without the period',
+ input: TestHelper.getPostMock({message: '@bbb. @ccc.ddd'}),
+ expected: new Set(['bbb.', 'bbb', 'ccc.ddd']),
+ },
+ {
+ name: 'should return at-mentions followed by hyphen both with and without the hyphen',
+ input: TestHelper.getPostMock({message: '@bbb- @ccc-ddd'}),
+ expected: new Set(['bbb-', 'bbb', 'ccc-ddd']),
+ },
+ {
+ name: 'should return at-mentions followed by underscores with the underscore',
+ input: TestHelper.getPostMock({message: '@bbb_ @ccc_ddd'}),
+ expected: new Set(['bbb_', 'ccc_ddd']),
+ },
+ {
+ name: 'should return at-mentions in brackets',
+ input: TestHelper.getPostMock({message: '(@bbb/@ccc)'}),
+ expected: new Set(['bbb', 'ccc']),
+ },
+ {
+ name: "shouldn't return anything when an at sign is in the middle of a word",
+ input: TestHelper.getPostMock({message: 'ddd@eee'}),
+ expected: new Set(),
+ },
+ {
+ name: 'should return at-mentions from inside message attachment props text and pretext',
+ input: TestHelper.getPostMock({
message: '@aaa @bbb',
props: {
attachments: [
@@ -659,23 +649,55 @@ describe('Actions.Posts', () => {
],
},
}),
- ]),
- ).toEqual(
- new Set(['bbb', 'ccc', 'ddd', 'eee', 'fff', 'ggg']),
- );
+ expected: new Set(['bbb', 'ccc', 'ddd', 'eee', 'fff', 'ggg']),
+ },
+ {
+ name: 'should return at-mentions from inside message attachment field values but not their titles',
+ input: TestHelper.getPostMock({
+ props: {
+ attachments: [
+ {
+ fields: [
+ {title: '@bbb', value: '@ccc'},
+ {value: '@ddd'},
+ ],
+ },
+ {
+ fields: [
+ {title: '@eee', value: '@fff'},
+ {value: '@ggg'},
+ ],
+ },
+ ],
+ },
+ }),
+ expected: new Set(['ccc', 'ddd', 'fff', 'ggg']),
+ },
+ ];
- // should never try to request usernames matching special mentions
- expect(
- Actions.getNeededAtMentionedUsernamesAndGroups(state, [
- TestHelper.getPostMock({message: '@all'}),
- TestHelper.getPostMock({message: '@here'}),
- TestHelper.getPostMock({message: '@channel'}),
- TestHelper.getPostMock({message: '@all.'}),
- TestHelper.getPostMock({message: '@here.'}),
- TestHelper.getPostMock({message: '@channel.'}),
- ])).toEqual(
- new Set(),
- );
+ for (const specialMention of [
+ '@all',
+ '@here',
+ '@channel',
+ '@all.',
+ '@here.',
+ '@channel.',
+ ]) {
+ testCases.push({
+ name: `should never return special mentions (${specialMention})`,
+ input: TestHelper.getPostMock({message: specialMention}),
+ expected: new Set(),
+ });
+ }
+
+ for (const testCase of testCases) {
+ test(testCase.name, () => {
+ expect(Actions.getNeededAtMentionedUsernamesAndGroups(
+ state,
+ [testCase.input],
+ )).toEqual(testCase.expected);
+ });
+ }
});
it('getPostsSince', async () => {
diff --git a/webapp/channels/src/packages/mattermost-redux/src/actions/posts.ts b/webapp/channels/src/packages/mattermost-redux/src/actions/posts.ts
index d9f7558eca..fcc8abaa9a 100644
--- a/webapp/channels/src/packages/mattermost-redux/src/actions/posts.ts
+++ b/webapp/channels/src/packages/mattermost-redux/src/actions/posts.ts
@@ -1090,6 +1090,12 @@ export function getNeededAtMentionedUsernamesAndGroups(state: GlobalState, posts
for (const attachment of post.props.attachments) {
findNeededUsernamesAndGroups(attachment.pretext);
findNeededUsernamesAndGroups(attachment.text);
+
+ if (attachment.fields) {
+ for (const field of attachment.fields) {
+ findNeededUsernamesAndGroups(field.value);
+ }
+ }
}
}
}