# 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 " /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