fix: technical debts left by the Sites, Authors and SEO slices, plus backend support for the new Sites and Authors pagination
This commit is contained in:
1 parent
9f4ce426a8
commit
daf3594316
86 files changed
+4223
-1485
No files matched your search
@@ -0,0 +1,216 @@
|
||||
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
|
||||
Reference in new issue
Block a user