Files
RenewCMS/features/localization_public_api_and_surfaces.qa.md
T

12 KiB

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.
  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.