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 sends GET /v1/article/ Then "capabilities" is 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//publish Then the response status is And 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 Then the response status is And the response body is 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 sends POST /v1/story/5/move with {"site":"atelier"} Then the response status is And the response body is 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 Then the response status is And the response body is 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