MM-59854 Fully allow at mentions in message attachment field values and add E2E tests (#28018)

* Add descriptions to test cases for getNeededAtMentionedUsernames

* MM-59854 Load users who are at-mentioned in message attachment fields

* MM-59854 Add server support for at-mentioning users in message attachment fields

* Migrate support/external_commands from JS to TS

* MM-59584 Ensure that at-mentions never show in other message attachment fields

* MM-59584 Add E2E tests for how we load users based on at mentions

* Use new E2E test helpers in other places

* Update snapshots
Этот коммит содержится в:
Harrison Healey
2024-09-24 09:41:03 -04:00
коммит произвёл GitHub
родитель 3e84d20dbe
Коммит 46cad5c552
22 изменённых файлов: 503 добавлений и 198 удалений

Просмотреть файл

@@ -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);

Просмотреть файл

@@ -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();

Просмотреть файл

@@ -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

Просмотреть файл

@@ -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);

Просмотреть файл

@@ -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}) => {

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

@@ -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');
});
}

Просмотреть файл

@@ -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}) => {

Просмотреть файл

@@ -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');

Просмотреть файл

@@ -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');

Просмотреть файл

@@ -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');

Просмотреть файл

@@ -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

Просмотреть файл

@@ -1,30 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/// <reference types="cypress" />
// ***************************************************************
// Each command should be properly documented using JSDoc.
// See https://jsdoc.app/index.html for reference.
// Basic requirements for documentation are the following:
// - Meaningful description
// - Each parameter with `@params`
// - Return value with `@returns`
// - Example usage with `@example`
// Custom command should follow naming convention of having `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;
}
}

Просмотреть файл

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

Просмотреть файл

@@ -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<ChannelMembership> {
const admin = getAdminAccount();
return cy.externalRequest({
user: admin,
method: 'POST',
path: `channels/${channelId}/members`,
data: {
user_id: userId,
},
}).then((response) => response.data);
}
Cypress.Commands.add('externalAddUserToChannel', externalAddUserToChannel);
function externalAddUserToTeam(userId: string, teamId: string): Cypress.Chainable<TeamMembership> {
const admin = getAdminAccount();
return cy.externalRequest({
user: admin,
method: 'POST',
path: `teams/${teamId}/members`,
data: {
team_id: teamId,
user_id: userId,
},
}).then((response) => response.data);
}
Cypress.Commands.add('externalAddUserToTeam', externalAddUserToTeam);
function externalCreatePostAsUser(user: Pick<UserProfile, 'username' | 'password'>, post: Partial<Post>): Cypress.Chainable<Post> {
return cy.externalRequest({
user,
method: 'POST',
path: 'posts',
data: post,
}).then((response) => response.data);
}
Cypress.Commands.add('externalCreatePostAsUser', externalCreatePostAsUser);
function externalCreateUser(user: Partial<UserProfile>): Cypress.Chainable<UserProfile> {
const admin = getAdminAccount();
const randomValue = getRandomId();
return cy.externalRequest({
user: admin,
method: 'POST',
path: 'users',
data: {
username: 'user' + randomValue,
email: 'email' + randomValue + '@example.mattermost.com',
password: 'password' + randomValue,
...user,
},
}).then((response) => {
// Re-add the password to the result so that we can make requests as that user
return {
...response.data,
password: 'password' + randomValue,
};
});
}
Cypress.Commands.add('externalCreateUser', externalCreateUser);
function externalUpdateUserRoles(userId: string, roles: string): Cypress.Chainable<unknown> {
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;
}
}
}