Skip to content

Commit 806fdb2

Browse files
committed
test(e2e): rewrite policy workbench persistence spec for current UI
Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
1 parent babb91e commit 806fdb2

1 file changed

Lines changed: 86 additions & 175 deletions

File tree

playwright/e2e/policy-workbench-system-default-persistence.spec.ts

Lines changed: 86 additions & 175 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,12 @@ test.describe.configure({ retries: 0, timeout: 45000 })
1313
const openPolicyButtonName = /Manage this setting|Open policy|Open setting policy/i
1414
const createRuleButtonName = /Create rule|Create policy rule/i
1515
const saveRuleButtonName = /Save rule changes|Save changes|Save policy rule changes/i
16-
const createDefaultButtonName = /Create default rule|Create global default rule/i
17-
const editGlobalDefaultButtonName = /Edit default|Edit global default/i
18-
const resetGlobalDefaultButtonName = /Reset default|Reset global default|Reset default rule/i
19-
const newGroupRuleButtonName = /New group rule|New group override/i
20-
const newUserRuleButtonName = /New user rule|New user override/i
21-
const editGroupRuleButtonName = /Edit group rule|Edit group override/i
22-
const deleteGroupRuleButtonName = /Delete group rule|Delete group override/i
23-
const editUserRuleButtonName = /Edit user rule|Edit user override/i
24-
const deleteUserRuleButtonName = /Delete user rule|Delete user override/i
25-
const groupSectionName = /Group rules|Group overrides/i
26-
const userSectionName = /User rules|User overrides/i
27-
const globalEditorHeadingName = /Default rule|Global default rule/i
28-
const inheritValueMessage = /Lower layers must inherit this rule|Lower layers must inherit this value/i
16+
const changeDefaultButtonName = /^Change$/i
17+
const removeExceptionButtonName = /Remove exception|Remove rule/i
18+
const groupRuleTargetLabel = 'admin'
19+
const userRuleTargetLabel = 'policy-e2e-user'
20+
const instanceWideTargetLabel = 'Default (instance-wide)'
21+
const globalEditorHeadingName = /Default rule|Global default rule|Instance default rule/i
2922

3023
async function openSigningOrderDialog(page: Page) {
3124
const manageButtons = page.getByRole('button', { name: openPolicyButtonName })
@@ -45,29 +38,6 @@ async function waitForEditorIdle(dialog: Locator) {
4538
await savingOverlays.first().waitFor({ state: 'hidden', timeout: 10000 }).catch(() => {})
4639
}
4740

48-
async function setAllowOverride(dialog: Locator, enabled: boolean): Promise<boolean> {
49-
const allowOverrideSwitch = dialog.getByLabel('Allow lower layers to override this rule').first()
50-
const label = dialog.getByText('Allow lower layers to override this rule').first()
51-
52-
if (!(await allowOverrideSwitch.count())) {
53-
return false
54-
}
55-
56-
if (enabled) {
57-
if (!(await allowOverrideSwitch.isChecked())) {
58-
await label.click()
59-
}
60-
await expect(allowOverrideSwitch).toBeChecked()
61-
return true
62-
}
63-
64-
if (await allowOverrideSwitch.isChecked()) {
65-
await label.click()
66-
}
67-
await expect(allowOverrideSwitch).not.toBeChecked()
68-
return true
69-
}
70-
7141
async function setSigningFlow(dialog: Locator, flow: 'parallel' | 'ordered_numeric'): Promise<boolean> {
7242
const label = flow === 'parallel'
7343
? /Simultaneous \(Parallel\)/i
@@ -88,39 +58,59 @@ async function setSigningFlow(dialog: Locator, flow: 'parallel' | 'ordered_numer
8858
async function submitRule(dialog: Locator) {
8959
await waitForEditorIdle(dialog)
9060

91-
const createButton = dialog.getByRole('button', { name: createRuleButtonName }).first()
61+
const editorPanel = dialog.locator('.policy-workbench__editor-panel').last()
62+
const createButton = editorPanel.getByRole('button', { name: /Create policy rule/i }).first()
9263
if (await createButton.isVisible().catch(() => false)) {
9364
await expect(createButton).toBeEnabled({ timeout: 8000 })
9465
await createButton.click()
9566
await waitForEditorIdle(dialog)
9667
return
9768
}
9869

99-
const saveButton = dialog.getByRole('button', { name: saveRuleButtonName }).first()
70+
const saveButton = editorPanel.getByRole('button', { name: /Save policy rule changes/i }).first()
10071
await expect(saveButton).toBeVisible({ timeout: 8000 })
10172
await expect(saveButton).toBeEnabled({ timeout: 8000 })
10273
await saveButton.click()
10374
await waitForEditorIdle(dialog)
10475
}
10576

106-
async function openGlobalRuleEditor(dialog: Locator, globalSection: Locator) {
107-
const createDefaultButton = globalSection.getByRole('button', { name: createDefaultButtonName }).first()
108-
if (await createDefaultButton.isVisible().catch(() => false)) {
109-
await createDefaultButton.click()
110-
return
111-
}
77+
function getRuleRow(dialog: Locator, _scope: 'Instance' | 'Group' | 'User', targetLabel: string) {
78+
return dialog.locator('tbody tr').filter({
79+
hasText: targetLabel,
80+
}).first()
81+
}
11282

113-
await globalSection.getByRole('button', { name: editGlobalDefaultButtonName }).first().click()
83+
async function openSystemDefaultEditor(dialog: Locator) {
84+
await dialog.getByRole('button', { name: changeDefaultButtonName }).first().click()
11485
}
11586

116-
async function expectGlobalBaselineState(globalSection: Locator) {
117-
const createDefaultButton = globalSection.getByRole('button', { name: createDefaultButtonName }).first()
118-
if (await createDefaultButton.isVisible().catch(() => false)) {
119-
await expect(createDefaultButton).toBeVisible()
87+
async function openRuleActions(dialog: Locator, scope: 'Instance' | 'Group' | 'User', targetLabel: string) {
88+
const row = getRuleRow(dialog, scope, targetLabel)
89+
await expect(row).toBeVisible({ timeout: 8000 })
90+
await row.getByRole('button', { name: 'Rule actions' }).first().click()
91+
return row
92+
}
93+
94+
async function clickRuleMenuAction(dialog: Locator, actionName: 'Edit' | 'Remove') {
95+
const menuItem = dialog.getByRole('menuitem', { name: actionName }).first()
96+
if (await menuItem.isVisible().catch(() => false)) {
97+
await menuItem.click()
12098
return
12199
}
122100

123-
await expect(globalSection.getByRole('button', { name: editGlobalDefaultButtonName }).first()).toBeVisible()
101+
await dialog.getByText(new RegExp(`^${actionName}$`, 'i')).last().click()
102+
}
103+
104+
async function editRule(dialog: Locator, scope: 'Instance' | 'Group' | 'User', targetLabel: string) {
105+
await openRuleActions(dialog, scope, targetLabel)
106+
await clickRuleMenuAction(dialog, 'Edit')
107+
}
108+
109+
async function removeRule(dialog: Locator, scope: 'Instance' | 'Group' | 'User', targetLabel: string) {
110+
await openRuleActions(dialog, scope, targetLabel)
111+
await clickRuleMenuAction(dialog, 'Remove')
112+
await dialog.getByRole('button', { name: removeExceptionButtonName }).first().click()
113+
await waitForEditorIdle(dialog)
124114
}
125115

126116
async function chooseTarget(dialog: Locator, ariaLabel: 'Target groups' | 'Target users', optionText: string) {
@@ -160,86 +150,23 @@ async function chooseTarget(dialog: Locator, ariaLabel: 'Target groups' | 'Targe
160150
}
161151
}
162152

163-
async function clickSectionAction(section: Locator, actionLabel: string | RegExp) {
164-
let lastError: unknown
165-
166-
for (let attempt = 0; attempt < 5; attempt++) {
167-
const button = section.getByRole('button', { name: actionLabel }).first()
168-
169-
try {
170-
await button.waitFor({ state: 'visible', timeout: 2000 })
171-
await button.scrollIntoViewIfNeeded().catch(() => {})
172-
await button.click({ timeout: 2000 })
173-
return
174-
} catch (error) {
175-
lastError = error
176-
await new Promise((resolve) => setTimeout(resolve, 250))
177-
}
153+
async function ensureRuleAbsent(dialog: Locator, scope: 'Group' | 'User', targetLabel: string) {
154+
const row = getRuleRow(dialog, scope, targetLabel)
155+
if (await row.count()) {
156+
await removeRule(dialog, scope, targetLabel)
157+
await expect(getRuleRow(dialog, scope, targetLabel)).toHaveCount(0)
178158
}
179-
180-
throw lastError instanceof Error
181-
? lastError
182-
: new Error(`Failed to click section action matching "${String(actionLabel)}"`)
183159
}
184160

185-
async function removeRuleWithConfirmation(page: Page, dialog: Locator, section: Locator, actionLabel: string | RegExp) {
186-
const confirmButton = page.getByRole('button', { name: 'Remove rule' }).first()
187-
if (await confirmButton.isVisible().catch(() => false)) {
188-
await confirmButton.click()
189-
await waitForEditorIdle(dialog)
190-
}
191-
192-
await clickSectionAction(section, actionLabel)
193-
194-
const confirmationShown = await confirmButton.waitFor({ state: 'visible', timeout: 4000 }).then(() => true).catch(() => false)
195-
if (confirmationShown) {
196-
await confirmButton.click()
197-
await waitForEditorIdle(dialog)
198-
}
199-
}
200-
201-
async function removeAllRulesByAction(
202-
page: Page,
203-
dialog: Locator,
204-
section: Locator,
205-
actionLabel: string | RegExp,
206-
) {
207-
for (let attempt = 0; attempt < 8; attempt++) {
208-
const currentCount = await section.getByRole('button', { name: actionLabel }).count()
209-
if (!currentCount) {
210-
return
211-
}
212-
213-
await removeRuleWithConfirmation(page, dialog, section, actionLabel)
214-
await waitForEditorIdle(dialog)
215-
await expect.poll(async () => {
216-
return section.getByRole('button', { name: actionLabel }).count()
217-
}, {
218-
timeout: 5000,
219-
}).toBeLessThan(currentCount)
220-
}
221-
222-
throw new Error(`Failed to remove all rules for action "${actionLabel}" after multiple attempts`)
223-
}
224-
225-
async function ensureBaselineRulesForAdminTarget(page: Page, dialog: Locator) {
226-
const globalSection = dialog.getByRole('region', { name: 'Global default rules' })
227-
const groupSection = dialog.getByRole('region', { name: groupSectionName })
228-
const userSection = dialog.getByRole('region', { name: userSectionName })
229-
230-
await removeAllRulesByAction(page, dialog, userSection, deleteUserRuleButtonName)
231-
await removeAllRulesByAction(page, dialog, groupSection, deleteGroupRuleButtonName)
232-
233-
// Normalize global default into a known baseline where lower layers may override.
234-
if (await globalSection.getByRole('button', { name: editGlobalDefaultButtonName }).count()) {
235-
await globalSection.getByRole('button', { name: editGlobalDefaultButtonName }).click()
236-
if (await setAllowOverride(dialog, true)) {
237-
await submitRule(dialog)
238-
}
161+
async function ensureSystemDefaultBaseline(dialog: Locator) {
162+
const customBadge = dialog.getByText(/\(custom\)/i).first()
163+
if (await customBadge.isVisible().catch(() => false)) {
164+
await removeRule(dialog, 'Instance', instanceWideTargetLabel)
165+
await expect(dialog.getByText(/\(system\)/i)).toBeVisible()
239166
}
240167
}
241168

242-
test('system default persists allow-override changes across edit cycles', async ({ page }) => {
169+
test('system default persists across edit cycles and can be reset to the system baseline', async ({ page }) => {
243170
await login(
244171
page.request,
245172
process.env.NEXTCLOUD_ADMIN_USER ?? 'admin',
@@ -252,38 +179,34 @@ test('system default persists allow-override changes across edit cycles', async
252179
await openSigningOrderDialog(page)
253180

254181
const signingOrderDialog = await getSigningOrderDialog(page)
255-
await ensureBaselineRulesForAdminTarget(page, signingOrderDialog)
182+
await ensureSystemDefaultBaseline(signingOrderDialog)
183+
await ensureRuleAbsent(signingOrderDialog, 'Group', groupRuleTargetLabel)
184+
await ensureRuleAbsent(signingOrderDialog, 'User', userRuleTargetLabel)
256185

257-
const globalSection = signingOrderDialog.getByRole('region', { name: 'Global default rules' })
258-
await openGlobalRuleEditor(signingOrderDialog, globalSection)
186+
await openSystemDefaultEditor(signingOrderDialog)
259187
await expect(signingOrderDialog.getByRole('heading', { name: globalEditorHeadingName }).last()).toBeVisible()
260-
await setAllowOverride(signingOrderDialog, true)
188+
expect(await setSigningFlow(signingOrderDialog, 'ordered_numeric'), 'Expected signing-flow radios in system editor').toBe(true)
261189
await submitRule(signingOrderDialog)
190+
await expect(getRuleRow(signingOrderDialog, 'Instance', instanceWideTargetLabel)).toContainText('Sequential')
262191

263-
await signingOrderDialog.getByRole('button', { name: editGlobalDefaultButtonName }).click()
264-
await setAllowOverride(signingOrderDialog, true)
265-
266-
await setAllowOverride(signingOrderDialog, false)
192+
await openSystemDefaultEditor(signingOrderDialog)
193+
expect(await setSigningFlow(signingOrderDialog, 'parallel'), 'Expected signing-flow radios in system editor').toBe(true)
267194
const saveChangesResponsePromise = page.waitForResponse((response) => {
268195
return response.request().method() === 'POST'
269196
&& response.url().includes('/apps/libresign/api/v1/policies/system/signature_flow')
270197
})
271198
await signingOrderDialog.getByRole('button', { name: saveRuleButtonName }).first().click()
272199
const saveChangesResponse = await saveChangesResponsePromise
273200
expect(saveChangesResponse.status(), 'Expected Save changes request to succeed').toBe(200)
201+
await expect(getRuleRow(signingOrderDialog, 'Instance', instanceWideTargetLabel)).toContainText('Simultaneous (Parallel)')
274202

275-
await signingOrderDialog.getByRole('button', { name: editGlobalDefaultButtonName }).click()
276-
await setAllowOverride(signingOrderDialog, false)
277-
278-
await expect(signingOrderDialog.getByText(inheritValueMessage)).toBeVisible()
279-
280-
// Reset should restore inherited baseline behavior.
281-
await removeRuleWithConfirmation(page, signingOrderDialog, globalSection, resetGlobalDefaultButtonName)
282-
await expectGlobalBaselineState(globalSection)
203+
await removeRule(signingOrderDialog, 'Instance', instanceWideTargetLabel)
204+
await expect(signingOrderDialog.getByText(/\(system\)/i)).toBeVisible()
205+
await expect(getRuleRow(signingOrderDialog, 'Instance', instanceWideTargetLabel)).toContainText('Let users choose')
283206
})
284207

285208
test('admin can create, edit, and delete global, group, and user rules from the policy workbench', async ({ page }) => {
286-
const userTarget = 'policy-e2e-user'
209+
const userTarget = userRuleTargetLabel
287210

288211
await ensureUserExists(page.request, userTarget)
289212

@@ -297,66 +220,54 @@ test('admin can create, edit, and delete global, group, and user rules from the
297220
await openSigningOrderDialog(page)
298221

299222
const dialog = await getSigningOrderDialog(page)
300-
const globalSection = dialog.getByRole('region', { name: 'Global default rules' })
301-
const groupSection = dialog.getByRole('region', { name: groupSectionName })
302-
const userSection = dialog.getByRole('region', { name: userSectionName })
303-
304-
await ensureBaselineRulesForAdminTarget(page, dialog)
223+
await ensureSystemDefaultBaseline(dialog)
224+
await ensureRuleAbsent(dialog, 'Group', groupRuleTargetLabel)
225+
await ensureRuleAbsent(dialog, 'User', userTarget)
305226

306227
// Global rule: edit
307-
await openGlobalRuleEditor(dialog, globalSection)
228+
await openSystemDefaultEditor(dialog)
308229
expect(await setSigningFlow(dialog, 'ordered_numeric'), 'Expected signing-flow radios in global editor').toBe(true)
309-
await setAllowOverride(dialog, true)
310-
await submitRule(dialog)
311-
await expect(globalSection.getByRole('button', { name: editGlobalDefaultButtonName })).toBeVisible()
312-
313-
// Global rule: enforce inheritance
314-
await globalSection.getByRole('button', { name: editGlobalDefaultButtonName }).click()
315-
expect(await setAllowOverride(dialog, false), 'Expected global allow-override switch in editor').toBe(true)
316-
await submitRule(dialog)
317-
await expect(globalSection.getByRole('button', { name: editGlobalDefaultButtonName })).toBeVisible()
318-
319-
await globalSection.getByRole('button', { name: editGlobalDefaultButtonName }).click()
320-
expect(await setAllowOverride(dialog, true), 'Expected global allow-override switch in editor').toBe(true)
321230
await submitRule(dialog)
322-
await expect(globalSection.getByRole('button', { name: editGlobalDefaultButtonName })).toBeVisible()
231+
await expect(getRuleRow(dialog, 'Instance', instanceWideTargetLabel)).toContainText('Sequential')
323232

324233
// Group rule: create
325-
await dialog.getByRole('button', { name: newGroupRuleButtonName }).first().click()
234+
await dialog.getByRole('button', { name: 'Create rule' }).first().click()
235+
await dialog.getByRole('option', { name: /Group/i }).click()
326236
await chooseTarget(dialog, 'Target groups', 'admin')
327237
expect(await setSigningFlow(dialog, 'ordered_numeric'), 'Expected signing-flow radios in group editor').toBe(true)
328-
await setAllowOverride(dialog, true)
329238
await submitRule(dialog)
330-
await expect(groupSection.getByRole('button', { name: editGroupRuleButtonName }).first()).toBeVisible()
239+
await expect(getRuleRow(dialog, 'Group', groupRuleTargetLabel)).toContainText('Sequential')
331240

332241
// Group rule: edit
333-
await groupSection.getByRole('button', { name: editGroupRuleButtonName }).first().click()
242+
await editRule(dialog, 'Group', groupRuleTargetLabel)
334243
expect(await setSigningFlow(dialog, 'parallel'), 'Expected signing-flow radios in group editor').toBe(true)
335244
await submitRule(dialog)
336-
await expect(groupSection.getByRole('button', { name: editGroupRuleButtonName }).first()).toBeVisible()
245+
await expect(getRuleRow(dialog, 'Group', groupRuleTargetLabel)).toContainText('Simultaneous (Parallel)')
337246

338247
// User rule: create
339-
await dialog.getByRole('button', { name: newUserRuleButtonName }).first().click()
248+
await dialog.getByRole('button', { name: 'Create rule' }).first().click()
249+
await dialog.getByRole('option', { name: /User/i }).click()
340250
await chooseTarget(dialog, 'Target users', userTarget)
341251
expect(await setSigningFlow(dialog, 'ordered_numeric'), 'Expected signing-flow radios in user editor').toBe(true)
342252
await submitRule(dialog)
343-
await expect(userSection.getByRole('button', { name: editUserRuleButtonName }).first()).toBeVisible()
253+
await expect(getRuleRow(dialog, 'User', userTarget)).toContainText('Sequential')
344254

345255
// User rule: edit
346-
await clickSectionAction(userSection, editUserRuleButtonName)
256+
await editRule(dialog, 'User', userTarget)
347257
expect(await setSigningFlow(dialog, 'parallel'), 'Expected signing-flow radios in user editor').toBe(true)
348258
await submitRule(dialog)
349-
await expect(userSection.getByRole('button', { name: editUserRuleButtonName }).first()).toBeVisible()
259+
await expect(getRuleRow(dialog, 'User', userTarget)).toContainText('Simultaneous (Parallel)')
350260

351261
// User rule: delete
352-
await removeAllRulesByAction(page, dialog, userSection, deleteUserRuleButtonName)
353-
await expect(userSection.getByRole('button', { name: deleteUserRuleButtonName })).toHaveCount(0)
262+
await removeRule(dialog, 'User', userTarget)
263+
await expect(getRuleRow(dialog, 'User', userTarget)).toHaveCount(0)
354264

355265
// Group rule: delete
356-
await removeAllRulesByAction(page, dialog, groupSection, deleteGroupRuleButtonName)
357-
await expect(groupSection.getByRole('button', { name: deleteGroupRuleButtonName })).toHaveCount(0)
266+
await removeRule(dialog, 'Group', groupRuleTargetLabel)
267+
await expect(getRuleRow(dialog, 'Group', groupRuleTargetLabel)).toHaveCount(0)
358268

359269
// Global rule: reset to inherited baseline
360-
await removeRuleWithConfirmation(page, dialog, globalSection, resetGlobalDefaultButtonName)
361-
await expectGlobalBaselineState(globalSection)
270+
await removeRule(dialog, 'Instance', instanceWideTargetLabel)
271+
await expect(dialog.getByText(/\(system\)/i)).toBeVisible()
272+
await expect(getRuleRow(dialog, 'Instance', instanceWideTargetLabel)).toContainText('Let users choose')
362273
})

0 commit comments

Comments
 (0)