Files
RenewCMS/features/tech_debt_hardening.feature
T

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