Files
RenewCMS/features/article_edit_revision.feature
T

184 lines
9.9 KiB
Gherkin

Feature: Edit title and slug, and protect overwrites with a revision
A title can be edited at any time (3 to 200 runes). Editing the title never changes the slug. The slug changes only when sent explicitly, before or after publication, with no redirect. Every article read carries a "revision"; a PATCH may send "if_revision" so a stale editor cannot overwrite silently.
Background:
Given error responses are plain text (a snake_case code or an existing apperr message)
And a "writer" holds content.update and content.read at scope all
And article 10 (site 1, lang en, story 5, online, title "Renovation costs", slug "renovation-costs", authors [Camille]) has revision 3 and content_revision 4
And article 12 (site 1, lang fr, story 5, offline, slug "couts") exists
Scenario Outline: Title boundaries on PATCH, counted in runes
Given a valid jwt cookie for a writer
When the client sends PATCH /v1/article/10 with {"title": <title>}
Then the response status is <status>
And <effect>
Examples:
| title | status | effect |
| 2 ASCII characters | 400 | the body is apperr.ArticleTitleErrorMessage, nothing changed |
| 3 ASCII characters | 200 | the title is stored |
| 200 ASCII characters | 200 | the title is stored |
| 201 ASCII characters | 400 | the title is unchanged |
| 200 repetitions of the 4-byte rune "😀" | 200 | the title is stored (200 runes, 800 bytes) |
| "" | 400 | the body is apperr.ArticleTitleErrorMessage, nothing changed |
| null | 400 | the body is apperr.BodyErrorMessage |
Scenario: Editing the title never changes the slug
Given a valid jwt cookie for a writer
When the client sends PATCH /v1/article/10 with {"title":"New title"}
Then the response status is 200
And the slug is still "renovation-costs"
And the article is still online
Scenario: Title and body in one request
Given a valid jwt cookie for a writer
When the client sends PATCH /v1/article/10 with {"title":"New title","body":"<p>x</p>"}
Then both are stored in one write
And revision rises by exactly 1
Scenario: The title is editable on a single-language site
Given site "archive" has a single language
When a writer sends PATCH on an archive article with {"title":"New title"}
Then the response status is 200
And the slug is unchanged
Scenario Outline: Explicit slug edits
Given a valid jwt cookie for a writer
When the client sends PATCH with <request>
Then the response status is <status>
And <effect>
Examples:
| request | status | effect |
| /v1/article/10 {"slug":"new-address"} (online) | 200 | still online, no redirect row, the old slug reads as not found publicly |
| /v1/article/12 {"slug":"new-address"} (offline) | 200 | the slug is stored |
| /v1/article/10 {"slug":"renovation-costs"} (same as current) | 200 | nothing changes, revision unchanged |
| /v1/article/10 {"slug":"Not A Slug"} | 400 | the body is apperr.InvalidSlugErrorMessage |
| /v1/article/10 {"slug":"couts"} (used by another language of the same story) | 200 | the slug is stored |
| /v1/article/10 {"slug":"taken-en"} (used by another en article of site 1) | 409 | the body is "slug_taken" with the header X-Slug-Suggestion |
| /v1/article/10 {"slug":""} | 200 | the slug is regenerated from the current title |
Scenario: The same slug in two languages is not a conflict
Given a valid jwt cookie for a writer
When the client sends PATCH /v1/article/10 with {"slug":"couts"}
Then the response status is 200
And article 10 (en) and article 12 (fr) both hold the slug "couts"
Scenario: Revision is exposed on every article read and write
Given a valid jwt cookie for a writer
When the client calls GET /v1/article/10, the list, POST, PATCH, publish and unpublish
Then each response carries "revision" as a positive integer
And every pre-existing key is unchanged in name, value and type
Scenario Outline: What raises revision and content_revision
Given a valid jwt cookie for a writer
When the client sends PATCH /v1/article/10 changing only <field>
Then revision rises by 1
And content_revision <content_effect>
Examples:
| field | content_effect |
| title | rises by 1 |
| body | rises by 1 |
| excerpt | rises by 1 |
| slug | rises by 1 |
| cover_image_alt | rises by 1 |
| seo_title | rises by 1 |
| seo_description | rises by 1 |
| social_title | rises by 1 |
| social_description | rises by 1 |
| author_ids | is unchanged |
| cover_image_id | is unchanged |
| social_image_id | is unchanged |
| site_id (single-version story) | is unchanged |
| canonical_url | is unchanged |
| noindex | is unchanged |
| seo_dismissed | is unchanged |
Scenario: Several content fields in one PATCH raise each counter once
When a writer sends PATCH /v1/article/10 changing title, body and excerpt
Then revision rises by 1 and content_revision rises by 1
Scenario: A PATCH that changes nothing leaves both counters alone
When a writer sends PATCH /v1/article/10 with the current values
Then the response status is 200
And revision and content_revision are unchanged
Scenario: Publishing and unpublishing do not touch revisions
When article 12 is published and then unpublished
Then its revision and content_revision are unchanged
Scenario: if_revision matches
When a writer sends PATCH /v1/article/10 with {"body":"x","if_revision":3}
Then the response status is 200
And the response revision is 4
Scenario: if_revision is stale
Given another writer already saved article 10, so its revision is 4
When a writer sends PATCH /v1/article/10 with {"body":"x","if_revision":3}
Then the response status is 409 and the body is "revision_conflict"
And the header X-Current-Revision is 4
And nothing is stored
And no successful "article.edit" activity event is recorded
Scenario: A stale if_revision wins over a no-op PATCH
Given article 10 has revision 4
When a writer sends PATCH /v1/article/10 with the current values and "if_revision":3
Then the response status is 409 and the body is "revision_conflict"
Scenario: Overwrite uses the revision the conflict dialog showed
Given the conflict dialog showed revision 4
When a writer sends PATCH /v1/article/10 with "if_revision":4
Then the response status is 200 and revision is 5
When another writer saved in between
Then the response is 409 "revision_conflict" with the then-current X-Current-Revision
And nothing is overwritten
Scenario: Concurrent saves with the same if_revision
When two PATCH requests with the same if_revision run at the same time
Then exactly one answers 200 and the other 409
And the compare and the write are one atomic step
Scenario: Without if_revision the last write wins
When a writer sends PATCH /v1/article/10 with {"body":"x"}
Then the response status is 200
And revision still rises by 1
Scenario Outline: Invalid if_revision
When a writer sends PATCH /v1/article/10 with {"body":"x","if_revision":<value>}
Then the response status is 400 and the body is apperr.BodyErrorMessage
Examples:
| value |
| 0 |
| -1 |
| "3" |
| 1.5 |
| null |
Scenario: if_revision alone is not an update
When a writer sends PATCH /v1/article/10 with {"if_revision":3}
Then the response status is 400 and the body is apperr.BodyErrorMessage
Scenario: Other languages are untouched by an overwrite
When a writer overwrites article 10 with a matching if_revision
Then article 12 keeps its content, its revision and its publication
Scenario: Permissions are unchanged
Then a caller without content.update gets 403 "missing_permission"
And a caller who cannot read the article gets 404 "Article not found"
And an anonymous caller gets 401
And a stale if_revision never reveals current content to a caller lacking permission because permission is checked first
Scenario: Editing an online article without content.publish changes the live page right away
Given a user holds content.update but not content.publish
When the user sends PATCH /v1/article/10 with {"title":"New title"}
Then the response status is 200
And a public read shows the new title
Scenario: Title and slug edits are recorded
When a writer edits the title and the slug of article 10
Then one "article.edit" event is recorded
And its changes include the title and the slug
And its params hold lang "en"