MM-60616: Shared Channels - System Console E2E (#28379)

* fix rc content types

* api spec

* don't ignore content type when parsing body

* secure connections:
- create
- read
- update
- delete
- share
- accept

* title

* wip

* manage shared channels, other fixes

* revert

* revert old

* maxlines

* add team selector, tooltip fix, msg fixes

* cleanup

* remote disabled state admindef

* add team label to channels input, fix i18n

* add filter

* review fixes, other fixes

* cleanup

* refactor channel fetching

* move modals

* fix imports, fix collected id

* remove unused action

* tabs visibility

when connection pending, hide tabs if none from home are shared.
when connection is connected, always show tabs

* api docs invite/uninvite response type

* fix modal id ref

* Remove the generate invite option for confirmed clusters

* e2e: add empty state test

* e2e: add create flow test

* fix modal id ref

* rev: double negation

* rev: server side password for create flow

* rev: remote cluster type 0 token, delete_at

* testing a11y, ids

* e2e

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
Co-authored-by: Miguel de la Cruz <miguel@mcrx.me>
This commit is contained in:
Caleb Roseland 2024-10-17 03:36:51 -05:00 committed by GitHub
parent cd43bac181
commit 9ae2752593
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 409 additions and 5 deletions

View File

@ -0,0 +1,393 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// ***************************************************************
// - [#] 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 @system_console
import timeouts from '../../../fixtures/timeouts';
import {getRandomId, stubClipboard} from '../../../utils';
describe('Connected Workspaces', () => {
let testTeam: Cypress.Team;
let testTeam2: Cypress.Team;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
let testUser: Cypress.UserProfile;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
let admin: Cypress.UserProfile;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
let testChannel: Cypress.Channel;
before(() => {
cy.apiGetMe().then(({user: adminUser}) => {
admin = adminUser;
cy.apiCreateTeam('testing-team-2', 'Testing Team 2').then(({team}) => {
testTeam2 = team;
});
cy.apiInitSetup().then(({team, user, channel}) => {
testTeam = team;
testUser = user;
testChannel = channel;
});
});
});
it('configured', () => {
cy.apiRequireLicenseForFeature('SharedChannels');
// @ts-expect-error types update, need ConnectedWorkspacesSettings
cy.apiGetConfig().then(({config: {ConnectedWorkspacesSettings}}) => {
expect(ConnectedWorkspacesSettings.EnableSharedChannels).equal(true);
expect(ConnectedWorkspacesSettings.EnableRemoteClusterService).equal(true);
});
});
it('empty state', () => {
cy.visit('/admin_console');
cy.get("a[id='environment/secure_connections']").click();
cy.findByTestId('secureConnectionsSection').within(() => {
cy.findByRole('heading', {name: 'Connected Workspaces'});
cy.findByRole('heading', {name: 'Share channels'});
cy.contains('Connecting with an external workspace allows you to share channels with them');
});
});
describe('accept invitation lifecycle', () => {
const orgDisplayName = 'Testing Org Name ' + getRandomId();
before(() => {
cy.visit('/admin_console/environment/secure_connections');
});
it('accept - bad codes', () => {
// # Open create menu
cy.findAllByRole('button', {name: 'Add a connection'}).eq(1).click();
// # Open accept dialog
cy.findAllByRole('menuitem', {name: 'Accept an invitation'}).click();
cy.findByRole('dialog', {name: 'Accept a connection invite'}).as('dialog');
// * Verify dialog
cy.get('@dialog').within(() => {
cy.uiGetHeading('Accept a connection invite');
// * Verify instructions
cy.findByText('Accept a secure connection from another server');
cy.findByText('Enter the encrypted invitation code shared to you by the admin of the server you are connecting with.');
// # Enter org name
cy.findByRole('textbox', {name: 'Organization name'}).type(orgDisplayName);
// # Enter bad invitation code
cy.findByRole('textbox', {name: 'Encrypted invitation code'}).type('abc123');
// # Enter bad password
cy.findByRole('textbox', {name: 'Password'}).type('123abc');
// # Try accept
cy.uiGetButton('Accept').click();
// * Verify error shown
cy.findByText('There was an error while accepting the invite.');
// # Close dialog
cy.uiGetButton('Cancel').click();
});
// * Verify dialog closed
cy.get('@dialog').should('not.exist');
});
});
describe('create new connection lifecycle', () => {
const orgDisplayName = 'Testing Org Name ' + getRandomId();
const orgDisplayName2 = 'new display name here ' + getRandomId();
before(() => {
cy.visit('/admin_console/environment/secure_connections');
});
it('create', () => {
// # Click create
cy.findAllByRole('button', {name: 'Add a connection'}).first().click();
cy.findAllByRole('menuitem', {name: 'Create a connection'}).click();
// * Verify on create page
cy.location('pathname').should('include', '/secure_connections/create');
// * Verify name focused
// # Enter name
cy.findByTestId('organization-name-input').
should('be.focused').
type(orgDisplayName);
// # Select team
cy.findByTestId('destination-team-input').click().
findByRole('textbox').type(`${testTeam.display_name}{enter}`);
// # Save
cy.uiGetButton('Save').click();
// * Verify page change
cy.location('pathname').should('not.include', '/secure_connections/create');
});
it('created dialog', () => {
// * Verify connection created dialog
verifyInviteDialog('Connection created');
});
it('basic details', () => {
// * Verify name
cy.findByTestId('organization-name-input').
should('not.be.focused').
should('have.value', orgDisplayName);
// * Verify team
cy.findByTestId('destination-team-input').should('have.text', testTeam.display_name);
// * Verify connection status label
cy.findByText('Connection Pending');
});
it('shared channels - empty', () => {
cy.findByTestId('shared_channels_section').within(() => {
cy.findByRole('heading', {name: "You haven't shared any channels"});
cy.findByText('Please add channels to start sharing');
});
});
it('add channel', () => {
cy.findByTestId('shared_channels_section').within(() => {
// * Verify title
cy.findByRole('heading', {name: 'Shared Channels'});
// * Verify subtitle
cy.findByText("A list of all the channels shared with your organization and channels you're sharing externally.");
// # Open add channels dialog
cy.uiGetButton('Add channels').click();
});
cy.findByRole('dialog', {name: 'Select channels'}).as('dialog');
cy.get('@dialog').within(() => {
// * Verify instructions
cy.findByText('Please select a team and channels to share');
cy.findByRole('textbox', {name: 'Search and add channels'}).
should('be.focused').
type(testChannel.display_name, {force: true}).
wait(timeouts.HALF_SEC).
type('{enter}');
// # Share
cy.uiGetButton('Share').click();
// * Verify create modal closed
cy.get('@dialog').should('not.exist');
});
});
it('shared channels', () => {
cy.findByTestId('shared_channels_section').within(() => {
// * Verify tabs
cy.findByRole('tab', {name: orgDisplayName});
cy.findByRole('tab', {name: 'Your channels', selected: true});
cy.findByRole('table').findAllByRole('row').as('rows');
// * Verify table headers
cy.get('@rows').first().within(() => {
cy.findByRole('columnheader', {name: 'Name'});
cy.findByRole('columnheader', {name: 'Current Team'});
});
// * Verify shared channel row
cy.get('@rows').eq(1).as('sharedChannelRow').within(() => {
cy.findByRole('cell', {name: testChannel.display_name});
cy.findByRole('cell', {name: testTeam.display_name});
});
});
});
it('remove channel', () => {
// # Prompt remove channel
cy.findByRole('table').findAllByRole('row').eq(1).findByRole('button', {name: 'Remove'}).click();
// * Verify channel remove prompt
cy.findByRole('dialog', {name: 'Remove channel'}).as('dialog');
cy.get('@dialog').within(() => {
// * Verify heading
cy.uiGetHeading('Remove channel');
// * Verify instructions
cy.findByText('The channel will be removed from this connection and will no longer be shared with it.');
cy.uiGetButton('Remove').click();
});
// * Verify no channels shared
cy.uiGetHeading("You haven't shared any channels");
});
it('change display name and destination team', () => {
// * Verify no changes to save
cy.uiGetButton('Save').should('be.disabled');
// # Enter name
cy.findByTestId('organization-name-input').
focus().
clear().
type(orgDisplayName2);
// # Select team
cy.findByTestId('destination-team-input').click().
findByRole('textbox').type(`${testTeam2.display_name}{enter}`);
// # Save
cy.uiGetButton('Save').click();
// * Verify name
cy.findByTestId('organization-name-input').
should('have.value', orgDisplayName2);
// * Verify team
cy.findByTestId('destination-team-input').should('have.text', testTeam2.display_name);
cy.wait(timeouts.ONE_SEC);
});
it('can go back', () => {
// # Go back to list page
cy.get('a.back').click();
// * Verify back at list page
cy.findByRole('heading', {name: 'Connected Workspaces'});
});
it('connection row - basics', () => {
cy.findAllByRole('link', {name: orgDisplayName2}).as('row');
// # Open connection detail
cy.get('@row').click();
// # Go back to list page
cy.get('a.back').click();
// * Verify connection status
cy.get('@row').findByText('Connection Pending');
// # Open menu, click edit
cy.get('@row').findByRole('button', {name: `Connection options for ${orgDisplayName2}`}).click();
cy.findByRole('menu').findByRole('menuitem', {name: 'Edit'}).click();
// # Go back to list page
cy.get('a.back').click();
});
it('connection row - generate invitation', () => {
cy.findAllByRole('link', {name: orgDisplayName2}).as('row');
// # Open menu
cy.get('@row').findByRole('button', {name: `Connection options for ${orgDisplayName2}`}).click();
// # Generate invite
cy.findByRole('menu').findByRole('menuitem', {name: 'Generate invitation code'}).click();
verifyInviteDialog('Invitation code');
});
it('connection row - delete connection', () => {
cy.findAllByRole('link', {name: orgDisplayName2}).as('row');
// # Open menu
cy.get('@row').findByRole('button', {name: `Connection options for ${orgDisplayName2}`}).click();
// # Prompt delete
cy.findByRole('menu').findByRole('menuitem', {name: 'Delete'}).click();
// * Verify delete dialog
cy.findByRole('dialog', {name: 'Delete secure connection'}).as('dialog');
cy.get('@dialog').within(() => {
// * Verify heading
cy.uiGetHeading('Delete secure connection');
// # Delete
cy.uiGetButton('Yes, delete').click();
});
// * Verify connection deleted
cy.get('@row').should('not.exist');
// * Verify dialog closed
cy.get('@dialog').should('not.exist');
});
});
});
const verifyInviteDialog = (name: string) => {
stubClipboard().as('clipboard');
cy.findByRole('dialog', {name}).as('dialog').within(() => {
// * Verify heading
cy.uiGetHeading(name);
// * Verify instructions
cy.findByText('Please share the invitation code and password with the administrator of the server you want to connect with.');
cy.findByText('Share these two separately to avoid a security compromise');
cy.findByText('Share this code and password');
cy.findByRole('group', {name: 'Encrypted invitation code'}).as('invite');
cy.findByRole('group', {name: 'Password'}).as('password');
// # Copy invite
// * Verify copy button text
cy.get('@invite').
findByRole('button', {name: 'Copy'}).
click().
should('have.text', 'Copied');
// * Verify invite copied to clipboard
cy.get('@invite').
findByRole('textbox').invoke('val').
then((value) => {
cy.get('@clipboard').
its('contents').
should('contain', value);
});
// # Copy password
// * Verify copy button text
cy.get('@password').
findByRole('button', {name: 'Copy'}).
click().
should('have.text', 'Copied');
// * Verify password copied to clipboard
cy.get('@password').
findByRole('textbox').invoke('val').
then((value) => {
cy.get('@clipboard').
its('contents').
should('contain', value);
});
// # Close dialog
cy.uiGetButton('Done').click();
});
// * Verify dialog closed
cy.get('@dialog').should('not.exist');
};

View File

@ -154,6 +154,7 @@ export default function SecureConnectionDetail(props: Props) {
> >
<Input <Input
type='text' type='text'
data-testid='organization-name-input'
value={remoteCluster?.display_name ?? ''} value={remoteCluster?.display_name ?? ''}
onChange={handleNameChange} onChange={handleNameChange}
autoFocus={isCreating} autoFocus={isCreating}
@ -170,6 +171,7 @@ export default function SecureConnectionDetail(props: Props) {
})} })}
> >
<TeamSelector <TeamSelector
testId='destination-team-input'
value={remoteCluster.default_team_id ?? ''} value={remoteCluster.default_team_id ?? ''}
teamsById={teamsById} teamsById={teamsById}
onChange={handleTeamChange} onChange={handleTeamChange}

View File

@ -25,9 +25,14 @@ type Props = {
export default function SecureConnectionRow(props: Props) { export default function SecureConnectionRow(props: Props) {
const {remoteCluster: rc} = props; const {remoteCluster: rc} = props;
const titleId = `${rc.remote_id}-title`;
return ( return (
<RowLink to={getEditLocation(rc)}> <RowLink
<Title>{rc.display_name}</Title> to={getEditLocation(rc)}
aria-labelledby={titleId}
>
<Title id={titleId}>{rc.display_name}</Title>
<Detail> <Detail>
<ConnectionStatusLabel rc={rc}/> <ConnectionStatusLabel rc={rc}/>
<RowMenu {...props}/> <RowMenu {...props}/>
@ -59,10 +64,11 @@ const RowMenu = ({remoteCluster: rc, onDeleteSuccess, disabled}: Props) => {
return ( return (
<Menu.Container <Menu.Container
menuButton={{ menuButton={{
id: `${menuId}-button`, id: `${menuId}-button-${rc.remote_id}`,
class: classNames('btn btn-tertiary btn-sm', {disabled}), class: classNames('btn btn-tertiary btn-sm connection-row-menu-button', {disabled}),
disabled, disabled,
children: !disabled && <DotsHorizontalIcon size={16}/>, children: !disabled && <DotsHorizontalIcon size={16}/>,
'aria-label': formatMessage({id: 'admin.secure_connection_row.menu-button.aria_label', defaultMessage: 'Connection options for {connection}'}, {connection: rc.display_name}),
}} }}
menu={{ menu={{
id: menuId, id: menuId,
@ -126,7 +132,7 @@ const RowLink = styled(Link<RemoteCluster>).attrs({className: 'secure-connection
border-bottom: 0; border-bottom: 0;
} }
#${menuId}-button { .connection-row-menu-button {
padding: 0px 8px; padding: 0px 8px;
} }
`; `;

View File

@ -14,6 +14,7 @@ export type Props = {
value: string; value: string;
teamsById: IDMappedObjects<Team>; teamsById: IDMappedObjects<Team>;
onChange: (teamId: string) => void; onChange: (teamId: string) => void;
testId: string;
} }
const TeamSelector = (props: Props): JSX.Element => { const TeamSelector = (props: Props): JSX.Element => {
@ -33,6 +34,7 @@ const TeamSelector = (props: Props): JSX.Element => {
return ( return (
<DropdownInput <DropdownInput
className='team_selector' className='team_selector'
testId={props.testId}
required={true} required={true}
onChange={handleTeamChange} onChange={handleTeamChange}
value={value ? {label: value.display_name, value: value.id} : undefined} value={value ? {label: value.display_name, value: value.id} : undefined}

View File

@ -2210,6 +2210,7 @@
"admin.secure_connection_detail.shared_channels.table.remote_actions.remove": "Remove", "admin.secure_connection_detail.shared_channels.table.remote_actions.remove": "Remove",
"admin.secure_connection_detail.shared_channels.table.team_home": "Current Team", "admin.secure_connection_detail.shared_channels.table.team_home": "Current Team",
"admin.secure_connection_detail.shared_channels.table.team_remote": "Destination Team", "admin.secure_connection_detail.shared_channels.table.team_remote": "Destination Team",
"admin.secure_connection_row.menu-button.aria_label": "Connection options for {connection}",
"admin.secure_connection_row.menu.aria_label": "secure connection row menu", "admin.secure_connection_row.menu.aria_label": "secure connection row menu",
"admin.secure_connection_row.menu.delete": "Delete", "admin.secure_connection_row.menu.delete": "Delete",
"admin.secure_connection_row.menu.edit": "Edit", "admin.secure_connection_row.menu.edit": "Edit",