Files
RenewCMS/features/article_story_lifecycle.feature
T

256 lines
13 KiB
Gherkin

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