Skip to content

Commit b07f4f4

Browse files
committed
test(e2e): fill Phase 1 spec assertions (structural) (UX-1208, UX-1198)
Follows up on skeleton scaffolding by filling every assertion that is verifiable without running the full testcontainer stack. Copy-dependent assertions (exact toast text emitted by formatToastErrorMessageGRPC) are left as // TODO(runtime, UX-1208) markers for the final runtime pass. Assertions added: - user-error-handling: form state preserved after CreateUser failure; URL stays on /create; success sentinel absent. - user-delete-error: seeds a real user via SecurityPage.createUser, mocks DeleteUser failure, asserts user still in list after failed delete. - acl-create-error: uses AclPage + minimal Cluster/DESCRIBE rule; asserts URL stays on /security/acls/create after both invalid_argument and timedout failures. - acl-delete-multi-match: seeds two ACLs across hosts for one principal, executes Delete (ACLs only) flow, asserts both acl-list-item testids are gone. Zero-match case asserts menuitem is hidden OR disabled (product behavior TBD — see TODO). - quota-error: asserts Quotas heading renders even on INTERNAL error; asserts raw 'permission_denied' string does not leak to user on 403. - users-deeplink: direct-load /security/users/e2euser/details; asserts headings + no suspicious console errors (cannot read|undefined|missing). - acl-principal-special-chars: full round-trip for 'User:test-colon-<ts>' through create → detail URL encoding → list → detail click. - users-authorization: full assertions ready behind VIEW_ONLY_FIXTURE_READY flag (UX-1209 unblocks). Runtime TODOs are clearly tagged // TODO(runtime, UX-1208): each is a toast-copy or specific-UI-copy assertion that requires a live backend to verify without guessing. Every other structural guarantee is in place. bun run type:check clean. bun run lint:check 0 errors. Not runtime-verified.
1 parent 50bb9bb commit b07f4f4

8 files changed

Lines changed: 403 additions & 84 deletions

File tree

frontend/tests/test-variant-console-enterprise/users-authorization.spec.ts

Lines changed: 99 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,65 +6,137 @@
66
* to list (all three are PERMISSION_VIEW in the dataplane proto) but MUST NOT be able to
77
* create/update/delete (those are PERMISSION_ADMIN).
88
*
9-
* Blocked on: new view-only auth fixture (playwright/.auth/view-only.json).
10-
* See global-setup.mjs + a new auth-view-only.setup.ts to seed the role + login.
9+
* Blocked on: new view-only auth fixture (playwright/.auth/view-only.json) — see UX-1209.
1110
*/
1211

1312
import { expect, test } from '@playwright/test';
1413

15-
// TODO(UX-1208): once auth fixture lands, replace the storageState below with the
16-
// real path and remove this describe-level `test.skip`.
14+
// TODO(UX-1209): flip to true once the view-only fixture lands. All assertions below are
15+
// ready; only the seeded role and storageState are missing.
1716
const VIEW_ONLY_STORAGE_STATE = 'playwright/.auth/view-only.json';
1817
const VIEW_ONLY_FIXTURE_READY = false;
1918

2019
test.describe('Authorization - Users page (view-only role)', () => {
21-
test.skip(!VIEW_ONLY_FIXTURE_READY, 'view-only auth fixture pending — see UX-1208');
20+
test.skip(!VIEW_ONLY_FIXTURE_READY, 'view-only auth fixture pending — see UX-1209');
2221
test.use({ storageState: VIEW_ONLY_STORAGE_STATE });
2322

24-
test.beforeEach(async ({ page }) => {
23+
test('users list renders for view-only role', async ({ page }) => {
2524
await page.goto('/security/users', { waitUntil: 'domcontentloaded' });
26-
});
2725

28-
test('users list renders for view-only role', async () => {
29-
// TODO: assert /security/users loads without a 403 page (page is reachable)
30-
// TODO: assert users table is visible and contains at least the seed user
26+
// Structural: the page loads (not a 403 / permission-denied boundary).
27+
await expect(page.getByRole('table')).toBeVisible({ timeout: 10_000 });
28+
29+
// At least one row (the seeded view-only user itself should be present).
30+
const rows = page.getByRole('row');
31+
await expect(rows).not.toHaveCount(0);
3132
});
3233

33-
test('create user button is hidden or disabled for view-only role', async () => {
34-
// TODO: assert getByTestId('create-user-button') is not visible OR is disabled
35-
// TODO: direct navigation to /security/users/create should redirect or show permission error
34+
test('create user button is hidden or disabled for view-only role', async ({ page }) => {
35+
await page.goto('/security/users', { waitUntil: 'domcontentloaded' });
36+
37+
// Structural: the Create button is either absent or disabled — both acceptable.
38+
const createButton = page.getByTestId('create-user-button');
39+
const isVisible = await createButton.isVisible().catch(() => false);
40+
if (isVisible) {
41+
await expect(createButton).toBeDisabled();
42+
} else {
43+
await expect(createButton).not.toBeVisible();
44+
}
45+
46+
// Direct navigation: /security/users/create should NOT show a working create form.
47+
await page.goto('/security/users/create', { waitUntil: 'domcontentloaded' });
48+
const submitButton = page.getByTestId('create-user-submit');
49+
const submitVisible = await submitButton.isVisible().catch(() => false);
50+
if (submitVisible) {
51+
await expect(submitButton).toBeDisabled();
52+
}
53+
54+
// TODO(runtime, UX-1208): confirm whether the route redirects away or shows an inline
55+
// permission-denied message; tighten the assertion once product behavior is known.
3656
});
3757

38-
test('delete user button is not available on details page for view-only role', async () => {
39-
// TODO: navigate to a user details page
40-
// TODO: assert the 'Delete user' button is hidden or disabled
58+
test('delete user button is not available on details page for view-only role', async ({ page }) => {
59+
// The seeded view-only user should be visible in the list.
60+
await page.goto('/security/users', { waitUntil: 'domcontentloaded' });
61+
await expect(page.getByRole('table')).toBeVisible({ timeout: 10_000 });
62+
63+
// Click the first user link (whichever exists — we don't care which one).
64+
const firstUserLink = page.locator("a[href^='/security/users/'][href$='/details']").first();
65+
await expect(firstUserLink).toBeVisible();
66+
await firstUserLink.click();
67+
68+
// Structural: Delete button is hidden or disabled.
69+
const deleteBtn = page.getByRole('button', { name: 'Delete user' });
70+
const deleteVisible = await deleteBtn.isVisible().catch(() => false);
71+
if (deleteVisible) {
72+
await expect(deleteBtn).toBeDisabled();
73+
} else {
74+
await expect(deleteBtn).not.toBeVisible();
75+
}
4176
});
4277
});
4378

4479
test.describe('Authorization - ACLs page (view-only role)', () => {
45-
test.skip(!VIEW_ONLY_FIXTURE_READY, 'view-only auth fixture pending — see UX-1208');
80+
test.skip(!VIEW_ONLY_FIXTURE_READY, 'view-only auth fixture pending — see UX-1209');
4681
test.use({ storageState: VIEW_ONLY_STORAGE_STATE });
4782

4883
test('ACLs list is visible but Create ACL is blocked', async ({ page }) => {
4984
await page.goto('/security/acls', { waitUntil: 'domcontentloaded' });
50-
// TODO: assert list renders
51-
// TODO: assert create button is disabled or hidden
52-
// TODO: direct navigation to /security/acls/create should show permission error or redirect
5385
expect(page.url()).toContain('/security/acls');
86+
87+
// Structural: Create ACL button hidden or disabled.
88+
const createAcl = page.getByTestId('create-acls');
89+
const createVisible = await createAcl.isVisible().catch(() => false);
90+
if (createVisible) {
91+
await expect(createAcl).toBeDisabled();
92+
} else {
93+
await expect(createAcl).not.toBeVisible();
94+
}
95+
96+
// Direct-nav: /security/acls/create should not present a working form.
97+
await page.goto('/security/acls/create', { waitUntil: 'domcontentloaded' });
98+
const principalInput = page.getByTestId('shared-principal-input');
99+
const principalVisible = await principalInput.isVisible().catch(() => false);
100+
if (principalVisible) {
101+
await expect(principalInput).toBeDisabled();
102+
}
54103
});
55104

56-
test('DeleteACLs dropdown items are hidden in permissions list for view-only', async () => {
57-
// TODO: goto /security/permissions-list
58-
// TODO: open the row dropdown and assert delete menuitems are absent
105+
test('DeleteACLs dropdown items are hidden in permissions list for view-only', async ({ page }) => {
106+
await page.goto('/security/permissions-list', { waitUntil: 'domcontentloaded' });
107+
108+
// Find any row and open its dropdown.
109+
const firstRow = page.getByRole('row').nth(1); // nth(0) is the header
110+
const rowExists = await firstRow.isVisible().catch(() => false);
111+
test.skip(!rowExists, 'no permission-list rows to exercise; seed required');
112+
113+
await firstRow.getByRole('button').click();
114+
115+
// Structural: all three delete options are absent or disabled for view-only role.
116+
for (const testId of ['delete-user-and-acls', 'delete-user-only', 'delete-acls-only']) {
117+
const item = page.getByTestId(testId);
118+
const isVisible = await item.isVisible().catch(() => false);
119+
if (isVisible) {
120+
await expect(item).toBeDisabled();
121+
} else {
122+
await expect(item).not.toBeVisible();
123+
}
124+
}
59125
});
60126
});
61127

62128
test.describe('Authorization - Quotas page (view-only role)', () => {
63-
test.skip(!VIEW_ONLY_FIXTURE_READY, 'view-only auth fixture pending — see UX-1208');
129+
test.skip(!VIEW_ONLY_FIXTURE_READY, 'view-only auth fixture pending — see UX-1209');
64130
test.use({ storageState: VIEW_ONLY_STORAGE_STATE });
65131

66-
test('quotas table loads for view-only role', async () => {
67-
// TODO: goto /security/quotas, assert column headers visible
68-
// TODO: assert any existing quota rows render without permission errors
132+
test('quotas table loads for view-only role', async ({ page }) => {
133+
await page.goto('/quotas', { waitUntil: 'domcontentloaded' });
134+
135+
// Structural: heading renders.
136+
await expect(page.getByRole('heading', { name: 'Quotas' })).toBeVisible({ timeout: 10_000 });
137+
138+
// Column headers are visible (the table is rendered even if zero quotas).
139+
await expect(page.getByRole('columnheader', { name: 'Type' })).toBeVisible();
140+
await expect(page.getByRole('columnheader', { name: 'Name' })).toBeVisible();
69141
});
70142
});
Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/** biome-ignore-all lint/performance/useTopLevelRegex: e2e test */
12
/**
23
* spec: UX-1208 — Phase 1 e2e test coverage (CRITICAL + HIGH)
34
* parent epic: UX-1198 — REST-to-Connect RPC migration
@@ -7,12 +8,31 @@
78
* through formatToastErrorMessageGRPC.
89
*/
910

10-
import { test } from '@playwright/test';
11+
import { expect, test } from '@playwright/test';
1112

13+
import {
14+
ModeCustom,
15+
OperationTypeAllow,
16+
ResourcePatternTypeLiteral,
17+
ResourceTypeCluster,
18+
type Rule,
19+
} from '../../../src/components/pages/security/shared/acl-model';
1220
import { mockConnectError, mockConnectNetworkFailure, rpcUrl } from '../../shared/connect-mock';
21+
import { AclPage } from '../utils/acl-page';
1322

1423
const ACL_SERVICE = 'redpanda.api.dataplane.v1.ACLService';
1524

25+
const MINIMAL_RULE: Rule = {
26+
id: 0,
27+
resourceType: ResourceTypeCluster,
28+
mode: ModeCustom,
29+
selectorType: ResourcePatternTypeLiteral,
30+
selectorValue: 'kafka-cluster',
31+
operations: {
32+
DESCRIBE: OperationTypeAllow,
33+
},
34+
};
35+
1636
test.describe('ACL creation - Connect RPC error handling', () => {
1737
test('CreateACL INVALID_ARGUMENT surfaces a field-level error', async ({ page }) => {
1838
await mockConnectError({
@@ -21,9 +41,21 @@ test.describe('ACL creation - Connect RPC error handling', () => {
2141
code: 'invalid_argument',
2242
message: 'principal cannot be empty',
2343
});
24-
// TODO: use AclPage to build a minimal valid rule (Cluster + DESCRIBE allow), submit
25-
// TODO: assert page stays on /security/acls/create (no navigation to detail)
26-
// TODO: assert error toast visible with mocked message OR inline field error
44+
45+
const aclPage = new AclPage(page);
46+
await aclPage.goto();
47+
await aclPage.setPrincipal(`err-${Date.now()}`);
48+
await aclPage.setHost('*');
49+
await aclPage.configureRules([MINIMAL_RULE]);
50+
await aclPage.submitForm();
51+
52+
// Structural: mock fails the create, so page must stay on /create and NOT reach detail.
53+
await expect(page).toHaveURL(/\/security\/acls\/create/);
54+
55+
// TODO(runtime, UX-1208): verify exact toast copy for INVALID_ARGUMENT.
56+
// formatToastErrorMessageGRPC runs collectBadRequestDetails + collectViolationDescriptions;
57+
// without field-level details our mock only produces a base message — expect a generic
58+
// "Failed to create ACL due to: principal cannot be empty" or similar.
2759
});
2860

2961
test('CreateACL network timeout keeps user on the form', async ({ page }) => {
@@ -32,8 +64,17 @@ test.describe('ACL creation - Connect RPC error handling', () => {
3264
urlGlob: rpcUrl(ACL_SERVICE, 'CreateACL'),
3365
reason: 'timedout',
3466
});
35-
// TODO: fill + submit form
36-
// TODO: assert URL still contains /security/acls/create
37-
// TODO: assert toast shown
67+
68+
const aclPage = new AclPage(page);
69+
await aclPage.goto();
70+
await aclPage.setPrincipal(`timeout-${Date.now()}`);
71+
await aclPage.setHost('*');
72+
await aclPage.configureRules([MINIMAL_RULE]);
73+
await aclPage.submitForm();
74+
75+
// Structural: URL still /create.
76+
await expect(page).toHaveURL(/\/security\/acls\/create/);
77+
78+
// TODO(runtime, UX-1208): verify toast appears for a transport timeout with no envelope.
3879
});
3980
});

frontend/tests/test-variant-console/acls/acl-delete-multi-match.spec.ts

Lines changed: 91 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,101 @@
88
* matching rows are actually removed.
99
*/
1010

11-
import { test } from '@playwright/test';
11+
import { expect, test } from '@playwright/test';
12+
13+
import {
14+
ModeCustom,
15+
OperationTypeAllow,
16+
ResourcePatternTypeLiteral,
17+
ResourceTypeCluster,
18+
type Rule,
19+
} from '../../../src/components/pages/security/shared/acl-model';
20+
import { AclPage } from '../utils/acl-page';
21+
22+
const MINIMAL_RULE: Rule = {
23+
id: 0,
24+
resourceType: ResourceTypeCluster,
25+
mode: ModeCustom,
26+
selectorType: ResourcePatternTypeLiteral,
27+
selectorValue: 'kafka-cluster',
28+
operations: {
29+
DESCRIBE: OperationTypeAllow,
30+
},
31+
};
1232

1333
test.describe
1434
.serial('ACL multi-match delete', () => {
15-
test('Delete (ACLs only) with multiple hosts deletes all matching rows', async () => {
16-
// TODO: create two ACLs for the same principal with different hosts (reuse multi-host
17-
// pattern from acl.spec.ts — AclPage.setHost('*') then repeat with '1.1.1.1')
18-
// TODO: navigate to /security/permissions-list, filter by principal
19-
// TODO: open row dropdown → 'Delete (ACLs only)'
20-
// TODO: assert confirmation dialog shows the matched count (expected: 2)
21-
// TODO: confirm deletion; reload list and assert both ACLs are gone
35+
test('Delete (ACLs only) with multiple hosts deletes all matching rows', async ({ page }) => {
36+
test.setTimeout(120_000);
37+
const principal = `multi-host-${Date.now()}`;
38+
39+
// Seed two ACLs for the same principal across two hosts.
40+
const aclPage = new AclPage(page);
41+
for (const host of ['*', '1.1.1.1']) {
42+
await aclPage.goto();
43+
await aclPage.setPrincipal(principal);
44+
await aclPage.setHost(host);
45+
await aclPage.configureRules([MINIMAL_RULE]);
46+
await aclPage.submitForm();
47+
await aclPage.waitForDetailPage();
48+
}
49+
50+
// Go to permissions-list, filter, open row dropdown, pick "Delete (ACLs only)".
51+
await page.goto('/security/permissions-list', { waitUntil: 'domcontentloaded' });
52+
await page.getByPlaceholder('Filter by name').fill(principal);
53+
const row = page.getByRole('row').filter({ hasText: principal });
54+
await expect(row).toBeVisible({ timeout: 5000 });
55+
await row.getByRole('button').click();
56+
57+
// Structural: the "Delete (ACLs only)" menuitem exists.
58+
await expect(page.getByTestId('delete-acls-only')).toBeVisible();
59+
await page.getByTestId('delete-acls-only').dispatchEvent('click');
60+
61+
// Confirmation appears — fill principal and confirm.
62+
await expect(page.getByTestId('txt-confirmation-delete')).toBeVisible({ timeout: 5000 });
63+
await page.getByTestId('txt-confirmation-delete').fill(principal);
64+
await page.getByTestId('test-delete-item').click();
65+
66+
// Verify all ACLs for this principal are gone from the ACLs list.
67+
await page.goto('/security/acls', { waitUntil: 'domcontentloaded' });
68+
await page.getByTestId('search-field-input').getByRole('textbox').fill(principal);
69+
await expect(page.getByTestId(`acl-list-item-${principal}-*`)).not.toBeVisible({ timeout: 5000 });
70+
await expect(page.getByTestId(`acl-list-item-${principal}-1.1.1.1`)).not.toBeVisible({ timeout: 5000 });
71+
72+
// TODO(runtime, UX-1208): if the confirmation dialog shows a matched-count string,
73+
// verify it reports 2. Current permissions-list-tab.tsx does not expose a testid for
74+
// the count — may need to add one before this assertion is valid.
2275
});
2376

24-
test('Delete ACLs for a principal with zero ACLs shows a zero-match UX', async () => {
25-
// TODO: create a SCRAM user with no ACLs
26-
// TODO: open row dropdown → 'Delete (ACLs only)'
27-
// TODO: assert appropriate empty/disabled state (exact behavior TBD by product)
77+
test('Delete (ACLs only) is disabled or absent when principal has zero ACLs', async ({ page }) => {
78+
const username = `no-acls-${Date.now()}`;
79+
80+
// Create a SCRAM user with no ACLs.
81+
await page.goto('/security/users', { waitUntil: 'domcontentloaded' });
82+
await expect(page.getByTestId('create-user-button')).toBeEnabled({ timeout: 10_000 });
83+
await page.getByTestId('create-user-button').click();
84+
await page.getByTestId('create-user-name').fill(username);
85+
await page.getByTestId('create-user-submit').click();
86+
await expect(page.getByTestId('user-created-successfully')).toBeVisible();
87+
await page.getByTestId('done-button').click();
88+
89+
await page.goto('/security/permissions-list', { waitUntil: 'domcontentloaded' });
90+
await page.getByPlaceholder('Filter by name').fill(username);
91+
const row = page.getByRole('row').filter({ hasText: username });
92+
await expect(row).toBeVisible({ timeout: 5000 });
93+
await row.getByRole('button').click();
94+
95+
// Structural: for a user with no ACLs, either the delete-acls-only menuitem is hidden,
96+
// or it is present but disabled. Either is acceptable UX.
97+
const deleteAclsOnly = page.getByTestId('delete-acls-only');
98+
const isVisible = await deleteAclsOnly.isVisible().catch(() => false);
99+
if (isVisible) {
100+
await expect(deleteAclsOnly).toBeDisabled();
101+
} else {
102+
await expect(deleteAclsOnly).not.toBeVisible();
103+
}
104+
105+
// TODO(runtime, UX-1208): confirm product-intended behavior (hidden vs disabled).
106+
// Update this test to the single correct assertion once decided.
28107
});
29108
});

0 commit comments

Comments
 (0)