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,255 @@
|
||||
Feature: Capabilities, translations, publish rules, delete and story move
|
||||
An article read tells the editor what the caller may do with the version and which readable sibling versions exist. Publishing respects disabled languages. Deleting the last version removes its story. A story moves to another site as a whole, atomically, never split.
|
||||
|
||||
Background:
|
||||
Given error responses are plain text, with extra data in headers
|
||||
And site "journal" (id 1) has enabled languages en (default), fr, de, ar and disabled language es
|
||||
And site "atelier" (id 2) has enabled languages en (default), fr, de
|
||||
And story 5 in journal holds article 10 (en, online, created_by 1, slug renovation-costs), 12 (fr, offline, 1, couts), 11 (de, offline, 2, kosten) and 13 (ar, online, 3, takalif)
|
||||
And "owner" holds every content permission at scope all
|
||||
And "writer1" (user 1) holds content.create, content.read, content.update, content.publish and content.delete at scope own, and content.read at scope all for online articles only
|
||||
And "reader" holds content.read at scope all only
|
||||
|
||||
Scenario: Capabilities of the owner
|
||||
When the owner sends GET /v1/article/12
|
||||
Then "capabilities" is ["read","edit","publish","delete","create_translation"]
|
||||
|
||||
Scenario Outline: Capabilities per caller and version
|
||||
When <caller> sends GET /v1/article/<id>
|
||||
Then "capabilities" is <capabilities>
|
||||
|
||||
Examples:
|
||||
| caller | id | capabilities |
|
||||
| writer1 | 12 | ["read","edit","publish","delete","create_translation"] |
|
||||
| writer1 | 11 | ["read","create_translation"] |
|
||||
| reader | 10 | ["read"] |
|
||||
| a user with content.update and content.read all | 12 | ["read","edit"] |
|
||||
| a user with content.publish and content.read all | 12 | ["read","publish"] |
|
||||
| anonymous | 10 | ["read"] |
|
||||
|
||||
Scenario: Translations list readable versions only
|
||||
When writer1 sends GET /v1/article/10
|
||||
Then "translations" has entries for 10, 12 and 13 and none for 11
|
||||
And each entry has id, lang, title, is_online, language_enabled and capabilities
|
||||
And no count, placeholder or total names the hidden version
|
||||
|
||||
Scenario: Translations are ordered default language first, then by tag
|
||||
When the owner sends GET /v1/article/12
|
||||
Then the translations are ordered en, ar, de, fr
|
||||
And the version being read is included
|
||||
|
||||
Scenario: Disabled languages are marked
|
||||
Given language "fr" is disabled on journal
|
||||
When the owner sends GET /v1/article/10
|
||||
Then the fr entry has "language_enabled": false and is still listed
|
||||
|
||||
Scenario: A single-version article lists itself
|
||||
Given article 20 is alone in story 8
|
||||
When the owner sends GET /v1/article/20
|
||||
Then "translations" has one entry, article 20 itself
|
||||
|
||||
Scenario: Anonymous reads expose nothing about siblings
|
||||
When an anonymous client sends GET /v1/article/10
|
||||
Then "translations" is []
|
||||
And "capabilities" is ["read"]
|
||||
And the public shape is otherwise unchanged
|
||||
|
||||
Scenario: The source on the read
|
||||
Given article 14 was created from article 10 at content_revision 4
|
||||
When the owner sends GET /v1/article/14
|
||||
Then "source" is {"article_id":10,"revision":4}
|
||||
When article 10 is deleted or unreadable to the reader
|
||||
Then "source" is null
|
||||
And a version created empty has "source": null
|
||||
|
||||
Scenario Outline: Publishing respects the language configuration
|
||||
Given a valid jwt cookie for a user holding content.publish
|
||||
When the client sends PATCH /v1/article/<id>/publish
|
||||
Then the response status is <status>
|
||||
And <effect>
|
||||
|
||||
Examples:
|
||||
| id | situation | status | effect |
|
||||
| 12 | fr enabled | 200 | "is_online": true |
|
||||
| 15 | es disabled, offline version | 422 | the body is "language_disabled" |
|
||||
| 16 | es disabled, already online | 200 | unchanged, "is_online": true |
|
||||
| 30 | unassigned lang "" | 200 | "is_online": true |
|
||||
|
||||
Scenario: Unpublish and edit stay available in a disabled language
|
||||
Given fr is disabled on journal and article 12 is online
|
||||
When the owner sends PATCH /v1/article/12/unpublish
|
||||
Then the response status is 200
|
||||
When the owner sends PATCH /v1/article/12 with {"body":"x"}
|
||||
Then the response status is 200
|
||||
|
||||
Scenario: Publishing one version leaves the others alone
|
||||
When the owner publishes article 12
|
||||
Then articles 10, 11 and 13 keep their publication and updated_at
|
||||
|
||||
Scenario: Re-enabling a language allows publishing again
|
||||
Given es is re-enabled on journal
|
||||
When the owner sends PATCH /v1/article/15/publish
|
||||
Then the response status is 200
|
||||
|
||||
Scenario Outline: Existing publish errors are unchanged
|
||||
When <request>
|
||||
Then the response status is <status>
|
||||
And the response body is <message>
|
||||
|
||||
Examples:
|
||||
| request | status | message |
|
||||
| the owner publishes article 9999 | 404 | Article not found |
|
||||
| the owner publishes article "abc" | 400 | apperr.IdUint32ErrorMessage |
|
||||
| an anonymous client publishes article 12 | 401 | Unauthorized |
|
||||
| the reader publishes article 12 | 403 | missing_permission |
|
||||
|
||||
Scenario: Deleting one version keeps the story
|
||||
When the owner sends DELETE /v1/article/12
|
||||
Then the response status is 204
|
||||
And articles 10, 11 and 13 and story 5 remain
|
||||
And the translations no longer list fr
|
||||
|
||||
Scenario: A deleted language can be created again
|
||||
Given article 12 (fr) was deleted
|
||||
When the owner sends POST /v1/article/10/translations with lang "fr"
|
||||
Then the response status is 201
|
||||
And fr of journal still counts as in use while a deleted fr version exists, per features/site_languages.feature
|
||||
|
||||
Scenario: Deleting the last version removes the story
|
||||
Given story 8 holds only article 20
|
||||
When the owner sends DELETE /v1/article/20
|
||||
Then the response status is 204 and story 8 is gone
|
||||
And creating a translation from story 8 answers 404 "Article not found"
|
||||
|
||||
Scenario: Deleting the last of several versions
|
||||
When the owner deletes 10, 11, 12 and 13 one after another
|
||||
Then story 5 exists after each of the first three deletions
|
||||
And story 5 is gone after the fourth
|
||||
|
||||
Scenario: Delete is checked per version
|
||||
When writer1 sends DELETE /v1/article/11
|
||||
Then the response status is 403 and the body is "missing_permission"
|
||||
And article 11 remains
|
||||
|
||||
Scenario: A story with a hidden version
|
||||
Given writer1 deletes the last version it can see while an unreadable version remains
|
||||
Then the story remains
|
||||
And nothing in the response mentions the other version
|
||||
|
||||
Scenario: Move a story
|
||||
When the owner sends POST /v1/story/5/move with {"site":"atelier"}
|
||||
Then the response status is 200
|
||||
And the body is {"story_id":5,"site":"atelier","site_id":2,"article_ids":[10,12,11,13]}
|
||||
And all four articles have site_id 2
|
||||
And their slugs, lang, publication, content and authors are unchanged
|
||||
And each moved version's revision rises by 1 and content_revision is unchanged
|
||||
|
||||
Scenario: A move requires every language to be enabled at the destination
|
||||
Given atelier has no "ar" language
|
||||
When the owner sends POST /v1/story/5/move with {"site":"atelier"}
|
||||
Then the response status is 422 and the body is "language_not_enabled"
|
||||
And the header X-Missing-Languages is "ar", readable versions only
|
||||
And nothing is moved
|
||||
|
||||
Scenario: A language disabled at the destination counts as missing
|
||||
Given de is listed but disabled on atelier
|
||||
When the owner moves story 5 to atelier
|
||||
Then the response status is 422
|
||||
And X-Missing-Languages includes "de"
|
||||
|
||||
Scenario: A language disabled at the source still needs the destination
|
||||
Given fr is disabled on journal and enabled on atelier
|
||||
When the owner moves story 5 to atelier
|
||||
Then the move succeeds
|
||||
Given fr is disabled on journal and missing on atelier
|
||||
When the owner moves story 5 to atelier
|
||||
Then the response status is 422 and the body is "language_not_enabled"
|
||||
And X-Missing-Languages is "fr"
|
||||
|
||||
Scenario: Address conflict at the destination
|
||||
Given atelier has an fr article with slug "couts" and an ar article with slug "takalif"
|
||||
And the caller can read 10, 11 and 12 but not 13
|
||||
When the caller moves story 5 to atelier
|
||||
Then the response status is 409 and the body is "slug_conflict"
|
||||
And the header X-Conflicting-Articles is 12
|
||||
And article 13 is neither named nor counted
|
||||
And nothing is moved
|
||||
|
||||
Scenario: Every conflict of the story is checked
|
||||
Given two versions of the story conflict at the destination
|
||||
When a caller who can read both moves the story
|
||||
Then no version moves
|
||||
And both readable conflicting versions are named in X-Conflicting-Articles
|
||||
|
||||
Scenario Outline: A move needs update permission on every version, readable or not
|
||||
When <caller> sends POST /v1/story/5/move with {"site":"atelier"}
|
||||
Then the response status is <status>
|
||||
And the response body is <message>
|
||||
And no version is moved
|
||||
|
||||
Examples:
|
||||
| caller | status | message |
|
||||
| an anonymous client | 401 | Unauthorized |
|
||||
| writer1 | 403 | missing_permission |
|
||||
| a user without content.update | 403 | missing_permission |
|
||||
| a user who can read no version | 404 | Article not found |
|
||||
|
||||
Scenario: Hidden versions never leak through the move
|
||||
Given the caller cannot read some versions of story 5
|
||||
Then a 403 body and headers, a 422 X-Missing-Languages and a 409 X-Conflicting-Articles name only readable versions
|
||||
And a 200 article_ids lists only readable versions
|
||||
|
||||
Scenario Outline: Move validation
|
||||
When the owner sends <request>
|
||||
Then the response status is <status>
|
||||
And the response body is <message>
|
||||
|
||||
Examples:
|
||||
| request | status | message |
|
||||
| POST /v1/story/5/move with {"site":"nowhere"} | 404 | site_not_found |
|
||||
| POST /v1/story/5/move with {} | 400 | apperr.BodyErrorMessage |
|
||||
| POST /v1/story/5/move with malformed JSON | 400 | apperr.BodyErrorMessage |
|
||||
| POST /v1/story/9999/move with {"site":"atelier"} | 404 | Article not found |
|
||||
| POST /v1/story/abc/move with {"site":"atelier"} | 400 | apperr.IdUint32ErrorMessage |
|
||||
|
||||
Scenario: Moving to the current site
|
||||
When the owner sends POST /v1/story/5/move with {"site":"journal"}
|
||||
Then the response status is 200
|
||||
And nothing changes
|
||||
And no activity event is recorded
|
||||
|
||||
Scenario: The move is atomic and revalidated inside the transaction
|
||||
When a version is edited, a language is disabled at the destination, or an address is taken between the pre-check and the write
|
||||
Then the move is refused with the matching error and no version moves
|
||||
When a write fails on the third version
|
||||
Then the first two are rolled back
|
||||
|
||||
Scenario: PATCH site_id cannot split a story
|
||||
When the owner sends PATCH /v1/article/10 with {"site_id":2}
|
||||
Then the response status is 409 and the body is "story_move_required"
|
||||
And nothing is changed
|
||||
|
||||
Scenario: PATCH site_id counts readable versions only
|
||||
Given a story holds three versions and a caller reads only one of them
|
||||
When that caller sends PATCH site_id on the readable version
|
||||
Then content.update is required on every version of the story, readable or not, and the whole story moves under the story move rules
|
||||
And every version, hidden ones included, ends up on the destination site in one transaction
|
||||
And a language, address or slug refusal rolls everything back and names readable versions only
|
||||
And a caller lacking update on any version gets 403 "missing_permission" with nothing naming a hidden version
|
||||
And a caller who reads two versions gets 409 "story_move_required"
|
||||
|
||||
Scenario: PATCH site_id on a single-version story applies the move rules
|
||||
Given story 8 holds only article 20 in lang "fr"
|
||||
When the owner sends PATCH /v1/article/20 with {"site_id":2}
|
||||
Then the article moves when fr is enabled at atelier
|
||||
And otherwise the response is 422 "language_not_enabled"
|
||||
And an unassigned article (lang "") moves without a language check
|
||||
|
||||
Scenario: Activity events carry the language
|
||||
Then publish, unpublish, edit and delete events hold params lang, omitted for an unassigned article
|
||||
And a move records one "article.edit" event per moved version with the site change and lang
|
||||
And a refused move records one failed event with a plain reason naming no hidden version
|
||||
|
||||
Scenario: Persistence portability
|
||||
Given the story delete rule, the (story_id, lang) re-creation after a delete and the atomic move with a unique (site_id, lang, slug) conflict
|
||||
Then they pass on SQLite, MySQL 8.4, MariaDB 11 and PostgreSQL
|
||||
Reference in new issue
Block a user