feat: storage, language rules and migration
This commit is contained in:
1 parent
8c4a1825ab
commit
0806733d5c
70 files changed
+1771
-132
No files matched your search
@@ -0,0 +1,150 @@
|
||||
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
|
||||
Reference in new issue
Block a user