Files
RenewCMS/features/localization_public_api_and_surfaces.qa.md
T

70 lines
12 KiB
Markdown

# QA: Localized public API, SEO output and existing surfaces
Accepted together with features/localization_public_api_and_surfaces.feature. One pair, full path: Go backend first (public routes, SEO, sitemap, languages read, MCP, llms), then the SPA, which mocks `/v1` from `docs/api/article-localization.md`.
## Backend (Go, `task test`, package `test`)
Build `NewHandler(...)`s over the real use cases wired to the in-memory fakes already used by `test/article_translation_create_http_test.go`, `test/seo_output_support_test.go` and `test/site_support_test.go` (reuse them, add no second copy; new fake types need unique names). Mount on a fresh `chi.NewRouter()` mirroring `api/router.go`: the two public routes under `/v1/public` with no auth middleware, `/v1/article` with `OptionalAccess`, `/v1/site` for the sitemap and languages. Call `validator.InitValidator()`, exercise with `httptest`. No live server, no SQLite file, no SMTP.
Fixture: the Background of the feature (journal with en default, fr fr_FR, de, ar, es disabled; story 5 with versions 10, 12, 11, 13, 14; stories 6 and 7; unassigned article 30; atelier with a published fr slug clash; notes without confirmed default). Add pt-BR versions 40 and 41 for the og:locale table.
1. **Exact lookup, Examples-driven** over the table: status, body code, and for 200 the key set equals the anonymous `GET /v1/article/{id}` key set plus `alternates`. Alternates order (default first, then tag), keys `id, lang, story_id, slug, url`, exclusion of the served version, drafts and disabled languages; `url` is the pattern result, or JSON `null` when the site has no pattern (assert the pattern-less case with the fake).
2. **Never falls back and never leaks**: wrong language with right slug, draft with owner cookie (cookie ignored), unpublished after unpublish, old slug after change, unassigned article, soft-deleted version: all `404 article_not_found` with the identical body. A raw-response grep shows the id, slug and title of a hidden sibling appear nowhere.
3. **Story resolution, Examples-driven** over the decision table of the design (requested published, missing without fallback, missing with fallback, requested unpublished, both unavailable, default requested and unpublished, no lang, default changed, no confirmed default, disabled language explicit, other site, bad fallback values, bad lang, bad id). Assert `lang`, `requested_lang`, `fallback` on every 200 and that version 11 (de) is never returned as a third language.
4. **Default change stability**: after changing the default to fr, lang-omitted resolves to 12 while `?lang=en`, the exact lookup and `GET /v1/article/10` are unchanged.
5. **SEO object**: key sets (additive `lang`, `alternates`, `og.locale_alternates`, `fallbacks.site_text_skipped`); canonical table (pattern, override wins, no pattern keeps legacy, unassigned keeps legacy, `{lang}` keeps `pt-BR`); `fallbacks.canonical` values; og:locale Examples table including `en` with Site.Locale `en` and `null` (empty string, alternates `[]`); `og.locale_alternates`; the description/title fallback tables and `site_text_skipped`; the default-change limitation scenario. Existing `test/seo_output_*` specs are amended only where they assert the exact key set, the `og.locale` of an assigned article, or the canonical of a site that has a pattern (unassigned and pattern-less cases must pass unchanged).
6. **Sitemap**: eligible versions only, `lang` key, localized urls with a pattern and legacy urls without, `X-Total-Count`, paging invariants unchanged.
7. **Languages read**: `social_locale_state` `derived` with `en_GB` for bare `en` and Site.Locale `en-GB`, `not_set` for bare Site.Locale; amend `test/site_languages_*` accordingly.
7b. **Rate limit**: TestPublicReadRateLimit sends 120 requests alternating the exact lookup and the sitemap from one address, then asserts 429 with Retry-After on the exact lookup, the sitemap and the story resolution, and 200 for another address.
7c. **Language-aware SEO check and status**: TestCheckArticleSEOIsLanguageAware (fakes) and TestArticleSEOIndex* "The site description counts for the default language only and follows a default change" (SQLite and real databases).
8. **MCP**: `list_articles` forwards `lang` (assert the request URL with a stub HTTP client in `internal/infrastructure/mcpserver`'s existing test style), tool descriptions contain no "cannot be changed".
9. **Catalogue drift**: `test/api_catalogue_drift_test.go` green with the two new routes; `web/src/data/apiCatalogue.ts` counts updated.
10. **Layering**: existing layering tests stay green; the public handler imports no persistence, models or gorm.
11. **Media usage** (extend `TestMediaUsage` in `test/media_library_http_test.go` and the usage cases of `test/media_merge_hardening_test.go` and `test/seo_data_media_http_test.go` rather than a parallel file; the fakes and `NewGetMediaUsageUseCase(...)` wiring already exist): fixture image 7 as in the feature's Rule "Media usage names the language and counts a story once". Assert: one article item per version, ordered by id (10, 11, 12, 20, 30); every article item carries `story_id` and `lang` (item 12 equals the exact JSON of the feature, key set `type,id,title,published,fields,story_id,lang`); the unassigned article 30 has `lang` `""` and is not grouped with any story; author, site and entry items have neither key (assert absence in the raw JSON); order articles, authors, sites, entries unchanged; unused image answers `[]`; `404 Image not found` and `400 apperr.IdUint32ErrorMessage` unchanged. Grouping check: items 10, 11, 12 share `story_id` 5. `GET /v1/media/` gives `used_in` 5 for image 7 (story 5 once, story 6, article 30, author, site) and the same count as before the slice on a one-language fixture. The delete-in-use count (unchanged endpoint, SPA side) is covered in the SPA list below. Amend only the usage assertions that compare an article item's exact JSON or `used_in` of an image shared by several versions of a story; every other media test must pass untouched.
Older features amended in the same commit (listed in the feature's scenario "Older features are amended in the same commit"): `media_library.feature` (Rule: Usage), `media_field_library.feature` (lines 11-12, articles heading, delete-in-use counts), `media_browse.feature` (usage mock), `seo_data_backend.feature` and `sites_backend.feature` (only exact usage key sets / `used_in`), `seo_output_backend.feature`, `site_languages.feature`, `integration.feature`; their `.qa.md` files follow.
Real-database runs are required (gate): `task test:mysql` and `task test:postgres` for a new persistence test in `test/article_localization_storage_persistence_test.go` (or a sibling file) that exercises the repository queries behind exact lookup and story resolution: same results on each driver, soft-deleted and offline rows excluded, unassigned excluded, `EXPLAIN`-free (assert correctness and the presence of `idx_articles_site_slug` and `idx_articles_story_lang`, not the plan). Run SQLite in `task test`. Run both real-DB tasks once, at the end of the backend stage. Use the podman shim if the Docker daemon is off.
Docs (the contract the SPA mocks): `docs/api/article-localization.md`, `docs/api.md`, `docs/api/articles.md`, `docs/llms.md`, `docs/llm/` recipes, `docs/release-notes.md`.
## Automated SPA (Vitest + Vue Test Utils, `task test:web`)
Mock `useApi`'s `request` per case (no backend), from the documented contract. Mount EditorView in DashboardShell with a verified session (user id 7, username "marta") and reuse `editorKit.ts` and `localizationKit.ts`. Fixture: the Background (journal, story 5, version 12 open). Specs next to the code in `__tests__/`; extend the existing `EditorSeo*.spec.ts` files rather than adding a parallel set.
1. **Search and sharing, one test per state**: loc.seo.noFallback (every heading, note, badge, row and help text listed in the feature), loc.slug.published, loc.url.unset (alert, button target `/settings/sites/journal/public-url`, no canonical, no alternates), loc.l10.noSocial.
2. **Fallback labels, Examples-driven** over every `fallbacks` source and `site_text_skipped` value; both site-text articles (default language, other language).
3. **Draft, canonical override, single-language site**: alternates and drafts, override note, no language text on a one-language site.
4. **Edit slug**: warning only when published; request body `{slug, if_revision}`; 400 `invalid_slug`, 409 slug conflict and 409 `revision_conflict` outcomes; focus returns to the Edit slug button.
5. **Integration page**: the four cards and sample shapes; catalogue entries for both routes; no "Titles can't be changed" sentence.
6. **Home**: request uses `group=story`; default-language title; the code chip only for non-default; French-only reader (loc.l12.homeFr); counts are articles; single-language site unchanged.
7. **Activity**: sentences with the language for every article action code that carries `params.lang` (publish, unpublish, create, update, delete), English and French; events without `lang` unchanged.
8. **Media usage**: mock `GET /v1/media/7/usage` with items 10, 11, 12 (story_id 5; langs en, de, fr), 20 (story 6) and 30 (lang ""): the Articles section shows story 5 once with the versions EN, DE, FR listed and the heading counts articles (3 with story 6 and article 30), not 5 items; a one-language site lists as before; the delete-in-use dialog counts stories; items without `story_id`/`lang` (older mocks) still render one row per item (existing `mediaUsage` specs stay green).
9. **Article menu labels**: Publish, Unpublish and Delete name the language on multilingual sites only.
10. **API URL copy**: the Details card text, copied text `/v1/article/14?lang=fr`, toast "API URL copied", unassigned version copies without `lang`, clipboard failure handling as today.
11. **SEO preview**: breadcrumb, title, description fallback line, the note.
12. **Keyboard and focus**: Edit slug dialog trap and Escape, row menu items unchanged.
Existing SPA specs to amend (and nothing else): `EditorSeoPanel`, `EditorSeoSlug`, `EditorSeoFrench`, `SeoIntegration`, `IntegrationView`, `HomeView`, `ActivityView`, media usage specs, `seoApiPreview.spec.ts`, `apiCatalogue` spec.
## Visual (`task visual:check -- <screen>`)
Add setups in `web/visual/states.ts` with the board's own fixture data (Journal, locale-fixtures.js; the French title "Comment nous chiffrons une rénovation", slugs and URLs of the board). Screens and states, light and dark, 1440 and 390 (and 1024 where the table says so):
| Screen file | States |
| --- | --- |
| localization-metadata-api | loc.seo.noFallback, loc.slug.published, loc.url.unset, loc.l10.noSocial, loc.l10.api (see open question below) |
| localization-surfaces | loc.l12.home, loc.l12.homeFr, loc.l12.activity, loc.l12.media (1440, 1024, 390), loc.l12.menu, loc.l12.apiCopy, loc.l12.seoPreview, loc.l12.siteFallback (1440, 390) |
Clock-sensitive: loc.l12.activity and loc.l12.home show relative times if the SPA renders them from the current date; if so mark `skip: 'clock-sensitive'` and cover the language text in Vitest with a fixed event list. No date freezing, no threshold change, no exception without the user's explicit approval.
Decision D6 (user): the specification works around these design conflicts. For loc.l10.api, loc.l12.siteFallback, loc.l12.apiCopy and loc.l12.seoPreview the coder must stop and return the measured differences or route gaps to the user; it must not edit docs/design, add a `skip`, change a threshold or add an exception. The route `language_disabled` is not returned by any public route (a disabled language stays readable). Known gaps (reported at specification time, not waived): loc.l10.api is drawn with the alert "Proposed capability / These requests don't exist yet" and the story id "st_01", which will be false once this slice ships; loc.l12.siteFallback draws two different articles in one frame (French and Arabic), which no single SPA screen can show; loc.l12.apiCopy and loc.l12.seoPreview are drawn as stand-alone "Details" and "Search preview" pages whose route is not named in docs/design/routes.md.
## Manual (not covered by screenshots or Vitest)
1. Keyboard and focus order in the Edit slug dialog and the new alternates and social locale cards; hover and motion.
2. Real browser: copy button writes to the real clipboard.
3. Real embedded frontend build (`task build`, run the binary): `/integration` shows the new cards; no console errors.
4. Real SMTP is not involved.
5. A real site with a Public URL pattern: open the canonical and an alternate URL in a browser is not possible (they point to the website, not RenewCMS); verify instead that the strings match the pattern.