196 lines
15 KiB
Gherkin
196 lines
15 KiB
Gherkin
Feature: Integration screen (API reference)
|
|
|
|
As a developer building a frontend or an API-key client for a RenewCMS site
|
|
I want /integration to be a complete, searchable API reference
|
|
So that I can call every route a key may call without reading the source
|
|
|
|
Background:
|
|
Given /integration is a verified route (meta.auth "verified") rendered as IntegrationPage
|
|
inside DashboardShell, per docs/design/routes.md
|
|
And the screen makes one call, GET /v1/image-variants, for owner and admin sessions only (features/integration_images.feature)
|
|
And the PageHeader shows breadcrumb "Integration", title "API reference" and description
|
|
"Everything a developer needs to build on RenewCMS."
|
|
And the endpoint catalogue is a typed static data file at web/src/data/apiCatalogue.ts, and the view only renders it
|
|
And the catalogue holds 20 key-callable endpoints in 4 expandable groups, in this order:
|
|
| group | routes | endpoints |
|
|
| Articles | 7 | GET /v1/article/, GET /v1/article/{id}, POST /v1/article/, PATCH /v1/article/{id}, PATCH /v1/article/{id}/publish, PATCH /v1/article/{id}/unpublish, DELETE /v1/article/{id} |
|
|
| Media | 6 | GET /v1/media/, POST /v1/media/, PATCH /v1/media/{id}, DELETE /v1/media/{id}, POST /v1/media/merge, GET /v1/media/{id}/usage |
|
|
| Users and invites | 6 | GET /v1/user/, PATCH /v1/user/me, POST /v1/user/invite, DELETE /v1/user/invite/{id}, POST /v1/user/invite/{id}/renew, GET /v1/auth/me |
|
|
| Settings | 1 | GET /v1/settings |
|
|
And the "Session only" group has 13 non-expandable rows in 3 sub-groups:
|
|
| sub-group | rows |
|
|
| System · owner only | GET /v1/system/update, POST /v1/system/update/check, POST /v1/system/update/download, POST /v1/system/update/restart |
|
|
| Sign in and account | POST /v1/auth/login, POST /v1/auth/logout, POST /v1/auth/register, POST /v1/auth/verify, DELETE /v1/auth/register, POST /v1/auth/password-reset |
|
|
| API key management | GET /v1/api-key, POST /v1/api-key, DELETE /v1/api-key/{id} |
|
|
And a "Public auth flow" sub-list follows with 6 non-expandable rows and the chip "Public":
|
|
| method | path | description |
|
|
| GET | /v1/ | Hello world. |
|
|
| GET | /v1/auth/registration | Whether registration is open. |
|
|
| POST | /v1/auth/password-reset/confirm | Set a new password with the emailed code. |
|
|
| GET | /v1/auth/invite | Check an invite code. |
|
|
| POST | /v1/auth/invite/accept | Accept an invite. |
|
|
| GET | /healthz | Plain text "ok". Outside /v1. |
|
|
And every catalogue endpoint has a non-empty description, method, path, access chip, key chip,
|
|
query-parameter table (or "No query parameters."), request-body table (or "No request body."),
|
|
fetch, curl and Response examples, and error captions
|
|
|
|
Scenario: Default, every card collapsed
|
|
When the user navigates to /integration
|
|
Then the intro reads "RenewCMS is headless: it stores your content and your own code displays it. There are two ways in. Public read-only access lets a website read online articles. API keys let a script or service manage articles, media and users without the back-office."
|
|
And a "Base URL" label sits above a CodeSnippet with the single tab "Base URL" showing window.location.origin and a "Copy" button (icon copy)
|
|
And every EndpointCard is collapsed as a 44 px row: method badge, mono path, one-line description, chips, chevron
|
|
And the sticky section index lists the sections on desktop and a "Jump to" select lists them on mobile
|
|
|
|
Scenario Outline: Method badge and chips per endpoint
|
|
Then <method> <path> shows the <method> badge, access chip "<access>" and key chip "API key OK"
|
|
Examples:
|
|
| method | path | access |
|
|
| GET | /v1/article/ | Public |
|
|
| GET | /v1/article/{id} | Public |
|
|
| POST | /v1/article/ | Verified user |
|
|
| DELETE | /v1/article/{id} | Verified user |
|
|
| GET | /v1/media/ | Verified user |
|
|
| PATCH | /v1/user/me | Verified user |
|
|
| GET | /v1/auth/me | Verified user |
|
|
| POST | /v1/user/invite | Owner only |
|
|
| DELETE | /v1/user/invite/{id} | Owner only |
|
|
| POST | /v1/user/invite/{id}/renew | Owner only |
|
|
| GET | /v1/settings | Verified user |
|
|
|
|
Scenario: Owner-only invite cards, user-approved divergence from the board
|
|
Then the three invite cards show "Owner only", where the board shows "Verified user"
|
|
And each lists a 403 error "Only the owner can do this."
|
|
And each says it is rate limited and that too many requests answer 429, with no number
|
|
|
|
Scenario: Group scrolled, one card expanded
|
|
When the user activates the "POST /v1/article/" row
|
|
Then the row's button reports aria-expanded "true"
|
|
And a "Query parameters" table lists "format", string, optional, "markdown, html or json. Default markdown."
|
|
And a "Request body" table lists:
|
|
| Name | Type | Required | Rule |
|
|
| title | string | Yes | 3 to 200 characters. Can't be changed later. |
|
|
| body | string | No | No length limit. |
|
|
And one CodeSnippet with the tabs fetch, curl and Response shows, fetch active, with a "Copy" button
|
|
And an "Errors" list shows a status chip with a one-line caption for 400, 401
|
|
|
|
Scenario: Query parameters table, new UI beyond the board, user-approved
|
|
Then the "Query parameters" table sits above the "Request body" table in each card
|
|
And it lists:
|
|
| route | name | type | required | rule |
|
|
| every /v1/article route | format | string | no | markdown, html or json. Default markdown. |
|
|
| GET /v1/media/ | q | string | no | Search text. |
|
|
| GET /v1/media/ | hash | string | no | SHA-256 of the content, lowercased. |
|
|
| GET /v1/media/ | cursor | string | no | The next_cursor of the previous page. |
|
|
| GET /v1/media/ | filter | string | no | all, unused or duplicates. Default all. |
|
|
| GET /v1/media/ | sort | string | no | newest, oldest, name or largest. Default newest. |
|
|
| GET /v1/media/ | limit | number | no | 1 to 100. Default 24. |
|
|
And a card without query parameters shows "No query parameters."
|
|
|
|
Scenario Outline: Request body fields come from the handler validate tags
|
|
Then <method> <path> lists these fields: <fields>
|
|
Examples:
|
|
| method | path | fields |
|
|
| POST | /v1/article/ | title string required 3-200; body string optional, no length limit |
|
|
| PATCH | /v1/article/{id} | body string optional, no length limit |
|
|
| PATCH | /v1/media/{id} | filename string required |
|
|
| POST | /v1/media/merge | keep_id number required >0; merge_ids number[] required min 1 each >0 |
|
|
| POST | /v1/media/ | multipart/form-data, 1 file field named "file" |
|
|
| PATCH | /v1/user/me | display_name string required max 60 |
|
|
| POST | /v1/user/invite | email string required valid email |
|
|
| GET | /v1/article/ | "No request body." |
|
|
|
|
Scenario: Response samples use the real domain JSON field names
|
|
Then the GET /v1/article/{id} Response sample has exactly the keys id, title, body, body_format, is_online, created_at, updated_at
|
|
And the sample has no "images" key and no "status" key
|
|
And POST /v1/media/merge has the keys articles_updated and merged
|
|
And GET /v1/media/ has the keys items, next_cursor and counts
|
|
And GET /v1/media/{id}/usage items have the keys id, title, published
|
|
And PATCH /v1/article/{id}/publish and /unpublish samples are articles with is_online true and false
|
|
|
|
Scenario: Article body format
|
|
Then a note reads "Article routes accept `?format=markdown`, `?format=html` or `?format=json` to choose how `body` is returned. With `json`, `body` is the stored JSON structure. Articles have `body_format: markdown` by default."
|
|
And every article card's Errors list includes 400 "The format must be one of: markdown, html, json."
|
|
|
|
Scenario: Authentication section
|
|
Then card "Public" reads "No key needed. Two read-only routes, and only online articles are returned." and names GET /v1/article/ and GET /v1/article/{id}
|
|
And card "API key" reads "Send `Authorization: Bearer rcms_…`. Requests act as the user who created the key, with that user's role (owner or admin). A key never has more rights than its creator."
|
|
And its CodeSnippet has tabs curl and fetch for GET /v1/auth/me, using the current origin
|
|
And it says "Keys start with `rcms_` and are shown in full once, when you create them."
|
|
And a "Manage API keys" link navigates to /integration/api-keys
|
|
And Note 1 reads "A key can't create, list or revoke API keys, verify or cancel a registration, or call anything under `/v1/system`. Those routes answer 403 "This action needs a real session." An invalid, unknown, expired or revoked key answers 401, even on public routes, and so does a key whose user was deleted or unverified."
|
|
And Note 2 reads "Each key can make 120 requests per minute. Beyond that the API answers 429 with a `Retry-After` header."
|
|
|
|
Scenario: Session only group
|
|
Then the banner reads "These routes are made for a signed-in browser session. With an API key, verify, DELETE /auth/register, /api-key/* and /system/* answer 403 "This action needs a real session.""
|
|
And each row has a dashed border and muted fill, uses no method colour and cannot be expanded
|
|
And rows that answer 403 to a key show the lock icon and the chip "Session only"
|
|
And Log in, Log out, Register and Reset password show the caption "Made for the browser. A key is ignored here, with no 403." and no 403 claim
|
|
And no /v1/system route appears in any expandable group
|
|
And the "Public auth flow" rows carry the chip "Public" and no lock icon
|
|
|
|
Scenario: Errors section
|
|
Then the intro reads "Every error is a plain-text body with a single sentence. The status code is in the HTTP status line, and the Content-Type is text/plain."
|
|
And the table lists:
|
|
| status | message |
|
|
| 400 | The request cannot be processed due to a mismatch in the format of the body. / The expiry must be a date in the future. |
|
|
| 401 | You must be logged in to access this resource. |
|
|
| 403 | This action needs a real session. / Only the owner can do this. / You must verify your account to access this resource. |
|
|
| 404 | Article not found / Image not found / Invite not found / API key not found |
|
|
| 409 | There's already a pending invite for this address. / This invite has already been accepted. |
|
|
| 429 | Too many requests. Please try again later. |
|
|
| 500 | Internal server error |
|
|
And a CodeSnippet with the single tab "Error response · text/plain" shows "HTTP/1.1 403 Forbidden", "Content-Type: text/plain", a blank line, and "This action needs a real session."
|
|
And the table does not use the SPA copy of docs/design/states.md
|
|
|
|
Scenario Outline: Per-endpoint error captions use the real strings
|
|
Then <method> <path> lists <status> "<caption>"
|
|
Examples:
|
|
| method | path | status | caption |
|
|
| POST | /v1/media/ | 400 | the image could not be read |
|
|
| POST | /v1/media/ | 413 | the file exceeds the maximum upload size |
|
|
| POST | /v1/media/ | 415 | the file must be a PNG, JPEG, WEBP, or SVG image |
|
|
| GET | /v1/media/ | 400 | The media query is invalid. |
|
|
| POST | /v1/media/merge | 400 | Choose an image to keep and at least one other image to merge into it. |
|
|
| POST | /v1/media/merge | 409 | Some of the images to merge changed. Refresh and try again. |
|
|
| PATCH | /v1/media/{id} | 400 | Enter a filename. |
|
|
| POST | /v1/article/ | 400 | Article title must be between 3 and 200 characters. |
|
|
| GET | /v1/article/{id} | 400 | The server expects the ID to be in the format of an unsigned 32-bit integer (uint32). |
|
|
| PATCH | /v1/user/me | 400 | Display name must be 60 characters or fewer. |
|
|
| POST | /v1/user/invite | 409 | This address already has an account. |
|
|
| POST | /v1/user/invite | 409 | There's already a pending invite for this address. |
|
|
| POST | /v1/user/invite/{id}/renew | 409 | This invite has already been accepted. |
|
|
| POST | /v1/user/invite | 403 | Only the owner can do this. |
|
|
|
|
Scenario: Notes
|
|
Then the image-prefix note reads "Images in body use paths like /static/uploadedImages/<uuid>.png. Prefix them with the base URL before rendering."
|
|
And the CORS note reads "Browsers can only call the API from origins listed in the server's CORS setting. Ask whoever runs the server to add your site's address."
|
|
|
|
Scenario: Copied, Base URL
|
|
Given the app is served from origin "{origin}"
|
|
When the user activates the Base URL "Copy" button
|
|
Then "{origin}" is written to the clipboard
|
|
And the button, named "Copy code", shows icon check and "Copied", announced politely
|
|
When 2 seconds elapse
|
|
Then the button reverts to icon copy and "Copy"
|
|
|
|
Scenario: Copied, CodeSnippet is independent of Base URL
|
|
Given a card is expanded
|
|
When the user activates that CodeSnippet's "Copy code" button
|
|
Then the active tab's exact code string is written to the clipboard and only that button reads "Copied"
|
|
And the Base URL button and other CodeSnippets are unaffected
|
|
|
|
Scenario: Examples use the current origin and the API key header
|
|
Then every fetch and curl example starts from "{origin}" and carries no ?format= query
|
|
And examples for non-public routes carry "Authorization: Bearer rcms_…"
|
|
And examples for public routes carry no Authorization header
|
|
|
|
Scenario: Navigation
|
|
When the user activates a section in the index (desktop)
|
|
Then the page scrolls to that anchor and the item reports aria-current "true"
|
|
And on mobile the "Jump to" select opens and lists the same sections
|
|
|
|
Scenario: Only the image variants calls
|
|
Then the only /v1 request is GET /v1/image-variants, sent for owner and admin sessions on mount, on "Try again" and on window focus
|
|
And expanding or collapsing any card makes no /v1 request
|
|
And a session that is neither owner nor admin makes no /v1 request
|