Files
RenewCMS/features/article_localization_storage.feature
T

151 lines
6.8 KiB
Gherkin

Feature: Article language, story and per-site language storage
Articles gain a full BCP 47 language tag, a story they belong to and a
content revision. Each site gets a language table with one default. Existing
articles keep their ids, bodies, publication and public API. They are
migrated without a guessed language: each gets its own story and an
unassigned language until the owner confirms the site default.
Background:
Given the supported languages are "en", "en-GB", "en-US", "fr", "fr-CA", "pt", "pt-BR", "es", "de", "ar"
And a language tag keeps its region, "pt-BR" is never reduced to "pt"
And the domain package internal/domain/language imports only the stdlib
And persistence is portable across SQLite, MySQL and PostgreSQL
Scenario Outline: Language tag normalization
When the tag "<input>" is normalized
Then the result is "<result>"
Examples:
| input | result |
| en | en |
| EN | en |
| pt-br | pt-BR |
| PT_BR | ErrInvalidLanguage |
| fr-ca | fr-CA |
| fr | fr |
| e | ErrInvalidLanguage |
| english | ErrInvalidLanguage |
| en-ZZZZ | ErrInvalidLanguage |
| | ErrInvalidLanguage |
| xx | ErrUnsupportedLanguage |
Scenario Outline: Suggestion for a site's default language from Site.Locale
Given a site whose Locale is "<locale>"
When the default language suggestion is computed
Then the suggestion is "<suggested>" and the substitution flag is <substituted>
Examples:
| locale | suggested | substituted |
| pt-BR | pt-BR | false |
| en-GB | en-GB | false |
| fr-BE | fr | true |
| fr | fr | false |
| xx-YY | none | false |
| null | none | false |
Scenario: Migration of an existing site and its articles
Given a database created before this change with site "default" (Locale "fr-CA") and articles 1, 2 and 3 in it, article 3 online
When database.Migrate runs
Then every article keeps its id, title, body, slug, is_online and timestamps
And each article has lang "" and its own distinct story_id
And each article has content_revision 1
And site "default" has one site_languages row "fr-CA" with is_default true, enabled true and confirmed false
And the migration marker for the article story backfill is recorded
Scenario: Site without a usable locale gets no suggestion row
Given a site with Locale null
When database.Migrate runs
Then the site has no site_languages row
Scenario: Migration runs twice
Given a migrated database
When database.Migrate runs again
Then no story is created or changed, no article changes and no site_languages row changes
And the schema is identical
Scenario: Migration is atomic
Given a database with 3 existing articles
And the backfill write fails while handling the second article
When database.Migrate runs
Then no article has a story_id and the marker is not recorded
And the next Migrate completes the whole backfill
Scenario: Soft-deleted articles are migrated
Given an article that is soft-deleted before the change
When database.Migrate runs
Then it has its own story_id and its tombstone slug is unchanged
Scenario: Slug is unique per site and language
Given site 1 holds an article with slug "bonjour" in language "fr"
When another article is created in site 1, language "fr", slug "bonjour"
Then the write fails with a unique violation mapped to ErrSlugTaken
When an article with slug "bonjour" is created in site 1, language "en"
Then it is stored
When an article with slug "bonjour" is created in site 2, language "fr"
Then it is stored
Scenario: Existing unassigned articles keep the current per-site slug rule
Given two unassigned articles in site 1
When the second is written with the first one's slug
Then ErrSlugTaken is returned
Scenario: One version per language in a story
Given story 7 holds an article in language "fr"
When a second article is stored in story 7, language "fr"
Then the write fails with a unique violation
When an article is stored in story 7, language "en"
Then it is stored
Scenario: Unassigned articles never collide on language
Given 3 articles each with lang "" and their own story
Then all 3 are stored
Scenario: Creating an article before the default is confirmed
Given site "default" has an unconfirmed suggestion "fr-CA"
When a user creates an article with title "Bonjour le monde"
Then the article has lang ""
And it has a new story_id and content_revision 1
Scenario: Creating an article after the default is confirmed
Given site "default" has a confirmed default "fr-CA"
When a user creates an article with title "Bonjour le monde"
Then the article has lang "fr-CA"
And it has a new story_id distinct from every other article
Scenario: Create racing a confirmation never strands an article
Given site "default" has unassigned articles
When the default is confirmed and assigned to unassigned articles in one transaction while articles are created concurrently
Then every article of the site ends with the confirmed default
And no article has a different language than the confirmed default
Scenario: Assigning a language is idempotent
Given a site whose unassigned articles were assigned "fr-CA"
When the assignment runs again
Then no article changes
Scenario Outline: Article reads expose lang and story_id
Given article 12 with lang "<lang>" in story 5
When it is read through "<read>"
Then the JSON contains "lang": "<lang>" and "story_id": 5
And every key present before this change is present with an unchanged value and type
Examples:
| lang | read |
| fr-CA | GET /v1/article/12 with a valid jwt |
| | GET /v1/article/12 with a valid jwt |
| fr-CA | GET /v1/article/ list item |
| fr-CA | public article read of an online article |
Scenario: Article write payloads cannot set lang or story_id
When POST or PATCH /v1/article carries "lang" or "story_id"
Then the fields are ignored and the stored values are unchanged
Scenario: Existing behaviour is unchanged
Then the existing article, pagination, publishing and SEO tests pass without edits
And offline articles still read as not found publicly
Scenario: Persistence portability
Given the repository contract cases above
Then they pass on SQLite, MySQL 8.4, MariaDB 11 and PostgreSQL
And no raw SQL uses a dialect-only construct