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
- The runner already exists (web/vitest.config.ts,
test:webin Taskfile.yml, vitest and @vue/test-utils in web/package.json). First runtask test:webon the unmodified tree and record exit code and failing specs. That is the baseline. - 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.
- Report each edited spec, and which ones already failed in the baseline.
- 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.
- 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
- 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.
- 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.
- As a Writer open /schema, /users, /settings/sites by link: the page-level state, URL unchanged.
- As a Contributor with content.update:events=own: another person's entry shows View only, Edit and Delete disabled with reasons; own entry editable.
- Media as a role with only media.view: no write control anywhere.
- French: all new copy translated. Dark mode and mobile for NoAccessState, OwnerOnlyTag, ReadOnlyBanner, ViewOnlyMark.
task lint:web(eslint and vue-tsc) green;task test:webexit 0 on the final tree.- 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.CreatedByisjson:"-". GET /v1/article/, GET /v1/article/{id} and write responses must exposecreated_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
ownon 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.