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); + } + } } } }