snapshot of https://github.com/mattermost/mattermost.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
This commit is contained in:
@@ -0,0 +1,395 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {Locator, Page} from '@playwright/test';
|
||||
import {expect} from '@playwright/test';
|
||||
import type {Client4} from '@mattermost/client';
|
||||
|
||||
import {newTestPassword} from '@mattermost/playwright-lib';
|
||||
|
||||
export async function enableABACConfig(client: Client4) {
|
||||
await client.patchConfig({
|
||||
AccessControlSettings: {
|
||||
EnableAttributeBasedAccessControl: true,
|
||||
EnableUserManagedAttributes: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable `ServiceSettings.EnableAPIUserDeletion` so test cleanup can permanently
|
||||
* delete the throwaway users it created. Without this, test cleanup spams the
|
||||
* console with "Permanent user deletion feature is not enabled" errors and leaks
|
||||
* disabled-but-not-deleted test users on the server.
|
||||
*/
|
||||
export async function enableAPIUserDeletion(client: Client4) {
|
||||
await client.patchConfig({
|
||||
ServiceSettings: {
|
||||
EnableAPIUserDeletion: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function ensureDepartmentAttribute(client: Client4) {
|
||||
let fields: any[] = [];
|
||||
try {
|
||||
fields = await (client as any).doFetch(`${client.getBaseRoute()}/custom_profile_attributes/fields`, {
|
||||
method: 'GET',
|
||||
});
|
||||
} catch {
|
||||
// May not exist yet
|
||||
}
|
||||
if (!fields.find((f: any) => f.name === 'Department')) {
|
||||
try {
|
||||
await (client as any).doFetch(`${client.getBaseRoute()}/custom_profile_attributes/fields`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({name: 'Department', type: 'text', attrs: {visibility: 'when_set'}}),
|
||||
});
|
||||
} catch {
|
||||
// Another parallel worker may have created the field between our GET and POST.
|
||||
// Re-fetch to confirm it now exists before propagating any error.
|
||||
const recheck: any[] = await (client as any).doFetch(
|
||||
`${client.getBaseRoute()}/custom_profile_attributes/fields`,
|
||||
{method: 'GET'},
|
||||
);
|
||||
if (!recheck.find((f: any) => f.name === 'Department')) {
|
||||
throw new Error('Failed to create Department custom profile attribute field');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function createParentPolicy(client: Client4, name: string) {
|
||||
return (client as any).doFetch(`${client.getBaseRoute()}/access_control_policies`, {
|
||||
method: 'put',
|
||||
body: JSON.stringify({
|
||||
id: '',
|
||||
name,
|
||||
type: 'parent',
|
||||
version: 'v0.2',
|
||||
revision: 0,
|
||||
rules: [{expression: 'true', actions: ['*']}],
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
// Parent policy with a real membership expression, assignable to a team (v0.3).
|
||||
export async function createParentMembershipPolicy(client: Client4, name: string, expression: string) {
|
||||
return (client as any).doFetch(`${client.getBaseRoute()}/access_control_policies`, {
|
||||
method: 'put',
|
||||
body: JSON.stringify({
|
||||
id: '',
|
||||
name,
|
||||
type: 'parent',
|
||||
version: 'v0.3',
|
||||
revision: 0,
|
||||
rules: [{expression, actions: ['membership']}],
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
export async function assignChannelsToPolicy(client: Client4, policyId: string, channelIds: string[]) {
|
||||
const url = `${client.getBaseRoute()}/access_control_policies/${policyId}/assign`;
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json', Authorization: `Bearer ${client.getToken()}`},
|
||||
body: JSON.stringify({channel_ids: channelIds}),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`assignChannelsToPolicy failed: ${response.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function unassignChannelsFromPolicy(
|
||||
client: Client4,
|
||||
policyId: string,
|
||||
channelIds: string[],
|
||||
teamId?: string,
|
||||
) {
|
||||
const url = `${client.getBaseRoute()}/access_control_policies/${policyId}/unassign`;
|
||||
const response = await fetch(url, {
|
||||
method: 'DELETE',
|
||||
headers: {'Content-Type': 'application/json', Authorization: `Bearer ${client.getToken()}`},
|
||||
body: JSON.stringify({channel_ids: channelIds, ...(teamId && {team_id: teamId})}),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`unassignChannelsFromPolicy failed: ${response.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deletePolicy(client: Client4, policyId: string, teamId?: string) {
|
||||
const teamParam = teamId ? `?team_id=${encodeURIComponent(teamId)}` : '';
|
||||
const url = `${client.getBaseRoute()}/access_control_policies/${policyId}${teamParam}`;
|
||||
const response = await fetch(url, {
|
||||
method: 'DELETE',
|
||||
headers: {'Content-Type': 'application/json', Authorization: `Bearer ${client.getToken()}`},
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`deletePolicy failed: ${response.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function searchPolicies(client: Client4, teamId: string): Promise<any[]> {
|
||||
const result: any = await (client as any).doFetch(`${client.getBaseRoute()}/access_control_policies/search`, {
|
||||
method: 'post',
|
||||
body: JSON.stringify({
|
||||
term: '',
|
||||
type: 'parent',
|
||||
cursor: {id: ''},
|
||||
limit: 100,
|
||||
include_children: true,
|
||||
team_id: teamId,
|
||||
}),
|
||||
});
|
||||
return result.policies || [];
|
||||
}
|
||||
|
||||
export async function setUserAttribute(adminClient: Client4, userId: string, fieldName: string, value: string) {
|
||||
// Get all fields to find the field ID
|
||||
const fields: any[] = await (adminClient as any).doFetch(
|
||||
`${adminClient.getBaseRoute()}/custom_profile_attributes/fields`,
|
||||
{
|
||||
method: 'GET',
|
||||
},
|
||||
);
|
||||
const field = fields.find((f: any) => f.name === fieldName);
|
||||
if (!field) {
|
||||
throw new Error(`Field "${fieldName}" not found`);
|
||||
}
|
||||
await adminClient.updateUserCustomProfileAttributesValues(userId, {[field.id]: value});
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait until all `expectedUserIds` show up as matching `expression` via the
|
||||
* access-control CEL test endpoint.
|
||||
*
|
||||
* Why this exists: ABAC queries (validateExpressionAgainstRequester,
|
||||
* calculateMembershipChanges) read from a Postgres materialized view
|
||||
* (`UserAttributeView`). The enterprise access-control service refreshes that view
|
||||
* at most once every 30 seconds — so freshly-written CPA values are not visible
|
||||
* until the next refresh tick. A test that writes a brand-new CPA value and
|
||||
* then immediately clicks "Save" on a rule referencing that value will hit
|
||||
* `requester_matches: false` and surface the self-exclusion modal instead of
|
||||
* the membership-changes confirmation modal.
|
||||
*
|
||||
* Polling forces wall-clock time to advance; the next CEL query after the 30s
|
||||
* gate elapses will refresh the view and pick up the new attribute values.
|
||||
* Default timeout 45s = 30s gate + 15s headroom.
|
||||
*/
|
||||
export async function waitForAttributeViewToInclude(
|
||||
adminClient: Client4,
|
||||
expression: string,
|
||||
expectedUserIds: string[],
|
||||
timeoutMs = 45_000,
|
||||
pollIntervalMs = 1_000,
|
||||
): Promise<void> {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
let lastSeen = new Set<string>();
|
||||
while (Date.now() < deadline) {
|
||||
const response: any = await (adminClient as any).doFetch(
|
||||
`${adminClient.getBaseRoute()}/access_control_policies/cel/test`,
|
||||
{
|
||||
method: 'post',
|
||||
body: JSON.stringify({
|
||||
expression,
|
||||
term: '',
|
||||
after: '',
|
||||
limit: 1000,
|
||||
}),
|
||||
},
|
||||
);
|
||||
lastSeen = new Set<string>((response?.users || []).map((u: any) => u.id));
|
||||
if (expectedUserIds.every((id) => lastSeen.has(id))) {
|
||||
return;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
||||
}
|
||||
const missing = expectedUserIds.filter((id) => !lastSeen.has(id));
|
||||
throw new Error(
|
||||
`UserAttributeView did not include users [${missing.join(', ')}] for expression "${expression}" within ${timeoutMs}ms`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function waitForAttributeViewToExclude(
|
||||
adminClient: Client4,
|
||||
expression: string,
|
||||
excludedUserIds: string[],
|
||||
timeoutMs = 45_000,
|
||||
pollIntervalMs = 1_000,
|
||||
): Promise<void> {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
let lastSeen = new Set<string>();
|
||||
while (Date.now() < deadline) {
|
||||
const response: any = await (adminClient as any).doFetch(
|
||||
`${adminClient.getBaseRoute()}/access_control_policies/cel/test`,
|
||||
{
|
||||
method: 'post',
|
||||
body: JSON.stringify({
|
||||
expression,
|
||||
term: '',
|
||||
after: '',
|
||||
limit: 1000,
|
||||
}),
|
||||
},
|
||||
);
|
||||
lastSeen = new Set<string>((response?.users || []).map((u: any) => u.id));
|
||||
if (excludedUserIds.every((id) => !lastSeen.has(id))) {
|
||||
return;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
||||
}
|
||||
const stillPresent = excludedUserIds.filter((id) => lastSeen.has(id));
|
||||
throw new Error(
|
||||
`UserAttributeView still includes users [${stillPresent.join(', ')}] for expression "${expression}" after ${timeoutMs}ms`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function createPrivateChannel(client: Client4, teamId: string) {
|
||||
const id = Date.now().toString(36) + Math.random().toString(36).substring(2, 7);
|
||||
return client.createChannel({team_id: teamId, name: `abac-${id}`, display_name: `ABAC-${id}`, type: 'P'} as any);
|
||||
}
|
||||
|
||||
export async function createGroupConstrainedPrivateChannel(client: Client4, teamId: string) {
|
||||
const id = Date.now().toString(36) + Math.random().toString(36).substring(2, 7);
|
||||
return client.createChannel({
|
||||
team_id: teamId,
|
||||
name: `gc-${id}`,
|
||||
display_name: `GC-${id}`,
|
||||
type: 'P',
|
||||
group_constrained: true,
|
||||
} as any);
|
||||
}
|
||||
|
||||
export async function createTeamAdmin(adminClient: Client4, teamId: string) {
|
||||
const id = Date.now().toString(36) + Math.random().toString(36).substring(2, 7);
|
||||
const password = newTestPassword();
|
||||
const user = await adminClient.createUser(
|
||||
{
|
||||
email: `teamadmin-${id}@sample.mattermost.com`,
|
||||
username: `teamadmin${id}`,
|
||||
password,
|
||||
} as any,
|
||||
'',
|
||||
'',
|
||||
);
|
||||
user.password = password;
|
||||
|
||||
await adminClient.savePreferences(user.id, [
|
||||
{user_id: user.id, category: 'tutorial_step', name: user.id, value: '999'},
|
||||
{user_id: user.id, category: 'onboarding', name: 'complete', value: 'true'},
|
||||
// Suppress the onboarding task-list overlay — without these two prefs the
|
||||
// overlay appears on first login and blocks hover/click interactions.
|
||||
{user_id: user.id, category: 'onboarding_task_list', name: 'onboarding_task_list_show', value: 'false'},
|
||||
{user_id: user.id, category: 'onboarding_task_list', name: 'onboarding_task_list_open', value: 'false'},
|
||||
]);
|
||||
await adminClient.addToTeam(teamId, user.id);
|
||||
await (adminClient as any).doFetch(`${adminClient.getBaseRoute()}/teams/${teamId}/members/${user.id}/roles`, {
|
||||
method: 'put',
|
||||
body: JSON.stringify({roles: 'team_user team_admin'}),
|
||||
});
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an attribute rule row in the policy editor and fills in a value.
|
||||
* Handles the auto-opening attribute selector menu.
|
||||
*/
|
||||
export async function addAttributeRule(container: Locator, page: Page, value: string, attributeName = 'Department') {
|
||||
const addAttrBtn = container.getByRole('button', {name: /Add attribute/});
|
||||
await expect(addAttrBtn).toBeEnabled({timeout: 10000});
|
||||
await addAttrBtn.click();
|
||||
|
||||
// The attribute selector menu auto-opens — select by name, not by position.
|
||||
// Clicking the first item is unreliable when multiple attributes exist (e.g. Location
|
||||
// sorts before Department and gets picked instead, causing self-inclusion to fail).
|
||||
const attributeMenu = page.locator('[id^="attribute-selector-menu"]');
|
||||
await attributeMenu.waitFor({state: 'visible', timeout: 5000});
|
||||
await attributeMenu.locator('li').filter({hasText: attributeName}).first().click();
|
||||
|
||||
// Fill value in the simple input (text-type attribute renders direct input)
|
||||
const valueInput = container.locator('.values-editor__simple-input').first();
|
||||
await valueInput.waitFor({state: 'visible', timeout: 10000});
|
||||
await valueInput.fill(value);
|
||||
|
||||
// Blur the input so React commits the onChange before the caller proceeds
|
||||
await valueInput.press('Tab');
|
||||
}
|
||||
|
||||
export async function enableTeamMembershipABACConfig(client: Client4) {
|
||||
await client.patchConfig({
|
||||
AccessControlSettings: {
|
||||
EnableAttributeBasedAccessControl: true,
|
||||
EnableUserManagedAttributes: true,
|
||||
},
|
||||
FeatureFlags: {
|
||||
TeamMembershipAccessControl: true,
|
||||
},
|
||||
} as any);
|
||||
}
|
||||
|
||||
export async function createTeamMembershipPolicy(client: Client4, teamId: string, expression: string, active = false) {
|
||||
return (client as any).doFetch(`${client.getBaseRoute()}/access_control_policies`, {
|
||||
method: 'put',
|
||||
body: JSON.stringify({
|
||||
id: teamId,
|
||||
name: `team-policy-${teamId}`,
|
||||
type: 'team',
|
||||
active,
|
||||
revision: 0,
|
||||
rules: [{expression, actions: ['membership']}],
|
||||
imports: [],
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
export async function assignTeamToParentPolicy(client: Client4, policyId: string, teamId: string) {
|
||||
const url = `${client.getBaseRoute()}/access_control_policies/${policyId}/assign`;
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json', Authorization: `Bearer ${client.getToken()}`},
|
||||
body: JSON.stringify({team_ids: [teamId]}),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`assignTeamToParentPolicy failed: ${response.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getTeamAccessControlPolicy(client: Client4, teamId: string) {
|
||||
return (client as any).doFetch(`${client.getBaseRoute()}/teams/${teamId}/access_control/policy`, {method: 'GET'});
|
||||
}
|
||||
|
||||
export async function createPublicTeam(client: Client4, suffix: string) {
|
||||
return client.createTeam({
|
||||
name: `pub-team-${suffix}`,
|
||||
display_name: `Pub Team ${suffix}`,
|
||||
type: 'O',
|
||||
allow_open_invite: true,
|
||||
} as any);
|
||||
}
|
||||
|
||||
export async function createPrivateTeam(client: Client4, suffix: string) {
|
||||
return client.createTeam({
|
||||
name: `priv-team-${suffix}`,
|
||||
display_name: `Priv Team ${suffix}`,
|
||||
type: 'I',
|
||||
allow_open_invite: false,
|
||||
} as any);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a channel to the policy via the channel selector modal.
|
||||
*/
|
||||
export async function addChannelToPolicy(container: Locator, page: Page, channelDisplayName: string) {
|
||||
await container.getByRole('button', {name: /Add channels/}).click();
|
||||
const channelModal = page.locator('.channel-selector-modal');
|
||||
await channelModal.waitFor();
|
||||
await expect(channelModal.locator('.more-modal__row').first()).toBeVisible({timeout: 10000});
|
||||
await channelModal.locator('.more-modal__row').filter({hasText: channelDisplayName}).click();
|
||||
await channelModal.getByRole('button', {name: 'Add'}).click();
|
||||
|
||||
// Wait for the modal to fully close before returning — callers must not proceed
|
||||
// until the channel is committed to form state, otherwise a save click races
|
||||
// against the React state update and the confirmation modal never appears.
|
||||
await channelModal.waitFor({state: 'hidden', timeout: 20000});
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {decomposeKorean, expect, koreanTestPhrase, test, typeHangulCharacterWithIme} from '@mattermost/playwright-lib';
|
||||
|
||||
async function openInvitePeopleModal(pw: any, adminUser: any, team: any) {
|
||||
const {channelsPage} = await pw.testBrowser.login(adminUser);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
await channelsPage.sidebarLeft.teamMenuButton.click();
|
||||
await channelsPage.teamMenu.toBeVisible();
|
||||
await channelsPage.teamMenu.clickInvitePeople();
|
||||
|
||||
const inviteModal = await channelsPage.getInvitePeopleModal(team.display_name);
|
||||
await inviteModal.toBeVisible();
|
||||
|
||||
return inviteModal;
|
||||
}
|
||||
|
||||
test('MM-66937 Invite modal results match the current input state', async ({pw}) => {
|
||||
const {adminUser, adminClient, team} = await pw.initSetup();
|
||||
|
||||
// # Create two users not on the team whose usernames share a prefix but differ at the end
|
||||
const randomPrefix = pw.random.id(8);
|
||||
const user1 = await adminClient.createUser(await pw.random.user(randomPrefix + 'a'), '', '');
|
||||
const user2 = await adminClient.createUser(await pw.random.user(randomPrefix + 'b'), '', '');
|
||||
|
||||
// # Log in as admin
|
||||
const {channelsPage} = await pw.testBrowser.login(adminUser);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Open the Invite People modal
|
||||
await channelsPage.sidebarLeft.teamMenuButton.click();
|
||||
await channelsPage.teamMenu.toBeVisible();
|
||||
await channelsPage.teamMenu.clickInvitePeople();
|
||||
const inviteModal = await channelsPage.getInvitePeopleModal(team.display_name);
|
||||
await inviteModal.toBeVisible();
|
||||
|
||||
// # Type the prefix to filter out all users
|
||||
await inviteModal.inviteInput.pressSequentially(randomPrefix);
|
||||
|
||||
// * Verify that both users appear in the results initially
|
||||
const listbox = inviteModal.container.getByRole('listbox');
|
||||
await expect(listbox.getByRole('option')).toHaveCount(2);
|
||||
await expect(listbox.getByRole('option', {name: `@${user1.username}`})).toBeVisible();
|
||||
await expect(listbox.getByRole('option', {name: `@${user2.username}`})).toBeVisible();
|
||||
|
||||
// # Type an 'a' to filter the results
|
||||
await inviteModal.inviteInput.press('a');
|
||||
|
||||
// * Verify that only user1 is now listed
|
||||
await expect(listbox.getByRole('option')).toHaveCount(1);
|
||||
await expect(listbox.getByRole('option', {name: `@${user1.username}`})).toBeVisible();
|
||||
await expect(listbox.getByRole('option', {name: `@${user2.username}`})).not.toBeAttached();
|
||||
|
||||
// # Backspace that 'a'
|
||||
await inviteModal.inviteInput.press('Backspace');
|
||||
|
||||
// * Verify that both users are listed again
|
||||
await expect(listbox.getByRole('option')).toHaveCount(2);
|
||||
await expect(listbox.getByRole('option', {name: `@${user1.username}`})).toBeVisible();
|
||||
await expect(listbox.getByRole('option', {name: `@${user2.username}`})).toBeVisible();
|
||||
|
||||
// # Type a 'b' to filter the results
|
||||
await inviteModal.inviteInput.press('b');
|
||||
|
||||
// * Verify that only user2 is now listed
|
||||
await expect(listbox.getByRole('option')).toHaveCount(1);
|
||||
await expect(listbox.getByRole('option', {name: `@${user1.username}`})).not.toBeAttached();
|
||||
await expect(listbox.getByRole('option', {name: `@${user2.username}`})).toBeVisible();
|
||||
});
|
||||
|
||||
test('Invite modal autocomplete is not clipped vertically', async ({pw}) => {
|
||||
const {adminUser, adminClient, team} = await pw.initSetup();
|
||||
|
||||
const randomPrefix = pw.random.id(8);
|
||||
const users = await Promise.all([
|
||||
adminClient.createUser(await pw.random.user(randomPrefix + 'a'), '', ''),
|
||||
adminClient.createUser(await pw.random.user(randomPrefix + 'b'), '', ''),
|
||||
adminClient.createUser(await pw.random.user(randomPrefix + 'c'), '', ''),
|
||||
adminClient.createUser(await pw.random.user(randomPrefix + 'd'), '', ''),
|
||||
adminClient.createUser(await pw.random.user(randomPrefix + 'e'), '', ''),
|
||||
adminClient.createUser(await pw.random.user(randomPrefix + 'f'), '', ''),
|
||||
adminClient.createUser(await pw.random.user(randomPrefix + 'g'), '', ''),
|
||||
adminClient.createUser(await pw.random.user(randomPrefix + 'h'), '', ''),
|
||||
]);
|
||||
|
||||
const inviteModal = await openInvitePeopleModal(pw, adminUser, team);
|
||||
|
||||
await inviteModal.inviteInput.pressSequentially(randomPrefix);
|
||||
|
||||
const modalContent = inviteModal.container.locator('.modal-content');
|
||||
const listbox = inviteModal.container.getByRole('listbox');
|
||||
const options = listbox.getByRole('option');
|
||||
await expect(options).toHaveCount(users.length);
|
||||
|
||||
const lastOption = options.last();
|
||||
await expect(lastOption).toBeVisible();
|
||||
|
||||
const geometry = await Promise.all([modalContent.boundingBox(), lastOption.boundingBox()]);
|
||||
const [modalContentBox, lastOptionBox] = geometry;
|
||||
expect(modalContentBox).not.toBeNull();
|
||||
expect(lastOptionBox).not.toBeNull();
|
||||
expect(lastOptionBox!.y + lastOptionBox!.height).toBeGreaterThan(modalContentBox!.y + modalContentBox!.height);
|
||||
|
||||
const modalContentOverflow = await modalContent.evaluate((element: HTMLElement) => {
|
||||
const style = window.getComputedStyle(element);
|
||||
|
||||
return {
|
||||
overflowX: style.overflowX,
|
||||
overflowY: style.overflowY,
|
||||
};
|
||||
});
|
||||
|
||||
expect(modalContentOverflow.overflowX).toBe('visible');
|
||||
expect(modalContentOverflow.overflowY).toBe('visible');
|
||||
});
|
||||
|
||||
test('Invite modal results are visible outside the modal', async ({pw}) => {
|
||||
const {adminUser, adminClient, team} = await pw.initSetup();
|
||||
|
||||
// # Create enough users that the results will extend outside the modal until past when the result list is capped
|
||||
const randomPrefix = pw.random.id(8);
|
||||
const users = await Promise.all([
|
||||
adminClient.createUser(await pw.random.user(randomPrefix + 'a'), '', ''),
|
||||
adminClient.createUser(await pw.random.user(randomPrefix + 'b'), '', ''),
|
||||
adminClient.createUser(await pw.random.user(randomPrefix + 'c'), '', ''),
|
||||
adminClient.createUser(await pw.random.user(randomPrefix + 'd'), '', ''),
|
||||
adminClient.createUser(await pw.random.user(randomPrefix + 'e'), '', ''),
|
||||
adminClient.createUser(await pw.random.user(randomPrefix + 'f'), '', ''),
|
||||
adminClient.createUser(await pw.random.user(randomPrefix + 'g'), '', ''),
|
||||
adminClient.createUser(await pw.random.user(randomPrefix + 'h'), '', ''),
|
||||
]);
|
||||
|
||||
// # Open the Invite People modal
|
||||
const inviteModal = await openInvitePeopleModal(pw, adminUser, team);
|
||||
|
||||
// # Type the prefix to filter out other users
|
||||
await inviteModal.inviteInput.type(randomPrefix);
|
||||
|
||||
// At time of writing, 4 results are fully visible, the next is partly visible, and the rest are clipped
|
||||
const visibleCount = 5;
|
||||
|
||||
for (let i = 0; i < users.length; i++) {
|
||||
const user = users[i];
|
||||
|
||||
const option = await inviteModal.container.getByRole('option', {name: user.username, exact: false});
|
||||
if (i < visibleCount) {
|
||||
// * Verify that this user is in the list and on the screen
|
||||
await expect(option).toBeVisible();
|
||||
await expect(option).toBeInViewport();
|
||||
} else {
|
||||
// * Verify that this user is in the list but off the screen
|
||||
await expect(option).toBeVisible();
|
||||
await expect(option).not.toBeInViewport();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('Invite modal remains width-constrained with long unbroken input', async ({pw}) => {
|
||||
const {adminUser, team} = await pw.initSetup();
|
||||
const inviteModal = await openInvitePeopleModal(pw, adminUser, team);
|
||||
|
||||
const modalContent = inviteModal.container.locator('.modal-content');
|
||||
const initialModalContentBox = await modalContent.boundingBox();
|
||||
expect(initialModalContentBox).not.toBeNull();
|
||||
|
||||
await inviteModal.inviteInput.pressSequentially(
|
||||
'averyveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryveryverylongunbrokeninviteinput@example.com',
|
||||
);
|
||||
|
||||
const modalContentMetrics = await modalContent.evaluate((element: HTMLElement) => {
|
||||
const style = window.getComputedStyle(element);
|
||||
const rect = element.getBoundingClientRect();
|
||||
|
||||
return {
|
||||
width: Math.round(rect.width),
|
||||
overflowX: style.overflowX,
|
||||
overflowY: style.overflowY,
|
||||
};
|
||||
});
|
||||
|
||||
expect(modalContentMetrics.width).toBeLessThanOrEqual(Math.ceil(initialModalContentBox!.width));
|
||||
expect(modalContentMetrics.overflowX).toBe('visible');
|
||||
expect(modalContentMetrics.overflowY).toBe('visible');
|
||||
});
|
||||
|
||||
test('MM-66937 Invite modal results match the current input state when typing in Korean', async ({browserName, pw}) => {
|
||||
test.skip(browserName !== 'chromium', 'The API used to test this is only available in Chrome');
|
||||
|
||||
const {adminUser, adminClient, team} = await pw.initSetup();
|
||||
|
||||
// # Create a users with a Korean name (plus a prefix to avoid interfering test runs)
|
||||
const randomPrefix = pw.random.id(8);
|
||||
const user = await adminClient.createUser(
|
||||
{
|
||||
...(await pw.random.user(randomPrefix + 'a')),
|
||||
first_name: randomPrefix + koreanTestPhrase,
|
||||
},
|
||||
'',
|
||||
'',
|
||||
);
|
||||
|
||||
// # Log in as admin
|
||||
const {channelsPage, page} = await pw.testBrowser.login(adminUser);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Open the Invite People modal
|
||||
await channelsPage.sidebarLeft.teamMenuButton.click();
|
||||
await channelsPage.teamMenu.toBeVisible();
|
||||
await channelsPage.teamMenu.clickInvitePeople();
|
||||
const inviteModal = await channelsPage.getInvitePeopleModal(team.display_name);
|
||||
await inviteModal.toBeVisible();
|
||||
|
||||
// # Type the prefix to filter out other users
|
||||
await inviteModal.inviteInput.pressSequentially(randomPrefix);
|
||||
|
||||
// * Verify that the user appears in the results initially
|
||||
const listbox = inviteModal.container.getByRole('listbox');
|
||||
await expect(listbox.getByRole('option')).toHaveCount(1);
|
||||
await expect(listbox.getByRole('option', {name: `@${user.username}`})).toBeVisible();
|
||||
|
||||
// # Type all 3 keys that form a single Hangul character
|
||||
const client = await page.context().newCDPSession(page);
|
||||
await typeHangulCharacterWithIme(client, decomposeKorean(koreanTestPhrase)[0], undefined);
|
||||
|
||||
// * Verify that the user is still listed
|
||||
await expect(listbox.getByRole('option')).toHaveCount(1);
|
||||
await expect(listbox.getByRole('option', {name: `@${user.username}`})).toBeVisible();
|
||||
|
||||
await client.detach();
|
||||
});
|
||||
@@ -0,0 +1,346 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/**
|
||||
* @objective Invite People modal enforces and displays team membership policy
|
||||
* @reference MM-69100
|
||||
*/
|
||||
|
||||
import {ChannelsPage, expect, newTestPassword, test} from '@mattermost/playwright-lib';
|
||||
|
||||
import {
|
||||
enableTeamMembershipABACConfig,
|
||||
ensureDepartmentAttribute,
|
||||
createPublicTeam,
|
||||
createPrivateTeam,
|
||||
createTeamMembershipPolicy,
|
||||
createTeamAdmin,
|
||||
setUserAttribute,
|
||||
waitForAttributeViewToInclude,
|
||||
} from './helpers';
|
||||
|
||||
test.describe('Invite People - Team Membership Policy', {tag: ['@abac', '@team_membership']}, () => {
|
||||
const createdTeamIds: string[] = [];
|
||||
const createdUserIds: string[] = [];
|
||||
|
||||
test.afterEach(async ({pw}) => {
|
||||
const {adminClient} = await pw.getAdminClient();
|
||||
for (const id of createdTeamIds.splice(0)) {
|
||||
await adminClient.deleteTeam(id).catch(() => {});
|
||||
}
|
||||
for (const id of createdUserIds.splice(0)) {
|
||||
await adminClient.updateUserActive(id, false).catch(() => {});
|
||||
}
|
||||
});
|
||||
|
||||
test('MM-69100_30 governed team shows the policy banner, attribute chips, and invite-link warning', async ({
|
||||
pw,
|
||||
}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminClient, team} = await pw.initSetup();
|
||||
await enableTeamMembershipABACConfig(adminClient);
|
||||
await ensureDepartmentAttribute(adminClient);
|
||||
|
||||
const teamAdmin = await createTeamAdmin(adminClient, team.id);
|
||||
createdUserIds.push(teamAdmin.id);
|
||||
|
||||
// # Create policy (advisory/public team)
|
||||
await createTeamMembershipPolicy(adminClient, team.id, 'user.attributes.Department == "Engineering"', false);
|
||||
|
||||
const {page} = await pw.testBrowser.login(teamAdmin);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Open Invite People modal
|
||||
await channelsPage.sidebarLeft.teamMenuButton.click();
|
||||
await channelsPage.teamMenu.toBeVisible();
|
||||
await channelsPage.teamMenu.clickInvitePeople();
|
||||
|
||||
const inviteModal = await channelsPage.getInvitePeopleModal(team.display_name);
|
||||
await expect(inviteModal.container).toBeVisible({timeout: 10000});
|
||||
|
||||
// * Policy banner visible with correct title
|
||||
const banner = inviteModal.container.locator('.InviteView__policyBanner');
|
||||
await expect(banner).toBeVisible({timeout: 10000});
|
||||
await expect(banner.getByText('Team access is restricted by user attributes')).toBeVisible();
|
||||
|
||||
// * Attribute chips are shown (async — wait on the chip text)
|
||||
await expect(inviteModal.container.getByText(/Department:/i)).toBeVisible({timeout: 15000});
|
||||
|
||||
// * Invite-link warning visible
|
||||
const linkWarning = inviteModal.container.locator('.InviteView__inviteLinkWarning');
|
||||
await expect(linkWarning).toBeVisible({timeout: 10000});
|
||||
await expect(linkWarning).toHaveText(/People who use this link must meet the membership requirements to join/i);
|
||||
});
|
||||
|
||||
test('MM-69100_31 PRIVATE governed team filters the candidate list to qualifying users only (strict)', async ({
|
||||
pw,
|
||||
}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminClient} = await pw.getAdminClient();
|
||||
const suffix = pw.random.id();
|
||||
const userPrefix = pw.random.id();
|
||||
await enableTeamMembershipABACConfig(adminClient);
|
||||
await ensureDepartmentAttribute(adminClient);
|
||||
|
||||
// # Create a private team
|
||||
const team = await createPrivateTeam(adminClient, suffix);
|
||||
createdTeamIds.push(team.id);
|
||||
const teamAdmin = await createTeamAdmin(adminClient, team.id);
|
||||
createdUserIds.push(teamAdmin.id);
|
||||
await setUserAttribute(adminClient, teamAdmin.id, 'Department', 'Engineering');
|
||||
|
||||
// # eng1 and mkt1 share userPrefix so one search term finds both — usernames are @{userPrefix}eng and @{userPrefix}mkt
|
||||
const createUser = async (dept: string, tag: string) => {
|
||||
const uid = `${userPrefix}${tag}`;
|
||||
const user = await adminClient.createUser(
|
||||
{email: `${uid}@sample.mattermost.com`, username: uid, password: newTestPassword()} as any,
|
||||
'',
|
||||
'',
|
||||
);
|
||||
user.password = newTestPassword();
|
||||
await setUserAttribute(adminClient, user.id, 'Department', dept);
|
||||
return user;
|
||||
};
|
||||
|
||||
const eng1 = await createUser('Engineering', 'eng');
|
||||
const mkt1 = await createUser('Marketing', 'mkt');
|
||||
createdUserIds.push(eng1.id, mkt1.id);
|
||||
|
||||
// # Policy applied — strict mode (private team)
|
||||
await createTeamMembershipPolicy(adminClient, team.id, 'user.attributes.Department == "Engineering"', false);
|
||||
|
||||
// The child policy row is written to the master DB; reads may go to a
|
||||
// read replica. Poll until policy_enforced=true before opening the modal
|
||||
// so isStrictlyFilteredTeam() is correct at componentDidMount.
|
||||
await expect
|
||||
.poll(async () => (await adminClient.getTeam(team.id)).policy_enforced, {
|
||||
timeout: 60_000,
|
||||
intervals: [1000, 2000, 5000, 5000, 5000],
|
||||
message: 'team should show policy_enforced=true before opening the invite modal',
|
||||
})
|
||||
.toBe(true);
|
||||
|
||||
// Confirm the attribute view is fresh AFTER policy_enforced is set. Placing this
|
||||
// wait here (not before createTeamMembershipPolicy) keeps the gap between the
|
||||
// confirmed view and the actual invite search to login+nav time (~8–11s), well
|
||||
// inside the 30s materialized-view refresh cycle. The original position before
|
||||
// policy creation left a 20–30s gap that reliably landed on the next refresh
|
||||
// boundary — causing the strict filter to read a mid-refresh (empty) view and
|
||||
// exclude eng1 from the results.
|
||||
await waitForAttributeViewToInclude(adminClient, 'user.attributes.Department == "Engineering"', [
|
||||
eng1.id,
|
||||
teamAdmin.id,
|
||||
]);
|
||||
|
||||
const {page} = await pw.testBrowser.login(teamAdmin);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Open Invite People
|
||||
await channelsPage.sidebarLeft.teamMenuButton.click();
|
||||
await channelsPage.teamMenu.toBeVisible();
|
||||
await channelsPage.teamMenu.clickInvitePeople();
|
||||
|
||||
const inviteModal = await channelsPage.getInvitePeopleModal(team.display_name);
|
||||
await expect(inviteModal.container).toBeVisible({timeout: 10000});
|
||||
|
||||
// The modal container is visible before the team policy fetch completes.
|
||||
// Wait for the policy banner — it only renders after the async policy fetch
|
||||
// returns — so that strict filtering is active before we search. Typing
|
||||
// before this resolves causes the first search to fire in non-strict mode;
|
||||
// strict mode then activates mid-search and clears the results.
|
||||
await expect(inviteModal.container.locator('.InviteView__policyBanner')).toBeVisible({timeout: 15000});
|
||||
|
||||
// # Search with the shared prefix to surface both users (strict filter will keep only qualifiers)
|
||||
await inviteModal.inviteInput.click();
|
||||
await inviteModal.inviteInput.pressSequentially(userPrefix);
|
||||
const listbox = inviteModal.container.getByRole('listbox');
|
||||
await expect(listbox).toBeVisible({timeout: 10000});
|
||||
|
||||
// * eng1 is an option (qualifies — Department=Engineering)
|
||||
await expect(listbox.getByRole('option', {name: `@${eng1.username}`})).toBeVisible({timeout: 30000});
|
||||
|
||||
// * mkt1 is NOT in the list at all (strict filtering strips non-qualifiers from the DOM)
|
||||
await expect(listbox.getByRole('option', {name: `@${mkt1.username}`})).not.toBeAttached();
|
||||
});
|
||||
|
||||
test('MM-69100_32 PUBLIC governed team does NOT filter candidates (advisory)', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminClient} = await pw.getAdminClient();
|
||||
const suffix = pw.random.id();
|
||||
await enableTeamMembershipABACConfig(adminClient);
|
||||
await ensureDepartmentAttribute(adminClient);
|
||||
|
||||
// # Create a public team
|
||||
const team = await createPublicTeam(adminClient, suffix);
|
||||
createdTeamIds.push(team.id);
|
||||
const teamAdmin = await createTeamAdmin(adminClient, team.id);
|
||||
createdUserIds.push(teamAdmin.id);
|
||||
await setUserAttribute(adminClient, teamAdmin.id, 'Department', 'Engineering');
|
||||
|
||||
// # eng1 and mkt1 share userPrefix so one search surfaces both — advisory mode must show both
|
||||
const userPrefix = pw.random.id();
|
||||
const createUser = async (dept: string, tag: string) => {
|
||||
const uid = `${userPrefix}${tag}`;
|
||||
const user = await adminClient.createUser(
|
||||
{email: `${uid}@sample.mattermost.com`, username: uid, password: newTestPassword()} as any,
|
||||
'',
|
||||
'',
|
||||
);
|
||||
user.password = newTestPassword();
|
||||
await setUserAttribute(adminClient, user.id, 'Department', dept);
|
||||
return user;
|
||||
};
|
||||
|
||||
const eng1 = await createUser('Engineering', 'eng');
|
||||
const mkt1 = await createUser('Marketing', 'mkt');
|
||||
createdUserIds.push(eng1.id, mkt1.id);
|
||||
|
||||
await waitForAttributeViewToInclude(adminClient, 'user.attributes.Department == "Engineering"', [eng1.id]);
|
||||
|
||||
// # Advisory policy (public team)
|
||||
await createTeamMembershipPolicy(adminClient, team.id, 'user.attributes.Department == "Engineering"', false);
|
||||
|
||||
const {page} = await pw.testBrowser.login(teamAdmin);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Open Invite People
|
||||
await channelsPage.sidebarLeft.teamMenuButton.click();
|
||||
await channelsPage.teamMenu.toBeVisible();
|
||||
await channelsPage.teamMenu.clickInvitePeople();
|
||||
|
||||
const inviteModal = await channelsPage.getInvitePeopleModal(team.display_name);
|
||||
await expect(inviteModal.container).toBeVisible({timeout: 10000});
|
||||
|
||||
// * Banner still shown (governed) but with softened advisory copy
|
||||
const banner = inviteModal.container.locator('.InviteView__policyBanner');
|
||||
await expect(banner).toBeVisible({timeout: 10000});
|
||||
await expect(banner.getByText('This team has membership requirements')).toBeVisible();
|
||||
await expect(banner.getByText(/can still join, but will not be automatically added/i)).toBeVisible();
|
||||
await expect(banner.getByText('Team access is restricted by user attributes')).toHaveCount(0);
|
||||
|
||||
// * Invite-link warning uses the advisory wording (no join block)
|
||||
const linkWarning = inviteModal.container.locator('.InviteView__inviteLinkWarning');
|
||||
await expect(linkWarning).toHaveText(/can join even if they do not meet the membership requirements/i);
|
||||
|
||||
// # Search with the shared prefix to surface both users
|
||||
await inviteModal.inviteInput.pressSequentially(userPrefix);
|
||||
const listbox = inviteModal.container.getByRole('listbox');
|
||||
await expect(listbox).toBeVisible({timeout: 10000});
|
||||
|
||||
// * Both eng1 AND mkt1 appear (advisory = no filtering)
|
||||
await expect(listbox.getByRole('option', {name: `@${eng1.username}`})).toBeVisible({timeout: 15000});
|
||||
await expect(listbox.getByRole('option', {name: `@${mkt1.username}`})).toBeVisible({timeout: 15000});
|
||||
});
|
||||
|
||||
test('MM-69100_33 non-policy team behaves normally (regression)', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminClient, team} = await pw.initSetup();
|
||||
await enableTeamMembershipABACConfig(adminClient);
|
||||
|
||||
const teamAdmin = await createTeamAdmin(adminClient, team.id);
|
||||
createdUserIds.push(teamAdmin.id);
|
||||
|
||||
const {page} = await pw.testBrowser.login(teamAdmin);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Open Invite People
|
||||
await channelsPage.sidebarLeft.teamMenuButton.click();
|
||||
await channelsPage.teamMenu.toBeVisible();
|
||||
await channelsPage.teamMenu.clickInvitePeople();
|
||||
|
||||
const inviteModal = await channelsPage.getInvitePeopleModal(team.display_name);
|
||||
await expect(inviteModal.container).toBeVisible({timeout: 10000});
|
||||
|
||||
// * Policy banner NOT present
|
||||
await expect(inviteModal.container.locator('.InviteView__policyBanner')).not.toBeVisible({timeout: 5000});
|
||||
|
||||
// * Normal invite input present
|
||||
await expect(inviteModal.inviteInput).toBeVisible();
|
||||
});
|
||||
|
||||
test('MM-69100_34 PRIVATE governed team narrows candidates by the typed term server-side', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminClient} = await pw.getAdminClient();
|
||||
const suffix = pw.random.id();
|
||||
const userPrefix = pw.random.id();
|
||||
await enableTeamMembershipABACConfig(adminClient);
|
||||
await ensureDepartmentAttribute(adminClient);
|
||||
|
||||
// # Private (strict) team governed by a policy
|
||||
const team = await createPrivateTeam(adminClient, suffix);
|
||||
createdTeamIds.push(team.id);
|
||||
const teamAdmin = await createTeamAdmin(adminClient, team.id);
|
||||
createdUserIds.push(teamAdmin.id);
|
||||
await setUserAttribute(adminClient, teamAdmin.id, 'Department', 'Engineering');
|
||||
|
||||
// # Two qualifying (Engineering) users sharing a prefix but with distinct
|
||||
// # suffixes, so a suffix-specific term matches exactly one of them. Since
|
||||
// # the strict picker searches server-side (no pre-buffered candidate set),
|
||||
// # the non-matching qualifier must be excluded by the term, not surfaced.
|
||||
const createEngUser = async (tag: string) => {
|
||||
const uid = `${userPrefix}eng${tag}`;
|
||||
const user = await adminClient.createUser(
|
||||
{email: `${uid}@sample.mattermost.com`, username: uid, password: newTestPassword()} as any,
|
||||
'',
|
||||
'',
|
||||
);
|
||||
await setUserAttribute(adminClient, user.id, 'Department', 'Engineering');
|
||||
return user;
|
||||
};
|
||||
|
||||
const engAlpha = await createEngUser('alpha');
|
||||
const engBeta = await createEngUser('beta');
|
||||
createdUserIds.push(engAlpha.id, engBeta.id);
|
||||
|
||||
await createTeamMembershipPolicy(adminClient, team.id, 'user.attributes.Department == "Engineering"', false);
|
||||
|
||||
await expect
|
||||
.poll(async () => (await adminClient.getTeam(team.id)).policy_enforced, {
|
||||
timeout: 60_000,
|
||||
intervals: [1000, 2000, 5000, 5000, 5000],
|
||||
message: 'team should show policy_enforced=true before opening the invite modal',
|
||||
})
|
||||
.toBe(true);
|
||||
|
||||
await waitForAttributeViewToInclude(adminClient, 'user.attributes.Department == "Engineering"', [
|
||||
engAlpha.id,
|
||||
engBeta.id,
|
||||
teamAdmin.id,
|
||||
]);
|
||||
|
||||
const {page} = await pw.testBrowser.login(teamAdmin);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Open Invite People
|
||||
await channelsPage.sidebarLeft.teamMenuButton.click();
|
||||
await channelsPage.teamMenu.toBeVisible();
|
||||
await channelsPage.teamMenu.clickInvitePeople();
|
||||
|
||||
const inviteModal = await channelsPage.getInvitePeopleModal(team.display_name);
|
||||
await expect(inviteModal.container).toBeVisible({timeout: 10000});
|
||||
await expect(inviteModal.container.locator('.InviteView__policyBanner')).toBeVisible({timeout: 15000});
|
||||
|
||||
// # Search a term that matches only engAlpha
|
||||
await inviteModal.inviteInput.click();
|
||||
await inviteModal.inviteInput.pressSequentially(`${userPrefix}engalpha`);
|
||||
const listbox = inviteModal.container.getByRole('listbox');
|
||||
await expect(listbox).toBeVisible({timeout: 10000});
|
||||
|
||||
// * engAlpha is returned by the server-side policy-scoped search
|
||||
await expect(listbox.getByRole('option', {name: `@${engAlpha.username}`})).toBeVisible({timeout: 30000});
|
||||
|
||||
// * engBeta — also a qualifier — is excluded by the term, proving the
|
||||
// search is applied server-side rather than dumping a buffered set
|
||||
await expect(listbox.getByRole('option', {name: `@${engBeta.username}`})).not.toBeAttached();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,97 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {ChannelsPage, expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify that a user with a valid email domain can be invited to a closed team,
|
||||
* and a user with an invalid email domain is rejected with the correct error message.
|
||||
*/
|
||||
test('MM-T388 Invite new user to closed team with email domain restriction', {tag: '@team_settings'}, async ({pw}) => {
|
||||
const emailDomain = 'sample.mattermost.com';
|
||||
|
||||
// # Set up admin user and team
|
||||
const {adminUser, adminClient, team} = await pw.initSetup();
|
||||
|
||||
// # Enable email invitations so the invite modal shows "Add email" option
|
||||
await adminClient.patchConfig({
|
||||
ServiceSettings: {EnableEmailInvitations: true},
|
||||
});
|
||||
|
||||
// # Create a new user NOT on the team (default email is @sample.mattermost.com)
|
||||
const newUser = await adminClient.createUser(await pw.random.user(), '', '');
|
||||
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
|
||||
// # Navigate to team
|
||||
await channelsPage.goto(team.name);
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Open Team Settings Modal and go to Access tab
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
const accessSettings = await teamSettings.openAccessTab();
|
||||
|
||||
// # Enable "Allow only users with a specific email domain" and add the domain
|
||||
await accessSettings.enableAllowedDomains();
|
||||
await accessSettings.addDomain(emailDomain);
|
||||
|
||||
// # Save changes
|
||||
await teamSettings.save();
|
||||
await teamSettings.verifySavedMessage();
|
||||
|
||||
// # Close the Team Settings modal and wait for it to disappear
|
||||
await teamSettings.close();
|
||||
await expect(teamSettings.container).not.toBeVisible();
|
||||
|
||||
await adminClient.patchConfig({
|
||||
ServiceSettings: {EnableEmailInvitations: true},
|
||||
});
|
||||
await pw.waitUntil(async () => {
|
||||
const cfg = await adminClient.getConfig();
|
||||
return cfg.ServiceSettings?.EnableEmailInvitations === true;
|
||||
});
|
||||
|
||||
// Re-apply email invitations immediately before opening the invite modal: a
|
||||
// concurrent initSetup() → patchConfig(defaultConfig) resets
|
||||
// ServiceSettings.EnableEmailInvitations: false between the initial patchConfig and here.
|
||||
|
||||
// # Open team menu and click 'Invite People'
|
||||
await channelsPage.sidebarLeft.teamMenuButton.click();
|
||||
await channelsPage.teamMenu.toBeVisible();
|
||||
await channelsPage.teamMenu.clickInvitePeople();
|
||||
|
||||
// # Get the invite people modal and invite user with valid email domain
|
||||
const inviteModal = await channelsPage.getInvitePeopleModal(team.display_name);
|
||||
await inviteModal.toBeVisible();
|
||||
await inviteModal.inviteByEmail(newUser.email);
|
||||
|
||||
// * Verify that the user has been successfully invited to the team
|
||||
const membersInvitedModal = await channelsPage.getMembersInvitedModal(team.display_name);
|
||||
await membersInvitedModal.toBeVisible();
|
||||
const sentReason = await membersInvitedModal.getSentResultReason();
|
||||
expect(sentReason).toBe('This member has been added to the team.');
|
||||
|
||||
await adminClient.patchConfig({
|
||||
ServiceSettings: {EnableEmailInvitations: true},
|
||||
});
|
||||
await pw.waitUntil(async () => {
|
||||
const cfg = await adminClient.getConfig();
|
||||
return cfg.ServiceSettings?.EnableEmailInvitations === true;
|
||||
});
|
||||
|
||||
// # Click 'Invite More People' to return to the invite form
|
||||
await membersInvitedModal.clickInviteMore();
|
||||
|
||||
// # Invite a user with an invalid email domain (not sample.mattermost.com)
|
||||
const invalidEmail = `user.${pw.random.id()}@invalid.com`;
|
||||
await inviteModal.inviteByEmail(invalidEmail);
|
||||
|
||||
// * Verify that the invite failed with the correct domain restriction error
|
||||
const membersInvitedModal2 = await channelsPage.getMembersInvitedModal(team.display_name);
|
||||
await membersInvitedModal2.toBeVisible();
|
||||
const notSentReason = await membersInvitedModal2.getNotSentResultReason();
|
||||
expect(notSentReason).toContain(
|
||||
`The following email addresses do not belong to an accepted domain: ${invalidEmail}.`,
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,190 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/**
|
||||
* @objective Team Members modal shows membership requirements banner and attribute chips for governed teams
|
||||
* @reference MM-69100
|
||||
*/
|
||||
|
||||
import {ChannelsPage, expect, newTestPassword, test} from '@mattermost/playwright-lib';
|
||||
|
||||
import {
|
||||
enableTeamMembershipABACConfig,
|
||||
ensureDepartmentAttribute,
|
||||
createPublicTeam,
|
||||
createTeamMembershipPolicy,
|
||||
createTeamAdmin,
|
||||
setUserAttribute,
|
||||
waitForAttributeViewToInclude,
|
||||
} from './helpers';
|
||||
|
||||
test.describe('Team Members Modal - Membership Policy Banner', {tag: ['@abac', '@team_membership']}, () => {
|
||||
const createdTeamIds: string[] = [];
|
||||
const createdUserIds: string[] = [];
|
||||
|
||||
test.afterEach(async ({pw}) => {
|
||||
const {adminClient} = await pw.getAdminClient();
|
||||
for (const id of createdTeamIds.splice(0)) {
|
||||
await adminClient.deleteTeam(id).catch(() => {});
|
||||
}
|
||||
for (const id of createdUserIds.splice(0)) {
|
||||
await adminClient.updateUserActive(id, false).catch(() => {});
|
||||
}
|
||||
});
|
||||
|
||||
test('MM-69100_34 governed team shows the policy banner in the Team Members modal', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminClient, team} = await pw.initSetup();
|
||||
await enableTeamMembershipABACConfig(adminClient);
|
||||
await ensureDepartmentAttribute(adminClient);
|
||||
|
||||
const teamAdmin = await createTeamAdmin(adminClient, team.id);
|
||||
createdUserIds.push(teamAdmin.id);
|
||||
|
||||
await createTeamMembershipPolicy(adminClient, team.id, 'user.attributes.Department == "Engineering"', false);
|
||||
|
||||
const {page} = await pw.testBrowser.login(teamAdmin);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Open Team Members modal via sidebar menu
|
||||
await channelsPage.sidebarLeft.teamMenuButton.click();
|
||||
await channelsPage.teamMenu.toBeVisible();
|
||||
await channelsPage.teamMenu.clickManageMembers();
|
||||
|
||||
const modal = page.locator('#teamMembersModal');
|
||||
await expect(modal).toBeVisible({timeout: 10000});
|
||||
|
||||
// * Banner present with correct title and description
|
||||
const banner = modal.locator('.teamMembersModal__policyBanner');
|
||||
await expect(banner).toBeVisible({timeout: 10000});
|
||||
await expect(banner.getByText('Team access is restricted by user attributes')).toBeVisible();
|
||||
await expect(
|
||||
banner.getByText('Only people who meet the membership requirements can be members of this team.'),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('MM-69100_35 attribute holder sees attribute chips inside the policy banner', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminClient, team} = await pw.initSetup();
|
||||
await enableTeamMembershipABACConfig(adminClient);
|
||||
await ensureDepartmentAttribute(adminClient);
|
||||
|
||||
// # Create team admin who is an attribute holder (Engineering)
|
||||
const teamAdmin = await createTeamAdmin(adminClient, team.id);
|
||||
createdUserIds.push(teamAdmin.id);
|
||||
await setUserAttribute(adminClient, teamAdmin.id, 'Department', 'Engineering');
|
||||
await waitForAttributeViewToInclude(adminClient, 'user.attributes.Department == "Engineering"', [teamAdmin.id]);
|
||||
|
||||
await createTeamMembershipPolicy(adminClient, team.id, 'user.attributes.Department == "Engineering"', false);
|
||||
|
||||
const {page} = await pw.testBrowser.login(teamAdmin);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Open Team Members modal
|
||||
await channelsPage.sidebarLeft.teamMenuButton.click();
|
||||
await channelsPage.teamMenu.toBeVisible();
|
||||
await channelsPage.teamMenu.clickManageMembers();
|
||||
|
||||
const modal = page.locator('#teamMembersModal');
|
||||
await expect(modal).toBeVisible({timeout: 10000});
|
||||
|
||||
// * Banner visible
|
||||
await expect(modal.locator('.teamMembersModal__policyBanner')).toBeVisible({timeout: 10000});
|
||||
|
||||
// * Attribute chip "Department: Engineering" visible (async — chip text loads after banner)
|
||||
await expect(modal.getByText(/Department: Engineering/i)).toBeVisible({timeout: 15000});
|
||||
});
|
||||
|
||||
test('MM-69100_36 non-admin team member sees the policy banner in the Team Members modal', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminClient} = await pw.getAdminClient();
|
||||
const suffix = pw.random.id();
|
||||
await enableTeamMembershipABACConfig(adminClient);
|
||||
await ensureDepartmentAttribute(adminClient);
|
||||
|
||||
// # Public team with Engineering policy
|
||||
const team = await createPublicTeam(adminClient, suffix);
|
||||
createdTeamIds.push(team.id);
|
||||
|
||||
// # Regular user (non-admin) — verifies the banner appears for ordinary members too
|
||||
const regularUser = await adminClient.createUser(
|
||||
{
|
||||
email: `regular${suffix}@sample.mattermost.com`,
|
||||
username: `regular${suffix}`,
|
||||
password: newTestPassword(),
|
||||
} as any,
|
||||
'',
|
||||
'',
|
||||
);
|
||||
createdUserIds.push(regularUser.id);
|
||||
regularUser.password = newTestPassword();
|
||||
await adminClient.savePreferences(regularUser.id, [
|
||||
{user_id: regularUser.id, category: 'tutorial_step', name: regularUser.id, value: '999'},
|
||||
{user_id: regularUser.id, category: 'onboarding', name: 'complete', value: 'true'},
|
||||
{
|
||||
user_id: regularUser.id,
|
||||
category: 'onboarding_task_list',
|
||||
name: 'onboarding_task_list_show',
|
||||
value: 'false',
|
||||
},
|
||||
{
|
||||
user_id: regularUser.id,
|
||||
category: 'onboarding_task_list',
|
||||
name: 'onboarding_task_list_open',
|
||||
value: 'false',
|
||||
},
|
||||
]);
|
||||
await adminClient.addToTeam(team.id, regularUser.id);
|
||||
|
||||
await createTeamMembershipPolicy(adminClient, team.id, 'user.attributes.Department == "Engineering"', false);
|
||||
|
||||
const {page} = await pw.testBrowser.login(regularUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Open Team Members modal (regular user sees "View members")
|
||||
await channelsPage.sidebarLeft.teamMenuButton.click();
|
||||
await channelsPage.teamMenu.toBeVisible();
|
||||
await channelsPage.teamMenu.container.getByRole('menuitem', {name: /View members|Manage members/i}).click();
|
||||
|
||||
const modal = page.locator('#teamMembersModal');
|
||||
await expect(modal).toBeVisible({timeout: 10000});
|
||||
|
||||
// * Banner present with softened advisory copy (public team)
|
||||
const banner = modal.locator('.teamMembersModal__policyBanner');
|
||||
await expect(banner).toBeVisible({timeout: 10000});
|
||||
await expect(banner.getByText('This team has membership requirements')).toBeVisible();
|
||||
await expect(banner.getByText(/can still join, but will not be automatically added/i)).toBeVisible();
|
||||
await expect(banner.getByText('Team access is restricted by user attributes')).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('MM-69100_37 ungoverned team does not show the policy banner in Team Members modal', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminClient, team} = await pw.initSetup();
|
||||
await enableTeamMembershipABACConfig(adminClient);
|
||||
|
||||
const teamAdmin = await createTeamAdmin(adminClient, team.id);
|
||||
createdUserIds.push(teamAdmin.id);
|
||||
|
||||
const {page} = await pw.testBrowser.login(teamAdmin);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Open Team Members modal
|
||||
await channelsPage.sidebarLeft.teamMenuButton.click();
|
||||
await channelsPage.teamMenu.toBeVisible();
|
||||
await channelsPage.teamMenu.clickManageMembers();
|
||||
|
||||
const modal = page.locator('#teamMembersModal');
|
||||
await expect(modal).toBeVisible({timeout: 10000});
|
||||
|
||||
// * No policy banner for ungoverned team
|
||||
await expect(modal.locator('.teamMembersModal__policyBanner')).not.toBeVisible({timeout: 5000});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/**
|
||||
* @objective E2E tests for Team Settings Modal - Access Tab
|
||||
* @reference MM-67920
|
||||
*/
|
||||
|
||||
import {ChannelsPage, expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
test.describe('Team Settings Modal - Access Tab', () => {
|
||||
/**
|
||||
* MM-67920 Access tab - add and remove allowed domain
|
||||
* @objective Verify allowed domains can be added and removed
|
||||
*/
|
||||
test('MM-67920 Access tab - add and remove allowed domain', async ({pw}) => {
|
||||
// # Set up admin user and login
|
||||
const {adminUser, adminClient, team} = await pw.initSetup();
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
|
||||
// # Navigate to team
|
||||
await channelsPage.goto(team.name);
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Open Team Settings Modal
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
|
||||
// # Switch to Access tab
|
||||
const accessSettings = await teamSettings.openAccessTab();
|
||||
|
||||
// * Verify Access tab is active
|
||||
await expect(teamSettings.accessTab).toHaveAttribute('aria-selected', 'true');
|
||||
|
||||
// # Enable allowed domains checkbox to show the input
|
||||
await accessSettings.enableAllowedDomains();
|
||||
|
||||
// # Add an allowed domain
|
||||
const testDomain = 'testdomain.com';
|
||||
await accessSettings.addDomain(testDomain);
|
||||
|
||||
// * Verify domain appears in the UI
|
||||
const domainChip = teamSettings.container.locator('#allowedDomains').getByText(testDomain);
|
||||
await expect(domainChip).toBeVisible();
|
||||
|
||||
// # Save changes
|
||||
await teamSettings.save();
|
||||
|
||||
// * Wait for "Settings saved" message
|
||||
await teamSettings.verifySavedMessage();
|
||||
|
||||
// * Verify domain was saved via API
|
||||
const updatedTeam = await adminClient.getTeam(team.id);
|
||||
expect(updatedTeam.allowed_domains).toContain(testDomain);
|
||||
|
||||
// # Remove the added domain
|
||||
await accessSettings.removeDomain(testDomain);
|
||||
|
||||
// # Save changes
|
||||
await teamSettings.save();
|
||||
await teamSettings.verifySavedMessage();
|
||||
|
||||
// * Verify domain was removed via API
|
||||
const finalTeam = await adminClient.getTeam(team.id);
|
||||
expect(finalTeam.allowed_domains).not.toContain(testDomain);
|
||||
|
||||
// # Close modal
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
/**
|
||||
* MM-67920 Access tab - regenerate invite ID
|
||||
* @objective Verify team invite ID can be regenerated
|
||||
*/
|
||||
test('MM-67920 Access tab - regenerate invite ID', async ({pw}) => {
|
||||
// # Set up admin user and login
|
||||
const {adminUser, adminClient, team} = await pw.initSetup();
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
|
||||
// Get original invite ID
|
||||
const originalInviteId = team.invite_id;
|
||||
|
||||
// # Navigate to team
|
||||
await channelsPage.goto(team.name);
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Open Team Settings Modal
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
|
||||
// # Switch to Access tab
|
||||
const accessSettings = await teamSettings.openAccessTab();
|
||||
|
||||
// * Verify Access tab is active
|
||||
await expect(teamSettings.accessTab).toHaveAttribute('aria-selected', 'true');
|
||||
|
||||
// # Click regenerate button
|
||||
await accessSettings.regenerateInviteId();
|
||||
|
||||
// * Verify invite ID changed via API
|
||||
const updatedTeam = await adminClient.getTeam(team.id);
|
||||
expect(updatedTeam.invite_id).not.toBe(originalInviteId);
|
||||
expect(updatedTeam.invite_id).toBeTruthy();
|
||||
|
||||
// # Close modal
|
||||
await teamSettings.close();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,532 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/**
|
||||
* @objective E2E tests for Team Settings - Access Tab discoverability (Public/Private cards)
|
||||
* @reference MM-69100
|
||||
*/
|
||||
|
||||
import {ChannelsPage, expect, newTestPassword, test} from '@mattermost/playwright-lib';
|
||||
|
||||
import {
|
||||
enableTeamMembershipABACConfig,
|
||||
ensureDepartmentAttribute,
|
||||
createPublicTeam,
|
||||
createPrivateTeam,
|
||||
createTeamMembershipPolicy,
|
||||
createParentMembershipPolicy,
|
||||
assignTeamToParentPolicy,
|
||||
setUserAttribute,
|
||||
waitForAttributeViewToInclude,
|
||||
waitForAttributeViewToExclude,
|
||||
} from './helpers';
|
||||
|
||||
type ModeFlipScenario = {
|
||||
adminClient: any;
|
||||
adminUser: any;
|
||||
team: any;
|
||||
};
|
||||
|
||||
async function setupModeFlipScenario(pw: any): Promise<ModeFlipScenario> {
|
||||
const {adminClient, adminUser} = await pw.getAdminClient();
|
||||
const suffix = pw.random.id();
|
||||
|
||||
await enableTeamMembershipABACConfig(adminClient);
|
||||
await ensureDepartmentAttribute(adminClient);
|
||||
|
||||
const team = await createPublicTeam(adminClient, suffix);
|
||||
|
||||
await setUserAttribute(adminClient, adminUser.id, 'Department', 'Engineering');
|
||||
|
||||
const createUser = async (dept: string, idx: number) => {
|
||||
const uid = `${suffix}u${idx}`;
|
||||
const user = await adminClient.createUser(
|
||||
{
|
||||
email: `testuser${uid}@sample.mattermost.com`,
|
||||
username: `testuser${uid}`,
|
||||
password: newTestPassword(),
|
||||
} as any,
|
||||
'',
|
||||
'',
|
||||
);
|
||||
await adminClient.addToTeam(team.id, user.id);
|
||||
await setUserAttribute(adminClient, user.id, 'Department', dept);
|
||||
return user;
|
||||
};
|
||||
|
||||
const [eng1, eng2, eng3] = await Promise.all([
|
||||
createUser('Engineering', 1),
|
||||
createUser('Engineering', 2),
|
||||
createUser('Engineering', 3),
|
||||
]);
|
||||
await createUser('Marketing', 4);
|
||||
|
||||
await createTeamMembershipPolicy(adminClient, team.id, 'user.attributes.Department == "Engineering"', false);
|
||||
|
||||
await waitForAttributeViewToInclude(adminClient, 'user.attributes.Department == "Engineering"', [
|
||||
adminUser.id,
|
||||
eng1.id,
|
||||
eng2.id,
|
||||
eng3.id,
|
||||
]);
|
||||
|
||||
return {adminClient, adminUser, team};
|
||||
}
|
||||
|
||||
test.describe('Team Settings Modal - Access Tab - Discoverability', {tag: ['@abac', '@team_membership']}, () => {
|
||||
test('MM-69100_1 renders Public Team and Private Team cards, not a checkbox', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminUser, adminClient, team} = await pw.initSetup();
|
||||
await enableTeamMembershipABACConfig(adminClient);
|
||||
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
|
||||
// # Navigate and open Team Settings
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
await teamSettings.openAccessTab();
|
||||
|
||||
// * Public and Private cards are visible
|
||||
await expect(teamSettings.container.getByText('Public Team', {exact: true})).toBeVisible();
|
||||
await expect(teamSettings.container.getByText('Private Team', {exact: true})).toBeVisible();
|
||||
|
||||
// * Old open-invite checkboxes are gone
|
||||
await expect(teamSettings.container.locator('input[name="allowOpenInvite"]')).not.toBeVisible();
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
test('MM-69100_2 non-policy team: Public card toggles allow_open_invite only, leaves type unchanged', async ({
|
||||
pw,
|
||||
}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminClient, adminUser} = await pw.getAdminClient();
|
||||
if (!adminUser) {
|
||||
throw new Error('Admin user not found');
|
||||
}
|
||||
const suffix = pw.random.id();
|
||||
await enableTeamMembershipABACConfig(adminClient);
|
||||
|
||||
// # Start with an open-typed team that is not discoverable (type=O, allow_open_invite=false).
|
||||
// No policy attached, so the cards must follow the legacy contract.
|
||||
const team = await adminClient.createTeam({
|
||||
name: `np-pub-team-${suffix}`,
|
||||
display_name: `NP Pub Team ${suffix}`,
|
||||
type: 'O',
|
||||
allow_open_invite: false,
|
||||
} as any);
|
||||
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
|
||||
// # Navigate and open Team Settings
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
await teamSettings.openAccessTab();
|
||||
|
||||
// * Private Team card is initially selected (not discoverable)
|
||||
await expect(teamSettings.container.locator('#public-private-selector-button-P')).toHaveClass(/selected/);
|
||||
|
||||
// # Click Public Team card
|
||||
await teamSettings.container.locator('#public-private-selector-button-O').click();
|
||||
await expect(teamSettings.saveButton).toBeVisible();
|
||||
await teamSettings.save();
|
||||
await teamSettings.verifySavedMessage();
|
||||
|
||||
// * Only allow_open_invite changed; type stays "O" (no normalization without a policy)
|
||||
const updatedTeam = await adminClient.getTeam(team.id);
|
||||
expect(updatedTeam.allow_open_invite).toBe(true);
|
||||
expect(updatedTeam.type).toBe('O');
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
test('MM-69100_3 non-policy team: Private card toggles allow_open_invite only, never sets type=I', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminClient, adminUser} = await pw.getAdminClient();
|
||||
if (!adminUser) {
|
||||
throw new Error('Admin user not found');
|
||||
}
|
||||
const suffix = pw.random.id();
|
||||
await enableTeamMembershipABACConfig(adminClient);
|
||||
|
||||
// # Start with a fully public team (type=O AND allow_open_invite=true), no policy
|
||||
const team = await createPublicTeam(adminClient, suffix);
|
||||
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
|
||||
// # Navigate and open Team Settings (team is public)
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
await teamSettings.openAccessTab();
|
||||
|
||||
// * Public Team card is initially selected
|
||||
await expect(teamSettings.container.locator('#public-private-selector-button-O')).toHaveClass(/selected/);
|
||||
|
||||
// # Click Private Team card (no policy → no mode-flip modal)
|
||||
await teamSettings.container.locator('#public-private-selector-button-P').click();
|
||||
await expect(teamSettings.saveButton).toBeVisible();
|
||||
await expect(page.getByText('Switch to Private Team?')).not.toBeVisible();
|
||||
await teamSettings.save();
|
||||
await teamSettings.verifySavedMessage();
|
||||
|
||||
// * Only allow_open_invite changed; type stays "O" — the legacy contract must not
|
||||
// flip a non-policy team to invite-only (would regress getInviteInfo, etc.)
|
||||
const updatedTeam = await adminClient.getTeam(team.id);
|
||||
expect(updatedTeam.allow_open_invite).toBe(false);
|
||||
expect(updatedTeam.type).toBe('O');
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
test('MM-69100_4 active ABAC policy leaves cards enabled and mode-flip reachable', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminUser, adminClient, team} = await pw.initSetup();
|
||||
await enableTeamMembershipABACConfig(adminClient);
|
||||
|
||||
// # Make the team fully public
|
||||
await adminClient.patchTeam({id: team.id, allow_open_invite: true} as any);
|
||||
|
||||
// # Attach a policy with auto-add on
|
||||
await createTeamMembershipPolicy(adminClient, team.id, 'true', true);
|
||||
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
|
||||
// # Navigate and open Team Settings
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
await teamSettings.openAccessTab();
|
||||
|
||||
// * No "managed by a policy" notice is shown
|
||||
await expect(teamSettings.container.getByText(/managed by a policy/i)).toHaveCount(0);
|
||||
|
||||
// * Neither card carries the disabled CSS class
|
||||
await expect(teamSettings.container.locator('#public-private-selector-button-O')).not.toHaveClass(/disabled/);
|
||||
await expect(teamSettings.container.locator('#public-private-selector-button-P')).not.toHaveClass(/disabled/);
|
||||
|
||||
// # Clicking Private opens the mode-flip confirmation (card is not blocked)
|
||||
await teamSettings.container.locator('#public-private-selector-button-P').click();
|
||||
const modeFlipModal = page.locator('.ConfirmModal').filter({hasText: 'Switch to Private Team?'});
|
||||
await expect(modeFlipModal).toBeVisible({timeout: 30000});
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
test('MM-69100_5 Public→Private mode-flip with active policy shows confirm modal with member count', async ({
|
||||
pw,
|
||||
}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminClient, adminUser, team} = await setupModeFlipScenario(pw);
|
||||
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
|
||||
// # Navigate and open Team Settings
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
await teamSettings.openAccessTab();
|
||||
|
||||
// * Public Team card is initially selected
|
||||
await expect(teamSettings.container.locator('#public-private-selector-button-O')).toHaveClass(/selected/);
|
||||
|
||||
// # Click Private Team card → mode-flip modal should appear (policy is enforced)
|
||||
await teamSettings.container.locator('#public-private-selector-button-P').click();
|
||||
|
||||
// * Mode-flip confirm modal appears
|
||||
const modeFlipModal = page.locator('.ConfirmModal').filter({hasText: 'Switch to Private Team?'});
|
||||
await expect(modeFlipModal).toBeVisible({timeout: 30000});
|
||||
|
||||
// * Modal shows the member count (1 marketing user does not meet the Engineering rule)
|
||||
await expect(modeFlipModal.getByText(/1 current member does not meet/i)).toBeVisible({timeout: 10000});
|
||||
|
||||
// * Switch to Private and Cancel buttons are present
|
||||
await expect(modeFlipModal.getByRole('button', {name: 'Switch to Private'})).toBeVisible();
|
||||
await expect(modeFlipModal.getByRole('button', {name: 'Cancel'})).toBeVisible();
|
||||
|
||||
// # Cancel → modal closes, team stays public
|
||||
await modeFlipModal.getByRole('button', {name: 'Cancel'}).click();
|
||||
await expect(modeFlipModal).not.toBeVisible({timeout: 5000});
|
||||
|
||||
// * Team is still public
|
||||
const apiTeam = await adminClient.getTeam(team.id);
|
||||
expect(apiTeam.type).toBe('O');
|
||||
expect(apiTeam.allow_open_invite).toBe(true);
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
test('MM-69100_6 mode-flip confirm saves team as Private and triggers sync job', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminClient, adminUser, team} = await setupModeFlipScenario(pw);
|
||||
const testStartTime = Date.now();
|
||||
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
|
||||
// # Navigate and open Team Settings
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
await teamSettings.openAccessTab();
|
||||
|
||||
// # Click Private card → mode-flip modal
|
||||
await teamSettings.container.locator('#public-private-selector-button-P').click();
|
||||
|
||||
const modeFlipModal = page.locator('.ConfirmModal').filter({hasText: 'Switch to Private Team?'});
|
||||
await expect(modeFlipModal).toBeVisible({timeout: 30000});
|
||||
|
||||
// # Click "Switch to Private" — confirming saves immediately (no second click)
|
||||
await modeFlipModal.getByRole('button', {name: 'Switch to Private'}).click();
|
||||
await expect(modeFlipModal).not.toBeVisible({timeout: 5000});
|
||||
|
||||
// * The save auto-completes and the panel reports success
|
||||
await teamSettings.verifySavedMessage();
|
||||
|
||||
// * Team is now private. Privacy is driven by allow_open_invite alone; the
|
||||
// feature never mutates team.type, so it stays 'O' from createPublicTeam.
|
||||
const updatedTeam = await adminClient.getTeam(team.id);
|
||||
expect(updatedTeam.type).toBe('O');
|
||||
expect(updatedTeam.allow_open_invite).toBe(false);
|
||||
|
||||
// * A sync job was created after the mode-flip was confirmed
|
||||
const jobs: any[] = await (adminClient as any).doFetch(
|
||||
`${adminClient.getBaseRoute()}/jobs/type/access_control_team_sync`,
|
||||
{method: 'GET'},
|
||||
);
|
||||
const recentJobs = jobs.filter((j: any) => j.create_at >= testStartTime);
|
||||
expect(recentJobs.length).toBeGreaterThan(0);
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
test('MM-69100_7 Private→Public flip saves directly, no modal, no sync job', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminClient, adminUser} = await pw.getAdminClient();
|
||||
if (!adminUser) {
|
||||
throw new Error('Admin user not found');
|
||||
}
|
||||
const suffix = pw.random.id();
|
||||
await enableTeamMembershipABACConfig(adminClient);
|
||||
|
||||
// # Start with a private team and create a policy on it
|
||||
const team = await createPrivateTeam(adminClient, suffix);
|
||||
await createTeamMembershipPolicy(adminClient, team.id, 'true', true);
|
||||
const testStartTime = Date.now();
|
||||
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
|
||||
// # Navigate and open Team Settings (team is private + policy_enforced)
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
await teamSettings.openAccessTab();
|
||||
|
||||
// # Click Public Team card
|
||||
await teamSettings.container.locator('#public-private-selector-button-O').click();
|
||||
|
||||
// * SaveChangesPanel appears immediately — no mode-flip modal
|
||||
await expect(teamSettings.saveButton).toBeVisible();
|
||||
await expect(page.getByText('Switch to Private Team?')).not.toBeVisible();
|
||||
|
||||
// # Save
|
||||
await teamSettings.save();
|
||||
await teamSettings.verifySavedMessage();
|
||||
|
||||
// * Team is now public. The feature never mutates team.type, so it stays
|
||||
// 'I' from createPrivateTeam; discoverability is driven by allow_open_invite.
|
||||
const updatedTeam = await adminClient.getTeam(team.id);
|
||||
expect(updatedTeam.type).toBe('I');
|
||||
expect(updatedTeam.allow_open_invite).toBe(true);
|
||||
|
||||
// * No new sync job was created by this Private→Public flip
|
||||
const jobs: any[] = await (adminClient as any).doFetch(
|
||||
`${adminClient.getBaseRoute()}/jobs/type/access_control_team_sync`,
|
||||
{method: 'GET'},
|
||||
);
|
||||
const recentJobs = jobs.filter((j: any) => j.create_at >= testStartTime);
|
||||
expect(recentJobs.length).toBe(0);
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
test('MM-69100_40 governed team switched public can be switched back to private', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminClient, adminUser} = await pw.getAdminClient();
|
||||
if (!adminUser) {
|
||||
throw new Error('Admin user not found');
|
||||
}
|
||||
const suffix = pw.random.id();
|
||||
await enableTeamMembershipABACConfig(adminClient);
|
||||
|
||||
// # Private team with an auto-add-on policy
|
||||
const team = await createPrivateTeam(adminClient, suffix);
|
||||
await createTeamMembershipPolicy(adminClient, team.id, 'true', true);
|
||||
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Switch the team public and save
|
||||
let teamSettings = await channelsPage.openTeamSettings();
|
||||
await teamSettings.openAccessTab();
|
||||
await teamSettings.container.locator('#public-private-selector-button-O').click();
|
||||
await teamSettings.save();
|
||||
await teamSettings.verifySavedMessage();
|
||||
await teamSettings.close();
|
||||
|
||||
// # Reopen Team Settings
|
||||
teamSettings = await channelsPage.openTeamSettings();
|
||||
await teamSettings.openAccessTab();
|
||||
|
||||
// * Cards are not disabled
|
||||
await expect(teamSettings.container.locator('#public-private-selector-button-O')).not.toHaveClass(/disabled/);
|
||||
await expect(teamSettings.container.locator('#public-private-selector-button-P')).not.toHaveClass(/disabled/);
|
||||
|
||||
// # Switch back to private via the mode-flip modal
|
||||
await teamSettings.container.locator('#public-private-selector-button-P').click();
|
||||
const modeFlipModal = page.locator('.ConfirmModal').filter({hasText: 'Switch to Private Team?'});
|
||||
await expect(modeFlipModal).toBeVisible({timeout: 30000});
|
||||
await modeFlipModal.getByRole('button', {name: 'Switch to Private'}).click();
|
||||
await expect(modeFlipModal).not.toBeVisible({timeout: 5000});
|
||||
|
||||
// Confirming saves immediately — the panel reports success on its own
|
||||
await teamSettings.verifySavedMessage();
|
||||
|
||||
// * Team is private again
|
||||
const updatedTeam = await adminClient.getTeam(team.id);
|
||||
expect(updatedTeam.allow_open_invite).toBe(false);
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
test('MM-69100_41 mode-flip modal shows member count for a parent-policy team', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminClient, adminUser} = await pw.getAdminClient();
|
||||
if (!adminUser) {
|
||||
throw new Error('Admin user not found');
|
||||
}
|
||||
const suffix = pw.random.id();
|
||||
const expression = 'user.attributes.Department == "Engineering"';
|
||||
|
||||
await enableTeamMembershipABACConfig(adminClient);
|
||||
await ensureDepartmentAttribute(adminClient);
|
||||
|
||||
// # Public team; admin qualifies, added member does not
|
||||
const team = await createPublicTeam(adminClient, suffix);
|
||||
await setUserAttribute(adminClient, adminUser.id, 'Department', 'Engineering');
|
||||
|
||||
const mkt = await adminClient.createUser(
|
||||
{
|
||||
email: `testuser${suffix}mkt@sample.mattermost.com`,
|
||||
username: `testuser${suffix}mkt`,
|
||||
password: newTestPassword(),
|
||||
} as any,
|
||||
'',
|
||||
'',
|
||||
);
|
||||
await adminClient.addToTeam(team.id, mkt.id);
|
||||
await setUserAttribute(adminClient, mkt.id, 'Department', 'Marketing');
|
||||
|
||||
// # Govern via a parent policy (child imports it, no own rules)
|
||||
const parent: any = await createParentMembershipPolicy(adminClient, `eng-parent-${suffix}`, expression);
|
||||
await assignTeamToParentPolicy(adminClient, parent.id, team.id);
|
||||
|
||||
await waitForAttributeViewToInclude(adminClient, expression, [adminUser.id]);
|
||||
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
await teamSettings.openAccessTab();
|
||||
|
||||
// # Click Private → mode-flip modal
|
||||
await teamSettings.container.locator('#public-private-selector-button-P').click();
|
||||
const modeFlipModal = page.locator('.ConfirmModal').filter({hasText: 'Switch to Private Team?'});
|
||||
await expect(modeFlipModal).toBeVisible({timeout: 30000});
|
||||
|
||||
// * Modal shows the resolved count, not the generic message
|
||||
await expect(modeFlipModal.getByText(/1 current member does not meet/i)).toBeVisible({timeout: 10000});
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
test('MM-69100_44 mode-flip is blocked when the admin does not meet the rules', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminClient, adminUser} = await pw.getAdminClient();
|
||||
if (!adminUser) {
|
||||
throw new Error('Admin user not found');
|
||||
}
|
||||
const suffix = pw.random.id();
|
||||
const expression = 'user.attributes.Department == "Engineering"';
|
||||
|
||||
await enableTeamMembershipABACConfig(adminClient);
|
||||
await ensureDepartmentAttribute(adminClient);
|
||||
|
||||
// # Public governed team where the admin does NOT satisfy the rule
|
||||
const team = await createPublicTeam(adminClient, suffix);
|
||||
await setUserAttribute(adminClient, adminUser.id, 'Department', 'Marketing');
|
||||
await createTeamMembershipPolicy(adminClient, team.id, expression, false);
|
||||
await waitForAttributeViewToExclude(adminClient, expression, [adminUser.id]);
|
||||
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
await teamSettings.openAccessTab();
|
||||
|
||||
// # Click Private Team card
|
||||
await teamSettings.container.locator('#public-private-selector-button-P').click();
|
||||
|
||||
// * Self-exclusion block appears; the mode-flip confirmation does not
|
||||
await expect(page.getByText('Cannot switch to Private Team')).toBeVisible({timeout: 30000});
|
||||
await expect(page.getByText('Switch to Private Team?')).not.toBeVisible();
|
||||
|
||||
await page.getByRole('button', {name: 'Back to editing'}).click();
|
||||
await expect(page.getByText('Cannot switch to Private Team')).not.toBeVisible({timeout: 5000});
|
||||
|
||||
// * Team is still public
|
||||
const apiTeam = await adminClient.getTeam(team.id);
|
||||
expect(apiTeam.allow_open_invite).toBe(true);
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
test('MM-69100_45 Access tab shows the updated discoverability help text', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminUser, adminClient, team} = await pw.initSetup();
|
||||
await enableTeamMembershipABACConfig(adminClient);
|
||||
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
|
||||
// # Navigate and open Team Settings
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
await teamSettings.openAccessTab();
|
||||
|
||||
// * Updated card-aligned help text is shown
|
||||
await expect(
|
||||
teamSettings.container.getByText(
|
||||
/Switching a team from Public to Private regenerates its invitation code/i,
|
||||
),
|
||||
).toBeVisible();
|
||||
|
||||
// * Stale checkbox-era copy is gone
|
||||
await expect(teamSettings.container.getByText(/Changing from 'Yes' to 'No'/i)).toHaveCount(0);
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,177 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/**
|
||||
* @objective E2E tests for Team Settings Modal - Info Tab
|
||||
* @reference MM-67920
|
||||
*/
|
||||
|
||||
import {getAsset} from '@/asset';
|
||||
import {ChannelsPage, expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
test.describe('Team Settings Modal - Info Tab', () => {
|
||||
/**
|
||||
* MM-67920 Open and close Team Settings Modal
|
||||
* @objective Verify basic modal open/close functionality
|
||||
*/
|
||||
test('MM-67920 Open and close Team Settings Modal', async ({pw}) => {
|
||||
// # Set up admin user and login
|
||||
const {adminUser} = await pw.initSetup();
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
|
||||
// # Navigate to a team
|
||||
await channelsPage.goto();
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Open Team Settings Modal
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
|
||||
// * Verify Info tab is selected by default
|
||||
await expect(teamSettings.infoTab).toHaveAttribute('aria-selected', 'true');
|
||||
|
||||
// # Close modal
|
||||
await teamSettings.close();
|
||||
|
||||
// * Verify modal closes
|
||||
await expect(teamSettings.container).not.toBeVisible();
|
||||
});
|
||||
|
||||
/**
|
||||
* MM-67920 Edit team name and save changes
|
||||
* @objective Verify team name can be edited and saved
|
||||
*/
|
||||
test('MM-67920 Edit team name and save changes', async ({pw}) => {
|
||||
// # Set up admin user and login
|
||||
const {adminUser, adminClient, team} = await pw.initSetup();
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
|
||||
// # Navigate to team
|
||||
await channelsPage.goto(team.name);
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Open Team Settings Modal
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
|
||||
// * Verify current team name is displayed
|
||||
await expect(teamSettings.infoSettings.nameInput).toHaveValue(team.display_name);
|
||||
|
||||
// # Edit team name
|
||||
const newTeamName = `Updated Team ${pw.random.id()}`;
|
||||
await teamSettings.infoSettings.updateName(newTeamName);
|
||||
|
||||
// # Save changes
|
||||
await teamSettings.save();
|
||||
|
||||
// * Wait for "Settings saved" message
|
||||
await teamSettings.verifySavedMessage();
|
||||
|
||||
// * Verify team name updated via API
|
||||
const updatedTeam = await adminClient.getTeam(team.id);
|
||||
expect(updatedTeam.display_name).toBe(newTeamName);
|
||||
|
||||
// # Close modal
|
||||
await teamSettings.close();
|
||||
|
||||
// * Verify modal closes without warning
|
||||
await expect(teamSettings.container).not.toBeVisible();
|
||||
});
|
||||
|
||||
/**
|
||||
* MM-67920 Edit team description and save changes
|
||||
* @objective Verify team description can be edited and saved
|
||||
*/
|
||||
test('MM-67920 Edit team description and save changes', async ({pw}) => {
|
||||
// # Set up admin user and login
|
||||
const {adminUser, adminClient, team} = await pw.initSetup();
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
|
||||
// # Navigate to team
|
||||
await channelsPage.goto(team.name);
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Open Team Settings Modal
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
|
||||
// # Edit team description
|
||||
const newDescription = `Test description ${pw.random.id()}`;
|
||||
await teamSettings.infoSettings.updateDescription(newDescription);
|
||||
|
||||
// # Save changes
|
||||
await teamSettings.save();
|
||||
|
||||
// * Wait for "Settings saved" message
|
||||
await teamSettings.verifySavedMessage();
|
||||
|
||||
// * Verify description updated via API
|
||||
const updatedTeam = await adminClient.getTeam(team.id);
|
||||
expect(updatedTeam.description).toBe(newDescription);
|
||||
|
||||
// # Close modal
|
||||
await teamSettings.close();
|
||||
|
||||
// * Verify modal closes
|
||||
await expect(teamSettings.container).not.toBeVisible();
|
||||
});
|
||||
|
||||
/**
|
||||
* MM-67920 Upload and Remove team icon
|
||||
* @see MM-T391 (legacy Zephyr ID)
|
||||
* @objective Verify team icon can be uploaded and removed
|
||||
*/
|
||||
test('MM-67920 Upload and Remove team icon', async ({pw}) => {
|
||||
// # Set up admin user and login
|
||||
const {adminUser, adminClient, team} = await pw.initSetup();
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
|
||||
// # Navigate to team
|
||||
await channelsPage.goto(team.name);
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Open Team Settings Modal
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
const infoSettings = teamSettings.infoSettings;
|
||||
|
||||
// # Upload team icon using asset file
|
||||
await infoSettings.uploadIcon(getAsset('mattermost-icon_128x128.png'));
|
||||
|
||||
// * Verify upload preview shows
|
||||
await expect(infoSettings.teamIconImage).toBeVisible();
|
||||
|
||||
// * Verify remove button appears
|
||||
await expect(infoSettings.removeImageButton).toBeVisible();
|
||||
|
||||
// # Save changes
|
||||
await teamSettings.save();
|
||||
await teamSettings.verifySavedMessage();
|
||||
|
||||
// * Get team data after upload to verify icon exists via API
|
||||
const teamWithIcon = await adminClient.getTeam(team.id);
|
||||
expect(teamWithIcon.last_team_icon_update).toBeGreaterThan(0);
|
||||
|
||||
// # Close and reopen modal to verify persistence
|
||||
await teamSettings.close();
|
||||
await expect(teamSettings.container).not.toBeVisible();
|
||||
const teamSettings2 = await channelsPage.openTeamSettings();
|
||||
|
||||
// * Verify uploaded icon persists after reopening modal
|
||||
await expect(teamSettings2.infoSettings.teamIconImage).toBeVisible();
|
||||
await expect(teamSettings2.infoSettings.removeImageButton).toBeVisible();
|
||||
|
||||
// # Remove the icon
|
||||
await teamSettings2.infoSettings.removeIcon();
|
||||
|
||||
// * Verify icon was removed - check for default icon initials in modal
|
||||
await expect(teamSettings2.infoSettings.teamIconInitial).toBeVisible();
|
||||
|
||||
// * Verify icon was removed via API
|
||||
const teamAfterRemove = await adminClient.getTeam(team.id);
|
||||
expect(teamAfterRemove.last_team_icon_update || 0).toBe(0);
|
||||
|
||||
// # Close modal
|
||||
await teamSettings2.close();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,270 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/**
|
||||
* @objective E2E tests for the Membership Policies tab in Team Settings Modal
|
||||
* @reference MM-67669
|
||||
*/
|
||||
|
||||
import {ChannelsPage, expect, getAdminClient, getRandomId, test} from '@mattermost/playwright-lib';
|
||||
|
||||
import {
|
||||
enableABACConfig,
|
||||
ensureDepartmentAttribute,
|
||||
createParentPolicy,
|
||||
assignChannelsToPolicy,
|
||||
createPrivateChannel,
|
||||
createTeamAdmin,
|
||||
} from './helpers';
|
||||
|
||||
async function setupMembershipPoliciesTest(pw: any) {
|
||||
const {adminClient, adminUser} = await pw.getAdminClient();
|
||||
const suffix = getRandomId();
|
||||
const team = await adminClient.createTeam({
|
||||
name: `mp-${suffix}`,
|
||||
display_name: `MP ${suffix}`,
|
||||
type: 'O',
|
||||
});
|
||||
const user = await pw.createNewUserProfile(adminClient, {prefix: 'mp-user'});
|
||||
await adminClient.addToTeam(team.id, user.id);
|
||||
await adminClient.addToTeam(team.id, adminUser.id);
|
||||
return {adminClient, adminUser, team, user};
|
||||
}
|
||||
|
||||
test.describe('Team Settings Modal - Membership Policies Tab', () => {
|
||||
// Serial: several tests toggle ABAC; parallel runs in this file race the same server config.
|
||||
test.describe.configure({mode: 'serial'});
|
||||
|
||||
test.afterAll(async () => {
|
||||
try {
|
||||
const {adminClient} = await getAdminClient({skipLog: true});
|
||||
await adminClient.patchConfig({
|
||||
AccessControlSettings: {
|
||||
EnableAttributeBasedAccessControl: true,
|
||||
EnableUserManagedAttributes: true,
|
||||
},
|
||||
} as any);
|
||||
} catch {
|
||||
// Best-effort cleanup.
|
||||
}
|
||||
});
|
||||
|
||||
test('MM-67669_1 Membership Policies tab visible for admin with ABAC enabled', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminUser, adminClient, team} = await setupMembershipPoliciesTest(pw);
|
||||
await enableABACConfig(adminClient);
|
||||
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name);
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
|
||||
// * Both tabs visible
|
||||
await expect(teamSettings.accessPoliciesTab).toBeVisible();
|
||||
await expect(teamSettings.accessTab).toContainText('Access');
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
test('MM-67669_2 Membership Policies tab hidden when ABAC disabled', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminUser, adminClient, team} = await setupMembershipPoliciesTest(pw);
|
||||
const original = await adminClient.getConfig();
|
||||
const originalEnabled = original.AccessControlSettings?.EnableAttributeBasedAccessControl ?? false;
|
||||
|
||||
try {
|
||||
await adminClient.patchConfig({
|
||||
AccessControlSettings: {EnableAttributeBasedAccessControl: false},
|
||||
});
|
||||
await pw.waitUntil(async () => {
|
||||
const cfg = await adminClient.getConfig();
|
||||
return cfg.AccessControlSettings?.EnableAttributeBasedAccessControl === false;
|
||||
});
|
||||
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name);
|
||||
await channelsPage.toBeVisible();
|
||||
// Force a full navigation so the team settings bundle reads the latest
|
||||
// AccessControlSettings (WebSocket config updates can lag in CI).
|
||||
// Re-apply guard immediately before reload: a concurrent initSetup() may have
|
||||
// re-enabled ABAC between the waitUntil check above and here.
|
||||
await adminClient.patchConfig({
|
||||
AccessControlSettings: {EnableAttributeBasedAccessControl: false},
|
||||
});
|
||||
await page.reload();
|
||||
await page.waitForLoadState('networkidle');
|
||||
await channelsPage.toBeVisible();
|
||||
// Re-apply once more after the page has settled to prevent a WebSocket
|
||||
// CONFIG_CHANGED event (from a concurrent initSetup()) from flipping it back.
|
||||
await adminClient.patchConfig({
|
||||
AccessControlSettings: {EnableAttributeBasedAccessControl: false},
|
||||
});
|
||||
await pw.waitUntil(async () => {
|
||||
const cfg = await adminClient.getConfig();
|
||||
return cfg.AccessControlSettings?.EnableAttributeBasedAccessControl === false;
|
||||
});
|
||||
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
|
||||
// * Tab is not visible (WebSocket config update can lag)
|
||||
await expect(teamSettings.accessPoliciesTab).not.toBeVisible({timeout: 30000});
|
||||
|
||||
await teamSettings.close();
|
||||
} finally {
|
||||
await adminClient.patchConfig({
|
||||
AccessControlSettings: {EnableAttributeBasedAccessControl: originalEnabled},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('MM-67669_4 Empty state displayed when no policies exist', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminUser, adminClient, team} = await setupMembershipPoliciesTest(pw);
|
||||
await enableABACConfig(adminClient);
|
||||
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name);
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
await teamSettings.openAccessPoliciesTab();
|
||||
|
||||
// * Empty state shown
|
||||
await expect(teamSettings.container.getByText('No policies found')).toBeVisible();
|
||||
|
||||
// * Sync footer hidden when no policies exist
|
||||
await expect(teamSettings.container.locator('.SyncStatusFooter')).not.toBeVisible();
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
test('MM-67669_5 Policy list shows team-scoped policy with channel count', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminUser, adminClient, team} = await setupMembershipPoliciesTest(pw);
|
||||
await enableABACConfig(adminClient);
|
||||
await ensureDepartmentAttribute(adminClient);
|
||||
|
||||
// # Create policy with one channel assigned
|
||||
const channel = await createPrivateChannel(adminClient, team.id);
|
||||
const policy = await createParentPolicy(adminClient, `Team Policy ${Date.now()}`);
|
||||
await assignChannelsToPolicy(adminClient, policy.id, [channel.id]);
|
||||
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name);
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
await teamSettings.openAccessPoliciesTab();
|
||||
|
||||
// * Policy name and channel count shown
|
||||
await expect(teamSettings.container.getByText(policy.name)).toBeVisible();
|
||||
await expect(teamSettings.container.getByText('1 channel')).toBeVisible();
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
test('MM-67669_6 Cross-team policy not shown in team settings', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminUser, adminClient, team} = await setupMembershipPoliciesTest(pw);
|
||||
await enableABACConfig(adminClient);
|
||||
await ensureDepartmentAttribute(adminClient);
|
||||
|
||||
const otherTeam = await adminClient.createTeam({
|
||||
name: `other-${Date.now()}`,
|
||||
display_name: 'Other Team',
|
||||
type: 'O',
|
||||
} as any);
|
||||
const channel1 = await createPrivateChannel(adminClient, team.id);
|
||||
const channel2 = await createPrivateChannel(adminClient, otherTeam.id);
|
||||
|
||||
// # Policy spans two teams
|
||||
const crossPolicy = await createParentPolicy(adminClient, `Cross Policy ${Date.now()}`);
|
||||
await assignChannelsToPolicy(adminClient, crossPolicy.id, [channel1.id, channel2.id]);
|
||||
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name);
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
await teamSettings.openAccessPoliciesTab();
|
||||
|
||||
// * Cross-team policy not visible
|
||||
await expect(teamSettings.container.getByText(crossPolicy.name)).not.toBeVisible();
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
test('MM-67669_7 Team Admin sees Membership Policies tab and team-scoped policies', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminClient, team} = await setupMembershipPoliciesTest(pw);
|
||||
await enableABACConfig(adminClient);
|
||||
await ensureDepartmentAttribute(adminClient);
|
||||
|
||||
const channel = await createPrivateChannel(adminClient, team.id);
|
||||
const policy = await createParentPolicy(adminClient, `TA Policy ${Date.now()}`);
|
||||
await assignChannelsToPolicy(adminClient, policy.id, [channel.id]);
|
||||
|
||||
const teamAdmin = await createTeamAdmin(adminClient, team.id);
|
||||
|
||||
const {page} = await pw.testBrowser.login(teamAdmin);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name);
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
|
||||
// * Tab visible for Team Admin
|
||||
await expect(teamSettings.accessPoliciesTab).toBeVisible();
|
||||
|
||||
await teamSettings.openAccessPoliciesTab();
|
||||
|
||||
// * Policy and channel count shown
|
||||
await expect(teamSettings.container.getByText(policy.name)).toBeVisible();
|
||||
await expect(teamSettings.container.getByText('1 channel')).toBeVisible();
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
test('MM-67669_8 Team Admin does not see cross-team policies', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminClient, team} = await setupMembershipPoliciesTest(pw);
|
||||
await enableABACConfig(adminClient);
|
||||
await ensureDepartmentAttribute(adminClient);
|
||||
|
||||
const otherTeam = await adminClient.createTeam({
|
||||
name: `other-${Date.now()}`,
|
||||
display_name: 'Other Team',
|
||||
type: 'O',
|
||||
} as any);
|
||||
const channel1 = await createPrivateChannel(adminClient, team.id);
|
||||
const channel2 = await createPrivateChannel(adminClient, otherTeam.id);
|
||||
|
||||
const teamPolicy = await createParentPolicy(adminClient, `TA Visible ${Date.now()}`);
|
||||
await assignChannelsToPolicy(adminClient, teamPolicy.id, [channel1.id]);
|
||||
|
||||
const crossPolicy = await createParentPolicy(adminClient, `TA Hidden ${Date.now()}`);
|
||||
await assignChannelsToPolicy(adminClient, crossPolicy.id, [channel1.id, channel2.id]);
|
||||
|
||||
const teamAdmin = await createTeamAdmin(adminClient, team.id);
|
||||
|
||||
const {page} = await pw.testBrowser.login(teamAdmin);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name);
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
await teamSettings.openAccessPoliciesTab();
|
||||
|
||||
// * Single-team policy visible, cross-team policy not visible
|
||||
await expect(teamSettings.container.getByText(teamPolicy.name)).toBeVisible();
|
||||
await expect(teamSettings.container.getByText(crossPolicy.name)).not.toBeVisible();
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,925 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/**
|
||||
* @objective E2E tests for policy create/edit/delete in Team Settings Modal
|
||||
* @reference MM-67594
|
||||
*/
|
||||
|
||||
import {ChannelsPage, expect, newTestPassword, test} from '@mattermost/playwright-lib';
|
||||
|
||||
import {
|
||||
enableABACConfig,
|
||||
ensureDepartmentAttribute,
|
||||
createParentPolicy,
|
||||
assignChannelsToPolicy,
|
||||
unassignChannelsFromPolicy,
|
||||
createPrivateChannel,
|
||||
createTeamAdmin,
|
||||
setUserAttribute,
|
||||
addAttributeRule,
|
||||
addChannelToPolicy,
|
||||
} from './helpers';
|
||||
|
||||
test.describe('Team Settings Modal - Policy Editor', () => {
|
||||
test('MM-67594_1 Add policy button opens the editor view', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminClient, team} = await pw.initSetup();
|
||||
await enableABACConfig(adminClient);
|
||||
await ensureDepartmentAttribute(adminClient);
|
||||
|
||||
const teamAdmin = await createTeamAdmin(adminClient, team.id);
|
||||
|
||||
const {page} = await pw.testBrowser.login(teamAdmin);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name);
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
await teamSettings.openAccessPoliciesTab();
|
||||
|
||||
// # Click Add policy
|
||||
await teamSettings.container.getByRole('button', {name: 'Add policy'}).click();
|
||||
|
||||
// * Editor view shown with back button and name input
|
||||
await expect(teamSettings.container.locator('.TeamPolicyEditor__back-btn')).toBeVisible();
|
||||
await expect(teamSettings.container.locator('#input_policyName')).toBeVisible();
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
test('MM-67594_2 Team Admin creates a new policy', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminClient, team} = await pw.initSetup();
|
||||
await enableABACConfig(adminClient);
|
||||
await ensureDepartmentAttribute(adminClient);
|
||||
|
||||
// # Create private channel and team admin with Department=Engineering
|
||||
const channel = await createPrivateChannel(adminClient, team.id);
|
||||
const teamAdmin = await createTeamAdmin(adminClient, team.id);
|
||||
await adminClient.addToChannel(teamAdmin.id, channel.id);
|
||||
await setUserAttribute(adminClient, teamAdmin.id, 'Department', 'Engineering');
|
||||
|
||||
const {page} = await pw.testBrowser.login(teamAdmin);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name);
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
await teamSettings.openAccessPoliciesTab();
|
||||
|
||||
// # Click Add policy
|
||||
await teamSettings.container.getByRole('button', {name: 'Add policy'}).click();
|
||||
|
||||
// # Fill policy name
|
||||
const policyName = `TA Policy ${Date.now()}`;
|
||||
await teamSettings.container.locator('#input_policyName').fill(policyName);
|
||||
|
||||
// # Add a rule row and fill in a value
|
||||
await addAttributeRule(teamSettings.container, page, 'Engineering');
|
||||
|
||||
// # Add channel via channel selector
|
||||
await addChannelToPolicy(teamSettings.container, page, channel.display_name);
|
||||
|
||||
// * Confirm the channel appears in the editor list before saving
|
||||
await expect(teamSettings.container.getByText(channel.display_name)).toBeVisible({timeout: 10000});
|
||||
|
||||
// Re-apply guard: a concurrent initSetup() on another shard may have disabled ABAC
|
||||
// between the initial enableABACConfig call and this save. Without ABAC enabled the
|
||||
// server may not create the policy and the confirmation modal will never appear.
|
||||
await enableABACConfig(adminClient);
|
||||
|
||||
// # Save via SaveChangesPanel — wait for button to be enabled (form fully dirty).
|
||||
const saveBtn = teamSettings.container.locator('[data-testid="SaveChangesPanel__save-btn"]');
|
||||
await expect(saveBtn).toBeEnabled({timeout: 20000});
|
||||
await saveBtn.click();
|
||||
|
||||
// Re-apply guard post-click: a concurrent initSetup() reset between the guard above
|
||||
// and the server processing the save request causes the confirmation modal to skip.
|
||||
await enableABACConfig(adminClient);
|
||||
|
||||
// # Confirm in PolicyConfirmationModal
|
||||
await page.locator('.TeamPolicyConfirmationModal').waitFor({timeout: 30000});
|
||||
await page.getByRole('button', {name: /Apply policy/}).click();
|
||||
|
||||
// * Auto-navigated back to list, policy name visible.
|
||||
await expect(teamSettings.container.getByText(policyName)).toBeVisible({timeout: 15000});
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
test('MM-67594_3 Team Admin edits existing policy via row click', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminClient, team} = await pw.initSetup();
|
||||
await enableABACConfig(adminClient);
|
||||
await ensureDepartmentAttribute(adminClient);
|
||||
|
||||
const channel = await createPrivateChannel(adminClient, team.id);
|
||||
const policy = await createParentPolicy(adminClient, `TA Edit ${Date.now()}`);
|
||||
await assignChannelsToPolicy(adminClient, policy.id, [channel.id]);
|
||||
|
||||
const teamAdmin = await createTeamAdmin(adminClient, team.id);
|
||||
|
||||
const {page} = await pw.testBrowser.login(teamAdmin);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name);
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
await teamSettings.openAccessPoliciesTab();
|
||||
|
||||
// # Click policy row
|
||||
await teamSettings.container.getByText(policy.name).click();
|
||||
|
||||
// * Editor shown with pre-populated name
|
||||
await expect(teamSettings.container.locator('#input_policyName')).toHaveValue(policy.name);
|
||||
|
||||
// # Modify name
|
||||
const newName = `TA Updated ${Date.now()}`;
|
||||
await teamSettings.container.locator('#input_policyName').clear();
|
||||
await teamSettings.container.locator('#input_policyName').fill(newName);
|
||||
|
||||
// # Save via SaveChangesPanel (name-only change skips confirmation modal)
|
||||
await teamSettings.container.locator('[data-testid="SaveChangesPanel__save-btn"]').click();
|
||||
|
||||
// * Auto-navigated back to list, updated name visible
|
||||
await expect(teamSettings.container.getByText(newName)).toBeVisible();
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
test('MM-67594_4 Team Admin uses three-dot Edit menu', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminClient, team} = await pw.initSetup();
|
||||
await enableABACConfig(adminClient);
|
||||
await ensureDepartmentAttribute(adminClient);
|
||||
|
||||
const channel = await createPrivateChannel(adminClient, team.id);
|
||||
const policy = await createParentPolicy(adminClient, `TA Menu ${Date.now()}`);
|
||||
await assignChannelsToPolicy(adminClient, policy.id, [channel.id]);
|
||||
|
||||
const teamAdmin = await createTeamAdmin(adminClient, team.id);
|
||||
|
||||
const {page} = await pw.testBrowser.login(teamAdmin);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name);
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
await teamSettings.openAccessPoliciesTab();
|
||||
|
||||
// # Click three-dot menu then Edit
|
||||
const menuButton = teamSettings.container.locator(`button[id="policy-menu-${policy.id}"]`);
|
||||
await menuButton.click();
|
||||
await page.getByRole('menuitem', {name: 'Edit'}).click();
|
||||
|
||||
// * Editor shown with pre-populated name
|
||||
await expect(teamSettings.container.locator('#input_policyName')).toHaveValue(policy.name);
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
test('MM-67594_5 Undo discards changes', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminClient, team} = await pw.initSetup();
|
||||
await enableABACConfig(adminClient);
|
||||
await ensureDepartmentAttribute(adminClient);
|
||||
|
||||
const channel = await createPrivateChannel(adminClient, team.id);
|
||||
const policy = await createParentPolicy(adminClient, `TA Undo ${Date.now()}`);
|
||||
await assignChannelsToPolicy(adminClient, policy.id, [channel.id]);
|
||||
|
||||
const teamAdmin = await createTeamAdmin(adminClient, team.id);
|
||||
|
||||
const {page} = await pw.testBrowser.login(teamAdmin);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name);
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
await teamSettings.openAccessPoliciesTab();
|
||||
|
||||
// # Open editor and modify name
|
||||
await teamSettings.container.getByText(policy.name).click();
|
||||
|
||||
await teamSettings.container.locator('#input_policyName').clear();
|
||||
await teamSettings.container.locator('#input_policyName').fill('Changed Name');
|
||||
|
||||
// # Click Undo in SaveChangesPanel
|
||||
await teamSettings.container.locator('[data-testid="SaveChangesPanel__cancel-btn"]').click();
|
||||
|
||||
// * Name reverted to original
|
||||
await expect(teamSettings.container.locator('#input_policyName')).toHaveValue(policy.name);
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
test('MM-67594_5b Back button then Undo navigates back to list', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminClient, team} = await pw.initSetup();
|
||||
await enableABACConfig(adminClient);
|
||||
await ensureDepartmentAttribute(adminClient);
|
||||
|
||||
const channel = await createPrivateChannel(adminClient, team.id);
|
||||
const policy = await createParentPolicy(adminClient, `TA BackUndo ${Date.now()}`);
|
||||
await assignChannelsToPolicy(adminClient, policy.id, [channel.id]);
|
||||
|
||||
const teamAdmin = await createTeamAdmin(adminClient, team.id);
|
||||
|
||||
const {page} = await pw.testBrowser.login(teamAdmin);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name);
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
await teamSettings.openAccessPoliciesTab();
|
||||
|
||||
// # Open editor and make a change so the SaveChangesPanel is shown
|
||||
await teamSettings.container.getByText(policy.name).click();
|
||||
await teamSettings.container.locator('#input_policyName').fill('Changed Name');
|
||||
|
||||
// # Click the back button — sets navigation intent but stays in editor because of unsaved changes
|
||||
await teamSettings.container.locator('.TeamPolicyEditor__back-btn').click();
|
||||
|
||||
// # Click Undo — should revert changes AND navigate back to the list.
|
||||
// This validates the fix for the bug where the navigation intent expired after 3 seconds,
|
||||
// causing Undo to revert changes but leave the user stranded in the editor.
|
||||
await teamSettings.container.locator('[data-testid="SaveChangesPanel__cancel-btn"]').click();
|
||||
|
||||
// * List view restored (back navigation happened)
|
||||
await expect(teamSettings.container.getByRole('button', {name: 'Add policy'})).toBeVisible({timeout: 10000});
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
test('MM-67594_6 Back button returns to list', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminClient, team} = await pw.initSetup();
|
||||
await enableABACConfig(adminClient);
|
||||
await ensureDepartmentAttribute(adminClient);
|
||||
|
||||
const teamAdmin = await createTeamAdmin(adminClient, team.id);
|
||||
|
||||
const {page} = await pw.testBrowser.login(teamAdmin);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name);
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
await teamSettings.openAccessPoliciesTab();
|
||||
|
||||
// # Open editor then click back
|
||||
await teamSettings.container.getByRole('button', {name: 'Add policy'}).click();
|
||||
await teamSettings.container.locator('.TeamPolicyEditor__back-btn').click();
|
||||
|
||||
// * List view restored
|
||||
await expect(teamSettings.container.getByRole('button', {name: 'Add policy'})).toBeVisible();
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
test('MM-67594_7 Delete policy from editor view', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminUser, adminClient, team} = await pw.initSetup();
|
||||
await enableABACConfig(adminClient);
|
||||
await ensureDepartmentAttribute(adminClient);
|
||||
|
||||
const channel = await createPrivateChannel(adminClient, team.id);
|
||||
const policy = await createParentPolicy(adminClient, `Delete Test ${Date.now()}`);
|
||||
await assignChannelsToPolicy(adminClient, policy.id, [channel.id]);
|
||||
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name);
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
await teamSettings.openAccessPoliciesTab();
|
||||
|
||||
// # Open editor
|
||||
await teamSettings.container.getByText(policy.name).click();
|
||||
|
||||
// # Remove the channel to enable delete (click Remove link in channel list)
|
||||
await teamSettings.container.getByText('Remove').first().click();
|
||||
|
||||
// # Click Delete in the delete section
|
||||
await teamSettings.container
|
||||
.locator('.TeamPolicyEditor__section--delete button')
|
||||
.filter({hasText: 'Delete'})
|
||||
.click();
|
||||
|
||||
// # Confirm in delete confirmation modal
|
||||
await page.locator('.TeamPolicyEditor__delete-modal').waitFor();
|
||||
await page.locator('.TeamPolicyEditor__delete-modal').getByRole('button', {name: 'Delete'}).click();
|
||||
|
||||
// * Back to list, policy removed
|
||||
await expect(teamSettings.container.getByText(policy.name)).not.toBeVisible();
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
// Delete action is hidden in the team settings three-dot menu (all listed policies have channels).
|
||||
|
||||
test('MM-67594_8 Save without name shows validation error', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminClient, team} = await pw.initSetup();
|
||||
await enableABACConfig(adminClient);
|
||||
await ensureDepartmentAttribute(adminClient);
|
||||
|
||||
const channel = await createPrivateChannel(adminClient, team.id);
|
||||
const teamAdmin = await createTeamAdmin(adminClient, team.id);
|
||||
await adminClient.addToChannel(teamAdmin.id, channel.id);
|
||||
await setUserAttribute(adminClient, teamAdmin.id, 'Department', 'Engineering');
|
||||
|
||||
const {page} = await pw.testBrowser.login(teamAdmin);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name);
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
await teamSettings.openAccessPoliciesTab();
|
||||
|
||||
// # Open create editor, add rule + channel but leave name empty
|
||||
await teamSettings.container.getByRole('button', {name: 'Add policy'}).click();
|
||||
await addAttributeRule(teamSettings.container, page, 'Engineering');
|
||||
await addChannelToPolicy(teamSettings.container, page, channel.display_name);
|
||||
|
||||
// # Click Save
|
||||
await teamSettings.container.locator('[data-testid="SaveChangesPanel__save-btn"]').click();
|
||||
|
||||
// * Error state shown in SaveChangesPanel (name required)
|
||||
await expect(teamSettings.container.locator('.SaveChangesPanel.error')).toBeVisible();
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
test('MM-67594_10 Save without channels shows validation error', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminClient, team} = await pw.initSetup();
|
||||
await enableABACConfig(adminClient);
|
||||
await ensureDepartmentAttribute(adminClient);
|
||||
|
||||
const teamAdmin = await createTeamAdmin(adminClient, team.id);
|
||||
await setUserAttribute(adminClient, teamAdmin.id, 'Department', 'Engineering');
|
||||
|
||||
const {page} = await pw.testBrowser.login(teamAdmin);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name);
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
await teamSettings.openAccessPoliciesTab();
|
||||
|
||||
// # Open create editor, add name + rule but no channels
|
||||
await teamSettings.container.getByRole('button', {name: 'Add policy'}).click();
|
||||
await teamSettings.container.locator('#input_policyName').fill(`No Channels ${Date.now()}`);
|
||||
await addAttributeRule(teamSettings.container, page, 'Engineering');
|
||||
|
||||
// # Click Save
|
||||
await teamSettings.container.locator('[data-testid="SaveChangesPanel__save-btn"]').click();
|
||||
|
||||
// * Error state shown in SaveChangesPanel (channels required)
|
||||
await expect(teamSettings.container.locator('.SaveChangesPanel.error')).toBeVisible();
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
test('MM-67594_11 Delete button disabled when policy has channels', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminClient, team} = await pw.initSetup();
|
||||
await enableABACConfig(adminClient);
|
||||
await ensureDepartmentAttribute(adminClient);
|
||||
|
||||
const channel = await createPrivateChannel(adminClient, team.id);
|
||||
const policy = await createParentPolicy(adminClient, `TA HasChannels ${Date.now()}`);
|
||||
await assignChannelsToPolicy(adminClient, policy.id, [channel.id]);
|
||||
|
||||
const teamAdmin = await createTeamAdmin(adminClient, team.id);
|
||||
|
||||
const {page} = await pw.testBrowser.login(teamAdmin);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name);
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
await teamSettings.openAccessPoliciesTab();
|
||||
|
||||
// # Open editor
|
||||
await teamSettings.container.getByText(policy.name).click();
|
||||
|
||||
// * Delete button is disabled (policy has channels)
|
||||
const deleteBtn = teamSettings.container
|
||||
.locator('.TeamPolicyEditor__section--delete button')
|
||||
.filter({hasText: 'Delete'});
|
||||
await expect(deleteBtn).toBeDisabled();
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
test('MM-67594_11b Add channels modal excludes channels already assigned to the policy', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminClient, team} = await pw.initSetup();
|
||||
await enableABACConfig(adminClient);
|
||||
await ensureDepartmentAttribute(adminClient);
|
||||
|
||||
// # Create two channels and assign one to the policy via API
|
||||
const assignedChannel = await createPrivateChannel(adminClient, team.id);
|
||||
const unassignedChannel = await createPrivateChannel(adminClient, team.id);
|
||||
const policy = await createParentPolicy(adminClient, `Duplicate Test ${Date.now()}`);
|
||||
await assignChannelsToPolicy(adminClient, policy.id, [assignedChannel.id]);
|
||||
|
||||
const teamAdmin = await createTeamAdmin(adminClient, team.id);
|
||||
await adminClient.addToChannel(teamAdmin.id, assignedChannel.id);
|
||||
await adminClient.addToChannel(teamAdmin.id, unassignedChannel.id);
|
||||
|
||||
const {page} = await pw.testBrowser.login(teamAdmin);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name);
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
await teamSettings.openAccessPoliciesTab();
|
||||
|
||||
// # Open the existing policy editor
|
||||
await teamSettings.container.getByText(policy.name).click();
|
||||
await expect(teamSettings.container.locator('#input_policyName')).toBeVisible({timeout: 10000});
|
||||
|
||||
// # Open Add channels modal
|
||||
await teamSettings.container.getByRole('button', {name: /Add channels/}).click();
|
||||
const channelModal = page.locator('.channel-selector-modal');
|
||||
await channelModal.waitFor();
|
||||
await expect(channelModal.locator('.more-modal__row').first()).toBeVisible({timeout: 10000});
|
||||
|
||||
// * Already assigned channel is NOT shown in the modal
|
||||
await expect(
|
||||
channelModal.locator('.more-modal__row').filter({hasText: assignedChannel.display_name}),
|
||||
).not.toBeVisible();
|
||||
|
||||
// * Unassigned channel IS shown in the modal
|
||||
await expect(
|
||||
channelModal.locator('.more-modal__row').filter({hasText: unassignedChannel.display_name}),
|
||||
).toBeVisible();
|
||||
|
||||
await page.keyboard.press('Escape');
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
test('MM-67594_12 Success message shown after saving policy', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminClient, team} = await pw.initSetup();
|
||||
await enableABACConfig(adminClient);
|
||||
await ensureDepartmentAttribute(adminClient);
|
||||
|
||||
const channel = await createPrivateChannel(adminClient, team.id);
|
||||
const policy = await createParentPolicy(adminClient, `Toast Test ${Date.now()}`);
|
||||
await assignChannelsToPolicy(adminClient, policy.id, [channel.id]);
|
||||
|
||||
const teamAdmin = await createTeamAdmin(adminClient, team.id);
|
||||
|
||||
const {page} = await pw.testBrowser.login(teamAdmin);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name);
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
await teamSettings.openAccessPoliciesTab();
|
||||
|
||||
// # Edit policy name (name-only change — no confirmation modal)
|
||||
await teamSettings.container.getByText(policy.name).click();
|
||||
|
||||
await teamSettings.container.locator('#input_policyName').clear();
|
||||
await teamSettings.container.locator('#input_policyName').fill(`Updated ${Date.now()}`);
|
||||
|
||||
// # Save
|
||||
await teamSettings.container.locator('[data-testid="SaveChangesPanel__save-btn"]').click();
|
||||
|
||||
// * Success message visible on list view
|
||||
await expect(teamSettings.container.locator('.SaveChangesPanel.saved')).toBeVisible();
|
||||
await expect(teamSettings.container.getByText('Policy updated')).toBeVisible();
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
test('MM-67594_13 System Admin can also create policy from Team Settings', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminUser, adminClient, team} = await pw.initSetup();
|
||||
await enableABACConfig(adminClient);
|
||||
await pw.waitUntil(async () => {
|
||||
const cfg = await adminClient.getConfig();
|
||||
return cfg.AccessControlSettings?.EnableAttributeBasedAccessControl === true;
|
||||
});
|
||||
await ensureDepartmentAttribute(adminClient);
|
||||
|
||||
// # Create private channel and set admin's Department attribute
|
||||
const channel = await createPrivateChannel(adminClient, team.id);
|
||||
await setUserAttribute(adminClient, adminUser.id, 'Department', 'Engineering');
|
||||
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
// # Navigate and wait for all API calls to settle (custom profile attributes
|
||||
// must be fetched before the self-inclusion check can validate the admin's Department)
|
||||
await channelsPage.goto(team.name);
|
||||
await channelsPage.toBeVisible();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
await teamSettings.openAccessPoliciesTab();
|
||||
|
||||
// # Create policy
|
||||
await teamSettings.container.getByRole('button', {name: 'Add policy'}).click();
|
||||
|
||||
const policyName = `SysAdmin Policy ${Date.now()}`;
|
||||
await teamSettings.container.locator('#input_policyName').fill(policyName);
|
||||
|
||||
// # Add a rule row and fill in a value
|
||||
await addAttributeRule(teamSettings.container, page, 'Engineering');
|
||||
|
||||
// # Add channel via channel selector
|
||||
await addChannelToPolicy(teamSettings.container, page, channel.display_name);
|
||||
|
||||
// * Confirm the channel appears in the editor list before saving
|
||||
await expect(teamSettings.container.getByText(channel.display_name)).toBeVisible({timeout: 10000});
|
||||
|
||||
// # Save via SaveChangesPanel — wait for button to be enabled (form fully dirty)
|
||||
const saveBtn = teamSettings.container.locator('[data-testid="SaveChangesPanel__save-btn"]');
|
||||
await expect(saveBtn).toBeEnabled({timeout: 10000});
|
||||
// Re-apply guard: concurrent initSetup() may reset ABAC between setup and save
|
||||
await enableABACConfig(adminClient);
|
||||
await saveBtn.click();
|
||||
|
||||
// Re-apply guard post-click: a concurrent initSetup() reset between the guard above
|
||||
// and the server processing the save request causes the confirmation modal to skip.
|
||||
await enableABACConfig(adminClient);
|
||||
|
||||
// # Confirm in PolicyConfirmationModal
|
||||
await page.locator('.TeamPolicyConfirmationModal').waitFor({timeout: 30000});
|
||||
await page.getByRole('button', {name: /Apply policy/}).click();
|
||||
|
||||
// * Auto-navigated back to list, policy appears
|
||||
await expect(teamSettings.container.getByText(policyName)).toBeVisible();
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
test('MM-67967_1 Team admin can trigger sync and see updated status', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminClient, team} = await pw.initSetup();
|
||||
await enableABACConfig(adminClient);
|
||||
await ensureDepartmentAttribute(adminClient);
|
||||
|
||||
const channel = await createPrivateChannel(adminClient, team.id);
|
||||
const policy = await createParentPolicy(adminClient, `Sync Test ${Date.now()}`);
|
||||
await assignChannelsToPolicy(adminClient, policy.id, [channel.id]);
|
||||
|
||||
const teamAdmin = await createTeamAdmin(adminClient, team.id);
|
||||
|
||||
const {page} = await pw.testBrowser.login(teamAdmin);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name);
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
await teamSettings.openAccessPoliciesTab();
|
||||
|
||||
// initSetup() on another worker can disable ABAC — without it the sync footer never completes reliably.
|
||||
await enableABACConfig(adminClient);
|
||||
await pw.waitUntil(async () => {
|
||||
const cfg = await adminClient.getConfig();
|
||||
return cfg.AccessControlSettings?.EnableAttributeBasedAccessControl === true;
|
||||
});
|
||||
|
||||
// * Footer visible with "Sync now" action
|
||||
const footer = teamSettings.container.locator('.SyncStatusFooter');
|
||||
await expect(footer).toBeVisible({timeout: 10000});
|
||||
await expect(teamSettings.container.getByText(/Sync now/)).toBeVisible();
|
||||
|
||||
// # Click Sync now
|
||||
await teamSettings.container.getByText(/Sync now/).click();
|
||||
|
||||
// * Syncing state appears
|
||||
await expect(teamSettings.container.getByText(/Syncing/)).toBeVisible({timeout: 5000});
|
||||
|
||||
// * Wait for sync to complete and "Sync now" to reappear
|
||||
await expect(teamSettings.container.getByText(/Sync now/)).toBeVisible({timeout: 90000});
|
||||
|
||||
// * Status updates to "Last synced just now" confirming a fresh sync completed
|
||||
await expect(teamSettings.container.getByText(/Last synced just now/)).toBeVisible({timeout: 30000});
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
test('MM-67967_2 System admin can trigger sync from team settings', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminUser, adminClient, team} = await pw.initSetup();
|
||||
await enableABACConfig(adminClient);
|
||||
await pw.waitUntil(async () => {
|
||||
const cfg = await adminClient.getConfig();
|
||||
return cfg.AccessControlSettings?.EnableAttributeBasedAccessControl === true;
|
||||
});
|
||||
await ensureDepartmentAttribute(adminClient);
|
||||
|
||||
const channel = await createPrivateChannel(adminClient, team.id);
|
||||
const policy = await createParentPolicy(adminClient, `SysAdmin Sync ${Date.now()}`);
|
||||
await assignChannelsToPolicy(adminClient, policy.id, [channel.id]);
|
||||
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name);
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
await teamSettings.openAccessPoliciesTab();
|
||||
|
||||
// * Footer visible with "Sync now" action
|
||||
await expect(teamSettings.container.getByText(/Sync now/)).toBeVisible({timeout: 15000});
|
||||
|
||||
// # Click Sync now
|
||||
await teamSettings.container.getByText(/Sync now/).click();
|
||||
|
||||
// * Wait for sync to complete and "Sync now" to reappear
|
||||
await expect(teamSettings.container.getByText(/Sync now/)).toBeVisible({timeout: 30000});
|
||||
|
||||
// * Status updates to "Last synced just now" confirming a fresh sync completed
|
||||
await expect(teamSettings.container.getByText(/Last synced just now/)).toBeVisible();
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
test('MM-67594_1 Team admin can delete policy after removing all channels', async ({pw}) => {
|
||||
// Scenario: Policy created via API → team admin opens editor → removes channel → deletes.
|
||||
// Validates that the scope field persists team ownership through channel removal.
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminClient, team} = await pw.initSetup();
|
||||
await enableABACConfig(adminClient);
|
||||
await ensureDepartmentAttribute(adminClient);
|
||||
|
||||
// # Setup: create channel, policy, and assign via API (fast, non-flaky)
|
||||
const channel = await createPrivateChannel(adminClient, team.id);
|
||||
const policyName = `Scope Delete ${Date.now()}`;
|
||||
const policy = await createParentPolicy(adminClient, policyName);
|
||||
await assignChannelsToPolicy(adminClient, policy.id, [channel.id]);
|
||||
|
||||
// # Create team admin and add to channel so they can see the policy
|
||||
const teamAdmin = await createTeamAdmin(adminClient, team.id);
|
||||
await setUserAttribute(adminClient, teamAdmin.id, 'Department', 'Engineering');
|
||||
await adminClient.addToChannel(teamAdmin.id, channel.id);
|
||||
|
||||
// # Team admin logs in and opens team settings
|
||||
const {page} = await pw.testBrowser.login(teamAdmin);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name);
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
await teamSettings.openAccessPoliciesTab();
|
||||
|
||||
// * Policy is visible
|
||||
await expect(teamSettings.container.getByText(policyName)).toBeVisible({timeout: 10000});
|
||||
|
||||
// # Open editor by clicking policy row
|
||||
await teamSettings.container.getByText(policyName).click();
|
||||
|
||||
// # Remove the channel to enable delete
|
||||
const removeLink = teamSettings.container.getByText('Remove').first();
|
||||
await expect(removeLink).toBeVisible({timeout: 10000});
|
||||
await removeLink.click();
|
||||
|
||||
// # Click Delete in the delete section
|
||||
const deleteBtn = teamSettings.container
|
||||
.locator('.TeamPolicyEditor__section--delete button')
|
||||
.filter({hasText: 'Delete'});
|
||||
await expect(deleteBtn).toBeEnabled({timeout: 10000});
|
||||
await deleteBtn.click();
|
||||
|
||||
// # Confirm deletion
|
||||
const deleteModal = page.locator('.TeamPolicyEditor__delete-modal');
|
||||
await expect(deleteModal).toBeVisible({timeout: 10000});
|
||||
await deleteModal.getByRole('button', {name: 'Delete'}).click();
|
||||
|
||||
// * Back to list, policy is removed
|
||||
await expect(teamSettings.container.getByText(policyName)).not.toBeVisible();
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
test('MM-67594_2 System admin cross-team channel changes toggle team admin visibility', async ({pw}) => {
|
||||
// Scenario: System admin creates policy with team A channels → team admin A sees it →
|
||||
// system admin adds team B channel (cross-team) → team admin A no longer sees it →
|
||||
// system admin removes team B channel → team admin A sees it again.
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminClient, team} = await pw.initSetup();
|
||||
await enableABACConfig(adminClient);
|
||||
await ensureDepartmentAttribute(adminClient);
|
||||
|
||||
// # Create a second team
|
||||
const otherTeam = await adminClient.createTeam({
|
||||
name: `other-${Date.now()}`,
|
||||
display_name: 'Other Team',
|
||||
type: 'O',
|
||||
} as any);
|
||||
|
||||
// # Create team admin for team A with Department attribute
|
||||
const teamAdmin = await createTeamAdmin(adminClient, team.id);
|
||||
await setUserAttribute(adminClient, teamAdmin.id, 'Department', 'Engineering');
|
||||
|
||||
// # Create private channels in both teams
|
||||
const channelA = await createPrivateChannel(adminClient, team.id);
|
||||
const channelB = await createPrivateChannel(adminClient, otherTeam.id);
|
||||
await adminClient.addToChannel(teamAdmin.id, channelA.id);
|
||||
|
||||
// # System admin creates policy and assigns team A channel
|
||||
const policyName = `Cross-Team Scope Test ${Date.now()}`;
|
||||
const policy = await createParentPolicy(adminClient, policyName);
|
||||
await assignChannelsToPolicy(adminClient, policy.id, [channelA.id]);
|
||||
|
||||
// # Team admin logs in and opens team settings
|
||||
const {page} = await pw.testBrowser.login(teamAdmin);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name);
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Step 1: Team admin can see the policy (all channels in their team)
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
await teamSettings.openAccessPoliciesTab();
|
||||
|
||||
await expect(teamSettings.container.getByText(policyName)).toBeVisible({timeout: 10000});
|
||||
await teamSettings.close();
|
||||
|
||||
// # Step 2: System admin adds a channel from team B (cross-team)
|
||||
await assignChannelsToPolicy(adminClient, policy.id, [channelB.id]);
|
||||
|
||||
// # Team admin reopens team settings — policy should NOT be visible (cross-team)
|
||||
const teamSettings2 = await channelsPage.openTeamSettings();
|
||||
await teamSettings2.openAccessPoliciesTab();
|
||||
|
||||
await expect(teamSettings2.container.getByText(policyName)).not.toBeVisible({timeout: 10000});
|
||||
await teamSettings2.close();
|
||||
|
||||
// # Step 3: System admin removes team B channel (back to single-team)
|
||||
await unassignChannelsFromPolicy(adminClient, policy.id, [channelB.id]);
|
||||
|
||||
// # Team admin reopens team settings — policy should be visible again
|
||||
const teamSettings3 = await channelsPage.openTeamSettings();
|
||||
await teamSettings3.openAccessPoliciesTab();
|
||||
|
||||
await expect(teamSettings3.container.getByText(policyName)).toBeVisible({timeout: 10000});
|
||||
|
||||
await teamSettings3.close();
|
||||
});
|
||||
|
||||
test.fixme('MM-67594_14 Add channels modal shows only private member channels even when team has >50 public channels', async ({
|
||||
pw,
|
||||
}) => {
|
||||
// Regression: the non-sysConsole fast path previously called AutocompleteChannelsForTeam
|
||||
// which ignored the private=true filter and returned a mixed set capped at 50.
|
||||
// With >50 public channels, private channels were cut off before client-side filtering.
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminClient, team} = await pw.initSetup();
|
||||
await enableABACConfig(adminClient);
|
||||
await ensureDepartmentAttribute(adminClient);
|
||||
|
||||
const teamAdmin = await createTeamAdmin(adminClient, team.id);
|
||||
await setUserAttribute(adminClient, teamAdmin.id, 'Department', 'Engineering');
|
||||
|
||||
// # Create 55 public channels — more than the 50-result autocomplete cap
|
||||
for (let i = 0; i < 55; i++) {
|
||||
const pub = await adminClient.createPublicChannel(team.id);
|
||||
await adminClient.addToChannel(teamAdmin.id, pub.id);
|
||||
}
|
||||
|
||||
// # Create 2 private channels the team admin is a member of
|
||||
const privateChannel1 = await createPrivateChannel(adminClient, team.id);
|
||||
const privateChannel2 = await createPrivateChannel(adminClient, team.id);
|
||||
await adminClient.addToChannel(teamAdmin.id, privateChannel1.id);
|
||||
await adminClient.addToChannel(teamAdmin.id, privateChannel2.id);
|
||||
|
||||
// # Create a private channel, add the team admin, then make it group-constrained.
|
||||
// Membership must be established before the constraint is set — the API rejects
|
||||
// addToChannel on an already-constrained channel.
|
||||
const gcChannel = await createPrivateChannel(adminClient, team.id);
|
||||
await adminClient.addToChannel(teamAdmin.id, gcChannel.id);
|
||||
await adminClient.patchChannel(gcChannel.id, {group_constrained: true} as any);
|
||||
|
||||
const {page} = await pw.testBrowser.login(teamAdmin);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name);
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
await teamSettings.openAccessPoliciesTab();
|
||||
|
||||
// # Open the policy editor and click Add channels
|
||||
await teamSettings.container.getByRole('button', {name: 'Add policy'}).click();
|
||||
await expect(teamSettings.container.locator('#input_policyName')).toBeVisible({timeout: 10000});
|
||||
await teamSettings.container.getByRole('button', {name: /Add channels/}).click();
|
||||
|
||||
const channelModal = page.locator('.channel-selector-modal');
|
||||
await channelModal.waitFor();
|
||||
await expect(channelModal.locator('.more-modal__row').first()).toBeVisible({timeout: 10000});
|
||||
|
||||
// * Both private channels appear despite 55 public channels exceeding the cap
|
||||
await expect(
|
||||
channelModal.locator('.more-modal__row').filter({hasText: privateChannel1.display_name}),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
channelModal.locator('.more-modal__row').filter({hasText: privateChannel2.display_name}),
|
||||
).toBeVisible();
|
||||
|
||||
// * Group-constrained channel does not appear
|
||||
await expect(
|
||||
channelModal.locator('.more-modal__row').filter({hasText: gcChannel.display_name}),
|
||||
).not.toBeVisible();
|
||||
|
||||
await page.keyboard.press('Escape');
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
test('MM-67920_sync_1 Sync job created on policy save adds matching user to channel', async ({pw}) => {
|
||||
/**
|
||||
* Regression test for the bug where a team admin creating a policy and selecting
|
||||
* "Apply policy" (add users immediately) would receive a 403 because the job
|
||||
* creation endpoint required manage_system when policy_id was present.
|
||||
*
|
||||
* Verifies the full end-to-end fix: policy creation → sync job creation → user added.
|
||||
*/
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminClient, team} = await pw.initSetup();
|
||||
await enableABACConfig(adminClient);
|
||||
await ensureDepartmentAttribute(adminClient);
|
||||
|
||||
// # Create channel — target user should be added here by the sync
|
||||
const channel = await createPrivateChannel(adminClient, team.id);
|
||||
|
||||
// # Create team admin with Department=Engineering and add to channel (self-inclusion check)
|
||||
const teamAdmin = await createTeamAdmin(adminClient, team.id);
|
||||
await setUserAttribute(adminClient, teamAdmin.id, 'Department', 'Engineering');
|
||||
await adminClient.addToChannel(teamAdmin.id, channel.id);
|
||||
|
||||
// # Create target user: team member with Department=Engineering but NOT in channel yet
|
||||
const id = Date.now().toString(36) + Math.random().toString(36).substring(2, 7);
|
||||
const targetUser = await adminClient.createUser(
|
||||
{
|
||||
email: `target-${id}@sample.mattermost.com`,
|
||||
username: `target${id}`,
|
||||
password: newTestPassword(),
|
||||
} as any,
|
||||
'',
|
||||
'',
|
||||
);
|
||||
await adminClient.addToTeam(team.id, targetUser.id);
|
||||
await setUserAttribute(adminClient, targetUser.id, 'Department', 'Engineering');
|
||||
|
||||
// # Log in as team admin and open policy editor
|
||||
const {page} = await pw.testBrowser.login(teamAdmin);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name);
|
||||
await channelsPage.toBeVisible();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
await teamSettings.openAccessPoliciesTab();
|
||||
|
||||
await teamSettings.container.getByRole('button', {name: 'Add policy'}).click();
|
||||
|
||||
// # Fill policy name and add attribute rule
|
||||
const policyName = `Sync Bug Fix ${Date.now()}`;
|
||||
await teamSettings.container.locator('#input_policyName').fill(policyName);
|
||||
await addAttributeRule(teamSettings.container, page, 'Engineering');
|
||||
|
||||
// # Add channel
|
||||
await addChannelToPolicy(teamSettings.container, page, channel.display_name);
|
||||
await expect(teamSettings.container.getByText(channel.display_name)).toBeVisible({timeout: 10000});
|
||||
|
||||
// # Enable Auto-add members for the channel so the sync job adds matching users
|
||||
await teamSettings.container.locator(`#auto-add-checkbox-${channel.id}`).click();
|
||||
|
||||
// # Save — triggers confirmation modal
|
||||
const saveBtn = teamSettings.container.locator('[data-testid="SaveChangesPanel__save-btn"]');
|
||||
await expect(saveBtn).toBeEnabled({timeout: 20000});
|
||||
await saveBtn.click();
|
||||
|
||||
// # Confirm "Apply policy" (add users immediately)
|
||||
await page.locator('.TeamPolicyConfirmationModal').waitFor({timeout: 30000});
|
||||
await page.getByRole('button', {name: /Apply policy/}).click();
|
||||
|
||||
// * Navigate back to policy list — confirms save succeeded (no 403 on job creation)
|
||||
await expect(teamSettings.container.getByText(policyName)).toBeVisible({timeout: 15000});
|
||||
|
||||
// * Target user is added to the channel by the sync job
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
try {
|
||||
await adminClient.getChannelMember(channel.id, targetUser.id);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
{timeout: 90000, intervals: [2000, 4000, 6000]},
|
||||
)
|
||||
.toBe(true);
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,120 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/**
|
||||
* @objective E2E tests for Team Settings Modal - four-tab layout and Channel Membership label
|
||||
* @reference MM-69100
|
||||
*/
|
||||
|
||||
import {ChannelsPage, expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
import {enableTeamMembershipABACConfig} from './helpers';
|
||||
|
||||
test.describe('Team Settings Modal - Tab Order', {tag: ['@abac', '@team_membership']}, () => {
|
||||
test('MM-69100_20 Four-tab order: Info, Access, Team Membership, Channel Membership', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminUser, adminClient, team} = await pw.initSetup();
|
||||
await enableTeamMembershipABACConfig(adminClient);
|
||||
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
|
||||
// # Collect all visible tab buttons in order
|
||||
const tabButtons = teamSettings.container.locator('[data-testid$="-tab-button"]');
|
||||
|
||||
// * Exactly 4 tabs are visible
|
||||
await expect(tabButtons).toHaveCount(4);
|
||||
|
||||
// * Tabs appear in the correct order
|
||||
await expect(tabButtons.nth(0)).toContainText('Info');
|
||||
await expect(tabButtons.nth(1)).toContainText('Access');
|
||||
await expect(tabButtons.nth(2)).toContainText('Team Membership');
|
||||
await expect(tabButtons.nth(3)).toContainText('Channel Membership');
|
||||
|
||||
// * Fourth tab does NOT contain the old label
|
||||
await expect(tabButtons.nth(3)).not.toContainText('Membership Policies');
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
test('MM-69100_21 Channel Membership tab label is "Channel Membership", not "Membership Policies"', async ({
|
||||
pw,
|
||||
}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminUser, adminClient, team} = await pw.initSetup();
|
||||
await enableTeamMembershipABACConfig(adminClient);
|
||||
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
|
||||
const channelMembershipTab = teamSettings.container.getByTestId('access_policies-tab-button');
|
||||
|
||||
// * Tab button is visible
|
||||
await expect(channelMembershipTab).toBeVisible();
|
||||
|
||||
// * Label is "Channel Membership"
|
||||
await expect(channelMembershipTab).toContainText('Channel Membership');
|
||||
|
||||
// * Old label is gone
|
||||
await expect(channelMembershipTab).not.toContainText('Membership Policies');
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
test('MM-69100_22 Channel Membership tab (4th) opens the policy editor content', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminUser, adminClient, team} = await pw.initSetup();
|
||||
await enableTeamMembershipABACConfig(adminClient);
|
||||
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
|
||||
// # Click Channel Membership tab
|
||||
await teamSettings.container.getByTestId('access_policies-tab-button').click();
|
||||
|
||||
// * Channel Membership tab content is functional (shows empty state or policy list)
|
||||
await expect(teamSettings.container.getByText('No policies found')).toBeVisible({timeout: 10000});
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
test('MM-69100_23 Team Membership and Channel Membership tabs are independently navigable', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminUser, adminClient, team} = await pw.initSetup();
|
||||
await enableTeamMembershipABACConfig(adminClient);
|
||||
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
|
||||
// # Navigate to Team Membership tab
|
||||
await teamSettings.container.getByTestId('team_membership-tab-button').click();
|
||||
await expect(teamSettings.container.locator('.TeamMembershipTab')).toBeVisible();
|
||||
|
||||
// # Navigate to Channel Membership tab
|
||||
await teamSettings.container.getByTestId('access_policies-tab-button').click();
|
||||
await expect(teamSettings.container.getByText('No policies found')).toBeVisible({timeout: 10000});
|
||||
await expect(teamSettings.container.locator('.TeamMembershipTab')).not.toBeVisible();
|
||||
|
||||
// # Navigate back to Team Membership tab
|
||||
await teamSettings.container.getByTestId('team_membership-tab-button').click();
|
||||
await expect(teamSettings.container.locator('.TeamMembershipTab')).toBeVisible();
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,489 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/**
|
||||
* @objective Team admin can manage team membership ABAC via the in-channel Team Settings modal
|
||||
* @reference MM-69100
|
||||
*/
|
||||
|
||||
import type {Page} from '@playwright/test';
|
||||
|
||||
import {ChannelsPage, expect, newTestPassword, test} from '@mattermost/playwright-lib';
|
||||
|
||||
import {
|
||||
enableTeamMembershipABACConfig,
|
||||
ensureDepartmentAttribute,
|
||||
createPublicTeam,
|
||||
createTeamMembershipPolicy,
|
||||
createTeamAdmin,
|
||||
setUserAttribute,
|
||||
waitForAttributeViewToInclude,
|
||||
waitForAttributeViewToExclude,
|
||||
addAttributeRule,
|
||||
getTeamAccessControlPolicy,
|
||||
createParentMembershipPolicy,
|
||||
assignTeamToParentPolicy,
|
||||
} from './helpers';
|
||||
|
||||
async function openTeamMembershipTab(page: Page, channelsPage: ChannelsPage) {
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
await teamSettings.container.getByTestId('team_membership-tab-button').click();
|
||||
const tab = teamSettings.container.locator('.TeamMembershipTab');
|
||||
await expect(tab).toBeVisible({timeout: 10000});
|
||||
return {teamSettings, tab};
|
||||
}
|
||||
|
||||
test.describe('Team Settings Modal - Team Membership as Team Admin', {tag: ['@abac', '@team_membership']}, () => {
|
||||
const createdTeamIds: string[] = [];
|
||||
const createdUserIds: string[] = [];
|
||||
const createdPolicyIds: string[] = [];
|
||||
|
||||
test.afterEach(async ({pw}) => {
|
||||
const {adminClient} = await pw.getAdminClient();
|
||||
const base = adminClient.getBaseRoute();
|
||||
const headers = {Authorization: `Bearer ${adminClient.getToken()}`};
|
||||
for (const id of createdPolicyIds.splice(0)) {
|
||||
await fetch(`${base}/access_control_policies/${id}`, {method: 'DELETE', headers}).catch(() => {});
|
||||
}
|
||||
for (const id of createdTeamIds.splice(0)) {
|
||||
await adminClient.deleteTeam(id).catch(() => {});
|
||||
}
|
||||
for (const id of createdUserIds.splice(0)) {
|
||||
await adminClient.updateUserActive(id, false).catch(() => {});
|
||||
}
|
||||
});
|
||||
|
||||
test('MM-69100_24 team admin can save team membership rules and the policy persists', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminClient, team} = await pw.initSetup();
|
||||
await enableTeamMembershipABACConfig(adminClient);
|
||||
await ensureDepartmentAttribute(adminClient);
|
||||
|
||||
const teamAdmin = await createTeamAdmin(adminClient, team.id);
|
||||
createdUserIds.push(teamAdmin.id);
|
||||
|
||||
// # Set teamAdmin Engineering so the rule does not self-exclude them
|
||||
await setUserAttribute(adminClient, teamAdmin.id, 'Department', 'Engineering');
|
||||
await waitForAttributeViewToInclude(adminClient, 'user.attributes.Department == "Engineering"', [teamAdmin.id]);
|
||||
|
||||
const {page} = await pw.testBrowser.login(teamAdmin);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const {teamSettings, tab} = await openTeamMembershipTab(page, channelsPage);
|
||||
|
||||
// # Add Engineering rule; leave auto-add OFF
|
||||
await addAttributeRule(tab, page, 'Engineering');
|
||||
|
||||
// # Save
|
||||
await tab.locator('[data-testid="SaveChangesPanel__save-btn"]').click();
|
||||
const confirmModal = page.locator('.ConfirmModal').filter({hasText: 'Save team membership rules?'});
|
||||
await expect(confirmModal).toBeVisible({timeout: 15000});
|
||||
await confirmModal.getByRole('button', {name: 'Save'}).click();
|
||||
|
||||
// * Save panel disappears
|
||||
await expect(confirmModal).not.toBeVisible({timeout: 10000});
|
||||
await expect(tab.locator('[data-testid="SaveChangesPanel__save-btn"]')).not.toBeVisible({timeout: 10000});
|
||||
|
||||
// * Policy persisted — expression contains "Engineering"
|
||||
const policyResult: any = await getTeamAccessControlPolicy(adminClient, team.id);
|
||||
expect(JSON.stringify(policyResult)).toContain('Engineering');
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
test('MM-69100_25 team admin enabling auto-add triggers a team sync job', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminClient, team} = await pw.initSetup();
|
||||
await enableTeamMembershipABACConfig(adminClient);
|
||||
await ensureDepartmentAttribute(adminClient);
|
||||
|
||||
const teamAdmin = await createTeamAdmin(adminClient, team.id);
|
||||
createdUserIds.push(teamAdmin.id);
|
||||
|
||||
// # Set teamAdmin Engineering to avoid self-exclusion
|
||||
await setUserAttribute(adminClient, teamAdmin.id, 'Department', 'Engineering');
|
||||
await waitForAttributeViewToInclude(adminClient, 'user.attributes.Department == "Engineering"', [teamAdmin.id]);
|
||||
|
||||
const testStartTime = Date.now();
|
||||
|
||||
const {page} = await pw.testBrowser.login(teamAdmin);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const {teamSettings, tab} = await openTeamMembershipTab(page, channelsPage);
|
||||
|
||||
// # Add rule and enable auto-add
|
||||
await addAttributeRule(tab, page, 'Engineering');
|
||||
await expect(tab.locator('#autoAddMembersCheckbox')).toBeEnabled({timeout: 5000});
|
||||
await tab.locator('#autoAddMembersCheckbox').click();
|
||||
await expect(tab.locator('#autoAddMembersCheckbox')).toBeChecked();
|
||||
|
||||
// # Save
|
||||
await tab.locator('[data-testid="SaveChangesPanel__save-btn"]').click();
|
||||
const confirmModal = page.locator('.ConfirmModal').filter({hasText: 'Save team membership rules?'});
|
||||
await expect(confirmModal).toBeVisible({timeout: 15000});
|
||||
await confirmModal.getByRole('button', {name: 'Save'}).click();
|
||||
await expect(confirmModal).not.toBeVisible({timeout: 10000});
|
||||
await expect(tab.locator('[data-testid="SaveChangesPanel__save-btn"]')).not.toBeVisible({timeout: 10000});
|
||||
|
||||
// * A sync job was created (auto-add ON)
|
||||
const jobs: any[] = await (adminClient as any).doFetch(
|
||||
`${adminClient.getBaseRoute()}/jobs/type/access_control_team_sync`,
|
||||
{method: 'GET'},
|
||||
);
|
||||
const recentJobs = jobs.filter((j: any) => j.create_at >= testStartTime);
|
||||
expect(recentJobs.length).toBeGreaterThan(0);
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
test('MM-69100_26 team admin is hard-blocked from saving a self-excluding rule', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminClient, team} = await pw.initSetup();
|
||||
await enableTeamMembershipABACConfig(adminClient);
|
||||
await ensureDepartmentAttribute(adminClient);
|
||||
|
||||
// # teamAdmin has NO Department — they do not match the Engineering rule
|
||||
const teamAdmin = await createTeamAdmin(adminClient, team.id);
|
||||
createdUserIds.push(teamAdmin.id);
|
||||
await waitForAttributeViewToExclude(adminClient, 'user.attributes.Department == "Engineering"', [teamAdmin.id]);
|
||||
|
||||
const {page} = await pw.testBrowser.login(teamAdmin);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const {teamSettings, tab} = await openTeamMembershipTab(page, channelsPage);
|
||||
|
||||
// # Add Engineering rule (teamAdmin would be excluded)
|
||||
await addAttributeRule(tab, page, 'Engineering');
|
||||
|
||||
// # Attempt save
|
||||
await tab.locator('[data-testid="SaveChangesPanel__save-btn"]').click();
|
||||
|
||||
// * Self-exclusion modal appears — not the save confirmation
|
||||
await expect(page.getByText('Cannot save access rules')).toBeVisible({timeout: 15000});
|
||||
await expect(page.getByText(/you cannot set these rules/i)).toBeVisible();
|
||||
await expect(page.getByText('Save team membership rules?')).not.toBeVisible();
|
||||
|
||||
// * "Back to editing" button dismisses the self-exclusion modal
|
||||
await expect(page.getByRole('button', {name: 'Back to editing'})).toBeVisible();
|
||||
await page.getByRole('button', {name: 'Back to editing'}).click();
|
||||
await expect(page.getByText('Cannot save access rules')).not.toBeVisible({timeout: 5000});
|
||||
|
||||
// * Policy unchanged via API
|
||||
try {
|
||||
const policyResult: any = await getTeamAccessControlPolicy(adminClient, team.id);
|
||||
expect(JSON.stringify(policyResult ?? {})).not.toContain('"Engineering"');
|
||||
} catch {
|
||||
// No policy exists — self-exclusion correctly blocked the save
|
||||
}
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
test('MM-69100_27 team admin save-confirmation shows advisory copy on a public team', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminUser, adminClient} = await pw.getAdminClient();
|
||||
if (!adminUser) {
|
||||
throw new Error('Admin user not found');
|
||||
}
|
||||
const suffix = pw.random.id();
|
||||
await enableTeamMembershipABACConfig(adminClient);
|
||||
await ensureDepartmentAttribute(adminClient);
|
||||
|
||||
// # Fully public team (allow_open_invite=true so team admin can navigate to it)
|
||||
const team = await createPublicTeam(adminClient, suffix);
|
||||
createdTeamIds.push(team.id);
|
||||
const teamAdmin = await createTeamAdmin(adminClient, team.id);
|
||||
createdUserIds.push(teamAdmin.id);
|
||||
|
||||
// # Create user1 (Engineering) and user2 (Marketing) and add them to the team
|
||||
const createMember = async (dept: string, idx: number) => {
|
||||
const uid = `${suffix}m${idx}`;
|
||||
const user = await adminClient.createUser(
|
||||
{
|
||||
email: `member${uid}@sample.mattermost.com`,
|
||||
username: `member${uid}`,
|
||||
password: newTestPassword(),
|
||||
} as any,
|
||||
'',
|
||||
'',
|
||||
);
|
||||
await adminClient.addToTeam(team.id, user.id);
|
||||
await setUserAttribute(adminClient, user.id, 'Department', dept);
|
||||
return user;
|
||||
};
|
||||
|
||||
const [user1, user2] = await Promise.all([createMember('Engineering', 1), createMember('Marketing', 2)]);
|
||||
createdUserIds.push(user1.id, user2.id);
|
||||
|
||||
// # Set teamAdmin Engineering; remove adminUser so counts are predictable
|
||||
await setUserAttribute(adminClient, teamAdmin.id, 'Department', 'Engineering');
|
||||
await adminClient.removeFromTeam(team.id, adminUser.id);
|
||||
|
||||
await waitForAttributeViewToInclude(adminClient, 'user.attributes.Department == "Engineering"', [
|
||||
teamAdmin.id,
|
||||
user1.id,
|
||||
]);
|
||||
|
||||
const {page} = await pw.testBrowser.login(teamAdmin);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const {teamSettings, tab} = await openTeamMembershipTab(page, channelsPage);
|
||||
|
||||
// # Add Engineering rule
|
||||
await addAttributeRule(tab, page, 'Engineering');
|
||||
|
||||
// # Click Save
|
||||
await tab.locator('[data-testid="SaveChangesPanel__save-btn"]').click();
|
||||
|
||||
const confirmModal = page.locator('.ConfirmModal').filter({hasText: 'Save team membership rules?'});
|
||||
await expect(confirmModal).toBeVisible({timeout: 15000});
|
||||
|
||||
// * Advisory copy on a public team: match count shown, no removal/affected wording
|
||||
await expect(confirmModal.getByText(/2 users match the current rules/i)).toBeVisible({timeout: 10000});
|
||||
await expect(confirmModal.getByText(/these rules are advisory: no one is blocked or removed/i)).toBeVisible();
|
||||
await expect(confirmModal.getByText(/does not match/i)).toHaveCount(0);
|
||||
await expect(confirmModal.getByText(/may be affected/i)).toHaveCount(0);
|
||||
|
||||
// # Cancel without saving
|
||||
await confirmModal.getByRole('button', {name: 'Cancel'}).click();
|
||||
await expect(confirmModal).not.toBeVisible();
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
test('MM-69100_28 team admin can flip a governed public team to private and trigger a sync job', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminClient} = await pw.getAdminClient();
|
||||
const suffix = pw.random.id();
|
||||
await enableTeamMembershipABACConfig(adminClient);
|
||||
await ensureDepartmentAttribute(adminClient);
|
||||
|
||||
// # Create a fully public team
|
||||
const team = await createPublicTeam(adminClient, suffix);
|
||||
createdTeamIds.push(team.id);
|
||||
const teamAdmin = await createTeamAdmin(adminClient, team.id);
|
||||
createdUserIds.push(teamAdmin.id);
|
||||
|
||||
// # Set teamAdmin Engineering so the flip does not remove them from the team
|
||||
await setUserAttribute(adminClient, teamAdmin.id, 'Department', 'Engineering');
|
||||
|
||||
// # Attach a policy so team is policy_enforced (advisory while public)
|
||||
await createTeamMembershipPolicy(adminClient, team.id, 'user.attributes.Department == "Engineering"', false);
|
||||
|
||||
await waitForAttributeViewToInclude(adminClient, 'user.attributes.Department == "Engineering"', [teamAdmin.id]);
|
||||
|
||||
const testStartTime = Date.now();
|
||||
|
||||
const {page} = await pw.testBrowser.login(teamAdmin);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
await teamSettings.openAccessTab();
|
||||
|
||||
// * Public card is initially selected
|
||||
await expect(teamSettings.container.locator('#public-private-selector-button-O')).toHaveClass(/selected/);
|
||||
|
||||
// # Click Private card — mode-flip modal appears (policy_enforced=true triggers it)
|
||||
await teamSettings.container.locator('#public-private-selector-button-P').click();
|
||||
|
||||
const modeFlipModal = page.locator('.ConfirmModal').filter({hasText: 'Switch to Private Team?'});
|
||||
await expect(modeFlipModal).toBeVisible({timeout: 30000});
|
||||
|
||||
// * The modal warns that the flip activates strict ABAC enforcement.
|
||||
await expect(modeFlipModal.getByText(/activate strict ABAC enforcement/i)).toBeVisible({timeout: 10000});
|
||||
|
||||
// # Confirm the flip — confirming saves immediately (no second click)
|
||||
await modeFlipModal.getByRole('button', {name: 'Switch to Private'}).click();
|
||||
await expect(modeFlipModal).not.toBeVisible({timeout: 5000});
|
||||
|
||||
// * The save auto-completes and the panel reports success
|
||||
await teamSettings.verifySavedMessage();
|
||||
|
||||
// * Team is now private. Privacy is driven by allow_open_invite alone; the
|
||||
// feature never mutates team.type, so it stays 'O' from createPublicTeam.
|
||||
const updatedTeam = await adminClient.getTeam(team.id);
|
||||
expect(updatedTeam.type).toBe('O');
|
||||
expect(updatedTeam.allow_open_invite).toBe(false);
|
||||
|
||||
// * A sync job was created
|
||||
const jobs: any[] = await (adminClient as any).doFetch(
|
||||
`${adminClient.getBaseRoute()}/jobs/type/access_control_team_sync`,
|
||||
{method: 'GET'},
|
||||
);
|
||||
const recentJobs = jobs.filter((j: any) => j.create_at >= testStartTime);
|
||||
expect(recentJobs.length).toBeGreaterThan(0);
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
test('MM-70055_1 team admin mode-flip modal resolves the member count on a parent-governed team', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminClient} = await pw.getAdminClient();
|
||||
const suffix = pw.random.id();
|
||||
await enableTeamMembershipABACConfig(adminClient);
|
||||
await ensureDepartmentAttribute(adminClient);
|
||||
|
||||
const team = await createPublicTeam(adminClient, suffix);
|
||||
createdTeamIds.push(team.id);
|
||||
const teamAdmin = await createTeamAdmin(adminClient, team.id);
|
||||
createdUserIds.push(teamAdmin.id);
|
||||
|
||||
// # The team admin must match the rule, otherwise self-exclusion blocks the flip
|
||||
await setUserAttribute(adminClient, teamAdmin.id, 'Department', 'Engineering');
|
||||
|
||||
// # A non-qualifying member so the count is non-zero
|
||||
const outsider = await adminClient.createUser(
|
||||
{
|
||||
email: `outsider${suffix}@sample.mattermost.com`,
|
||||
username: `outsider${suffix}`,
|
||||
password: newTestPassword(),
|
||||
} as any,
|
||||
'',
|
||||
'',
|
||||
);
|
||||
createdUserIds.push(outsider.id);
|
||||
await adminClient.addToTeam(team.id, outsider.id);
|
||||
await setUserAttribute(adminClient, outsider.id, 'Department', 'Marketing');
|
||||
|
||||
// # Govern the team by a parent policy only — no custom rules in Team Settings
|
||||
const parent = await createParentMembershipPolicy(
|
||||
adminClient,
|
||||
`Parent Policy ${suffix}`,
|
||||
'user.attributes.Department == "Engineering"',
|
||||
);
|
||||
createdPolicyIds.push(parent.id);
|
||||
await assignTeamToParentPolicy(adminClient, parent.id, team.id);
|
||||
|
||||
await waitForAttributeViewToInclude(adminClient, 'user.attributes.Department == "Engineering"', [teamAdmin.id]);
|
||||
|
||||
const {page} = await pw.testBrowser.login(teamAdmin);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
await teamSettings.openAccessTab();
|
||||
|
||||
// # Click the Private card
|
||||
await teamSettings.container.locator('#public-private-selector-button-P').click();
|
||||
|
||||
const modeFlipModal = page.locator('.ConfirmModal').filter({hasText: 'Switch to Private Team?'});
|
||||
await expect(modeFlipModal).toBeVisible({timeout: 30000});
|
||||
|
||||
// * The count resolves from the parent policy instead of falling back to the
|
||||
// generic copy, which is all a team admin used to get (MM-70055).
|
||||
await expect(modeFlipModal.getByText(/not meet criteria and will be removed/i)).toBeVisible({timeout: 15000});
|
||||
await expect(modeFlipModal.getByText(/Some members may not meet/i)).toHaveCount(0);
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
test('MM-70057_1 team admin sees the last-synced timestamp in the sync footer', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminClient} = await pw.getAdminClient();
|
||||
const suffix = pw.random.id();
|
||||
await enableTeamMembershipABACConfig(adminClient);
|
||||
await ensureDepartmentAttribute(adminClient);
|
||||
|
||||
const team = await createPublicTeam(adminClient, suffix);
|
||||
createdTeamIds.push(team.id);
|
||||
const teamAdmin = await createTeamAdmin(adminClient, team.id);
|
||||
createdUserIds.push(teamAdmin.id);
|
||||
await setUserAttribute(adminClient, teamAdmin.id, 'Department', 'Engineering');
|
||||
|
||||
// # A policy must exist for the sync footer to render at all
|
||||
await createTeamMembershipPolicy(adminClient, team.id, 'user.attributes.Department == "Engineering"', false);
|
||||
await waitForAttributeViewToInclude(adminClient, 'user.attributes.Department == "Engineering"', [teamAdmin.id]);
|
||||
|
||||
// # Run a sync and wait for it to complete, so there is a timestamp to show
|
||||
await (adminClient as any).doFetch(`${adminClient.getBaseRoute()}/jobs`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({type: 'access_control_team_sync', data: {policy_id: team.id}}),
|
||||
});
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const jobs: any[] = await (adminClient as any).doFetch(
|
||||
`${adminClient.getBaseRoute()}/jobs/type/access_control_team_sync`,
|
||||
{method: 'GET'},
|
||||
);
|
||||
return jobs.some((j: any) => j.data?.policy_id === team.id && j.status === 'success');
|
||||
},
|
||||
{
|
||||
timeout: 30000,
|
||||
intervals: [500, 1000, 2000, 3000],
|
||||
message: 'the team sync job should complete before checking the footer',
|
||||
},
|
||||
)
|
||||
.toBe(true);
|
||||
|
||||
const {page} = await pw.testBrowser.login(teamAdmin);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const {teamSettings, tab} = await openTeamMembershipTab(page, channelsPage);
|
||||
|
||||
// * The footer reports the completed sync. Reading the job list used to be
|
||||
// system-admin only, so a team admin always saw "Never synced" (MM-70057).
|
||||
const syncFooterText = tab.locator('.SyncStatusFooter .SyncStatusFooter__text');
|
||||
await expect(syncFooterText).toContainText(/Last synced/i, {timeout: 15000});
|
||||
await expect(syncFooterText).not.toContainText(/Never synced/i);
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
test('MM-69100_29 team admin existing policy and auto-add state load correctly on tab open', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminClient, team} = await pw.initSetup();
|
||||
await enableTeamMembershipABACConfig(adminClient);
|
||||
await ensureDepartmentAttribute(adminClient);
|
||||
|
||||
// # Create teamAdmin before the policy so addToTeam isn't gated by the
|
||||
// Engineering rule (teamAdmin has no Department attribute).
|
||||
const teamAdmin = await createTeamAdmin(adminClient, team.id);
|
||||
createdUserIds.push(teamAdmin.id);
|
||||
|
||||
// # Pre-create policy with auto-add=true via API
|
||||
await createTeamMembershipPolicy(adminClient, team.id, 'user.attributes.Department == "Engineering"', true);
|
||||
|
||||
// Wait until the server has fully processed the policy (policy_enforced=true).
|
||||
// The tab's loadTeamPolicy fetch may hit a read replica; without this the
|
||||
// policy arrives as null, autoAddMembers stays false, and the checkbox fails.
|
||||
await expect
|
||||
.poll(async () => (await adminClient.getTeam(team.id)).policy_enforced, {
|
||||
timeout: 60_000,
|
||||
intervals: [1000, 2000, 5000, 5000, 5000],
|
||||
})
|
||||
.toBe(true);
|
||||
|
||||
const {page} = await pw.testBrowser.login(teamAdmin);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const {teamSettings, tab} = await openTeamMembershipTab(page, channelsPage);
|
||||
|
||||
// * Auto-add checkbox is checked (active=true was loaded from API)
|
||||
await expect(tab.locator('#autoAddMembersCheckbox')).toBeChecked({timeout: 15000});
|
||||
|
||||
// * Table editor is present
|
||||
await expect(tab.getByTestId('table-editor')).toBeVisible();
|
||||
|
||||
// * No SaveChangesPanel (nothing dirty after initial load)
|
||||
await expect(tab.locator('[data-testid="SaveChangesPanel__save-btn"]')).not.toBeVisible();
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,754 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/**
|
||||
* @objective E2E tests for Team Settings Modal - Team Membership tab
|
||||
* @reference MM-69100
|
||||
*/
|
||||
|
||||
import type {Page} from '@playwright/test';
|
||||
|
||||
import {ChannelsPage, expect, newTestPassword, test} from '@mattermost/playwright-lib';
|
||||
|
||||
import {
|
||||
enableTeamMembershipABACConfig,
|
||||
ensureDepartmentAttribute,
|
||||
createPrivateTeam,
|
||||
createTeamMembershipPolicy,
|
||||
createTeamAdmin,
|
||||
setUserAttribute,
|
||||
waitForAttributeViewToInclude,
|
||||
waitForAttributeViewToExclude,
|
||||
addAttributeRule,
|
||||
createParentPolicy,
|
||||
assignTeamToParentPolicy,
|
||||
} from './helpers';
|
||||
|
||||
async function openTeamMembershipTab(page: Page, channelsPage: ChannelsPage) {
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
await teamSettings.container.getByTestId('team_membership-tab-button').click();
|
||||
const tab = teamSettings.container.locator('.TeamMembershipTab');
|
||||
await expect(tab).toBeVisible({timeout: 10000});
|
||||
return {teamSettings, tab};
|
||||
}
|
||||
|
||||
test.describe('Team Settings Modal - Team Membership Tab', {tag: ['@abac', '@team_membership']}, () => {
|
||||
const createdPolicyIds: string[] = [];
|
||||
const createdTeamIds: string[] = [];
|
||||
const createdUserIds: string[] = [];
|
||||
|
||||
test.afterEach(async ({pw}) => {
|
||||
const {adminClient} = await pw.getAdminClient();
|
||||
const base = adminClient.getBaseRoute();
|
||||
const headers = {Authorization: `Bearer ${adminClient.getToken()}`};
|
||||
for (const id of createdPolicyIds.splice(0)) {
|
||||
await fetch(`${base}/access_control_policies/${id}`, {method: 'DELETE', headers}).catch(() => {});
|
||||
}
|
||||
for (const id of createdTeamIds.splice(0)) {
|
||||
await adminClient.deleteTeam(id).catch(() => {});
|
||||
}
|
||||
for (const id of createdUserIds.splice(0)) {
|
||||
await adminClient.updateUserActive(id, false).catch(() => {});
|
||||
}
|
||||
});
|
||||
|
||||
test('MM-69100_8 Team Membership tab visible for system admin with ABAC + FF enabled', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminUser, adminClient, team} = await pw.initSetup();
|
||||
await enableTeamMembershipABACConfig(adminClient);
|
||||
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
|
||||
// * Team Membership tab button is visible
|
||||
await expect(teamSettings.container.getByTestId('team_membership-tab-button')).toBeVisible();
|
||||
|
||||
// # Click Team Membership tab
|
||||
await teamSettings.container.getByTestId('team_membership-tab-button').click();
|
||||
const tab = teamSettings.container.locator('.TeamMembershipTab');
|
||||
await expect(tab).toBeVisible();
|
||||
|
||||
// * Tab title is visible
|
||||
await expect(teamSettings.container.getByText('Team Membership Rules')).toBeVisible();
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
test('MM-69100_9 Team Membership tab visible for team admin', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminClient, team} = await pw.initSetup();
|
||||
await enableTeamMembershipABACConfig(adminClient);
|
||||
|
||||
const teamAdmin = await createTeamAdmin(adminClient, team.id);
|
||||
createdUserIds.push(teamAdmin.id);
|
||||
|
||||
const {page} = await pw.testBrowser.login(teamAdmin);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
|
||||
// * Team Membership tab button is visible for team admin
|
||||
await expect(teamSettings.container.getByTestId('team_membership-tab-button')).toBeVisible();
|
||||
|
||||
await teamSettings.container.getByTestId('team_membership-tab-button').click();
|
||||
await expect(teamSettings.container.locator('.TeamMembershipTab')).toBeVisible();
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
test('MM-69100_10 Empty state: no banner, table editor visible, auto-add disabled', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminUser, adminClient, team} = await pw.initSetup();
|
||||
await enableTeamMembershipABACConfig(adminClient);
|
||||
await ensureDepartmentAttribute(adminClient);
|
||||
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const {teamSettings, tab} = await openTeamMembershipTab(page, channelsPage);
|
||||
|
||||
// * No system policy banner (no parent policy assigned)
|
||||
await expect(tab.locator('.TeamMembershipTab__systemPolicies')).not.toBeVisible();
|
||||
|
||||
// * Table editor is present
|
||||
await expect(tab.getByTestId('table-editor')).toBeVisible();
|
||||
|
||||
// * Auto-add checkbox is disabled (no rules yet)
|
||||
await expect(tab.locator('#autoAddMembersCheckbox')).toBeDisabled();
|
||||
|
||||
// * No SaveChangesPanel (nothing is dirty)
|
||||
await expect(tab.locator('[data-testid="SaveChangesPanel__save-btn"]')).not.toBeVisible();
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
test('MM-69100_11 System policy InfoBanner visible when team is assigned to a parent policy', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminUser, adminClient, team} = await pw.initSetup();
|
||||
await enableTeamMembershipABACConfig(adminClient);
|
||||
await ensureDepartmentAttribute(adminClient);
|
||||
|
||||
// # Create a parent policy and assign the team to it
|
||||
const policyName = `Global Team Policy ${pw.random.id()}`;
|
||||
const policy = await createParentPolicy(adminClient, policyName);
|
||||
createdPolicyIds.push(policy.id);
|
||||
await assignTeamToParentPolicy(adminClient, policy.id, team.id);
|
||||
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const {teamSettings, tab} = await openTeamMembershipTab(page, channelsPage);
|
||||
|
||||
// * System policy banner is visible
|
||||
await expect(tab.locator('.TeamMembershipTab__systemPolicies')).toBeVisible();
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
test('MM-70056_1 System policy InfoBanner visible to a TEAM ADMIN when the team is assigned to a parent policy', async ({
|
||||
pw,
|
||||
}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminClient, team} = await pw.initSetup();
|
||||
await enableTeamMembershipABACConfig(adminClient);
|
||||
await ensureDepartmentAttribute(adminClient);
|
||||
|
||||
// # Create a parent policy and assign the team to it
|
||||
const policyName = `Global Team Policy ${pw.random.id()}`;
|
||||
const policy = await createParentPolicy(adminClient, policyName);
|
||||
createdPolicyIds.push(policy.id);
|
||||
await assignTeamToParentPolicy(adminClient, policy.id, team.id);
|
||||
|
||||
// # Log in as a TEAM ADMIN (only ManageTeamAccessRules, not ManageSystem)
|
||||
const teamAdmin = await createTeamAdmin(adminClient, team.id);
|
||||
createdUserIds.push(teamAdmin.id);
|
||||
|
||||
const {page} = await pw.testBrowser.login(teamAdmin);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const {teamSettings, tab} = await openTeamMembershipTab(page, channelsPage);
|
||||
|
||||
// * Banner is visible to the team admin, not just the system admin (MM-70056),
|
||||
// and names the resolved parent policy.
|
||||
const banner = tab.locator('.TeamMembershipTab__systemPolicies');
|
||||
await expect(banner).toBeVisible({timeout: 10000});
|
||||
await expect(banner.getByText(policyName)).toBeVisible();
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
test('MM-69100_12 Auto-add disabled with no expression, enabled after adding a rule', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminUser, adminClient, team} = await pw.initSetup();
|
||||
await enableTeamMembershipABACConfig(adminClient);
|
||||
await ensureDepartmentAttribute(adminClient);
|
||||
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const {teamSettings, tab} = await openTeamMembershipTab(page, channelsPage);
|
||||
|
||||
// * Auto-add starts disabled (empty expression)
|
||||
await expect(tab.locator('#autoAddMembersCheckbox')).toBeDisabled();
|
||||
|
||||
// # Add an attribute rule
|
||||
await addAttributeRule(tab, page, 'Engineering');
|
||||
|
||||
// * Auto-add checkbox becomes enabled
|
||||
await expect(tab.locator('#autoAddMembersCheckbox')).toBeEnabled({timeout: 5000});
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
test('MM-69100_13 Save attribute rules without auto-add still triggers a sync job (immediate reconcile)', async ({
|
||||
pw,
|
||||
}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminUser, adminClient, team} = await pw.initSetup();
|
||||
await enableTeamMembershipABACConfig(adminClient);
|
||||
await ensureDepartmentAttribute(adminClient);
|
||||
|
||||
await setUserAttribute(adminClient, adminUser.id, 'Department', 'Engineering');
|
||||
await waitForAttributeViewToInclude(adminClient, 'user.attributes.Department == "Engineering"', [adminUser.id]);
|
||||
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const {teamSettings, tab} = await openTeamMembershipTab(page, channelsPage);
|
||||
|
||||
// # Add attribute rule (auto-add stays OFF)
|
||||
await addAttributeRule(tab, page, 'Engineering');
|
||||
await expect(tab.locator('#autoAddMembersCheckbox')).toBeEnabled({timeout: 5000});
|
||||
|
||||
// # Snapshot jobs for THIS team before saving. Filter by policy_id so
|
||||
// parallel workers and pre-existing jobs from other teams don't skew the count.
|
||||
const fetchTeamJobs = async () => {
|
||||
const all: any[] = await (adminClient as any).doFetch(
|
||||
`${adminClient.getBaseRoute()}/jobs/type/access_control_team_sync`,
|
||||
{method: 'GET'},
|
||||
);
|
||||
return all.filter((j: any) => j.data?.policy_id === team.id);
|
||||
};
|
||||
const teamJobCountBefore = (await fetchTeamJobs()).length;
|
||||
|
||||
// # Click Save → confirmation modal appears
|
||||
await tab.locator('[data-testid="SaveChangesPanel__save-btn"]').click();
|
||||
|
||||
const confirmModal = page.locator('.ConfirmModal').filter({hasText: 'Save team membership rules?'});
|
||||
await expect(confirmModal).toBeVisible({timeout: 15000});
|
||||
|
||||
// * Allowed count is at least 1 (adminUser matches Engineering)
|
||||
await expect(confirmModal.getByText(/user.*match.*current rules/i)).toBeVisible({timeout: 10000});
|
||||
|
||||
// # Confirm save
|
||||
await confirmModal.getByRole('button', {name: 'Save'}).click();
|
||||
await expect(confirmModal).not.toBeVisible({timeout: 10000});
|
||||
await expect(tab.locator('[data-testid="SaveChangesPanel__save-btn"]')).not.toBeVisible({timeout: 10000});
|
||||
|
||||
// * Policy is persisted via API
|
||||
const policyResult: any = await (adminClient as any).doFetch(
|
||||
`${adminClient.getBaseRoute()}/teams/${team.id}/access_control/policy`,
|
||||
{method: 'GET'},
|
||||
);
|
||||
expect(JSON.stringify(policyResult)).toContain('Engineering');
|
||||
|
||||
// * A sync job WAS created for this team even though auto-add is OFF —
|
||||
// membership changes must apply on save, not wait for the hourly scheduler.
|
||||
// On a strict (private) team the reconcile removes non-qualifying members;
|
||||
// on advisory teams the worker no-ops, but the job is still enqueued.
|
||||
await expect
|
||||
.poll(async () => (await fetchTeamJobs()).length, {
|
||||
timeout: 15000,
|
||||
intervals: [500, 1000, 2000, 3000],
|
||||
message: 'saving custom rules should enqueue a team sync job even with auto-add off',
|
||||
})
|
||||
.toBeGreaterThan(teamJobCountBefore);
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
test('MM-69100_14 Enable auto-add on save triggers team sync job', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminUser, adminClient, team} = await pw.initSetup();
|
||||
await enableTeamMembershipABACConfig(adminClient);
|
||||
await ensureDepartmentAttribute(adminClient);
|
||||
|
||||
await setUserAttribute(adminClient, adminUser.id, 'Department', 'Engineering');
|
||||
await waitForAttributeViewToInclude(adminClient, 'user.attributes.Department == "Engineering"', [adminUser.id]);
|
||||
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const {teamSettings, tab} = await openTeamMembershipTab(page, channelsPage);
|
||||
|
||||
// # Add attribute rule
|
||||
await addAttributeRule(tab, page, 'Engineering');
|
||||
await expect(tab.locator('#autoAddMembersCheckbox')).toBeEnabled({timeout: 5000});
|
||||
|
||||
// # Enable auto-add
|
||||
await tab.locator('#autoAddMembersCheckbox').click();
|
||||
await expect(tab.locator('#autoAddMembersCheckbox')).toBeChecked();
|
||||
|
||||
// # Snapshot the most-recent job id right before saving.
|
||||
// Comparing IDs (not counts) is immune to the API page-size cap: once the
|
||||
// total job count reaches the default page size (~60 in CI), both before/after
|
||||
// snapshots return the same count even when a new job is created.
|
||||
const jobsBefore: any[] = await (adminClient as any).doFetch(
|
||||
`${adminClient.getBaseRoute()}/jobs/type/access_control_team_sync`,
|
||||
{method: 'GET'},
|
||||
);
|
||||
const latestJobIdBefore = jobsBefore[0]?.id ?? null;
|
||||
|
||||
// # Save → confirmation modal
|
||||
await tab.locator('[data-testid="SaveChangesPanel__save-btn"]').click();
|
||||
|
||||
const confirmModal = page.locator('.ConfirmModal').filter({hasText: 'Save team membership rules?'});
|
||||
await expect(confirmModal).toBeVisible({timeout: 15000});
|
||||
|
||||
// # Confirm
|
||||
await confirmModal.getByRole('button', {name: 'Save'}).click();
|
||||
await expect(confirmModal).not.toBeVisible({timeout: 10000});
|
||||
await expect(tab.locator('[data-testid="SaveChangesPanel__save-btn"]')).not.toBeVisible({timeout: 10000});
|
||||
|
||||
// * A sync job was created — poll until a newer job appears at the top of the list
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const jobs: any[] = await (adminClient as any).doFetch(
|
||||
`${adminClient.getBaseRoute()}/jobs/type/access_control_team_sync`,
|
||||
{method: 'GET'},
|
||||
);
|
||||
return jobs[0]?.id !== latestJobIdBefore;
|
||||
},
|
||||
{timeout: 15000, intervals: [500, 1000, 2000, 3000]},
|
||||
)
|
||||
.toBe(true);
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
test('MM-69100_15 Toggling auto-add OFF and saving does NOT create a sync job', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminUser, adminClient, team} = await pw.initSetup();
|
||||
await enableTeamMembershipABACConfig(adminClient);
|
||||
|
||||
// # Create policy with auto-add=true via API
|
||||
await createTeamMembershipPolicy(adminClient, team.id, 'true', true);
|
||||
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const {teamSettings, tab} = await openTeamMembershipTab(page, channelsPage);
|
||||
|
||||
// * Auto-add checkbox starts checked (policy.active=true was loaded)
|
||||
await expect(tab.locator('#autoAddMembersCheckbox')).toBeChecked({timeout: 5000});
|
||||
|
||||
// # Uncheck auto-add
|
||||
await tab.locator('#autoAddMembersCheckbox').click();
|
||||
await expect(tab.locator('#autoAddMembersCheckbox')).not.toBeChecked();
|
||||
|
||||
// # Snapshot job count right before saving
|
||||
const jobsBefore: any[] = await (adminClient as any).doFetch(
|
||||
`${adminClient.getBaseRoute()}/jobs/type/access_control_team_sync`,
|
||||
{method: 'GET'},
|
||||
);
|
||||
const jobCountBefore = jobsBefore.length;
|
||||
|
||||
// # Save → confirmation modal
|
||||
await tab.locator('[data-testid="SaveChangesPanel__save-btn"]').click();
|
||||
|
||||
const confirmModal = page.locator('.ConfirmModal').filter({hasText: 'Save team membership rules?'});
|
||||
await expect(confirmModal).toBeVisible({timeout: 15000});
|
||||
|
||||
await confirmModal.getByRole('button', {name: 'Save'}).click();
|
||||
await expect(confirmModal).not.toBeVisible({timeout: 10000});
|
||||
await expect(tab.locator('[data-testid="SaveChangesPanel__save-btn"]')).not.toBeVisible({timeout: 10000});
|
||||
|
||||
// * No sync job was created (auto-add was turned OFF, not ON)
|
||||
const jobsAfter: any[] = await (adminClient as any).doFetch(
|
||||
`${adminClient.getBaseRoute()}/jobs/type/access_control_team_sync`,
|
||||
{method: 'GET'},
|
||||
);
|
||||
expect(jobsAfter.length).toBe(jobCountBefore);
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
test('MM-69100_16 Self-exclusion: admin blocked when their own rule would exclude them', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminUser, adminClient, team} = await pw.initSetup();
|
||||
await enableTeamMembershipABACConfig(adminClient);
|
||||
await ensureDepartmentAttribute(adminClient);
|
||||
|
||||
// # Ensure adminUser has NO Department attribute (a prior test may have set it to "Engineering").
|
||||
// Clear it and wait for the materialized AttributeView to reflect the removal so that
|
||||
// validateExpressionAgainstRequester correctly returns requester_matches=false.
|
||||
await setUserAttribute(adminClient, adminUser.id, 'Department', '');
|
||||
await waitForAttributeViewToExclude(adminClient, 'user.attributes.Department == "Engineering"', [adminUser.id]);
|
||||
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const {teamSettings, tab} = await openTeamMembershipTab(page, channelsPage);
|
||||
|
||||
// # Add rule that adminUser does NOT match
|
||||
await addAttributeRule(tab, page, 'Engineering');
|
||||
|
||||
// # Click Save
|
||||
await tab.locator('[data-testid="SaveChangesPanel__save-btn"]').click();
|
||||
|
||||
// * Self-exclusion modal appears, not the save confirmation modal
|
||||
await expect(page.getByText('Cannot save access rules')).toBeVisible({timeout: 15000});
|
||||
await expect(page.getByText(/you cannot set these rules/i)).toBeVisible();
|
||||
|
||||
// * Save confirmation modal did NOT appear
|
||||
await expect(page.getByText('Save team membership rules?')).not.toBeVisible();
|
||||
|
||||
// * "Back to editing" button is visible
|
||||
await expect(page.getByRole('button', {name: 'Back to editing'})).toBeVisible();
|
||||
|
||||
// # Click Back to editing — self-exclusion modal closes
|
||||
await page.getByRole('button', {name: 'Back to editing'}).click();
|
||||
await expect(page.getByText('Cannot save access rules')).not.toBeVisible({timeout: 5000});
|
||||
|
||||
// * Policy was NOT changed
|
||||
try {
|
||||
const policyResult: any = await (adminClient as any).doFetch(
|
||||
`${adminClient.getBaseRoute()}/teams/${team.id}/access_control/policy`,
|
||||
{method: 'GET'},
|
||||
);
|
||||
// Policy should be empty/not contain Engineering if no prior policy existed
|
||||
const policyStr = JSON.stringify(policyResult ?? {});
|
||||
expect(policyStr).not.toContain('"Engineering"');
|
||||
} catch {
|
||||
// No policy exists — self-exclusion correctly blocked the save
|
||||
}
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
test('MM-69100_43 Self-exclusion does not block on a public team (advisory)', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminUser, adminClient, team} = await pw.initSetup();
|
||||
await enableTeamMembershipABACConfig(adminClient);
|
||||
await ensureDepartmentAttribute(adminClient);
|
||||
|
||||
// # Make the team public (advisory mode)
|
||||
await adminClient.patchTeam({id: team.id, allow_open_invite: true});
|
||||
|
||||
// # Ensure adminUser does NOT match the rule (would be self-exclusion on a private team)
|
||||
await setUserAttribute(adminClient, adminUser.id, 'Department', '');
|
||||
await waitForAttributeViewToExclude(adminClient, 'user.attributes.Department == "Engineering"', [adminUser.id]);
|
||||
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const {teamSettings, tab} = await openTeamMembershipTab(page, channelsPage);
|
||||
|
||||
await addAttributeRule(tab, page, 'Engineering');
|
||||
await tab.locator('[data-testid="SaveChangesPanel__save-btn"]').click();
|
||||
|
||||
// * No self-exclusion block — the save proceeds to the normal confirmation
|
||||
const confirmModal = page.locator('.ConfirmModal').filter({hasText: 'Save team membership rules?'});
|
||||
await expect(confirmModal).toBeVisible({timeout: 15000});
|
||||
await expect(page.getByText('Cannot save access rules')).not.toBeVisible();
|
||||
|
||||
// * Confirmation uses advisory copy (no block/removal claims) on a public team
|
||||
await expect(confirmModal.getByText(/these rules are advisory: no one is blocked or removed/i)).toBeVisible();
|
||||
await expect(confirmModal.getByText(/may be affected/i)).toHaveCount(0);
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
test('MM-69100_17 Save confirmation modal shows correct allowed and restricted counts', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminUser, adminClient, team} = await pw.initSetup();
|
||||
await enableTeamMembershipABACConfig(adminClient);
|
||||
await ensureDepartmentAttribute(adminClient);
|
||||
|
||||
// # Create 2 additional team members with different departments
|
||||
const suffix = pw.random.id();
|
||||
const createMember = async (dept: string, idx: number) => {
|
||||
const uid = `${suffix}m${idx}`;
|
||||
const user = await adminClient.createUser(
|
||||
{
|
||||
email: `member${uid}@sample.mattermost.com`,
|
||||
username: `member${uid}`,
|
||||
password: newTestPassword(),
|
||||
} as any,
|
||||
'',
|
||||
'',
|
||||
);
|
||||
await adminClient.addToTeam(team.id, user.id);
|
||||
await setUserAttribute(adminClient, user.id, 'Department', dept);
|
||||
return user;
|
||||
};
|
||||
|
||||
await setUserAttribute(adminClient, adminUser.id, 'Department', 'Engineering');
|
||||
const [user1, user2] = await Promise.all([createMember('Engineering', 1), createMember('Marketing', 2)]);
|
||||
createdUserIds.push(user1.id, user2.id);
|
||||
|
||||
await waitForAttributeViewToInclude(adminClient, 'user.attributes.Department == "Engineering"', [
|
||||
adminUser.id,
|
||||
user1.id,
|
||||
]);
|
||||
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const {teamSettings, tab} = await openTeamMembershipTab(page, channelsPage);
|
||||
|
||||
// # Add attribute rule (Department == Engineering)
|
||||
await addAttributeRule(tab, page, 'Engineering');
|
||||
|
||||
// # Click Save
|
||||
await tab.locator('[data-testid="SaveChangesPanel__save-btn"]').click();
|
||||
|
||||
const confirmModal = page.locator('.ConfirmModal').filter({hasText: 'Save team membership rules?'});
|
||||
await expect(confirmModal).toBeVisible({timeout: 15000});
|
||||
|
||||
// * 2 users match (adminUser + user1). 2 members do not match: marketingUser plus
|
||||
// the regular user that initSetup creates and adds to the team (no Department set).
|
||||
await expect(confirmModal.getByText(/2 users match/i)).toBeVisible({timeout: 10000});
|
||||
await expect(confirmModal.getByText(/2 current members do not match/i)).toBeVisible({timeout: 10000});
|
||||
|
||||
// # Cancel without saving
|
||||
await confirmModal.getByRole('button', {name: 'Cancel'}).click();
|
||||
await expect(confirmModal).not.toBeVisible();
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
test('MM-69100_18 Confirmation modal shows restricted count when some team members do not match the rules', async ({
|
||||
pw,
|
||||
}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminUser, adminClient} = await pw.initSetup();
|
||||
const suffix = pw.random.id();
|
||||
await enableTeamMembershipABACConfig(adminClient);
|
||||
await ensureDepartmentAttribute(adminClient);
|
||||
|
||||
// # Create a private team with adminUser as creator (automatically a member)
|
||||
const team = await createPrivateTeam(adminClient, suffix);
|
||||
createdTeamIds.push(team.id);
|
||||
|
||||
// # Set adminUser's Department to Marketing so the rule doesn't self-exclude them
|
||||
await setUserAttribute(adminClient, adminUser.id, 'Department', 'Marketing');
|
||||
|
||||
// # Add one team member with Engineering (doesn't match Marketing rule)
|
||||
const uid = suffix;
|
||||
const engUser = await adminClient.createUser(
|
||||
{
|
||||
email: `eng${uid}@sample.mattermost.com`,
|
||||
username: `eng${uid}`,
|
||||
password: newTestPassword(),
|
||||
} as any,
|
||||
'',
|
||||
'',
|
||||
);
|
||||
createdUserIds.push(engUser.id);
|
||||
await adminClient.addToTeam(team.id, engUser.id);
|
||||
await setUserAttribute(adminClient, engUser.id, 'Department', 'Engineering');
|
||||
|
||||
// Wait for BOTH users to appear in the attribute view so computeConfirmCounts
|
||||
// gets accurate counts (adminUser matches Marketing; engUser does not).
|
||||
await waitForAttributeViewToInclude(adminClient, 'user.attributes.Department == "Marketing"', [adminUser.id]);
|
||||
await waitForAttributeViewToInclude(adminClient, 'user.attributes.Department == "Engineering"', [engUser.id]);
|
||||
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const {teamSettings, tab} = await openTeamMembershipTab(page, channelsPage);
|
||||
|
||||
// # Add rule: Department == Marketing (only adminUser matches)
|
||||
await addAttributeRule(tab, page, 'Marketing');
|
||||
|
||||
// # Click Save
|
||||
await tab.locator('[data-testid="SaveChangesPanel__save-btn"]').click();
|
||||
|
||||
// * Confirmation modal appears (self-exclusion passes since adminUser IS Marketing)
|
||||
const confirmModal = page.locator('.ConfirmModal').filter({hasText: 'Save team membership rules?'});
|
||||
await expect(confirmModal).toBeVisible({timeout: 15000});
|
||||
|
||||
// * 1 user matches (adminUser) and 1 member does not (engUser / Engineering)
|
||||
await expect(confirmModal.getByText(/1 user matches/i)).toBeVisible({timeout: 10000});
|
||||
await expect(confirmModal.getByText(/1 current member does not match/i)).toBeVisible({timeout: 10000});
|
||||
|
||||
// # Cancel — don't save
|
||||
await confirmModal.getByRole('button', {name: 'Cancel'}).click();
|
||||
await expect(confirmModal).not.toBeVisible();
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
test('MM-69100_19 Existing rules and auto-add state load correctly when tab opened', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminUser, adminClient, team} = await pw.initSetup();
|
||||
await enableTeamMembershipABACConfig(adminClient);
|
||||
|
||||
// # Pre-create policy with auto-add=true
|
||||
await createTeamMembershipPolicy(adminClient, team.id, 'user.attributes.Department == "Engineering"', true);
|
||||
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const {teamSettings, tab} = await openTeamMembershipTab(page, channelsPage);
|
||||
|
||||
// * Auto-add is checked (active=true was loaded from API)
|
||||
await expect(tab.locator('#autoAddMembersCheckbox')).toBeChecked({timeout: 5000});
|
||||
|
||||
// * Table editor is present and the panel is NOT shown (nothing is dirty after load)
|
||||
await expect(tab.getByTestId('table-editor')).toBeVisible();
|
||||
await expect(tab.locator('[data-testid="SaveChangesPanel__save-btn"]')).not.toBeVisible();
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
test('MM-69100_42 Removing the last rule saves cleanly without a false error', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminUser, adminClient, team} = await pw.initSetup();
|
||||
await enableTeamMembershipABACConfig(adminClient);
|
||||
await ensureDepartmentAttribute(adminClient);
|
||||
|
||||
// # Pre-create a saved rule with auto-add ON (equivalent to "add a rule and save it")
|
||||
await createTeamMembershipPolicy(adminClient, team.id, 'user.attributes.Department == "Engineering"', true);
|
||||
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const {teamSettings, tab} = await openTeamMembershipTab(page, channelsPage);
|
||||
await expect(tab.locator('#autoAddMembersCheckbox')).toBeChecked();
|
||||
|
||||
// # Remove the only rule via the trash icon
|
||||
await tab.getByRole('button', {name: 'Remove row'}).first().click();
|
||||
|
||||
// * Auto-add is unchecked and disabled once no rules remain
|
||||
await expect(tab.locator('#autoAddMembersCheckbox')).not.toBeChecked();
|
||||
await expect(tab.locator('#autoAddMembersCheckbox')).toBeDisabled();
|
||||
|
||||
// # Save → the removal warning (not the count modal) is shown
|
||||
await tab.locator('[data-testid="SaveChangesPanel__save-btn"]').click();
|
||||
const confirmModal = page.locator('.ConfirmModal').filter({hasText: 'Remove membership rules?'});
|
||||
await expect(confirmModal).toBeVisible({timeout: 15000});
|
||||
await expect(confirmModal.getByText(/no longer be restricted by user attributes/i)).toBeVisible();
|
||||
await confirmModal.getByRole('button', {name: 'Remove rules'}).click();
|
||||
await expect(confirmModal).not.toBeVisible({timeout: 10000});
|
||||
|
||||
// * Save succeeds: no "Failed to save access rules" error surfaces
|
||||
await expect(tab.getByText('Failed to save access rules')).not.toBeVisible();
|
||||
await expect(tab.locator('[data-testid="SaveChangesPanel__save-btn"]')).not.toBeVisible({timeout: 10000});
|
||||
|
||||
// * Policy is actually removed on the server
|
||||
const policyResult: any = await (adminClient as any)
|
||||
.doFetch(`${adminClient.getBaseRoute()}/teams/${team.id}/access_control/policy`, {method: 'GET'})
|
||||
.catch(() => null);
|
||||
expect(JSON.stringify(policyResult ?? {})).not.toContain('Engineering');
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
test('MM-69100_38 Sync footer shows "Never synced" in Team Membership tab when a policy exists', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminUser, adminClient, team} = await pw.initSetup();
|
||||
await enableTeamMembershipABACConfig(adminClient);
|
||||
|
||||
// # Pre-create a team policy via API so hasPolicy=true when the tab loads
|
||||
await createTeamMembershipPolicy(adminClient, team.id, 'true', false);
|
||||
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const {teamSettings, tab} = await openTeamMembershipTab(page, channelsPage);
|
||||
|
||||
// * Sync footer is visible once the job-status fetch resolves
|
||||
const syncFooter = tab.locator('.SyncStatusFooter');
|
||||
await expect(syncFooter).toBeVisible({timeout: 10000});
|
||||
|
||||
// * "Never synced." is shown — no sync job has run for this brand-new team
|
||||
await expect(syncFooter.locator('.SyncStatusFooter__text')).toContainText(/Never synced/i);
|
||||
|
||||
// * "Sync now" link is present
|
||||
await expect(syncFooter.locator('.SyncStatusFooter__link')).toBeVisible();
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
|
||||
test('MM-69100_39 Clicking "Sync now" in Team Membership tab enqueues a team sync job', async ({pw}) => {
|
||||
await pw.skipIfNoLicense();
|
||||
const {adminUser, adminClient, team} = await pw.initSetup();
|
||||
await enableTeamMembershipABACConfig(adminClient);
|
||||
|
||||
// # Pre-create a team policy so the sync footer renders
|
||||
await createTeamMembershipPolicy(adminClient, team.id, 'true', false);
|
||||
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
const {teamSettings, tab} = await openTeamMembershipTab(page, channelsPage);
|
||||
|
||||
const syncFooter = tab.locator('.SyncStatusFooter');
|
||||
await expect(syncFooter).toBeVisible({timeout: 10000});
|
||||
await expect(syncFooter.locator('.SyncStatusFooter__link')).toBeVisible();
|
||||
|
||||
const testStartTime = Date.now();
|
||||
|
||||
// # Click "Sync now"
|
||||
await syncFooter.locator('.SyncStatusFooter__link').click();
|
||||
|
||||
// * A new team sync job scoped to this team is enqueued
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const jobs: any[] = await (adminClient as any).doFetch(
|
||||
`${adminClient.getBaseRoute()}/jobs/type/access_control_team_sync`,
|
||||
{method: 'GET'},
|
||||
);
|
||||
return jobs.some((j: any) => j.data?.policy_id === team.id && j.create_at >= testStartTime);
|
||||
},
|
||||
{
|
||||
timeout: 15000,
|
||||
intervals: [500, 1000, 2000, 3000],
|
||||
message: 'clicking Sync now should enqueue a team sync job scoped to this team',
|
||||
},
|
||||
)
|
||||
.toBe(true);
|
||||
|
||||
await teamSettings.close();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,165 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/**
|
||||
* @objective E2E tests for Team Settings Modal - Unsaved Changes behavior
|
||||
* @reference MM-67920
|
||||
*/
|
||||
|
||||
import {ChannelsPage, expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
test.describe('Team Settings Modal - Unsaved Changes', () => {
|
||||
/**
|
||||
* MM-67920 Warn on close with unsaved changes
|
||||
* @objective Verify unsaved changes warning behavior (warn-once pattern)
|
||||
*/
|
||||
test('MM-67920 Warn on close with unsaved changes', async ({pw}) => {
|
||||
// # Set up admin user and login
|
||||
const {adminUser, team} = await pw.initSetup();
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
|
||||
// # Navigate to team
|
||||
await channelsPage.goto(team.name);
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Open Team Settings Modal
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
|
||||
// # Edit team name to create unsaved changes
|
||||
const newTeamName = `Modified Team ${pw.random.id()}`;
|
||||
await teamSettings.infoSettings.updateName(newTeamName);
|
||||
|
||||
// # Try to close modal (first attempt)
|
||||
await teamSettings.close();
|
||||
|
||||
// * Verify "You have unsaved changes" warning appears
|
||||
await teamSettings.verifyUnsavedChanges();
|
||||
|
||||
// * Verify Save button is visible
|
||||
await expect(teamSettings.saveButton).toBeVisible();
|
||||
|
||||
// * Verify modal is still open
|
||||
await expect(teamSettings.container).toBeVisible();
|
||||
|
||||
// # Try to close modal again (second attempt - warn-once behavior)
|
||||
await teamSettings.close();
|
||||
|
||||
// * Verify modal closes on second attempt
|
||||
await expect(teamSettings.container).not.toBeVisible();
|
||||
});
|
||||
|
||||
/**
|
||||
* MM-67920 Prevent tab switch with unsaved changes
|
||||
* @objective Verify tab switching blocked with unsaved changes
|
||||
*/
|
||||
test('MM-67920 Prevent tab switch with unsaved changes', async ({pw}) => {
|
||||
// # Set up admin user and login
|
||||
const {adminUser, team} = await pw.initSetup();
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
|
||||
// # Navigate to team
|
||||
await channelsPage.goto(team.name);
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Open Team Settings Modal
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
|
||||
// * Verify Access tab is visible (admin has INVITE_USER permission)
|
||||
await expect(teamSettings.accessTab).toBeVisible();
|
||||
|
||||
// # Edit team name in Info tab (create unsaved changes)
|
||||
const newTeamName = `Modified Team ${pw.random.id()}`;
|
||||
await teamSettings.infoSettings.updateName(newTeamName);
|
||||
|
||||
// # Try to switch to Access tab
|
||||
await teamSettings.openAccessTab();
|
||||
|
||||
// * Verify "You have unsaved changes" error appears
|
||||
await teamSettings.verifyUnsavedChanges();
|
||||
|
||||
// * Verify still on Info tab
|
||||
await expect(teamSettings.infoTab).toHaveAttribute('aria-selected', 'true');
|
||||
|
||||
// # Click Undo button
|
||||
await teamSettings.undo();
|
||||
|
||||
// * Verify can now switch to Access tab
|
||||
await teamSettings.openAccessTab();
|
||||
await expect(teamSettings.accessTab).toHaveAttribute('aria-selected', 'true');
|
||||
});
|
||||
|
||||
/**
|
||||
* MM-67920 Save changes and close modal without warning
|
||||
* @objective Verify that after saving, modal closes without warning
|
||||
*/
|
||||
test('MM-67920 Save changes and close modal without warning', async ({pw}) => {
|
||||
// # Set up admin user and login
|
||||
const {adminUser, adminClient, team} = await pw.initSetup();
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
|
||||
// # Navigate to team
|
||||
await channelsPage.goto(team.name);
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Open Team Settings Modal
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
|
||||
// # Edit team name
|
||||
const newTeamName = `Updated Team ${pw.random.id()}`;
|
||||
await teamSettings.infoSettings.updateName(newTeamName);
|
||||
|
||||
// # Save changes
|
||||
await teamSettings.save();
|
||||
|
||||
// * Wait for "Settings saved" message
|
||||
await teamSettings.verifySavedMessage();
|
||||
|
||||
// * Verify team name updated via API
|
||||
const updatedTeam = await adminClient.getTeam(team.id);
|
||||
expect(updatedTeam.display_name).toBe(newTeamName);
|
||||
|
||||
// # Close modal immediately after save (should work without warning)
|
||||
await teamSettings.close();
|
||||
|
||||
// * Verify modal closes without warning
|
||||
await expect(teamSettings.container).not.toBeVisible();
|
||||
});
|
||||
|
||||
/**
|
||||
* MM-67920 Undo changes resets form state
|
||||
* @objective Verify Undo button restores original values
|
||||
*/
|
||||
test('MM-67920 Undo changes resets form state', async ({pw}) => {
|
||||
// # Set up admin user and login
|
||||
const {adminUser, team} = await pw.initSetup();
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
const channelsPage = new ChannelsPage(page);
|
||||
|
||||
// # Navigate to team
|
||||
await channelsPage.goto(team.name);
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Open Team Settings Modal
|
||||
const teamSettings = await channelsPage.openTeamSettings();
|
||||
|
||||
// # Edit team name
|
||||
const newTeamName = `Modified Team ${pw.random.id()}`;
|
||||
await teamSettings.infoSettings.updateName(newTeamName);
|
||||
|
||||
// * Verify input shows new value
|
||||
await expect(teamSettings.infoSettings.nameInput).toHaveValue(newTeamName);
|
||||
|
||||
// # Click Undo button
|
||||
await teamSettings.undo();
|
||||
|
||||
// * Verify input restored to original value
|
||||
await expect(teamSettings.infoSettings.nameInput).toHaveValue(team.display_name);
|
||||
|
||||
// * Verify can close modal without warning
|
||||
await teamSettings.close();
|
||||
await expect(teamSettings.container).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user