Files
RenewCMS/features/api_keys_screen.feature

352 lines
19 KiB
Gherkin
Raw Permalink 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.
Feature: API keys screen
As a verified RenewCMS owner or admin
I want /integration/api-keys to list my API keys, create new ones and revoke them
So that a script or service can call the API with my rights, and I can cut it off at any time
Background:
Given /integration/api-keys is a verified route (meta.auth "verified") rendered as ApiKeysPage
with CreateApiKeyModal, ApiKeyReveal and a revoke confirmation dialog, inside
DashboardShell, per docs/design/routes.md
And the screen is a sub-page of Integration, per docs/design/screens/integration.md
And the screen calls, with relative URLs through useApi only, GET /v1/api-key,
POST /v1/api-key and DELETE /v1/api-key/{id}
And a GET /v1/api-key entry has the fields id, user_id, name, prefix (12 characters), status
("active", "expired" or "revoked"), created_at, last_used_at, expires_at and revoked_at, and
never a key or a hash, per features/api_keys.feature
And an owner's entries also carry created_by_name, the creator's display name or username (see
the API scenarios below)
And the list arrives newest first; an owner receives every user's keys, an admin only their own
And the PageHeader shows breadcrumb "Integration › API keys" (Integration links to
/integration), title "API keys" and a primary "Create key" button on the right at desktop
And the description reads "Keys act as the user who created them. As an owner you see
everyone's keys." for an owner, and "Keys act as you, with your role. You only see your own
keys." for an admin; a session without a role gets the admin rendering
And the table, on desktop, has the columns Name, Key, Status, Created by (owner only), Created,
Last used, Expires and an action column, in a UTable with a 36 px header and 48 px rows
And below md, each key is a card: name and status badge, prefix in monospace, then
"Created {date}[ by {name}] · Last used {date} · Expires {date}", then a 44 px "Revoke" button
And the Key column and the card show the prefix cut to its first 9 characters followed by "…",
for example "rcms_Kf3x…", and the full key is never shown in the list
And the revoke text uses the same 9-character prefix plus "…"
And dates are absolute and formatted for the current locale, for example "12 Mar 2026" in
English; a null expires_at reads "None" and a null last_used_at reads "Never"
And a Created by cell with an empty created_by_name shows "—"
And the Status badge is a UBadge: active success soft, expired warning soft, revoked neutral
with a muted row
And "Revoke" is offered on active and expired keys, never on revoked keys
And a note with an info icon always closes the page: "Resetting a user's password revokes all of
their keys."
And the Integration page (/integration, features/integration.feature) gains a "Manage API
keys" link to /integration/api-keys (see the Integration link scenario)
And on /integration/api-keys the sidebar and drawer item "Integration" carries
aria-current="page"
And every visible string goes through t(...), with the English text as the key and its French
entry in web/src/i18n/messages.ts
And the screen uses Nuxt UI components and design tokens only, with no raw colors
And the owner fixture is, newest first as the board draws it:
| name | prefix | status | created by | created | last used | expires |
| Website build | rcms_Kf3x… | active | Marta Alves | 12 Mar 2026 | 3 Oct 2026 | None |
| Nightly media import | rcms_Mq7b… | active | Jonas | 2 Apr 2026 | 3 Oct 2026 | 31 Dec 2026 |
| Staging preview | rcms_Zt2d… | expired | Marta Alves | 14 Jun 2026 | 20 Aug 2026 | 30 Aug 2026 |
| Old migration script | rcms_Hn5w… | revoked | Inès Duarte | 3 Apr 2026 | 9 May 2026 | None |
And the admin fixture (user "Jonas") is:
| name | prefix | status | created | last used | expires |
| Nightly media import | rcms_Mq7b… | active | 2 Apr 2026 | 3 Oct 2026 | 31 Dec 2026 |
| Old CI token | rcms_Lp9c… | revoked | 18 Jan 2026 | 2 Feb 2026 | None |
Scenario: Owner: list
Given the session role is "owner"
And GET /v1/api-key answers 200 with the owner fixture
When the user navigates to /integration/api-keys
Then the four rows show in fixture order with the columns Name, Key, Status, Created by,
Created, Last used, Expires
And "Staging preview" shows an "expired" badge and "Revoke"
And "Old migration script" shows a "revoked" badge, a muted row and no "Revoke"
And the description is the owner description
Scenario: Admin: own keys only
Given the session role is "admin"
And GET /v1/api-key answers 200 with the admin fixture
When the user navigates to /integration/api-keys
Then the two rows show with no "Created by" column
And on mobile the cards read "Created 2 Apr 2026 · Last used 3 Oct 2026 · Expires 31 Dec 2026"
without " by {name}"
And the description is the admin description
Scenario: Loading
Given GET /v1/api-key is pending
When the user navigates to /integration/api-keys
Then four skeleton rows are drawn in the table (two-bar cards below md) and the table has
aria-busy "true"
And the header "Create key" button is shown
Scenario: Empty
Given GET /v1/api-key answers 200 with []
When the user navigates to /integration/api-keys
Then a panel shows title "No API keys yet", text "Create a key to call the API from a script or
service." and a primary "Create key" button
And no table is drawn
Scenario Outline: Error on load
Given GET /v1/api-key fails with <failure>
When the user navigates to /integration/api-keys
Then an alert (role "alert") shows "Couldn't load your API keys" and "Check your connection and
try again." with a "Try again" action
And no table, no cards and no empty panel are drawn
Examples:
| failure |
| status 500 |
| a network error |
Scenario: Try again after an error
Given the load failed and the error alert is shown
When the user activates "Try again"
Then GET /v1/api-key is requested again and, on 200, the list replaces the alert
Scenario Outline: Session problems use the app-wide handling
Given GET /v1/api-key answers <status>
When the user navigates to /integration/api-keys
Then <outcome>
Examples:
| status | outcome |
| 401 | the user lands on /login?redirect=/integration/api-keys&reason=expired |
| 403 | the user lands on /register/pending |
Scenario: Create key: modal opened
Given the list is shown
When the user activates "Create key"
Then a modal titled "Create API key" opens with the text "The key acts as you, with your role. It
is shown in full once."
And it has a Name field with a "0 / 60" counter and help "Required. Up to 60 characters."
And it has an "Expiry date (optional)" date input with help "Optional. Must be in the future.
Leave it empty for a key that does not expire."
And it has the buttons "Cancel" and "Create key"
And Name has focus
Scenario Outline: Create key: validation
Given the Create API key modal is open
When the user submits with name "<name>" and expiry "<expiry>"
Then no request is sent
And <name_error>
And <expiry_error>
Examples:
| name | expiry | name_error | expiry_error |
| | 12 Mar 2026 | Name shows the error "Enter a name." in red | Expiry shows the error "Choose a date in the future." in red |
| | | Name shows the error "Enter a name." in red | Expiry shows no error |
| Deploy | 12 Mar 2026 | Name shows no error | Expiry shows the error "Choose a date in the future." in red |
Scenario: Create key: name too long
Given the Create API key modal is open
When the user types a name of 75 characters
Then the counter reads "75 / 60" in the error color
And Name shows the error "Use 60 characters or fewer." in red
And submitting sends no request
Scenario: Create key: success reveals the key once
Given the Create API key modal is open
When the user submits name "Nightly media import" and an empty expiry
Then exactly one POST /v1/api-key is sent with body {"name": "Nightly media import"} and no
expires_at
And on 201 the form modal is replaced by a modal titled "Your new API key" with the text
"\"Nightly media import\" is ready. Copy the key and store it somewhere safe."
And a warning alert (UAlert, color warning, triangle-alert icon) reads "You won't be able to
see this key again."
And the full key from the response is shown in a CodeSnippet with the single tab "API key" and a "Copy" button (icon copy, accessible name "Copy code"), focused when the modal opens
And the note "Closing this window discards the key from the screen. Copy it first." is shown
And the only button is a primary "Done"
Scenario: Create key: with an expiry
Given the Create API key modal is open
And the local date is D
When the user submits name "temp" and the expiry date D + 30 days
Then the POST body carries name "temp" and expires_at equal to 23:59:59 local time on D + 30
days, as an RFC3339 instant with the local offset
Scenario Outline: The expiry date is checked against the same instant the API checks
Given the Create API key modal is open and the current time is T
When the user submits a name and the expiry date <date>
Then <outcome>
Examples:
| date | outcome |
| today | the request is sent, because 23:59:59 today is after T |
| yesterday | no request is sent and Expiry shows "Choose a date in the future." |
| tomorrow | the request is sent |
Scenario: One-time reveal: copied
Given the key reveal is open
When the user activates "Copy"
Then the full key is written to the clipboard
And the button shows icon check and text "Copied" in the success color, announced politely
When 2 seconds elapse
Then the button reverts to icon copy and text "Copy"
Scenario: One-time reveal: Done discards the key
Given the key reveal is open
When the user activates "Done"
Then the modal closes
And the full key is no longer in the DOM, the component state, the URL or any storage
And the new key is listed first with its 9-character prefix and "…" only, status "active",
created by the current user (owner view), last used "Never", expires per the request, and no
second GET /v1/api-key is made
And reopening "Create key" shows an empty form
Scenario Outline: Dismissing the modals
Given <modal> is open
When the user <action>
Then <outcome>
Examples:
| modal | action | outcome |
| the key reveal | presses Escape | the modal closes and the key is discarded, as for Done |
| the key reveal | clicks the backdrop | the modal stays open |
| the Create key form | presses Escape | the modal closes with no request |
| the Create key form | clicks the backdrop | the modal stays open |
Scenario: Create key: Cancel discards the form
Given the Create API key modal is open with typed values
When the user activates "Cancel"
Then the modal closes without a request, focus returns to the opening button, and reopening it
shows an empty form
Scenario: Revoke: confirm
Given the owner list is shown
When the user activates "Revoke" on "Nightly media import"
Then a dialog titled "Revoke this key?" opens with the text "\"Nightly media import\"
(rcms_Mq7b…) will stop working right away. Requests that use it answer 401. This can't be
undone."
And its buttons are "Keep key" and a destructive "Revoke key"
And "Keep key" closes the dialog with no request and the row unchanged
Scenario: Revoked
Given the revoke dialog is open for "Nightly media import" (id 2)
When the user activates "Revoke key"
Then exactly one DELETE /v1/api-key/2 is sent
And on 204 the dialog closes and the row stays listed with status "revoked", a muted row and no
"Revoke" button
And a success toast shows title "Key revoked" and text "\"Nightly media import\" can no longer
be used."
And no second GET /v1/api-key is made
Scenario: Mobile Create key button, not drawn on board
Given the viewport is below md
When the list, the loading skeleton or the error alert is shown
Then a primary full-width 48 px "Create key" button is pinned to the bottom of the screen
And it is hidden while any modal or dialog is open, and in the Empty state where the panel has
its own "Create key" button
And the header has no "Create key" button below md
Scenario: Integration page links to the keys screen
Given the user is on /integration
Then a "Manage API keys" link (UButton variant link, arrow-right trailing icon) is shown below
the three info notes
When the user activates it
Then the user lands on /integration/api-keys and the sidebar "Integration" item keeps
aria-current="page"
Scenario: Language: every string has a French entry
Given the language is "fr"
When the user opens each state of the screen
Then every visible string is the French message and none falls back to English
And dates are formatted for "fr"
Scenario: Create key: sending, not drawn on board, no visual state
Given the Create API key modal is open with a valid name
When the user submits and POST /v1/api-key is pending
Then the primary button reads "Creating…", Name, Expiry and "Cancel" are disabled
And Escape does not close the modal
Scenario Outline: Create key: failed, not drawn on board, no visual state
Given the Create API key modal is open with name "Nightly media import"
When POST /v1/api-key fails with <failure>
Then an alert in the modal reads "<copy>"
And the typed values are kept, the form is enabled again, no row is added, and no field error
is shown
Examples:
| failure | copy |
| status 500 | Couldn't create the key. Try again in a moment. |
| a network error | Couldn't reach RenewCMS. Check your connection and try again. |
| status 429 | Too many requests. Wait a moment, then try again. |
| status 400 | Something in the form isn't valid. Check the highlighted fields. |
Scenario: Create key: retry after failure, not drawn on board, no visual state
Given a failed create left the modal open
When the user submits again and POST answers 201
Then the reveal opens and the row is added
Scenario: Revoke: sending, not drawn on board, no visual state
Given the revoke dialog is open
When the user activates "Revoke key" and DELETE is pending
Then "Revoke key" reads "Revoking…", both buttons are disabled, and Escape does not close the
dialog
Scenario Outline: Revoke: failed, not drawn on board, no visual state
Given the revoke dialog is open for "Nightly media import"
When DELETE fails with <failure>
Then a notice in the dialog reads "<copy>"
And the dialog stays open, the buttons are enabled again, and the row is unchanged
Examples:
| failure | copy |
| status 500 | Couldn't revoke the key. Try again in a moment. |
| a network error | Couldn't reach RenewCMS. Check your connection and try again. |
Scenario: Revoke: key already gone, 404, not drawn on board, no visual state
Given the revoke dialog is open for "Nightly media import" (id 2)
When DELETE /v1/api-key/2 answers 404
Then the dialog closes, the row is removed from the list, and an error toast reads "That key no
longer exists."
Scenario: Last used, never, not drawn on board, no visual state
Given an entry whose last_used_at is null
Then its Last used cell and its mobile card text read "Never"
Scenario: API: an owner's list carries the creator's name
Given owner 1 "marta" with display name "Marta Alves", admin 2 "dev" with display name "Dev Two"
and admin 3 "pat" with an empty display name
And keys "a" by user 1, "b" by user 2 and "c" by user 3
When owner 1 sends GET /v1/api-key with a jwt cookie
Then each entry has the key created_by_name
And entry "a" has "Marta Alves", "b" has "Dev Two" and "c" has "pat"
And the other nine fields are unchanged from features/api_keys.feature
Scenario: API: a whitespace-only display name falls back to the username
Given admin 3 "pat" with display name " " and a key "c"
When owner 1 sends GET /v1/api-key
Then entry "c" has created_by_name "pat"
Scenario: API: an admin's list is unchanged
Given admin 2 with keys "a" and "b"
When admin 2 sends GET /v1/api-key with a jwt cookie
Then every entry has exactly the keys id, user_id, name, prefix, status, created_at,
last_used_at, expires_at, revoked_at
And no entry has created_by_name
Scenario: API: a key whose creator no longer exists
Given a key whose user record was deleted
When owner 1 sends GET /v1/api-key
Then its entry has created_by_name ""
And the response status is 200 and the other entries are unaffected
Scenario: API: a failing user lookup fails the list
Given the user repository fake returns an unexpected error on Get for a creator
When owner 1 sends GET /v1/api-key
Then the response status is 500 and the body is "Internal server error"
Scenario: API: the creator name is resolved once per user
Given owner 1 has 3 keys and admin 2 has 2 keys
When owner 1 sends GET /v1/api-key
Then the user repository received one Get per distinct creator, not one per key
Scenario: Use case: ListAPIKeys returns the creator name only to an owner
Given a fake apikey.Repository and a fake user.Repository
When ListAPIKeys is called for an owner
Then every APIKeyEntry carries the creator's name
When ListAPIKeys is called for an admin
Then every APIKeyEntry has an empty creator name and the handler omits the field