15 KiB
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.
- 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). - Whitespace display name - user 3 display name " "; assert "pat".
- Admin unchanged - admin 2 cookie; assert every entry has exactly the nine existing fields
(use
apiKeyAssertExactFields) and nocreated_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. - Deleted creator - key whose user was removed from the fake; owner GET; assert 200 and
created_by_name"" for it. - User lookup fails - the fake user repository returns an error on Get for a creator; assert 500 and body "Internal server error".
- 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).
- 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.
- 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).
- 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 emptycreated_by_nameshows "—". - 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.
- Loading - unresolved GET; 4 skeleton rows, aria-busy "true", header "Create key"; resolve and assert rows.
- Empty -
[]; the panel copy and its "Create key", no table. - 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.
- 401 and 403 - the app-wide redirects, no alert on the page.
- Create key: opened - title, text, "0 / 60", helps, buttons, focus on Name.
- Create key: validation, Examples-driven - the three Outline rows; error text, no request, errors clear when fixed.
- Create key: name too long - 75 characters; counter, error class and text, no request.
- 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 mockedkey, "Copy", note, only "Done". - 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. - Reveal: copied - fake timers; click "Copy"; writeText with the full key, icon check, "Copied", aria-live announcement; advance 2 s; "Copy" again.
- 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. - Create key: Cancel and Escape - no request, empty on reopen, focus back on the opener; a backdrop click does not close.
- Revoke: confirm and Keep key - dialog copy with the 9-character prefix, both buttons; "Keep key" closes with no request.
- Revoked - confirm; one DELETE with the key's id; row "revoked", muted, no "Revoke"; toast title and text; no second GET.
- Not drawn: Create sending - pending POST; "Creating…", disabled fields and Cancel, Escape ignored.
- 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.
- Not drawn: Revoke sending - "Revoking…", both buttons disabled, Escape ignored.
- 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.
- Not drawn: Revoke 404 - the dialog closes, the row is removed, the error toast "That key no longer exists.".
- Not drawn: Last used "Never" - null
last_used_at, desktop cell and mobile card. - 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.
- 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.
- French -
setLocale('fr'); walk cases 1, 4, 7, 10, 15, 16, 17, 21; no visible English source string remains; dates use the French format. - 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
- 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, runcurl -H "Authorization: Bearer <key>" <origin>/v1/auth/meand 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