|
| 1 | +/** |
| 2 | + * SPDX-FileCopyrightText: 2026 LibreCode coop and contributors |
| 3 | + * SPDX-License-Identifier: AGPL-3.0-or-later |
| 4 | + */ |
| 5 | + |
| 6 | +/** |
| 7 | + * Scenario: Policies menu visibility follows delegated group rules. |
| 8 | + * |
| 9 | + * 1. (API) Instance admin creates a group policy for GROUP_ID with |
| 10 | + * allowChildOverride:true, so the group admin can manage rules. |
| 11 | + * 2. (Browser) Log in as group admin → "Policies" nav item must be visible. |
| 12 | + * 3. (Browser) Navigate to Policies → see the editable policy card. |
| 13 | + * 4. (Browser) Click "Configure" → setting dialog opens. |
| 14 | + * 5. (Browser) Click "Create rule" inside dialog → scope-selector dialog opens. |
| 15 | + * 6. (API) Instance admin deletes the group policy. |
| 16 | + * 7. (Browser) Reload as group admin → "Policies" nav item must be hidden. |
| 17 | + * |
| 18 | + * All admin-side operations are performed via the OCS API so no admin browser |
| 19 | + * session is needed, keeping the test as fast as possible. |
| 20 | + */ |
| 21 | + |
| 22 | +import { expect, request, test, type APIRequestContext } from '@playwright/test' |
| 23 | +import { login } from '../support/nc-login' |
| 24 | +import { |
| 25 | + ensureGroupExists, |
| 26 | + ensureSubadminOfGroup, |
| 27 | + ensureUserExists, |
| 28 | + ensureUserInGroup, |
| 29 | +} from '../support/nc-provisioning' |
| 30 | + |
| 31 | +// One serial block: a single browser session for the group admin |
| 32 | +// across both phases avoids repeated login overhead. |
| 33 | +test.describe.configure({ mode: 'serial', retries: 0, timeout: 90000 }) |
| 34 | + |
| 35 | +const ADMIN_USER = process.env.NEXTCLOUD_ADMIN_USER ?? 'admin' |
| 36 | +const ADMIN_PASSWORD = process.env.NEXTCLOUD_ADMIN_PASSWORD ?? 'admin' |
| 37 | +const GROUP_ID = 'policy-menu-visibility-group' |
| 38 | +const GROUP_ADMIN = 'policy-menu-visibility-admin' |
| 39 | +const GROUP_ADMIN_PASSWORD = '123456' |
| 40 | + |
| 41 | +// signature_flow: well-known valid values; "Signing order" UI title. |
| 42 | +// Using a system type so we don't need to set up complex JSON values (add_footer). |
| 43 | +const POLICY_KEY = 'signature_flow' |
| 44 | + |
| 45 | +// ─── Admin API helpers (no browser needed) ──────────────────────────────────── |
| 46 | + |
| 47 | +async function makeAdminContext(): Promise<APIRequestContext> { |
| 48 | + return request.newContext({ |
| 49 | + baseURL: process.env.PLAYWRIGHT_BASE_URL ?? 'https://localhost', |
| 50 | + ignoreHTTPSErrors: true, |
| 51 | + extraHTTPHeaders: { |
| 52 | + 'OCS-ApiRequest': 'true', |
| 53 | + Accept: 'application/json', |
| 54 | + Authorization: 'Basic ' + Buffer.from(`${ADMIN_USER}:${ADMIN_PASSWORD}`).toString('base64'), |
| 55 | + 'Content-Type': 'application/json', |
| 56 | + }, |
| 57 | + }) |
| 58 | +} |
| 59 | + |
| 60 | +/** |
| 61 | + * POST /policies/system/{key} — establish the instance-wide default and allow |
| 62 | + * group admins to override it (allowChildOverride: true). |
| 63 | + */ |
| 64 | +async function setSystemPolicy( |
| 65 | + ctx: APIRequestContext, |
| 66 | + value: string | null, |
| 67 | + allowChildOverride: boolean, |
| 68 | +): Promise<void> { |
| 69 | + const resp = await ctx.post( |
| 70 | + `./ocs/v2.php/apps/libresign/api/v1/policies/system/${POLICY_KEY}`, |
| 71 | + { data: { value, allowChildOverride }, failOnStatusCode: false }, |
| 72 | + ) |
| 73 | + expect(resp.status(), `setSystemPolicy: expected 200 but got ${resp.status()}`).toBe(200) |
| 74 | +} |
| 75 | + |
| 76 | +/** |
| 77 | + * PUT /policies/group/{group}/{key} — create/update a rule scoped to GROUP_ID. |
| 78 | + * This increments groupCount in effective-policies so the menu visibility check |
| 79 | + * passes in Settings.vue. |
| 80 | + */ |
| 81 | +async function setGroupPolicy( |
| 82 | + ctx: APIRequestContext, |
| 83 | + value: string, |
| 84 | + allowChildOverride: boolean, |
| 85 | +): Promise<void> { |
| 86 | + const resp = await ctx.put( |
| 87 | + `./ocs/v2.php/apps/libresign/api/v1/policies/group/${GROUP_ID}/${POLICY_KEY}`, |
| 88 | + { data: { value, allowChildOverride }, failOnStatusCode: false }, |
| 89 | + ) |
| 90 | + expect(resp.status(), `setGroupPolicy: expected 200 but got ${resp.status()}`).toBe(200) |
| 91 | +} |
| 92 | + |
| 93 | +/** |
| 94 | + * DELETE /policies/group/{group}/{key} — remove the rule so groupCount drops to 0 |
| 95 | + * and the Policies nav item disappears for the group admin. |
| 96 | + */ |
| 97 | +async function deleteGroupPolicy(ctx: APIRequestContext): Promise<void> { |
| 98 | + await ctx.delete( |
| 99 | + `./ocs/v2.php/apps/libresign/api/v1/policies/group/${GROUP_ID}/${POLICY_KEY}`, |
| 100 | + { failOnStatusCode: false }, |
| 101 | + ) |
| 102 | +} |
| 103 | + |
| 104 | +// ─── Test ───────────────────────────────────────────────────────────────────── |
| 105 | + |
| 106 | +test('policies nav item appears for group admin when a group rule exists, and hides when removed', async ({ page }) => { |
| 107 | + const adminCtx = await makeAdminContext() |
| 108 | + |
| 109 | + try { |
| 110 | + // ── 0. Provision users/groups (idempotent; safe to call on every run) ── |
| 111 | + |
| 112 | + await ensureUserExists(page.request, GROUP_ADMIN, GROUP_ADMIN_PASSWORD) |
| 113 | + await ensureGroupExists(page.request, GROUP_ID) |
| 114 | + await ensureUserInGroup(page.request, GROUP_ADMIN, GROUP_ID) |
| 115 | + // subadmin role → can_manage_group_policies: true in initial state |
| 116 | + await ensureSubadminOfGroup(page.request, GROUP_ADMIN, GROUP_ID) |
| 117 | + |
| 118 | + // ── 1. Admin: create group policy ───────────────────────────────────── |
| 119 | + // |
| 120 | + // System policy must allow child overrides so the workbench unlocks the |
| 121 | + // "Create rule" button for the group admin. |
| 122 | + await setSystemPolicy(adminCtx, 'parallel', true) |
| 123 | + // Group-scoped rule → groupCount becomes ≥ 1 in the effective-policies |
| 124 | + // API response seen by the group admin. |
| 125 | + await setGroupPolicy(adminCtx, 'ordered_numeric', true) |
| 126 | + |
| 127 | + // ── 2. Log in as group admin ─────────────────────────────────────────── |
| 128 | + |
| 129 | + await login(page.request, GROUP_ADMIN, GROUP_ADMIN_PASSWORD) |
| 130 | + |
| 131 | + // Preferences page mounts Preferences.vue which calls fetchEffectivePolicies() |
| 132 | + // on onMounted, populating the Pinia store that Settings.vue reads reactively. |
| 133 | + await page.goto('./apps/libresign/f/preferences') |
| 134 | + |
| 135 | + // ── 3. "Policies" must appear in the settings sidebar ───────────────── |
| 136 | + |
| 137 | + const policiesNavItem = page.getByRole('link', { name: 'Policies' }) |
| 138 | + await expect(policiesNavItem, 'Policies link should be visible when a delegated group rule exists').toBeVisible({ timeout: 20000 }) |
| 139 | + |
| 140 | + // ── 4. Navigate to the Policies page ────────────────────────────────── |
| 141 | + |
| 142 | + await policiesNavItem.click() |
| 143 | + await expect(page).toHaveURL(/\/f\/policies/, { timeout: 10000 }) |
| 144 | + |
| 145 | + // ── 5. The editable policy card must be visible in the workbench ────── |
| 146 | + // |
| 147 | + // In group-admin viewMode, only policies satisfying |
| 148 | + // groupCount > 0 AND editableByCurrentActor === true |
| 149 | + // are rendered. "Signing order" (signature_flow) matches because we set |
| 150 | + // system allowChildOverride:true and a group rule for GROUP_ID. |
| 151 | + |
| 152 | + const configureButton = page |
| 153 | + .getByRole('button', { name: /Configure/i }) |
| 154 | + .first() |
| 155 | + await expect(configureButton, 'At least one Configure button should be visible for the group admin').toBeVisible({ timeout: 15000 }) |
| 156 | + |
| 157 | + // ── 6. Open the setting dialog ("Signing order") ────────────────────── |
| 158 | + |
| 159 | + await configureButton.click() |
| 160 | + |
| 161 | + const settingDialog = page.getByRole('dialog', { name: /Signing order/i }) |
| 162 | + await expect(settingDialog, '"Signing order" dialog should open on click').toBeVisible({ timeout: 10000 }) |
| 163 | + |
| 164 | + // ── 7. "Create rule" button must be available inside the dialog ─────── |
| 165 | + |
| 166 | + const createRuleButton = settingDialog.getByRole('button', { name: /Create rule/i }) |
| 167 | + await expect(createRuleButton, '"Create rule" button should be enabled in the policy dialog').toBeVisible() |
| 168 | + await expect(createRuleButton).toBeEnabled() |
| 169 | + |
| 170 | + // ── 8. Clicking "Create rule" opens the scope-selector ("create policy modal") ── |
| 171 | + |
| 172 | + await createRuleButton.click() |
| 173 | + |
| 174 | + // The scope-selector dialog title is "What do you want to create?" |
| 175 | + // (falls back to "Create rule" if the workbench skips the selector) |
| 176 | + const createPolicyDialog = page |
| 177 | + .getByRole('dialog', { name: /What do you want to create\?|Create rule/i }) |
| 178 | + .last() |
| 179 | + await expect(createPolicyDialog, 'Create-policy modal should appear after clicking Create rule').toBeVisible({ timeout: 10000 }) |
| 180 | + |
| 181 | + // Close with Escape — no actual rule is created |
| 182 | + await page.keyboard.press('Escape') |
| 183 | + await expect(createPolicyDialog).toBeHidden({ timeout: 5000 }) |
| 184 | + |
| 185 | + // ── 9. Admin: remove the group policy ───────────────────────────────── |
| 186 | + |
| 187 | + await deleteGroupPolicy(adminCtx) |
| 188 | + |
| 189 | + // ── 10. Reload as group admin to refresh effective-policies state ────── |
| 190 | + // |
| 191 | + // A full navigation re-triggers fetchEffectivePolicies() in Preferences.vue, |
| 192 | + // causing Settings.vue's hasDelegatedPolicies computed to update reactively. |
| 193 | + |
| 194 | + await page.goto('./apps/libresign/f/preferences') |
| 195 | + |
| 196 | + // ── 11. "Policies" must no longer appear in the settings sidebar ─────── |
| 197 | + |
| 198 | + await expect(page.getByRole('link', { name: 'Policies' }), 'Policies link should be gone after the group rule is removed').toBeHidden({ timeout: 20000 }) |
| 199 | + } finally { |
| 200 | + // Always restore the environment so other tests are not affected. |
| 201 | + await deleteGroupPolicy(adminCtx).catch(() => {}) |
| 202 | + await setSystemPolicy(adminCtx, null, true).catch(() => {}) |
| 203 | + await adminCtx.dispose() |
| 204 | + } |
| 205 | +}) |
0 commit comments