217 lines
9.3 KiB
Gherkin
217 lines
9.3 KiB
Gherkin
Feature: Hardening after the Sites, Authors and SEO slices
|
|
|
|
As the maintainer of a RenewCMS installation
|
|
I want the SEO panel, the SEO check cache, the chunked lookups and the slug backfill to hold on large data
|
|
So that behaviour stays correct beyond the first page of media, on every database, and on every boot
|
|
|
|
Background:
|
|
Given this file covers four items; items 4, 8 and 9 are behaviour-preserving and item 3 adds one read endpoint, GET /v1/media/{id}
|
|
And every existing scenario stays green without edits to its assertions
|
|
And no comment is added to any file
|
|
|
|
Rule: The SEO panel names its images by id (item 3)
|
|
|
|
Background:
|
|
Given GET /v1/media/{id} is mounted in NewMediaRouter behind the same middleware as GET /v1/media/: jwtauth.Verifier, the API key authentication, AuditContext, RequireSignedIn, jwtauth.Authenticator, RequireCurrentToken and RequireVerifiedUser
|
|
And it reads one image and writes nothing, so it records no activity event
|
|
And it answers 200 with the same JSON item shape as one element of items in GET /v1/media/
|
|
And a missing image answers 404 with apperr.ImageNotFoundErrorMessage ("Image not found")
|
|
And a malformed id answers 400 with apperr.IdUint32ErrorMessage
|
|
And the SPA useMediaNames resolves the wanted ids with GET /v1/media/{id} and no longer reads the first 100 items of the list
|
|
|
|
Scenario Outline: Who can read one image
|
|
Given the image 5 exists
|
|
When GET /v1/media/5 is sent with <credentials>
|
|
Then the status is <status>
|
|
|
|
Examples:
|
|
| credentials | status |
|
|
| the owner's jwt cookie | 200 |
|
|
| a verified admin's jwt cookie | 200 |
|
|
| a verified account with another role | 200 |
|
|
| a valid API key | 200 |
|
|
| an unverified account's jwt cookie | 403 |
|
|
| no credentials | 401 |
|
|
|
|
Scenario: The item has the shape of the list
|
|
Given the image 5 exists with a name, a hash, a size, dimensions, a used_in count and variants information
|
|
When a verified account sends GET /v1/media/5
|
|
Then the body is the JSON object equal to the element of items for the image 5 in GET /v1/media/
|
|
And no other field is added or removed
|
|
|
|
Scenario: A missing image is not found
|
|
When a verified account sends GET /v1/media/999
|
|
Then the status is 404 and the body is "Image not found"
|
|
|
|
Scenario Outline: A malformed id is refused
|
|
When a verified account sends GET /v1/media/<id>
|
|
Then the status is 400 and the body is apperr.IdUint32ErrorMessage
|
|
|
|
Examples:
|
|
| id |
|
|
| abc |
|
|
| -1 |
|
|
| 4294967296 |
|
|
| 1.5 |
|
|
|
|
Scenario: The route does not shadow the existing media routes
|
|
When GET /v1/media/5/usage and GET /v1/media/5/variants are sent as before
|
|
Then they answer exactly as before this change
|
|
|
|
Scenario: A non-GET method keeps its meaning
|
|
When PATCH /v1/media/5 and DELETE /v1/media/5 are sent as before
|
|
Then they behave exactly as before this change
|
|
|
|
Scenario: Reading one image records nothing
|
|
When a verified account sends GET /v1/media/5
|
|
Then no activity event is recorded
|
|
|
|
Scenario: A selected image outside the first 100 media items is named
|
|
Given the article's cover image has id 150 and is not among the first 100 media items
|
|
When the SEO panel opens
|
|
Then the panel requests GET /v1/media/150 and shows the name and thumbnail of the image 150
|
|
And the same holds for the social image
|
|
|
|
Scenario: Images already known are not fetched again
|
|
Given the panel has resolved the images 150 and 151
|
|
When the panel re-renders with the same selection
|
|
Then no lookup request is sent
|
|
|
|
Scenario: An image that no longer exists
|
|
Given GET /v1/media/150 answers 404
|
|
Then the panel shows its existing "unknown image" fallback and no error notice
|
|
|
|
Scenario: A failing lookup never blocks the panel
|
|
Given GET /v1/media/150 fails with a 500 or a network error
|
|
Then the panel stays usable, shows the existing fallback and raises no notice
|
|
|
|
Scenario: Behaviour with fewer than 100 media items is unchanged
|
|
Given the library holds 12 media items
|
|
Then the names shown are the same as before
|
|
|
|
Scenario: The list is no longer requested for names
|
|
When the SEO panel resolves any selected image
|
|
Then no GET /v1/media/?page=1&size=100&sort=newest request is sent for names
|
|
|
|
Scenario: The docs describe the route
|
|
Then docs/api.md documents GET /v1/media/{id} with its auth, the 200 shape, the 404 and the 400
|
|
And web/src/data/apiCatalogue.ts lists GET /v1/media/{id} in the Media group, with its spec and the Integration reference counts updated
|
|
|
|
Rule: The SEO check answer cache (item 4)
|
|
|
|
Scenario: The same effective input is checked once
|
|
Given a check was answered for an article body and fields
|
|
When the same fields are checked again with a different but irrelevant difference in the request (key order or whitespace of the JSON)
|
|
Then no second POST /v1/article/{id}/seo/check is sent and the cached answer is shown
|
|
|
|
Scenario Outline: A field that affects the result invalidates the cache
|
|
Given a check was answered
|
|
When <field> changes and the check runs again
|
|
Then POST /v1/article/{id}/seo/check is sent again
|
|
|
|
Examples:
|
|
| field |
|
|
| the title |
|
|
| the body |
|
|
| the description |
|
|
| the excerpt |
|
|
| the slug |
|
|
| the cover image |
|
|
| the social image |
|
|
| the noindex switch |
|
|
| the site |
|
|
| the canonical URL |
|
|
|
|
Scenario: A field that does not affect the result does not invalidate it
|
|
Given a check was answered
|
|
When only a field the check ignores changes
|
|
Then the cached answer is reused
|
|
|
|
Scenario: A failed check is not cached
|
|
Given a check failed with a 500
|
|
When the same input is checked again
|
|
Then the request is sent again
|
|
|
|
Scenario: The cache stays bounded
|
|
Given 200 distinct inputs were checked
|
|
Then the cache holds no more than its existing limit and a repeated recent input is still a hit
|
|
|
|
Rule: Chunked IN lookups on a real database (item 8)
|
|
|
|
Background:
|
|
Given the article repository looks ids up in batches of 500 (articles by id, article_authors by article id, authors by id)
|
|
|
|
Scenario: A page-sized lookup keeps every row once and in order
|
|
Given 1,200 articles, so the lookup spans 3 batches
|
|
When the repository loads all 1,200 ids
|
|
Then 1,200 articles come back, each once, in the requested order
|
|
|
|
Scenario Outline: Batch boundaries
|
|
Given <n> ids are requested and all exist
|
|
Then exactly <n> rows come back
|
|
|
|
Examples:
|
|
| n |
|
|
| 0 |
|
|
| 1 |
|
|
| 499 |
|
|
| 500 |
|
|
| 501 |
|
|
| 1000 |
|
|
| 1001 |
|
|
|
|
Scenario: Missing ids are absent
|
|
Given 600 ids of which 100 do not exist
|
|
Then 500 rows come back and none is invented
|
|
|
|
Scenario: Credits keep their order across batches
|
|
Given 700 articles each crediting 3 authors in a chosen order
|
|
When the credits are loaded
|
|
Then every article lists its authors in its stored order (position, then id)
|
|
|
|
Scenario: Soft-deleted rows follow the existing rule
|
|
Given 3 of the 1,200 articles are soft-deleted
|
|
Then the lookup returns the same rows as the single-query path would
|
|
|
|
Scenario: It works on every supported database
|
|
When the tests run against SQLite, MySQL/MariaDB and PostgreSQL
|
|
Then the results are identical
|
|
|
|
Rule: The article slug backfill (item 9)
|
|
|
|
Scenario: A large database is filled in batches
|
|
Given a database from before slugs holding 5,000 articles with empty slugs
|
|
When the backfill runs
|
|
Then every article has a non-empty slug unique within its site
|
|
And the writes were made in several transactions of bounded size and not in one
|
|
And the batch size is a named constant chosen by the implementation
|
|
|
|
Scenario: The second boot changes nothing
|
|
Given the backfill already ran
|
|
When it runs again
|
|
Then no row is written, no slug changes and updated_at is untouched
|
|
|
|
Scenario: A crashed backfill resumes
|
|
Given the backfill stopped after its first batch
|
|
When it runs again
|
|
Then the remaining articles are filled and the already filled ones keep their slugs
|
|
|
|
Scenario: Duplicates get the first free suffix
|
|
Given 3 articles with the same title in one site and 1 in another site
|
|
When the backfill runs
|
|
Then the first keeps the plain slug, the others get -2 and -3, and the other site's article keeps the plain slug
|
|
|
|
Scenario: Soft-deleted rows get their tombstone once
|
|
Given 2 soft-deleted articles
|
|
When the backfill runs twice
|
|
Then each has the tombstone slug after the first run and is not rewritten by the second
|
|
|
|
Scenario: Existing slugs are kept
|
|
Given an article whose slug was set by the owner
|
|
When the backfill runs
|
|
Then its slug is unchanged
|
|
|
|
Scenario: The unique index exists afterwards and a repeat does not fail
|
|
When the backfill and index creation run on a second boot
|
|
Then idx_articles_site_slug exists and no error is returned
|