Files
RenewCMS/features/api_keys_screen.qa.md

15 KiB
Raw Permalink Blame History

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