feat: editor write path
This commit is contained in:
1 parent
d9158025d4
commit
a0b3c58c52
88 files changed
+5738
-263
No files matched your search
@@ -0,0 +1,183 @@
|
||||
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"
|
||||
Reference in new issue
Block a user