feat: roles SPA foundations
This commit is contained in:
1 parent
80f7037d3a
commit
907949aa12
180 files changed
+4308
-835
No files matched your search
@@ -0,0 +1,71 @@
|
||||
# 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
|
||||
0. 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.
|
||||
1. 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.
|
||||
2. Report each edited spec, and which ones already failed in the baseline.
|
||||
3. 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.
|
||||
4. 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.
|
||||
Reference in new issue
Block a user