From b1f360c3e711cca98ab098074fbae8dc07820ffa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A1n=20Vor=C4=8D=C3=A1k?= Date: Wed, 20 May 2026 23:53:42 +0200 Subject: [PATCH 1/4] Add E2E tests for new security page and fix testid wiring - Add enterprise E2E tests for new security users and roles pages (security-users.spec.ts, security-roles.spec.ts, utils/security-page-new.ts) - Fix Combobox testid: use inputTestId instead of testId so the attribute lands on the element, not the PopoverPrimitive.Root wrapper - Fix Input testid in ChangePasswordModal: use testId prop instead of data-testid attribute to avoid being overridden by the component's spread - Add save-password-button testid to the ChangePasswordModal submit button --- frontend/src/components/constants.ts | 2 +- .../security/roles/role-create-dialog.tsx | 6 +- .../security/roles/role-detail-page-new.tsx | 2 +- .../pages/security/tabs/users-tab-new.tsx | 9 +- .../pages/security/users/add-acl-dialog.tsx | 2 +- .../pages/security/users/user-details-new.tsx | 8 +- .../pages/security/users/user-edit-modals.tsx | 3 +- .../security/security-roles.spec.ts | 106 +++++++++++ .../security/security-users.spec.ts | 112 +++++++++++ .../security/utils/security-page-new.ts | 174 ++++++++++++++++++ 10 files changed, 416 insertions(+), 8 deletions(-) create mode 100644 frontend/tests/test-variant-console-enterprise/security/security-roles.spec.ts create mode 100644 frontend/tests/test-variant-console-enterprise/security/security-users.spec.ts create mode 100644 frontend/tests/test-variant-console-enterprise/security/utils/security-page-new.ts diff --git a/frontend/src/components/constants.ts b/frontend/src/components/constants.ts index 779fefbfbe..d888613253 100644 --- a/frontend/src/components/constants.ts +++ b/frontend/src/components/constants.ts @@ -16,7 +16,7 @@ export const FEATURE_FLAGS = { enableNewPipelineLogs: false, enablePipelineDiagrams: false, enableConnectSlashMenu: false, - enableNewSecurityPage: true, + enableNewSecurityPage: false, enableTeamsBridge: false, }; diff --git a/frontend/src/components/pages/security/roles/role-create-dialog.tsx b/frontend/src/components/pages/security/roles/role-create-dialog.tsx index 70a9f23649..947988ce17 100644 --- a/frontend/src/components/pages/security/roles/role-create-dialog.tsx +++ b/frontend/src/components/pages/security/roles/role-create-dialog.tsx @@ -97,7 +97,11 @@ export const RoleCreateDialog = ({ open, onOpenChange }: RoleCreateDialogProps) - diff --git a/frontend/src/components/pages/security/roles/role-detail-page-new.tsx b/frontend/src/components/pages/security/roles/role-detail-page-new.tsx index 4bf9098dc3..e0ce091ef4 100644 --- a/frontend/src/components/pages/security/roles/role-detail-page-new.tsx +++ b/frontend/src/components/pages/security/roles/role-detail-page-new.tsx @@ -121,10 +121,10 @@ export const RoleDetailPageNew = () => { className="w-56" clearable={false} disabled={isSubmitting} + inputTestId="add-principal-combobox" onChange={addMember} options={availablePrincipalOptions} placeholder="Add a principal..." - testId="add-principal-combobox" value="" /> } diff --git a/frontend/src/components/pages/security/tabs/users-tab-new.tsx b/frontend/src/components/pages/security/tabs/users-tab-new.tsx index 19f995c27f..94812e9cd9 100644 --- a/frontend/src/components/pages/security/tabs/users-tab-new.tsx +++ b/frontend/src/components/pages/security/tabs/users-tab-new.tsx @@ -556,12 +556,18 @@ const UserActions = ({ user }: { user: PrincipalEntry }) => { - { e.stopPropagation(); setIsChangePasswordModalOpen(true); @@ -571,6 +577,7 @@ const UserActions = ({ user }: { user: PrincipalEntry }) => { Change password { e.stopPropagation(); setIsDeleteModalOpen(true); diff --git a/frontend/src/components/pages/security/users/add-acl-dialog.tsx b/frontend/src/components/pages/security/users/add-acl-dialog.tsx index 0b7136236d..054197887a 100644 --- a/frontend/src/components/pages/security/users/add-acl-dialog.tsx +++ b/frontend/src/components/pages/security/users/add-acl-dialog.tsx @@ -459,7 +459,7 @@ export const AddAclDialog = ({ open, onOpenChange, principal }: AddAclDialogProp - diff --git a/frontend/src/components/pages/security/users/user-details-new.tsx b/frontend/src/components/pages/security/users/user-details-new.tsx index e2b57ea09b..45b70654a7 100644 --- a/frontend/src/components/pages/security/users/user-details-new.tsx +++ b/frontend/src/components/pages/security/users/user-details-new.tsx @@ -123,12 +123,16 @@ export const UserDetailsPageNew = ({ userName }: UserDetailsPageProps) => {
- {Boolean(isServiceAccount) && ( - diff --git a/frontend/src/components/pages/security/users/user-edit-modals.tsx b/frontend/src/components/pages/security/users/user-edit-modals.tsx index 2824d6d5b6..3b2c257975 100644 --- a/frontend/src/components/pages/security/users/user-edit-modals.tsx +++ b/frontend/src/components/pages/security/users/user-edit-modals.tsx @@ -85,9 +85,9 @@ export const ChangePasswordModal = ({ userName, isOpen, setIsOpen }: ChangePassw
setPassword(e.target.value)} + testId="create-user-password" type="password" value={password} /> @@ -150,6 +150,7 @@ export const ChangePasswordModal = ({ userName, isOpen, setIsOpen }: ChangePassw Cancel )} {principal && ( - )} @@ -374,7 +374,12 @@ export const AclsCard = ({ acls, principal, isLoading }: AclsCardProps) => { - diff --git a/frontend/src/components/pages/security/users/user-roles-card-new.tsx b/frontend/src/components/pages/security/users/user-roles-card-new.tsx index deaee09872..52650cecb4 100644 --- a/frontend/src/components/pages/security/users/user-roles-card-new.tsx +++ b/frontend/src/components/pages/security/users/user-roles-card-new.tsx @@ -159,10 +159,10 @@ export const UserRolesCardNew = ({ roles, userName, isLoading }: UserRolesCardNe ) : undefined diff --git a/frontend/tests/test-variant-console-enterprise/security/security-users-advanced.spec.ts b/frontend/tests/test-variant-console-enterprise/security/security-users-advanced.spec.ts new file mode 100644 index 0000000000..cb39be1402 --- /dev/null +++ b/frontend/tests/test-variant-console-enterprise/security/security-users-advanced.spec.ts @@ -0,0 +1,152 @@ +/** biome-ignore-all lint/performance/useTopLevelRegex: this is a test */ +import { generateSecurityName, SecurityPageNew } from './utils/security-page-new'; +import { expect, test } from '../fixtures'; + +test.use({ featureFlags: { enableNewSecurityPage: true } }); + +test.describe('Security › Users › advanced', () => { + // ── Role assignment during user creation ───────────────────────────────── + + test('role selected at creation appears in user details', async ({ page }) => { + const roleName = generateSecurityName('role'); + const userName = generateSecurityName('usr'); + const sp = new SecurityPageNew(page); + + await sp.createRole(roleName); + await sp.createUserWithRole(userName, roleName); + + await test.step('verify role shown in user details Roles card', async () => { + await sp.gotoUserDetails(userName); + await expect(page.getByTestId(`role-name-${roleName}`)).toBeVisible({ timeout: 10_000 }); + }); + + await sp.deleteUserFromDetails(userName); + await sp.deleteRoleFromList(roleName); + }); + + // ── Allow all operations ────────────────────────────────────────────────── + + test('Allow all operations creates ACLs for all resource types', async ({ page }) => { + const userName = generateSecurityName('usr'); + const sp = new SecurityPageNew(page); + + await sp.createUser(userName); + await sp.gotoUserDetails(userName); + + await sp.allowAllOperations(); + + await test.step('verify ACL rows appear for all resources', async () => { + // Wait for at least 4 Allow permission cells (Topic/Group/Cluster/TransactionalId) + const allowCells = page.getByRole('cell', { name: 'Allow' }); + await expect(allowCells.first()).toBeVisible({ timeout: 10_000 }); + await expect(allowCells).toHaveCount(4); + // Each row should show "All" operation + await expect(page.getByRole('cell', { name: 'All' }).first()).toBeVisible(); + }); + + await sp.deleteUserFromDetails(userName); + }); + + // ── Assign / remove role from user details page ─────────────────────────── + + test('assign role from user details', async ({ page }) => { + const roleName = generateSecurityName('role'); + const userName = generateSecurityName('usr'); + const sp = new SecurityPageNew(page); + + await sp.createRole(roleName); + await sp.createUser(userName); + await sp.gotoUserDetails(userName); + + await sp.assignRoleFromDetails(roleName); + + await test.step('verify role row visible', async () => { + await expect(page.getByTestId(`role-name-${roleName}`)).toBeVisible({ timeout: 10_000 }); + }); + + await sp.deleteUserFromDetails(userName); + await sp.deleteRoleFromList(roleName); + }); + + test('remove role from user details', async ({ page }) => { + const roleName = generateSecurityName('role'); + const userName = generateSecurityName('usr'); + const sp = new SecurityPageNew(page); + + await sp.createRole(roleName); + await sp.createUser(userName); + await sp.gotoUserDetails(userName); + await sp.assignRoleFromDetails(roleName); + + await sp.removeRoleFromDetails(roleName); + + await test.step('verify role row gone', async () => { + await expect(page.getByTestId(`role-name-${roleName}`)).not.toBeVisible({ timeout: 10_000 }); + }); + + await sp.deleteUserFromDetails(userName); + await sp.deleteRoleFromList(roleName); + }); + + // ── Role membership visible in Permissions page ─────────────────────────── + + test('user with role shows role-based ACLs in Permissions page', async ({ page }) => { + const roleName = generateSecurityName('role'); + const userName = generateSecurityName('usr'); + const topicName = generateSecurityName('topic'); + const sp = new SecurityPageNew(page); + + await sp.createRole(roleName); + + // Add an ACL to the role so it has something to show + await sp.gotoRoleDetails(roleName); + await test.step('add ACL to role', async () => { + await page.getByTestId('add-acl-button').click(); + await page.getByRole('dialog', { name: 'Add ACL' }).waitFor({ state: 'visible' }); + await page.getByPlaceholder('e.g. my-topic').fill(topicName); + await page.getByTestId('add-acl-submit-button').click(); + await page.getByRole('dialog').waitFor({ state: 'hidden' }); + }); + + // Create user and assign the role + await sp.createUser(userName); + await sp.gotoUserDetails(userName); + await sp.assignRoleFromDetails(roleName); + + await test.step('permissions page shows user row', async () => { + await page.goto('/security/permissions', { waitUntil: 'domcontentloaded' }); + await page.getByTestId(`row-${userName}`).waitFor({ state: 'visible', timeout: 15_000 }); + await expect(page.getByTestId(`row-${userName}`)).toBeVisible(); + }); + + await sp.deleteUserFromDetails(userName); + await sp.deleteRoleFromList(roleName); + }); + + // ── SASL mechanism is preserved in confirmation step ───────────────────── + + test('SCRAM-SHA-512 mechanism set during creation shows in user list', async ({ page }) => { + const userName = generateSecurityName('usr'); + const sp = new SecurityPageNew(page); + + await sp.gotoUsers(); + await page.getByTestId('create-user-button').click(); + await page.getByTestId('create-user-name').waitFor({ state: 'visible' }); + await page.getByLabel('Username').fill(userName); + + // Switch from default SCRAM-SHA-256 to SCRAM-SHA-512 + await page.getByRole('combobox').click(); + await page.getByRole('option', { name: 'SCRAM-SHA-512' }).click(); + + await page.getByTestId('create-user-submit').click(); + await page.getByTestId('done-button').waitFor({ state: 'visible' }); + + await test.step('mechanism label shown in user list', async () => { + await sp.gotoUsers(); + await sp.filterUsers(userName); + await expect(page.getByText('SCRAM-SHA-512')).toBeVisible({ timeout: 10_000 }); + }); + + await sp.deleteUserFromList(userName); + }); +}); diff --git a/frontend/tests/test-variant-console-enterprise/security/utils/security-page-new.ts b/frontend/tests/test-variant-console-enterprise/security/utils/security-page-new.ts index 6fb244dede..6aadaf6a0c 100644 --- a/frontend/tests/test-variant-console-enterprise/security/utils/security-page-new.ts +++ b/frontend/tests/test-variant-console-enterprise/security/utils/security-page-new.ts @@ -171,4 +171,57 @@ export class SecurityPageNew { async removePrincipalFromRole(principalName: string) { await this.page.getByTestId(`remove-user-${principalName}-button`).click(); } + + // --- Users: advanced --- + + async createUserWithRole(name: string, roleName: string) { + return test.step(`Create user "${name}" with role "${roleName}"`, async () => { + await this.gotoUsers(); + await this.page.getByTestId('create-user-button').click(); + await this.page.getByTestId('create-user-name').waitFor({ state: 'visible' }); + await this.page.getByLabel('Username').fill(name); + + // Open the role multi-select and pick the role + await this.page.getByText('Select roles...').click(); + await this.page.getByPlaceholder('Search...').fill(roleName); + await this.page.getByRole('option', { name: roleName }).click(); + // Close the popover by pressing Escape + await this.page.keyboard.press('Escape'); + + await this.page.getByTestId('create-user-submit').click(); + await this.page.getByTestId('done-button').waitFor({ state: 'visible' }); + }); + } + + // Clicks "Allow all operations" on the current user details page and confirms. + async allowAllOperations() { + return test.step('Allow all operations', async () => { + await this.page.getByTestId('allow-all-operations-button').click(); + await this.page.getByRole('dialog', { name: 'Allow all operations' }).waitFor({ state: 'visible' }); + await this.page.getByTestId('confirm-allow-all-button').click(); + await this.page.getByRole('dialog').waitFor({ state: 'hidden' }); + }); + } + + // Assigns a role to the user from the user details page. + async assignRoleFromDetails(roleName: string) { + return test.step(`Assign role "${roleName}" from user details`, async () => { + const combobox = this.page.getByTestId('assign-role-combobox'); + await combobox.click(); + await combobox.fill(roleName); + await this.page.getByRole('option', { name: roleName }).click(); + // Wait for the role row to appear + await this.page.getByTestId(`role-name-${roleName}`).waitFor({ state: 'visible' }); + }); + } + + // Removes a role from the user on the user details page. + async removeRoleFromDetails(roleName: string) { + return test.step(`Remove role "${roleName}" from user details`, async () => { + await this.page.getByTestId(`remove-role-${roleName}`).click(); + await this.page.getByTestId('confirm-remove-role-button').waitFor({ state: 'visible' }); + await this.page.getByTestId('confirm-remove-role-button').click(); + await this.page.getByTestId(`role-name-${roleName}`).waitFor({ state: 'hidden' }); + }); + } } diff --git a/frontend/tests/test-variant-kafka/fixtures.ts b/frontend/tests/test-variant-kafka/fixtures.ts new file mode 100644 index 0000000000..c323ae866d --- /dev/null +++ b/frontend/tests/test-variant-kafka/fixtures.ts @@ -0,0 +1,19 @@ +import { test as base } from '@playwright/test'; + +type CustomFixtures = { + featureFlags: Record; +}; + +export const test = base.extend({ + featureFlags: [{}, { option: true }], + page: async ({ page, featureFlags }, use) => { + if (Object.keys(featureFlags).length > 0) { + await page.addInitScript((flags: Record) => { + (window as Window & { __E2E_FEATURE_FLAGS__?: Record }).__E2E_FEATURE_FLAGS__ = flags; + }, featureFlags); + } + await use(page); + }, +}); + +export { expect } from '@playwright/test'; diff --git a/frontend/tests/test-variant-kafka/security/security-kafka.spec.ts b/frontend/tests/test-variant-kafka/security/security-kafka.spec.ts new file mode 100644 index 0000000000..f40eb5a3d5 --- /dev/null +++ b/frontend/tests/test-variant-kafka/security/security-kafka.spec.ts @@ -0,0 +1,116 @@ +/** biome-ignore-all lint/performance/useTopLevelRegex: this is a test */ +import { expect, test } from '../fixtures'; + +// ── Old security layout (no feature flag: Kafka default) ────────────────────── + +test.describe('Security (Kafka) › old layout', () => { + test('navigates to ACLs page by default', async ({ page }) => { + await page.goto('/security', { waitUntil: 'domcontentloaded' }); + await page.waitForURL('**/security/acls**'); + }); + + test('Roles tab is disabled', async ({ page }) => { + await page.goto('/security/acls', { waitUntil: 'domcontentloaded' }); + const rolesTab = page.getByRole('tab', { name: 'Roles' }); + await rolesTab.waitFor({ state: 'visible' }); + await expect(rolesTab).toBeDisabled(); + }); + + test('Users tab is disabled', async ({ page }) => { + await page.goto('/security/acls', { waitUntil: 'domcontentloaded' }); + const usersTab = page.getByRole('tab', { name: 'Users' }); + await usersTab.waitFor({ state: 'visible' }); + await expect(usersTab).toBeDisabled(); + }); + + test('ACLs tab is enabled', async ({ page }) => { + await page.goto('/security/acls', { waitUntil: 'domcontentloaded' }); + const aclsTab = page.getByRole('tab', { name: 'ACLs' }); + await aclsTab.waitFor({ state: 'visible' }); + await expect(aclsTab).not.toBeDisabled(); + }); + + test('clicking disabled Roles tab does not navigate', async ({ page }) => { + await page.goto('/security/acls', { waitUntil: 'domcontentloaded' }); + await page.getByRole('tab', { name: 'Roles' }).waitFor({ state: 'visible' }); + await page.getByRole('tab', { name: 'Roles' }).click({ force: true }); + await expect(page).toHaveURL(/\/security\/acls/); + }); +}); + +// ── New security layout (enableNewSecurityPage: true) ───────────────────────── + +test.describe('Security (Kafka) › new layout', () => { + test.use({ featureFlags: { enableNewSecurityPage: true } }); + + test('navigates to users page by default', async ({ page }) => { + await page.goto('/security', { waitUntil: 'domcontentloaded' }); + await page.waitForURL('**/security/users**'); + }); + + test('Roles tab is disabled with tooltip', async ({ page }) => { + await page.goto('/security/users', { waitUntil: 'domcontentloaded' }); + const rolesTab = page.getByRole('tab', { name: 'Roles' }); + await rolesTab.waitFor({ state: 'visible' }); + await expect(rolesTab).toBeDisabled(); + + // Tooltip shows why Roles are unavailable + await rolesTab.hover(); + await expect(page.getByText('Roles are not supported by your cluster.')).toBeVisible({ timeout: 5000 }); + }); + + test('clicking disabled Roles tab does not navigate', async ({ page }) => { + await page.goto('/security/users', { waitUntil: 'domcontentloaded' }); + await page.getByRole('tab', { name: 'Roles' }).waitFor({ state: 'visible' }); + await page.getByRole('tab', { name: 'Roles' }).click({ force: true }); + // Still on users page + await expect(page).toHaveURL(/\/security\/users/); + }); + + test('Users tab is disabled — no SCRAM user management in Kafka', async ({ page }) => { + await page.goto('/security/users', { waitUntil: 'domcontentloaded' }); + const usersTab = page.getByRole('tab', { name: 'Users' }); + await usersTab.waitFor({ state: 'visible' }); + await expect(usersTab).toBeDisabled(); + }); + + test('no Create user button on users page', async ({ page }) => { + await page.goto('/security/users', { waitUntil: 'domcontentloaded' }); + await page.waitForLoadState('networkidle'); + await expect(page.getByTestId('create-user-button')).not.toBeVisible(); + }); + + test('Roles tab is disabled on Permissions page', async ({ page }) => { + await page.goto('/security/permissions', { waitUntil: 'domcontentloaded' }); + const rolesTab = page.getByRole('tab', { name: 'Roles' }); + await rolesTab.waitFor({ state: 'visible' }); + await expect(rolesTab).toBeDisabled(); + }); + + test('Create ACL button is available on Permissions page', async ({ page }) => { + await page.goto('/security/permissions', { waitUntil: 'domcontentloaded' }); + const createBtn = page.getByRole('button', { name: 'Create ACL' }); + await createBtn.waitFor({ state: 'visible' }); + await expect(createBtn).not.toBeDisabled(); + }); + + test('can open Add ACL dialog and set a user principal', async ({ page }) => { + await page.goto('/security/permissions', { waitUntil: 'domcontentloaded' }); + await page.getByRole('button', { name: 'Create ACL' }).click(); + + // Dialog opens + await page.getByRole('dialog', { name: 'Add ACL' }).waitFor({ state: 'visible' }); + + // User principal input is shown (creatable combobox) + const principalInput = page.getByPlaceholder('Select or type a user...'); + await principalInput.waitFor({ state: 'visible' }); + await principalInput.click(); + await principalInput.fill('e2e-kafka-principal'); + // Confirm option appears (creatable) + await page.getByText('Create "e2e-kafka-principal"').waitFor({ state: 'visible' }); + + // Close without submitting + await page.keyboard.press('Escape'); + await page.getByRole('dialog').waitFor({ state: 'hidden' }); + }); +}); From 53e7e65e3ca6d1c1bc7df905240d431dcef607aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A1n=20Vor=C4=8D=C3=A1k?= Date: Thu, 21 May 2026 14:52:40 +0200 Subject: [PATCH 3/4] tests: fix Allow all operations assertion to handle Schema Registry ACLs Enterprise variant enables schemaRegistryACLApi, producing 6 Allow cells (not 4). Use nth(3) instead of toHaveCount(4) to assert at least 4 exist. --- .../security/security-users-advanced.spec.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/tests/test-variant-console-enterprise/security/security-users-advanced.spec.ts b/frontend/tests/test-variant-console-enterprise/security/security-users-advanced.spec.ts index cb39be1402..52cd09d8e5 100644 --- a/frontend/tests/test-variant-console-enterprise/security/security-users-advanced.spec.ts +++ b/frontend/tests/test-variant-console-enterprise/security/security-users-advanced.spec.ts @@ -36,10 +36,11 @@ test.describe('Security › Users › advanced', () => { await sp.allowAllOperations(); await test.step('verify ACL rows appear for all resources', async () => { - // Wait for at least 4 Allow permission cells (Topic/Group/Cluster/TransactionalId) + // Wait for at least 4 Allow permission cells (Topic/Group/Cluster/TransactionalId, + // plus Subject/SchemaRegistry when schemaRegistryACLApi is enabled) const allowCells = page.getByRole('cell', { name: 'Allow' }); await expect(allowCells.first()).toBeVisible({ timeout: 10_000 }); - await expect(allowCells).toHaveCount(4); + await expect(allowCells.nth(3)).toBeVisible(); // Each row should show "All" operation await expect(page.getByRole('cell', { name: 'All' }).first()).toBeVisible(); }); From 9bd73dce221aaa573e61fb061894ad1f17d40a80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A1n=20Vor=C4=8D=C3=A1k?= Date: Thu, 21 May 2026 15:47:55 +0200 Subject: [PATCH 4/4] Revert: Enabled security page flag by default --- frontend/src/components/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/constants.ts b/frontend/src/components/constants.ts index d888613253..779fefbfbe 100644 --- a/frontend/src/components/constants.ts +++ b/frontend/src/components/constants.ts @@ -16,7 +16,7 @@ export const FEATURE_FLAGS = { enableNewPipelineLogs: false, enablePipelineDiagrams: false, enableConnectSlashMenu: false, - enableNewSecurityPage: false, + enableNewSecurityPage: true, enableTeamsBridge: false, };