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

This commit is contained in:
upstream-mirror
2026-09-16 00:29:55 +05:00
commit 240db4e2a7
15309 changed files with 2173406 additions and 0 deletions

View File

@@ -0,0 +1,176 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {
expect,
test,
enableABAC,
navigateToABACPage,
runSyncJob,
verifyUserInChannel,
updateUserAttributes,
createUserWithAttributes,
} from '@mattermost/playwright-lib';
import {
ensureUserAttributes,
createPrivateChannelForABAC,
createBasicPolicy,
activatePolicy,
waitForLatestSyncJob,
} from '../support';
/**
* ABAC User Attributes - Attribute Changes
* Tests for user attribute changes affecting ABAC policies
*/
test.describe('ABAC User Attributes - Attribute Changes', () => {
/**
* MM-T5794: User is auto-added to channel when a qualifying attribute is added to their profile (auto-add true)
*
* Step 1:
* With at least one access policy in existence on the server, set to auto-add, and applied to a channel:
* 1. As system admin make a note of the attribute needed for a user to be auto-added to a channel
* 2. As a user not in the channel and not having the required attribute
* 3. Click user's own profile picture top right and select Profile
* 4. Scroll down to the required custom attribute, click Edit, and add the required value
*/
test('MM-T5794 User auto-added when qualifying attribute is added to profile', async ({pw}) => {
test.setTimeout(120000);
await pw.skipIfNoLicense();
// ============================================================
// SETUP: Create attribute, policy, and channel
// ============================================================
const {adminUser, adminClient, team} = await pw.initSetup();
// Setup attributes (using ensureUserAttributes like MM-T5800 does)
await ensureUserAttributes(adminClient);
// Create test user with NON-qualifying Department attribute (same pattern as MM-T5800)
// MM-T5800 creates user with Department=Sales, then changes to Engineering
// We do the same: Start with Sales (non-qualifying), then change to Engineering (qualifying)
const testUser = await createUserWithAttributes(adminClient, {Department: 'Sales'});
await adminClient.addToTeam(team.id, testUser.id);
// Create private channel
const privateChannel = await createPrivateChannelForABAC(adminClient, team.id);
// ============================================================
// STEP 1: Create ABAC policy with auto-add enabled
// Policy requirement: Department == "Engineering"
// ============================================================
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
await navigateToABACPage(systemConsolePage.page);
await enableABAC(systemConsolePage.page);
const policyName = `Engineering Access ${pw.random.id()}`;
const jobId = await createBasicPolicy(systemConsolePage.page, {
name: policyName,
attribute: 'Department',
operator: '==',
value: 'Engineering',
autoSync: true, // ✅ Auto-add enabled
channels: [privateChannel.display_name],
});
// Activate policy (EXACT same pattern as MM-T5800)
await waitForLatestSyncJob(systemConsolePage.page, undefined, jobId);
const searchInput = systemConsolePage.page.locator('input[placeholder*="Search" i]').first();
await searchInput.waitFor({state: 'visible', timeout: 5000});
const idMatch = policyName.match(/([a-z0-9]+)$/i);
const uniqueId = idMatch ? idMatch[1] : policyName;
await searchInput.fill(uniqueId);
await systemConsolePage.page.waitForTimeout(1000);
const policyRow = systemConsolePage.page.locator('.policy-name').first();
const policyId = (await policyRow.getAttribute('id'))?.replace('customDescription-', '');
if (policyId) {
await activatePolicy(adminClient, policyId);
}
await searchInput.clear();
// Re-apply guard: concurrent initSetup() resets ABAC between enableABAC() UI call and sync
await adminClient.patchConfig({
AccessControlSettings: {EnableAttributeBasedAccessControl: true},
});
// ============================================================
// STEP 2: Verify user is NOT in channel initially
// ============================================================
const syncJob1 = await runSyncJob(systemConsolePage.page);
await waitForLatestSyncJob(systemConsolePage.page, undefined, syncJob1);
const initialInChannel = await verifyUserInChannel(adminClient, testUser.id, privateChannel.id);
expect(initialInChannel).toBe(false);
// ============================================================
// STEPS 3-5: Add qualifying attribute to user's profile
// Note: Using API for attribute update. UI testing for profile editing
// is covered in separate user profile test suite.
// ============================================================
await updateUserAttributes(adminClient, testUser.id, {Department: 'Engineering'});
// ============================================================
// STEP 6: Run sync job to trigger auto-add
// ============================================================
// DEBUG: Verify attribute was updated before sync
await adminClient.getUserCustomProfileAttributesValues(testUser.id);
// Get the Department field to check its value
await adminClient.getCustomProfileAttributeFields();
const syncJob2 = await runSyncJob(systemConsolePage.page);
await waitForLatestSyncJob(systemConsolePage.page, undefined, syncJob2);
// ============================================================
// VERIFICATION: User should now be auto-added to channel
// ============================================================
// DEBUG: Check all channel members
await adminClient.getChannelMembers(privateChannel.id);
const finalInChannel = await verifyUserInChannel(adminClient, testUser.id, privateChannel.id);
if (!finalInChannel) {
// console.error('\n[ERROR] User NOT in channel after sync!');
// console.error('[ERROR] This means the ABAC sync did not add the user.');
// console.error('[ERROR] Possible causes:');
// console.error('[ERROR] 1. Policy not active');
// console.error('[ERROR] 2. Attribute value not matching policy');
// console.error('[ERROR] 3. Sync job failed silently');
}
expect(finalInChannel).toBe(true);
// ============================================================
// VERIFICATION: Check for "User added" system message
// ============================================================
// Get recent posts from the channel
const posts = await adminClient.getPosts(privateChannel.id, 0, 10);
const postList = posts.order.map((postId: string) => posts.posts[postId]);
// Find system message for user being added
const userAddedMessage = postList.find((post: any) => {
return (
post.type === 'system_add_to_channel' &&
post.props?.addedUserId === testUser.id &&
post.user_id === 'system'
);
});
if (userAddedMessage) {
// System message found
} else {
// System message not found (may be disabled in test env)
}
// System messages might be disabled in test env, so we don't fail the test
// The important verification is that the user was added
expect(finalInChannel).toBe(true);
});
});

View File

@@ -0,0 +1,139 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {
expect,
test,
enableABAC,
navigateToABACPage,
verifyUserInChannel,
updateUserAttributes,
} from '@mattermost/playwright-lib';
import type {CustomProfileAttribute} from '../../../channels/custom_profile_attributes/helpers';
import {setupCustomProfileAttributeFields} from '../../../channels/custom_profile_attributes/helpers';
import {
createUserForABAC,
createPrivateChannelForABAC,
createBasicPolicy,
enableUserManagedAttributes,
} from '../support';
/**
* ABAC User Attributes - Attribute Changes
* Tests for user attribute changes affecting ABAC policies
*/
test.describe('ABAC User Attributes - Attribute Changes', () => {
/**
* MM-T5795: User can be added to channel by system admin after a qualifying attribute is added to their profile (auto-add false)
*
* Preconditions:
* - Access policy with auto-add set to FALSE
*
* Steps:
* 1. As system admin, note the required attribute for channel access
* 2. As a user not in the channel and lacking the required attribute:
* - Add the required attribute value to user profile
* 3. As system admin, go to the channel and add the user
*
* Expected:
* - User who now meets the policy CAN be added to the channel by the admin
* - "User added" message is posted in the channel by System
*/
test('MM-T5795 User can be added by admin after attribute added (auto-add false)', async ({pw}) => {
test.setTimeout(120000);
await pw.skipIfNoLicense();
// ============================================================
// SETUP: Create attribute, policy with auto-add FALSE, and channel
// ============================================================
const {adminUser, adminClient, team} = await pw.initSetup();
await enableUserManagedAttributes(adminClient);
const attributeFields: CustomProfileAttribute[] = [{name: 'Department', type: 'text', value: ''}];
const attributeFieldsMap = await setupCustomProfileAttributeFields(adminClient, attributeFields);
// Create test user WITHOUT the qualifying attribute
const testUser = await createUserForABAC(adminClient, attributeFieldsMap, []);
await adminClient.addToTeam(team.id, testUser.id);
const privateChannel = await createPrivateChannelForABAC(adminClient, team.id);
// ============================================================
// STEP 1: Create policy with auto-add DISABLED
// ============================================================
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
await navigateToABACPage(systemConsolePage.page);
await enableABAC(systemConsolePage.page);
await adminClient.patchConfig({
AccessControlSettings: {EnableAttributeBasedAccessControl: true},
});
await pw.waitUntil(async () => {
const cfg = await adminClient.getConfig();
return cfg.AccessControlSettings?.EnableAttributeBasedAccessControl === true;
});
const policyName = `Engineering Manual Add ${pw.random.id()}`;
await createBasicPolicy(systemConsolePage.page, {
name: policyName,
attribute: 'Department',
operator: '==',
value: 'Engineering',
autoSync: false, // ✅ Auto-add DISABLED
channels: [privateChannel.display_name],
});
// ============================================================
// STEP 2: Add qualifying attribute to user
// ============================================================
await updateUserAttributes(adminClient, testUser.id, {Department: 'Engineering'});
// ============================================================
// STEP 3: Admin manually adds user to channel
// ============================================================
await adminClient.patchConfig({
AccessControlSettings: {EnableAttributeBasedAccessControl: true},
});
await pw.waitUntil(async () => {
const cfg = await adminClient.getConfig();
return cfg.AccessControlSettings?.EnableAttributeBasedAccessControl === true;
});
// Re-create the Department CPA field if a concurrent initSetup() deleted it.
// Without the field the server returns "An attribute is missing from the expression"
// on addToChannel, because the policy references a field id that no longer exists.
await setupCustomProfileAttributeFields(adminClient, attributeFields);
// Verify user can be added (policy allows it since user has qualifying attribute)
await adminClient.addToChannel(testUser.id, privateChannel.id);
// Membership + ABAC evaluation can lag behind the REST response in CI.
await expect
.poll(async () => verifyUserInChannel(adminClient, testUser.id, privateChannel.id), {
timeout: 60000,
intervals: [1000, 2000, 3000],
})
.toBe(true);
// ============================================================
// VERIFICATION: Check for "User added" system message
// ============================================================
const posts = await adminClient.getPosts(privateChannel.id, 0, 10);
const postList = posts.order.map((postId: string) => posts.posts[postId]);
const userAddedMessage = postList.find((post: any) => {
return post.type === 'system_add_to_channel' && post.props?.addedUserId === testUser.id;
});
if (userAddedMessage) {
// System message found
} else {
// System message not found (may be disabled in test env)
}
});
});

View File

@@ -0,0 +1,215 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {
expect,
test,
enableABAC,
navigateToABACPage,
runSyncJob,
verifyUserInChannel,
updateUserAttributes,
} from '@mattermost/playwright-lib';
import type {CustomProfileAttribute} from '../../../channels/custom_profile_attributes/helpers';
import {setupCustomProfileAttributeFields} from '../../../channels/custom_profile_attributes/helpers';
import {
createUserForABAC,
createPrivateChannelForABAC,
createBasicPolicy,
activatePolicy,
waitForLatestSyncJob,
enableUserManagedAttributes,
} from '../support';
/**
* ABAC User Attributes - Attribute Changes
* Tests for user attribute changes affecting ABAC policies
*/
test.describe('ABAC User Attributes - Attribute Changes', () => {
/**
* MM-T5796: User is auto-removed from channel when required attribute is removed
*
* Test Scenario 1 & 2 (Auto-add: False & True):
* Steps:
* 1. As system admin, identify the required attribute for channel access
* 2. Log in as a user currently in the channel with the required attribute
* 3. Edit user's profile
* 4. Remove or change the required attribute value
* 5. Save changes
*
* Expected:
* - User is automatically removed from the channel
* - System posts a "User removed" message in the channel
*/
test('MM-T5796 User auto-removed when required attribute is removed', async ({pw}) => {
test.setTimeout(180000);
await pw.skipIfNoLicense();
// ============================================================
// SETUP
// ============================================================
const {adminUser, adminClient, team} = await pw.initSetup();
await enableUserManagedAttributes(adminClient);
const attributeFields: CustomProfileAttribute[] = [{name: 'Department', type: 'text', value: ''}];
const attributeFieldsMap = await setupCustomProfileAttributeFields(adminClient, attributeFields);
// Create test user WITH the qualifying attribute (starts with Department=Engineering)
const testUser = await createUserForABAC(adminClient, attributeFieldsMap, [
{name: 'Department', type: 'text', value: 'Engineering'},
]);
await adminClient.addToTeam(team.id, testUser.id);
const privateChannel = await createPrivateChannelForABAC(adminClient, team.id);
// ============================================================
// TEST SCENARIO 1: Auto-add FALSE
// ============================================================
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
await navigateToABACPage(systemConsolePage.page);
await enableABAC(systemConsolePage.page);
const policy1Name = `Engineering Access NoAutoAdd ${pw.random.id()}`;
const jobId1 = await createBasicPolicy(systemConsolePage.page, {
name: policy1Name,
attribute: 'Department',
operator: '==',
value: 'Engineering',
autoSync: false, // Auto-add FALSE
channels: [privateChannel.display_name],
});
// Manually add user to channel
await adminClient.addToChannel(testUser.id, privateChannel.id);
const initialInChannel = await verifyUserInChannel(adminClient, testUser.id, privateChannel.id);
expect(initialInChannel).toBe(true);
// Get policy ID and activate
await waitForLatestSyncJob(systemConsolePage.page, undefined, jobId1);
const searchInput = systemConsolePage.page.locator('input[placeholder*="Search" i]').first();
await searchInput.waitFor({state: 'visible', timeout: 5000});
const idMatch = policy1Name.match(/([a-z0-9]+)$/i);
const uniqueId = idMatch ? idMatch[1] : policy1Name;
await searchInput.fill(uniqueId);
await systemConsolePage.page.waitForTimeout(1000);
const policyRow = systemConsolePage.page.locator('.policy-name').first();
const policyElementId = await policyRow.getAttribute('id');
const policyId = policyElementId?.replace('customDescription-', '');
if (policyId) {
await activatePolicy(adminClient, policyId);
}
await searchInput.clear();
// Remove the qualifying attribute
await updateUserAttributes(adminClient, testUser.id, {Department: 'Sales'});
// Wait for attribute change to propagate
await systemConsolePage.page.waitForTimeout(1000);
await adminClient.patchConfig({
AccessControlSettings: {EnableAttributeBasedAccessControl: true},
});
// Run sync job
const syncJob1 = await runSyncJob(systemConsolePage.page);
await waitForLatestSyncJob(systemConsolePage.page, undefined, syncJob1);
// Wait for membership updates to apply
await systemConsolePage.page.waitForTimeout(1000);
// Verify user is removed
const userInChannelAfterRemoval = await verifyUserInChannel(adminClient, testUser.id, privateChannel.id);
expect(userInChannelAfterRemoval).toBe(false);
// Check for removal system message
const posts = await adminClient.getPosts(privateChannel.id, 0, 10);
const postList = posts.order.map((postId: string) => posts.posts[postId]);
const userRemovedMessage = postList.find((post: any) => {
return (
(post.type === 'system_remove_from_channel' || post.type === 'system_leave_channel') &&
(post.props?.removedUserId === testUser.id || post.user_id === testUser.id)
);
});
if (userRemovedMessage) {
// System message found
} else {
// System message not found (may be disabled in test env)
}
// ============================================================
// TEST SCENARIO 2: Auto-add TRUE
// ============================================================
// Restore user attribute and create new policy with auto-add=true
await updateUserAttributes(adminClient, testUser.id, {Department: 'Engineering'});
const channel2 = await createPrivateChannelForABAC(adminClient, team.id);
await navigateToABACPage(systemConsolePage.page);
const policy2Name = `Engineering Access WithAutoAdd ${pw.random.id()}`;
const jobId2 = await createBasicPolicy(systemConsolePage.page, {
name: policy2Name,
attribute: 'Department',
operator: '==',
value: 'Engineering',
autoSync: true, // Auto-add TRUE
channels: [channel2.display_name],
});
// Activate and run sync to auto-add user
await waitForLatestSyncJob(systemConsolePage.page, undefined, jobId2);
await searchInput.fill(policy2Name.match(/([a-z0-9]+)$/i)?.[1] || policy2Name);
await systemConsolePage.page.waitForTimeout(1000);
const policyRow2 = systemConsolePage.page.locator('.policy-name').first();
const policyId2 = (await policyRow2.getAttribute('id'))?.replace('customDescription-', '');
if (policyId2) {
await activatePolicy(adminClient, policyId2);
}
await searchInput.clear();
// Re-apply ABAC enable guard: a concurrent initSetup() on another shard may have
// disabled ABAC between the initial enableABAC call and this sync job.
// Without ABAC enabled the server skips policy evaluation and won't auto-add the user.
await adminClient.patchConfig({
AccessControlSettings: {EnableAttributeBasedAccessControl: true},
});
const syncJob2 = await runSyncJob(systemConsolePage.page);
await waitForLatestSyncJob(systemConsolePage.page, undefined, syncJob2);
const userAutoAdded = await verifyUserInChannel(adminClient, testUser.id, channel2.id);
expect(userAutoAdded).toBe(true);
// Remove attribute again
await updateUserAttributes(adminClient, testUser.id, {Department: 'Marketing'});
// Wait for attribute change to propagate
await systemConsolePage.page.waitForTimeout(1000);
await adminClient.patchConfig({
AccessControlSettings: {EnableAttributeBasedAccessControl: true},
});
// Run sync
const syncJob3 = await runSyncJob(systemConsolePage.page);
await waitForLatestSyncJob(systemConsolePage.page, undefined, syncJob3);
// Small delay for channel membership update
await systemConsolePage.page.waitForTimeout(1000);
// Verify user is removed
const userRemovedFromChannel2 = await verifyUserInChannel(adminClient, testUser.id, channel2.id);
expect(userRemovedFromChannel2).toBe(false);
});
});

View File

@@ -0,0 +1,204 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {Client4} from '@mattermost/client';
import type {UserPropertyField} from '@mattermost/types/properties_user';
import {expect, test, enableABAC, navigateToABACPage} from '@mattermost/playwright-lib';
import type {CustomProfileAttribute} from '../../../channels/custom_profile_attributes/helpers';
import {
deleteCustomProfileAttributes,
setupCustomProfileAttributeFields,
} from '../../../channels/custom_profile_attributes/helpers';
import {fillSingleConditionValue, getPolicyIdByName} from '../support';
type FieldsMap = Record<string, UserPropertyField>;
async function clearExistingFields(client: Client4): Promise<void> {
try {
const existing = await client.getCustomProfileAttributeFields();
if (existing?.length) {
const map: FieldsMap = {};
for (const f of existing) {
map[f.id] = f;
}
await deleteCustomProfileAttributes(client, map);
}
} catch {
// No fields to clean up
}
}
test.describe('ABAC Attribute Selector - display_name rendering and filtering', () => {
/**
* @objective Verify the attribute selector renders display_name when set,
* falls back to `name`, filters on both, and persists the CEL identifier
* in saved policy expressions.
*
* @precondition
* Two admin-managed CPA fields seeded via REST: `dept_head` with
* display_name 'Department Head', and `office` with no display_name.
*/
test(
'renders and filters by display_name while persisting CEL identifier',
{tag: '@user_attributes'},
async ({pw}) => {
test.setTimeout(120000);
await pw.ensureLicense();
await pw.skipIfNoLicense();
const {adminUser, adminClient} = await pw.initSetup();
await clearExistingFields(adminClient);
const seedAttributes: CustomProfileAttribute[] = [
{
name: 'dept_head',
type: 'text',
attrs: {
display_name: 'Department Head',
visibility: 'when_set',
managed: 'admin',
},
},
{
name: 'office',
type: 'text',
attrs: {
visibility: 'when_set',
managed: 'admin',
},
},
];
const fieldsMap = await setupCustomProfileAttributeFields(adminClient, seedAttributes);
const policyName = `Display Name Selector ${pw.random.id()}`;
let policyId: string | null = null;
try {
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
const {page} = systemConsolePage;
await navigateToABACPage(page);
await enableABAC(page);
// # Open the new-policy form
await page.getByRole('button', {name: 'Add policy'}).click();
await page.waitForLoadState('networkidle');
const nameInput = page.locator('#admin\\.access_control\\.policy\\.edit_policy\\.policyName');
await nameInput.waitFor({state: 'visible', timeout: 10000});
await nameInput.fill(policyName);
// # Add an attribute rule and open its selector
const addAttributeButton = page.getByRole('button', {name: /add attribute/i});
await expect(addAttributeButton).toBeEnabled({timeout: 10000});
await addAttributeButton.click();
const attributeButton = page.locator('[data-testid="attributeSelectorMenuButton"]').first();
await attributeButton.waitFor({state: 'visible', timeout: 5000});
const attributeMenu = page.locator('[id^="attribute-selector-menu"]');
if (!(await attributeMenu.isVisible({timeout: 1000}).catch(() => false))) {
await attributeButton.click();
}
await attributeMenu.waitFor({state: 'visible', timeout: 5000});
const deptHeadItem = page.locator('[id^="attribute-selector-menu"] li:has-text("Department Head")');
const officeItem = page.locator('[id^="attribute-selector-menu"] li:has-text("office")');
// * Both fields render: 'Department Head' (display_name) and 'office' (name fallback)
await expect(deptHeadItem).toBeVisible();
await expect(officeItem).toBeVisible();
const filterInput = attributeMenu.locator('.attribute-selector-search input');
await filterInput.waitFor({state: 'visible', timeout: 5000});
// * Filter by display_name keeps only 'Department Head'
await filterInput.fill('department');
await expect(deptHeadItem).toBeVisible();
await expect(officeItem).toHaveCount(0);
// * Filter by CEL identifier keeps only 'Department Head'
await filterInput.fill('');
await filterInput.fill('dept_head');
await expect(deptHeadItem).toBeVisible();
await expect(officeItem).toHaveCount(0);
// * Filter on the no-display_name field's `name` keeps only 'office'
await filterInput.fill('');
await filterInput.fill('office');
await expect(officeItem).toBeVisible();
await expect(deptHeadItem).toHaveCount(0);
// # Select 'Department Head'
await filterInput.fill('');
await deptHeadItem.first().click({force: true});
// * The trigger button shows display_name, not the CEL identifier
await expect(attributeButton).toContainText('Department Head', {timeout: 5000});
// # Wait for the attribute-selector popover to close before opening the next menu
const attributeMenuPopover = page.locator('[id^="attribute-selector-menu"]');
await attributeMenuPopover.waitFor({state: 'hidden', timeout: 5000});
const operatorButton = page.locator('[data-testid="operatorSelectorMenuButton"]').first();
await operatorButton.waitFor({state: 'visible', timeout: 5000});
await operatorButton.click();
const operatorMenu = page.locator('[id^="operator-selector-menu"]');
await operatorMenu.waitFor({state: 'visible', timeout: 5000});
await operatorMenu.locator('li:has-text("is")').first().click();
await fillSingleConditionValue(page, 'engineering');
const saveButton = page.getByRole('button', {name: 'Save'});
await expect(saveButton).toBeEnabled({timeout: 10000});
// # Click Save and wait on the create-policy PUT (button unmounts on navigate)
const createPolicyResponse = page.waitForResponse(
(r) => /\/access_control_policies(?:\?|$)/.test(r.url()) && r.request().method() === 'PUT',
{timeout: 15000},
);
await saveButton.click();
const createResponse = await createPolicyResponse;
expect(createResponse.ok()).toBe(true);
await page.waitForURL(/\/admin_console\/system_attributes\/membership_policies/, {timeout: 10000});
// * The persisted CEL uses the canonical identifier, not display_name
policyId = await getPolicyIdByName(adminClient, policyName);
expect(policyId).not.toBeNull();
const policy = await (adminClient as any).doFetch(
`${adminClient.getBaseRoute()}/access_control_policies/${policyId}`,
{method: 'GET'},
);
const rules = (policy?.rules || []) as Array<{actions?: string[]; expression?: string}>;
const membershipRule = rules.find((r) => r.actions?.includes('membership')) || rules[0];
expect(membershipRule).toBeDefined();
expect(membershipRule?.expression || '').toContain('user.attributes.dept_head');
expect(membershipRule?.expression || '').not.toContain('Department Head');
} finally {
if (policyId) {
try {
await (adminClient as any).doFetch(
`${adminClient.getBaseRoute()}/access_control_policies/${policyId}`,
{method: 'DELETE'},
);
} catch {
// best-effort cleanup
}
}
await deleteCustomProfileAttributes(adminClient, fieldsMap);
}
},
);
});