Files
RenewCMS/features/api_keys_screen.qa.md
T

236 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# QA: API keys screen
Two parts: the Go API change (HTTP-level, in `test/`) and the SPA (Vitest and visual).
## What changes in the backend
Only `GET /v1/api-key` called by an owner changes: each entry gains `created_by_name`, the creator's
display name (trimmed, non-empty) or else username, and `""` when the creator no longer exists.
Admin entries keep exactly the nine existing fields and never carry `created_by_name`.
`features/api_keys.feature` is not edited. Its exact-fields scenario is made as an admin and stays
true. No domain or persistence change: `ListAPIKeysUseCase` already holds `user.Repository` and
resolves each distinct creator once through `Get`.
## Go: created_by_name on GET /v1/api-key
Extend `test/api_keys_http_test.go`. Build the handler from `NewHandler(...)` over use cases wired
to the existing in-memory fakes (apikey.Repository, user.Repository, KeyGenerator, fake clock).
Mount it on a fresh `chi.NewRouter()` that mirrors `api/router.go`, wrapped in `jwtauth.Verifier`
and `jwtauth.Authenticator` with a test `jwtauth.New("HS256", ...)`; call
`validator.InitValidator()`; exercise it with `httptest`. No live server, no real SQLite, no real
SMTP. Users: owner 1 "marta" (display name "Marta Alves"), admin 2 "dev" ("Dev Two"), admin 3
"pat" (empty display name), all verified.
1. **Owner sees names** - keys a (user 1), b (user 2), c (user 3); owner cookie GET /v1/api-key;
assert 200, every entry has `created_by_name`, a "Marta Alves", b "Dev Two", c "pat", and the
other nine fields as before (newest first).
2. **Whitespace display name** - user 3 display name " "; assert "pat".
3. **Admin unchanged** - admin 2 cookie; assert every entry has exactly the nine existing fields
(use `apiKeyAssertExactFields`) and no `created_by_name`. Check that the existing exact-fields
test at line 341 is made as an admin; if it is made as an owner, update it and report it.
4. **Deleted creator** - key whose user was removed from the fake; owner GET; assert 200 and
`created_by_name` "" for it.
5. **User lookup fails** - the fake user repository returns an error on Get for a creator; assert
500 and body "Internal server error".
6. **One lookup per creator** - the fake counts Get calls; owner with 3 + 2 keys over 2 creators;
assert the creator lookups equal the distinct creators (plus the requester's own Get).
7. **Use case level** - ListAPIKeys for an owner returns entries with the creator name; for an
admin returns empty creator names; the other existing ListAPIKeys tests pass unchanged.
8. **Regression** - the existing api_keys tests still pass; list order, statuses, 401 and 403
behaviour are unchanged.
Run with `task test`; one test with `task test -- -run 'TestName'`.
## Automated SPA (Vitest + Vue Test Utils, `task test:web`)
Mock `useApi`'s `request` for GET /v1/api-key, POST /v1/api-key and DELETE /v1/api-key/{id} per
case. Mount ApiKeysPage inside DashboardShell with a resolved verified session (id 1, "marta",
display name "Marta Alves", role "owner") and a test router at /integration/api-keys; admin cases
use role "admin", username "jonas". Mocked owner entries carry `created_by_name`. Stub
`navigator.clipboard.writeText` per case. No test freezes the system date; specs assert on the
fixtures' absolute dates only, and the expiry cases build their dates from the real clock. Run
layout cases at desktop and below-md (mock `matchMedia`).
1. **Owner: list** - breadcrumb (with the Integration link), title, owner description, header
labels, rows in fixture order with every cell (prefix cut to 9 characters plus "…", Created by
from `created_by_name`), badge words, "Revoke" on rows 1 to 3 and not row 4, row 4 muted, the
note; the below-md cards. An entry with an empty `created_by_name` shows "—".
2. **Admin: own keys only** - no "Created by" header or cell, admin description, two rows, cards
without " by {name}"; a session with no role gets the same rendering.
3. **Loading** - unresolved GET; 4 skeleton rows, aria-busy "true", header "Create key"; resolve
and assert rows.
4. **Empty** - `[]`; the panel copy and its "Create key", no table.
5. **Error, Examples-driven** - ApiError(500), then NetworkError; alert copy as drawn, "Try again",
no rows; click "Try again" and assert the second request and the rows.
6. **401 and 403** - the app-wide redirects, no alert on the page.
7. **Create key: opened** - title, text, "0 / 60", helps, buttons, focus on Name.
8. **Create key: validation, Examples-driven** - the three Outline rows; error text, no request,
errors clear when fixed.
9. **Create key: name too long** - 75 characters; counter, error class and text, no request.
10. **Create key: success** - one POST with `{"name":"Nightly media import"}` and no expires_at
key; the reveal: title, text, warning alert, key equal to the mocked `key` inside a CodeSnippet with
the single tab "API key", "Copy" (named "Copy code") focused on open, note, only "Done".
11. **Create key: expiry** - date D + 30 days; assert expires_at is 23:59:59 local on that day
with the local offset. Examples today (sent), yesterday (blocked), tomorrow (sent); build the
dates from `new Date()` in the test.
12. **Reveal: copied** - fake timers; click "Copy"; writeText with the full key, icon check,
"Copied", aria-live announcement; advance 2 s; "Copy" again.
13. **Reveal: Done and Escape discard** - assert the modal is gone, the key string appears nowhere
in `wrapper.html()`, the router URL, localStorage or sessionStorage; the new row shows the
prefix only, status "active", "Never", created by the session user; no second GET; a reopened
form is empty. A backdrop click leaves the reveal open.
14. **Create key: Cancel and Escape** - no request, empty on reopen, focus back on the opener;
a backdrop click does not close.
15. **Revoke: confirm and Keep key** - dialog copy with the 9-character prefix, both buttons;
"Keep key" closes with no request.
16. **Revoked** - confirm; one DELETE with the key's id; row "revoked", muted, no "Revoke"; toast
title and text; no second GET.
17. **Not drawn: Create sending** - pending POST; "Creating…", disabled fields and Cancel, Escape
ignored.
18. **Not drawn: Create failed, Examples-driven** - ApiError(500) "Couldn't create the key. Try
again in a moment.", NetworkError "Couldn't reach RenewCMS. Check your connection and try
again.", ApiError(429) "Too many requests. Wait a moment, then try again.", ApiError(400)
"Something in the form isn't valid. Check the highlighted fields."; values kept, form
re-enabled, no row added; submit again, resolve 201, assert the reveal.
19. **Not drawn: Revoke sending** - "Revoking…", both buttons disabled, Escape ignored.
20. **Not drawn: Revoke failed, Examples-driven** - ApiError(500) "Couldn't revoke the key. Try
again in a moment." and NetworkError "Couldn't reach RenewCMS. Check your connection and try
again."; the notice in the open dialog, row unchanged; retry succeeds.
21. **Not drawn: Revoke 404** - the dialog closes, the row is removed, the error toast "That key no
longer exists.".
22. **Not drawn: Last used "Never"** - null `last_used_at`, desktop cell and mobile card.
23. **Mobile pinned button** - below md the pinned 48 px "Create key" exists in list, loading and
error; hidden while a modal is open and in Empty; no header button below md.
24. **Integration link and navigation** - on /integration assert "Manage API keys" is a link to
/integration/api-keys below the three info notes; navigate; assert the sidebar and drawer
"Integration" item has aria-current="page" on /integration/api-keys. Update the existing
integration specs (features/integration.qa.md case 1) for the extra link.
25. **French** - `setLocale('fr')`; walk cases 1, 4, 7, 10, 15, 16, 17, 21; no visible English
source string remains; dates use the French format.
26. **Messages** - every new `t()` key has a French entry (extend the i18n spec pattern).
Un-run web tests are reported as NOT RUN, never as passing.
## Visual (`task visual:check -- integration-api-keys`, desktop and mobile, light and dark)
Reference: docs/design/screens/integration.md, board `docs/design/source/RenewCMS Screens API
keys.dc.html`, exact values in `docs/design/source/ApiKeysPage.dc.html`. Add each state's setup to
`web/visual/states.ts` with the board's own sample data (mocked GET /v1/api-key responses include
`created_by_name` and 12-character prefixes whose first 9 characters match the board). Hard gate:
every capture of every state within 10 %.
Sample data: the owner and admin fixtures from the feature's Background; Create modal name
"Nightly media import" (20 / 60) and expiry 31 Dec 2026; validation: empty name (0 / 60) and
expiry 12 Mar 2026; too long: "Nightly import of the new media library from the Brussels office
file server" (75 / 60); reveal key rcms_Kf3x9QwL0aBcT7nV2uYdE5sHjR8mPz4WqXg6C1oIyNe for
"Nightly media import"; revoke and toast for "Nightly media import" (rcms_Mq7b…).
No state is clock-sensitive, since every date is fixed text. The Create sample expiry 31 Dec 2026
becomes a past date after that day, but the state only displays it, so it gets no skip.
States:
- [ ] Owner: list
- [ ] Admin: own keys only
- [ ] Loading (the setup holds the request)
- [ ] Empty
- [ ] Error
- [ ] Create key
- [ ] Create key: validation
- [ ] Create key: name too long
- [ ] One-time reveal (re-run after the CodeSnippet migration, within 10 %)
- [ ] One-time reveal: copied (timer-driven, so it is checked)
- [ ] Revoke: confirm
- [ ] Revoked (list with the revoked row and the toast)
- [ ] Layout from the page component, not components.md: 36 px header, 48 px rows, 20 px status
badge; desktop modal 480 px wide 120 px from the top; mobile modal full width, bottom-aligned,
buttons stacked with Cancel under the primary; mobile card Revoke 44 px; toast 320 px wide
bottom-right on desktop and 16 px inset on mobile.
- [ ] Not drawn on the board, no visual state, Vitest only: Create sending, Create failed (500,
network, 429, 400), Revoke sending, Revoke failed, Revoke 404, "Never", the dismissal rules,
and the Integration "Manage API keys" link.
Risk: the mobile pinned "Create key" button is not on the board, and the Integration link is not
on the board's frames for the current Integration page. Either may push a mobile or Integration
capture over 10 %. Re-run `task visual:check -- integration` and the API keys screen. If either
fails, the user must decide: an approved per-state exception needs `approvedBy`, or a design
update.
Manual checks screenshots cannot cover:
- Keyboard and focus order: Tab reaches "Create key", then each "Revoke" in row order; modal focus
starts on Name, is trapped, and returns to the opener on close; the reveal's initial focus lands
on "Copy".
- Hover and motion: outline and primary button hover; modal motion; skeleton pulse stopped under
reduced motion.
## Manual verification
- Real round trip against `task dev`: create a key, copy it, run
`curl -H "Authorization: Bearer <key>" <origin>/v1/auth/me` and get 200; revoke it from the
screen and get 401; confirm the key gets 403 "This action needs a real session." on
/v1/api-key.
- Owner and admin: as owner confirm the Created by names (display name, else username); as admin
confirm only your own keys and no Created by column.
- Reset-password revocation: reset a user's password and confirm their keys show "revoked" after
reload.
- Clipboard on a real HTTPS or localhost origin.
## French strings for web/src/i18n/messages.ts
Keyed by the English text. `{0}` and `{1}` are the `t()` placeholders.
- API keys = Clés d'API
- Integration › API keys = Intégration › Clés d'API
- Create key = Créer une clé
- Name = Nom
- Key = Clé
- Status = Statut
- Created by = Créée par
- Created = Créée le
- Last used = Dernière utilisation
- Expires = Expire le
- active = active
- expired = expirée
- revoked = révoquée
- Revoke = Révoquer
- None = Aucune
- Never = Jamais
- Keys act as the user who created them. As an owner you see everyone's keys. = Les clés agissent comme l'utilisateur qui les a créées. En tant que propriétaire, vous voyez les clés de tout le monde.
- Keys act as you, with your role. You only see your own keys. = Les clés agissent en votre nom, avec votre rôle. Vous ne voyez que vos propres clés.
- Resetting a user's password revokes all of their keys. = Réinitialiser le mot de passe d'un utilisateur révoque toutes ses clés.
- No API keys yet = Aucune clé d'API pour l'instant
- Create a key to call the API from a script or service. = Créez une clé pour appeler l'API depuis un script ou un service.
- Couldn't load your API keys = Impossible de charger vos clés d'API
- Check your connection and try again. = Vérifiez votre connexion et réessayez.
- Try again = Réessayer
- Create API key = Créer une clé d'API
- The key acts as you, with your role. It is shown in full once. = La clé agit en votre nom, avec votre rôle. Elle n'est affichée en entier qu'une seule fois.
- Required. Up to 60 characters. = Obligatoire. 60 caractères maximum.
- Expiry date (optional) = Date d'expiration (facultative)
- Optional. Must be in the future. Leave it empty for a key that does not expire. = Facultative. Doit être dans le futur. Laissez vide pour une clé qui n'expire pas.
- Enter a name. = Saisissez un nom.
- Use 60 characters or fewer. = Utilisez 60 caractères ou moins.
- Choose a date in the future. = Choisissez une date dans le futur.
- Cancel = Annuler
- Your new API key = Votre nouvelle clé d'API
- "{0}" is ready. Copy the key and store it somewhere safe. = « {0} » est prête. Copiez la clé et conservez-la en lieu sûr.
- You won't be able to see this key again. = Vous ne pourrez plus revoir cette clé.
- Copy = Copier
- Copied = Copié
- Closing this window discards the key from the screen. Copy it first. = Fermer cette fenêtre supprime la clé de l'écran. Copiez-la d'abord.
- Done = Terminé
- Revoke this key? = Révoquer cette clé ?
- "{0}" ({1}) will stop working right away. Requests that use it answer 401. This can't be undone. = « {0} » ({1}) cessera de fonctionner immédiatement. Les requêtes qui l'utilisent recevront une réponse 401. Cette action est irréversible.
- Keep key = Garder la clé
- Revoke key = Révoquer la clé
- Key revoked = Clé révoquée
- "{0}" can no longer be used. = « {0} » ne peut plus être utilisée.
- Creating… = Création…
- Revoking… = Révocation…
- Couldn't create the key. Try again in a moment. = Impossible de créer la clé. Réessayez dans un instant.
- Couldn't reach RenewCMS. Check your connection and try again. = Impossible de joindre RenewCMS. Vérifiez votre connexion et réessayez.
- Too many requests. Wait a moment, then try again. = Trop de requêtes. Patientez un instant, puis réessayez.
- Something in the form isn't valid. Check the highlighted fields. = Un élément du formulaire n'est pas valide. Vérifiez les champs signalés.
- Couldn't revoke the key. Try again in a moment. = Impossible de révoquer la clé. Réessayez dans un instant.
- That key no longer exists. = Cette clé n'existe plus.
- Manage API keys = Gérer les clés d'API