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,189 @@
|
||||
Feature: Create a translation of an article version
|
||||
A user who can create articles adds a version of an existing article in another language. The version joins the story of the version it is created from, in the same site, as an offline draft. Two entry points share one implementation: POST /v1/article/{id}/translations and POST /v1/article/ with story_id. Hidden versions are never named, counted or hinted at.
|
||||
|
||||
Background:
|
||||
Given the language rules of features/article_localization_storage.feature apply
|
||||
And error responses are plain text whose body is the snake_case code, with extra data in response headers
|
||||
And a "writer" is a verified user with content.create, content.update and content.read at scope all
|
||||
And a "restricted writer" holds content.create and content.read at scope own
|
||||
And site "journal" (id 1) has enabled languages en (default), fr, de, ar and disabled language es
|
||||
And story 5 in "journal" holds article 10 (lang en, online, created by user 1, title "Renovation costs") and article 11 (lang de, offline, created by user 2)
|
||||
And article 10 has body, excerpt, seo_title, seo_description, social_title, social_description, cover_image_id 3, cover_image_alt, social_image_id 4, canonical_url, noindex true, authors [Camille] and content_revision 4
|
||||
|
||||
Scenario: Copy from the source version
|
||||
Given a valid jwt cookie for a writer
|
||||
When the client sends POST /v1/article/10/translations with {"lang":"fr","title":"Couts de renovation","copy":true}
|
||||
Then the response status is 201 and the body is the created article JSON
|
||||
And it has a new id, lang "fr", story_id 5, site_id 1, is_online false and the given title
|
||||
And body, excerpt, seo_title, seo_description, social_title, social_description, cover_image_id, cover_image_alt, social_image_id and authors equal article 10's
|
||||
And the slug is generated from the new title, not copied
|
||||
And canonical_url is empty, noindex is false and seo_dismissed is empty
|
||||
And created_by is the caller and revision is 1
|
||||
And the source is article 10 with source_revision 4, captured in the same transaction as the copy
|
||||
And articles 10 and 11 are unchanged
|
||||
And no file or image row is duplicated
|
||||
|
||||
Scenario: Copy from a chosen source
|
||||
Given a valid jwt cookie for a writer
|
||||
When the client sends POST /v1/article/10/translations with {"lang":"fr","title":"Kosten","copy":true,"source_article_id":11}
|
||||
Then the response status is 201
|
||||
And the content is copied from article 11
|
||||
And the source is article 11 with its content_revision at that moment
|
||||
|
||||
Scenario: Start empty
|
||||
Given a valid jwt cookie for a writer
|
||||
When the client sends POST /v1/article/10/translations with {"lang":"fr","title":"Couts de renovation","copy":false}
|
||||
Then the response status is 201
|
||||
And body, excerpt, SEO and social text are empty
|
||||
And there is no cover image and no social image
|
||||
And authors is the creating user's byline
|
||||
And there is no source and GET shows "source": null
|
||||
|
||||
Scenario Outline: Request validation
|
||||
Given a valid jwt cookie for a writer
|
||||
When the client sends POST /v1/article/10/translations with <body>
|
||||
Then the response status is <status>
|
||||
And the response body is <message>
|
||||
And nothing is stored
|
||||
|
||||
Examples:
|
||||
| body | status | message |
|
||||
| {"title":"Couts","copy":true} | 400 | the validate tag error text |
|
||||
| {"lang":"fr","copy":true} | 400 | the validate tag error text |
|
||||
| {"lang":"fr","title":"ab","copy":true} | 400 | the validate tag error text |
|
||||
| {"lang":"fr","title":"<201 characters>","copy":true} | 400 | the validate tag error text |
|
||||
| {"lang":"fr","title":"Couts"} | 400 | apperr.BodyErrorMessage |
|
||||
| {"lang":"fr","title":"Couts","copy":false,"source_article_id":10} | 400 | apperr.BodyErrorMessage |
|
||||
| {"lang":"fr", | 400 | apperr.BodyErrorMessage |
|
||||
| {"lang":"english","title":"Couts","copy":true} | 422 | invalid_language |
|
||||
| {"lang":"xx","title":"Couts","copy":true} | 422 | unsupported_language |
|
||||
| {"lang":"ja-JP","title":"Couts","copy":true} | 422 | unsupported_language |
|
||||
|
||||
Scenario: The language tag is normalized
|
||||
Given a valid jwt cookie for a writer
|
||||
When the client sends POST /v1/article/10/translations with {"lang":"FR","title":"Couts","copy":true}
|
||||
Then the response status is 201
|
||||
And the stored lang is "fr"
|
||||
|
||||
Scenario: Language not configured on the site
|
||||
Given site "journal" has no language row for "pt"
|
||||
When a writer sends POST /v1/article/10/translations with {"lang":"pt","title":"Custos","copy":true}
|
||||
Then the response status is 422 and the body is "language_not_enabled"
|
||||
And nothing is stored
|
||||
|
||||
Scenario Outline: One neutral answer when the language cannot be used
|
||||
Given a valid jwt cookie for the caller
|
||||
And <situation>
|
||||
When the caller sends POST /v1/article/10/translations with {"lang":"fr","title":"Couts","copy":true}
|
||||
Then the response status is 409 and the body is "language_unavailable"
|
||||
And there is no X-Existing-Article-Id header
|
||||
And the body and the header set are identical across the three situations
|
||||
And nothing is stored
|
||||
|
||||
Examples:
|
||||
| situation |
|
||||
| an fr version exists but is unreadable to the restricted writer (draft of another user) |
|
||||
| fr is disabled on the site |
|
||||
| a concurrent request created fr first (unique violation on (story_id, lang)) |
|
||||
|
||||
Scenario: The language already exists and is readable
|
||||
Given a valid jwt cookie for a writer
|
||||
When the client sends POST /v1/article/10/translations with {"lang":"de","title":"Kosten","copy":true}
|
||||
Then the response status is 409 and the body is "language_exists"
|
||||
And the header X-Existing-Article-Id is 11
|
||||
And nothing is stored
|
||||
When the client sends the same request with "lang":"en"
|
||||
Then the response status is 409 and the body is "language_exists"
|
||||
And the header X-Existing-Article-Id is 10
|
||||
|
||||
Scenario: Two concurrent requests for the same language
|
||||
Given a valid jwt cookie for a writer
|
||||
When two requests for lang "fr" on story 5 run at the same time
|
||||
Then exactly one answers 201
|
||||
And the other answers 409 "language_unavailable" and stores nothing
|
||||
And story 5 holds exactly one fr version
|
||||
|
||||
Scenario Outline: The path version and the source must be readable
|
||||
When the caller sends <request>
|
||||
Then the response status is <status>
|
||||
And the response body is <message>
|
||||
And nothing is stored
|
||||
|
||||
Examples:
|
||||
| request | status | message |
|
||||
| anonymous POST /v1/article/10/translations | 401 | Unauthorized |
|
||||
| reader without content.create POST /v1/article/10/translations | 403 | missing_permission |
|
||||
| writer POST /v1/article/9999/translations | 404 | Article not found |
|
||||
| restricted writer POST /v1/article/11/translations | 404 | Article not found |
|
||||
| writer POST /v1/article/abc/translations | 400 | apperr.IdUint32ErrorMessage |
|
||||
| writer POST /v1/article/10/translations with source_article_id 9999 | 404 | source_unavailable |
|
||||
| restricted writer POST /v1/article/10/translations with source_article_id 11 | 404 | source_unavailable |
|
||||
| writer POST /v1/article/10/translations with source_article_id 20 (other story) | 422 | source_other_story |
|
||||
|
||||
Scenario: Access to the source is rechecked at submit
|
||||
Given a writer opened the translation dialog with source article 11
|
||||
And article 11 was deleted after the dialog opened
|
||||
When the writer submits POST /v1/article/10/translations with source_article_id 11
|
||||
Then the response status is 404 and the body is "source_unavailable", the same as for an unreadable source
|
||||
And nothing is stored
|
||||
|
||||
Scenario: Unassigned articles cannot be translated yet
|
||||
Given article 30 has lang ""
|
||||
When a writer sends POST /v1/article/30/translations with {"lang":"fr","title":"Couts","copy":true}
|
||||
Then the response status is 409 and the body is "setup_required"
|
||||
And nothing is stored
|
||||
|
||||
Scenario: The slug of the new version is free within site and language
|
||||
Given site "journal" already has an fr article with slug "couts-de-renovation"
|
||||
When a writer creates an fr translation titled "Couts de renovation"
|
||||
Then the new slug is the next free suffixed slug in (site 1, fr)
|
||||
When a writer creates a translation with the same title in a language with no clash
|
||||
Then the slug is the plain slug
|
||||
|
||||
Scenario: Entry point 2 behaves exactly like entry point 1
|
||||
Given a valid jwt cookie for a writer
|
||||
When the client sends POST /v1/article/ with {"title":"Couts","lang":"fr","story_id":5,"source_article_id":10,"copy":true}
|
||||
Then the response status is 201
|
||||
And the result, errors, headers and stored data are exactly those of POST /v1/article/10/translations with the same lang, title and source
|
||||
And the same use case runs for both
|
||||
|
||||
Scenario Outline: POST /v1/article/ is unchanged when it is not a translation
|
||||
Given a valid jwt cookie for a writer
|
||||
When the client sends POST /v1/article/ with <body>
|
||||
Then the response status is <status>
|
||||
And <effect>
|
||||
|
||||
Examples:
|
||||
| body | status | effect |
|
||||
| {"title":"Plain","body":"x"} | 201 | a new story is created, lang is the site's starting language as before |
|
||||
| {"title":"Neuf","lang":"fr"} | 201 | a new story is created in fr |
|
||||
| {"title":"Neuf","lang":"pt"} | 422 | the body is "language_not_enabled" |
|
||||
| {"title":"Neuf","lang":"es"} | 409 | the body is "language_unavailable" (disabled) |
|
||||
| {"title":"Neuf","story_id":5} | 400 | the body is apperr.BodyErrorMessage |
|
||||
| {"title":"Neuf","lang":"fr","story_id":999} | 404 | the body is "Article not found" |
|
||||
| {"title":"Neuf","copy":true} | 400 | the body is apperr.BodyErrorMessage |
|
||||
|
||||
Scenario: PATCH still ignores lang, story_id and source fields
|
||||
Given a valid jwt cookie for a writer
|
||||
When the client sends PATCH /v1/article/10 with {"body":"x","lang":"fr","story_id":9}
|
||||
Then the response status is 200
|
||||
And lang and story_id are unchanged
|
||||
|
||||
Scenario: Activity records the creation
|
||||
Given a valid jwt cookie for a writer
|
||||
When a translation is created
|
||||
Then one "article.create" event (articles category) targets the new article
|
||||
And its params hold lang "fr", story_id 5 and source_article_id 10
|
||||
When a creation is refused
|
||||
Then one failed event is recorded with a plain reason
|
||||
And nothing in it names a hidden version
|
||||
|
||||
Scenario: A new version does not appear publicly
|
||||
Given a translation was created
|
||||
When a client sends GET /v1/article/{new id} without a jwt cookie
|
||||
Then the response status is 404 and the body is "Article not found"
|
||||
And the public list is unchanged
|
||||
|
||||
Scenario: Persistence portability
|
||||
Given the unique (story_id, lang) violation, the free-slug retry and the concurrent create
|
||||
Then they pass on SQLite, MySQL 8.4, MariaDB 11 and PostgreSQL
|
||||
Reference in new issue
Block a user