Files
RenewCMS/features/seo_output_backend.qa.md

10 KiB

QA procedure: SEO output backend (slice 2 of 4)

Spec: features/seo_output_backend.feature. Go tests only, in test/ (package test), run with task test. No live server, no real SMTP, no real SQLite file outside the temp-dir repository-contract harness. Call validator.InitValidator() before any handler call. Error assertions compare the plain-text body to the exact token or constant named in the spec. Success bodies are JSON. Assert exact key sets by decoding into map[string]any and comparing sorted key lists.

Divergences from docs/design

docs/design is read-only; slices 3 and 4 align to the code.

  • published_at: created_at for online articles, null for drafts; modified_at is updated_at. No new column, no migration (user decision).
  • Social image name: the change-list fallback chain says og_image_id; the article field is social_image_id. The code name is used.
  • Sitemap envelope: the sitemap answers a JSON array with the total in X-Total-Count, as integration.md says. Articles and media list routes answer an envelope without that header. The two conventions coexist. X-Total-Count is added to the CORS ExposedHeaders (alongside Link) and documented in docs/api.md.
  • Sitemap sizes: 25, 50, 100, 500 with default 100 (integration.md), unlike the 5, 25, 50, 100 of the other paged routes. Order is id ascending.
  • Unknown site key: the body is the existing token site_not_found (apperr.SiteNotFoundErrorMessage), not the copy "Site not found" of integration.md.
  • Check body: accepts the 12 SEO fields and an optional body; site_id and author_ids answer 400. {} checks the saved state.
  • Thresholds: no_h needs at least 1000 characters of plain text and no h2-h6; short is under 300 characters.
  • Status precedence: noindex gives hidden regardless of suggestions; seo_suggestions still counts non-dismissed ones.
  • %site% with an empty site_name: replaced by an empty string, then the title is trimmed (whitespace only).
  • same_as: social link URLs only; the website is in url only; JSON-LD sameAs equals same_as.

Setup

  • New files test/seo_output_*_test.go. Fakes have unique names: mockSeoOutArticleRepo (articles with per-site slugs, author credits, list filters q, status, site, sort, seo with paging, a call counter, failure injection), mockSeoOutSiteRepo (sites with key lookup), mockSeoOutAuthorRepo, mockSeoOutImageRepo and the fake audit repository (reuse, with a counter that asserts no Append).
  • Routers mirror api/router.go: the public /{id}/seo route and the authenticated /{id}/seo/check route on the article router, the public /{key}/sitemap route on the site router. Protected routes sit behind jwtauth.Verifier + jwtauth.Authenticator with a test jwtauth.New("HS256", ...). Signed jwt cookies with the user_id claim for: owner, admin, a verified account with another role, an unverified account, plus a malformed cookie. API-key requests use the key authentication path.
  • Fixtures: the background sample site 1 and article 12, builders for variants (seo_title lengths, descriptions, covers, authors with website and links, bodies with headings and images). Fixed created_at/updated_at times; no clock reads, so no clock-sensitive items.
  • Assertions read the fakes' call counters and stored state, never only the JSON.

A. Resolver

  1. Shape: exact key sets of the root, og, twitter, image, fallbacks; no body, id or seo_dismissed; raw response has no body text.
  2. Bare article: every output through the fallbacks with the exact values of the background; fallbacks values; published_at and modified_at.
  3. Tables: title chain and template variants; empty site_name with each template (trimmed); description chain including first-sentence extraction (tags stripped, entities, terminator or none, heading plus paragraph) and the empty chain; image chain (social, cover with alt, site, none) with image, og.image, twitter.image, twitter.card; canonical and robots (override, site default index/noindex, noindex: true).
  4. Social overrides apply to og and twitter only; social title and description fall back independently.
  5. Authors: ordered, url from website or "", same_as from links in order and never the website; no authors gives [] and no author key in json_ld.
  6. JSON-LD: compare the full decoded object to the expected one; headline uses the meta title without the template; dates in RFC 3339; image omitted when none; datePublished omitted for drafts.
  7. ?format=html|json|pdf returns the identical body as the bare call; second site uses its own base_url, template and robots.

B. Visibility

  1. Credentials table for a draft: owner, admin, other verified role, API key give 200; none, unverified, malformed cookie give 404 Article not found. An online article is 200 for everyone.
  2. Draft has published_at null and no datePublished. Unpublish makes the object 404 for anonymous; missing article 404; abc gives apperr.IdUint32ErrorMessage; read performs no write and no audit Append; repository failure gives 500.

C. Check endpoint

  1. Shape: exact keys of root, suggestion, slug.
  2. Code table: one article per code with the exact field and params and the thresholds above.
  3. Edge tables: title 60/61, description 70/69 and 160/161, slug 75/76, no_h 999/1000 with h2, h6, h1 only, short 299/300, rune counting with 2-byte characters, title measured before the template, text length ignoring markup.
  4. Fully prepared article gives ready and []; overrides replace saved values (set, clear, null image id, body); the saved article, its updated_at, the site, the media usage and the activity log are unchanged after every call.
  5. Dismissed: saved codes filtered; override seo_dismissed filters; all 11 dismissed on a noindex article gives hidden with []; fixed code order (title_long first, no_au before short).
  6. Status table: ready, suggestions, hidden (noindex with and without suggestions); a noindex override gives hidden without saving.
  7. Slug table: own slug, free, other site's, taken with -3 suggestion where -2 is also taken, empty string regeneration (free and taken), 100-character suggestion, taken slug is 200 not 409.
  8. Validation table identical to PATCH (status and token); malformed and null bodies (including body: null) give 400 apperr.BodyErrorMessage; {} gives 200; site_id and author_ids give 400.
  9. Credentials: owner, admin, other verified role, API key give 200; unverified 403; none 401; draft 200; missing 404; abc IdUint32ErrorMessage.
  10. Publishing a draft with suggestions still returns 200.

D. List

  1. seo_status and seo_suggestions per item for ready, suggestions, hidden and a partly dismissed article; no seo_dismissed key on items; existing fields unchanged.
  2. Agreement test: for each fixture article, list values equal the check result of the same use case (guards the one-rule-set requirement).
  3. Filter table ready, suggestions, hidden (ids and totals); composition with q, status, site, sort and page/size; pages disjoint and stable; counts unchanged; filter applied before paging; anonymous caller sees online items only; dismissed codes not counted.
  4. Invalid table (maybe, empty, READY, ready,hidden, 1) gives 400 apperr.BodyErrorMessage and the repository counter stays 0; format=pdf is reported first; no-filter behaviour unchanged.

E. Sitemap

  1. Only online articles of the key; X-Total-Count equals the filtered total; exact item key set; noindex flag included; url from slug and base_url; drafts excluded even with a verified cookie; unpublish removes.
  2. Public: no credentials gives 200; empty site gives [] and 0.
  3. Paging table (250 articles: default 100, size 25, page 3, size 500, out of range gives empty array and the real total); 60 articles partition into 25, 25, 10 in id order with no overlap.
  4. Invalid table (page=0, -1, abc, empty, size=0, 10, 1000, abc) gives 400 apperr.BodyErrorMessage and the repository counter stays 0.
  5. Unknown key gives 404 site_not_found; an id as the key gives 404; PATCH /v1/site/{id} still owner only; repository failure gives 500.
  6. CORS: with the CORS middleware enabled, Access-Control-Expose-Headers lists X-Total-Count and Link.

F. Quality gates

  1. Layering guard: use cases import no persistence, mailer, models, gorm, net/http or chi; domain packages import no GORM or HTTP; handlers import no persistence; api/dependecyInjection.go builds the new use cases and api/router.go wires them; validation uses validate tags through validator.Validate.Struct; no comments added.
  2. Scoped mutation run through task mutate only, on the new use cases (resolver, suggestion rules, check, list status, sitemap), e.g. MUTATE_FILES=<file names> MUTATE_RUN='TestSeoOutput' MUTATE_WORKERS=2 task mutate -- <use case package path>. Report LIVED and NOT COVERED mutants and re-check filtered suspects without MUTATE_RUN. Boundary mutants (60/61, 70/69, 160/161, 75/76, 999/1000, 299/300) and the fallback-chain order are the expected high-value targets. Also task lint:go and task fmt.

Non-behavioural deliverables (checked by reading)

  • docs/api.md: the three new endpoints, the seo list parameter, the seo_status and seo_suggestions fields, the X-Total-Count header on the sitemap and its CORS exposure.
  • docs/api/articles.md: the resolved object, fallback chain, codes and soft ranges, draft visibility.
  • docs/release-notes.md: announcement.
  • web/src/data/apiCatalogue.ts: entries for GET /v1/article/{id}/seo, POST /v1/article/{id}/seo/check, GET /v1/site/{key}/sitemap, plus the seo list parameter and the two list fields; its spec and web/src/views/IntegrationView.spec.ts counts updated. Run under task test:web; report NOT RUN if the runner is not installed.

Manual

  • Real boot against a database with slice 1 data: GET /v1/article/{id}/seo for a published article, a draft with and without a session, the sitemap for the default site and a second site.
  • Cross-origin check that X-Total-Count is readable when CORS is configured.
  • Paste a resolved json_ld into a schema.org / Rich Results validator.
  • Not covered here: the SPA (slices 3 and 4).