Files
RenewCMS/features/roles_permissions_spa_foundation.qa.md
T

8.8 KiB

QA procedure: SPA foundation for roles and permissions (slice 5)

Spec: features/roles_permissions_spa_foundation.feature. Everything below is Vitest + Vue Test Utils under task test:web with /v1 mocked through the existing kits. No live server, no real SMTP.

Honesty rules

  1. The runner already exists (web/vitest.config.ts, test:web in Taskfile.yml, vitest and @vue/test-utils in web/package.json). First run task test:web on the unmodified tree and record exit code and failing specs. That is the baseline.
  2. If the runner cannot execute (bun missing, install failing), every SPA item is reported NOT RUN, never passing. A green Go suite never counts for the SPA.
  3. Report each edited spec, and which ones already failed in the baseline.
  4. The backend change (created_by on article responses for signed-in callers) is part of this slice and is covered by Go tests in test/; own-scope article specs are normal specs, not todo.
  5. The sentence in CLAUDE.md saying the runner is added by the first SPA slice is stale; the coordinator updates it.

Shared helper

web/src/test/access.ts (new) builds the Owner (34, all), Admin (29, no owner-only key), Writer (9 grants of roles_permissions_foundation.feature), custom and none presets. Kits keep their role option and call it; no spec writes a raw role string. Legacy mapping: owner, admin, member and user become Owner, Admin, Writer and no-grants; undefined becomes no access.

Automated (Vitest)

Spec Cases
stores/tests/permissions.spec.ts (new, replaces sessionRoles.spec.ts) every row of the can() table; scopeOf; no session; typed none; wildcard for a type without row; unknown key false
composables/tests/useApi.spec.ts (extend or new) getMe maps access and drops role; 403 matrix (missing_permission with and without header, escalation, role_locked, own_role, role_built_in, built_in_name, session_required); passthrough option; refresh debounce 5 s and visibility refresh 31 s vs 10 s with fake timers (no real dates); failed refresh keeps grants; unverified redirect wins; 401, 429, network unchanged; 409 codes mapped without toast
lib/tests/permissionNames.spec.ts (new) all 34 keys have an EN and FR human name; content.delete is "Delete entries"; a typed key shows the base-key name
router/tests/routerGuard.spec.ts (extend) each route of the guard table denied and allowed; status checking shows nothing; unverified and signed-out unchanged; typed param route /content/events; lowered-while-open switch; URL unchanged
components/tests/NoAccessState.spec.ts (new) title, description with role name, single action Articles vs Home, no "See what I can do"
components/tests/OwnerOnlyTag.spec.ts, ReadOnlyBanner.spec.ts, ViewOnlyMark.spec.ts (new) lock icon, texts EN and FR, tooltip reason
components/tests/DashboardShell.spec.ts (update) Writer, Admin, Owner, custom menus; drawer equals sidebar; no Roles entry
views/tests/UsersView.spec.ts, UsersRowActions.spec.ts users.invite shows Invite; role object rendered through RoleBadge (Owner locked, Admin built-in, custom, null)
views/tests/ApiKeysView.spec.ts apikeys.view all vs own for Created by
views/tests/Settings{Updates,UpdateDownload,Restart,ContainerUpdates,Retention,MediaCleanup}.spec.ts, HomeUpdate*.spec.ts, stores/tests/update.spec.ts system.apply_updates, system.edit_retention, settings.edit_general gating; Owner only tag; Admin Retention state
views/tests/SitesView.spec.ts, Schema*.spec.ts, composables/tests/useSchemaGate.spec.ts page-level state replaces the old forbidden copy; Delete site disabled for Admin
views/tests/Authors*.spec.ts, EditorAddAuthor.spec.ts, CachePage.spec.ts, VariantsPage.spec.ts, Activity*.spec.ts, Home*.spec.ts kits emit the new access body; owner and admin behaviour unchanged; permission-driven cases
views/tests/ContentList*.spec.ts, EntryEditor*.spec.ts (extend) the entry table of the feature: View only mark, Edit and Delete disabled with their exact reasons, own note, New entry hidden without create, direct link to a viewed entry shows View only and no Save
views/tests/Articles*.spec.ts, Editor*.spec.ts, composables PublishControl / useArticleRowActions specs (extend) all/none table: Publish hidden with "Ask an editor to publish.", Delete disabled with "Your role can't delete articles.", Writer header copy, stale-menu 403 toast then refresh and disabled control
same own-scope article rows (created_by from the API)
views/tests/MediaView.spec.ts, MediaUpload.spec.ts, mediaTestKit the media table: upload, rename, delete, merge, purge by grants; own behaves as none; no media.view shows the page state
data/tests/apiCatalogue.spec.ts sample shapes; no role string in me or user list; role_id documented
i18n/spa.spec.ts (update) new keys exist in EN and FR

Visual (task visual:check -- <screen>, hard gate 10 %)

Step 1. After migrating the /auth/me mocks in web/visual/states.ts (about 48) to access/helper bodies, run every screen of visualStates. Everything not listed below must still PASS; clock-sensitive states stay skip: 'clock-sensitive'.

Step 2. States whose rendering changes by design and whose board frames still carry the old or different copy. The user approved per-state exceptions in the slice 5 acceptance. Each is recorded in web/visual/states.ts as exception with approvedBy: 'user (slice 5 acceptance)' ONLY if the run prints FAIL for it (do not add an exception for a state that passes). Candidates, from a static read of states.ts:

Screen State Why it may fail
sites Forbidden (403) old "You don't have access to Sites" copy replaced by the page-level state
schema Forbidden (403) old "Only owners and admins can change content types" copy replaced
variants Forbidden the permission guard shows the page-level state instead of the local one
activity Forbidden (403) same, only if the mock session now lacks activity.view
content Forbidden (403) same, only if the mock now lacks content.read for the type
settings Retention for an admin (board shows it hidden for non-owners), Software for an admin (Owner only tag) new Owner-only treatment
settings / cache / variants Media cleanup read-only new read-only copy and ReadOnlyBanner
users Invite action gating, role badge from a role object badge rendering from RoleRef

Step 3. New or changed rendering with no board frame: NoAccessState (reference frame "Page: no access" in roles.md section 7), ReadOnlyBanner and OwnerOnlyTag ("Admin: read-only settings", "Admin: owner-only settings"), the View only mark and disabled row menus ("Own scope: entry list", "Own scope: row menu", "Writer: Articles list", "Writer: row menu", "Writer: entry editor"). If the board file does not expose them as states of an existing screen, record "no board frame, manual" and list it in the report as a design-doc gap. Do not invent a screen entry.

Manual checks

  1. Keyboard: Tab order and visible focus on NoAccessState action; disabled row-menu items are reachable and announce their reason; View only mark has an accessible name and the tooltip opens on focus.
  2. Two real browsers, two roles: change a role through PUT /v1/user/{id}/role; the other browser's next refused action shows the toast and the page updates within 30 s of returning to the tab.
  3. As a Writer open /schema, /users, /settings/sites by link: the page-level state, URL unchanged.
  4. As a Contributor with content.update:events=own: another person's entry shows View only, Edit and Delete disabled with reasons; own entry editable.
  5. Media as a role with only media.view: no write control anywhere.
  6. French: all new copy translated. Dark mode and mobile for NoAccessState, OwnerOnlyTag, ReadOnlyBanner, ViewOnlyMark.
  7. task lint:web (eslint and vue-tsc) green; task test:web exit 0 on the final tree.
  8. The TestRouterRoutesAreInTheCatalogue Go failure is pre-existing and reported, not fixed.

Backend items blocking or limiting this slice (for the coordinator)

  • B1 (done in this slice, signed-in callers only): Article.CreatedBy is json:"-". GET /v1/article/, GET /v1/article/{id} and write responses must expose created_by. Privacy decision: anonymous callers should probably not get it.
  • B2 ("Created by" column of the Writer Articles frame): needs names for created_by ids; GET /v1/user/lookup?ids= exists and answers for roles with no grants, so it can serve, but the column is not part of this draft unless asked.
  • B3 (info): media has no owner; delete, merge and purge need scope all, so own on media.delete cannot be honoured in the UI.
  • B4 (info): entries already expose created_by; no backend change.
  • B5 (design gap): the design shows Publish and Status for entries, but the backend has no entry publish route.