10 KiB
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_atfor online articles,nullfor drafts;modified_atisupdated_at. No new column, no migration (user decision). - Social image name: the change-list fallback chain says
og_image_id; the article field issocial_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-Countis added to the CORSExposedHeaders(alongsideLink) 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_idandauthor_idsanswer 400.{}checks the saved state. - Thresholds:
no_hneeds at least 1000 characters of plain text and no h2-h6;shortis under 300 characters. - Status precedence: noindex gives
hiddenregardless of suggestions;seo_suggestionsstill counts non-dismissed ones. %site%with an emptysite_name: replaced by an empty string, then the title is trimmed (whitespace only).- same_as: social link URLs only; the website is in
urlonly; JSON-LDsameAsequalssame_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,mockSeoOutImageRepoand the fake audit repository (reuse, with a counter that asserts no Append). - Routers mirror
api/router.go: the public/{id}/seoroute and the authenticated/{id}/seo/checkroute on the article router, the public/{key}/sitemaproute on the site router. Protected routes sit behindjwtauth.Verifier+jwtauth.Authenticatorwith a testjwtauth.New("HS256", ...). Signedjwtcookies with theuser_idclaim 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_attimes; no clock reads, so no clock-sensitive items. - Assertions read the fakes' call counters and stored state, never only the JSON.
A. Resolver
- Shape: exact key sets of the root,
og,twitter,image,fallbacks; nobody,idorseo_dismissed; raw response has no body text. - Bare article: every output through the fallbacks with the exact values of the background;
fallbacksvalues;published_atandmodified_at. - Tables: title chain and template variants; empty
site_namewith 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) withimage,og.image,twitter.image,twitter.card; canonical and robots (override, site defaultindex/noindex,noindex: true). - Social overrides apply to og and twitter only; social title and description fall back independently.
- Authors: ordered,
urlfrom website or"",same_asfrom links in order and never the website; no authors gives[]and noauthorkey in json_ld. - 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;
datePublishedomitted for drafts. ?format=html|json|pdfreturns the identical body as the bare call; second site uses its own base_url, template and robots.
B. Visibility
- 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. - Draft has
published_atnull and nodatePublished. Unpublish makes the object 404 for anonymous; missing article 404;abcgivesapperr.IdUint32ErrorMessage; read performs no write and no audit Append; repository failure gives 500.
C. Check endpoint
- Shape: exact keys of root, suggestion,
slug. - Code table: one article per code with the exact
fieldandparamsand the thresholds above. - Edge tables: title 60/61, description 70/69 and 160/161, slug 75/76,
no_h999/1000 with h2, h6, h1 only,short299/300, rune counting with 2-byte characters, title measured before the template, text length ignoring markup. - Fully prepared article gives
readyand[]; overrides replace saved values (set, clear, null image id, body); the saved article, itsupdated_at, the site, the media usage and the activity log are unchanged after every call. - Dismissed: saved codes filtered; override
seo_dismissedfilters; all 11 dismissed on a noindex article giveshiddenwith[]; fixed code order (title_long first, no_au before short). - Status table: ready, suggestions, hidden (noindex with and without suggestions); a
noindexoverride giveshiddenwithout saving. - Slug table: own slug, free, other site's, taken with
-3suggestion where-2is also taken, empty string regeneration (free and taken), 100-character suggestion, taken slug is 200 not 409. - Validation table identical to PATCH (status and token); malformed and null bodies (including
body: null) give 400apperr.BodyErrorMessage;{}gives 200;site_idandauthor_idsgive 400. - Credentials: owner, admin, other verified role, API key give 200; unverified 403; none 401; draft 200; missing 404;
abcIdUint32ErrorMessage. - Publishing a draft with suggestions still returns 200.
D. List
seo_statusandseo_suggestionsper item for ready, suggestions, hidden and a partly dismissed article; noseo_dismissedkey on items; existing fields unchanged.- Agreement test: for each fixture article, list values equal the check result of the same use case (guards the one-rule-set requirement).
- Filter table
ready,suggestions,hidden(ids and totals); composition withq,status,site,sortandpage/size; pages disjoint and stable; counts unchanged; filter applied before paging; anonymous caller sees online items only; dismissed codes not counted. - Invalid table (
maybe, empty,READY,ready,hidden,1) gives 400apperr.BodyErrorMessageand the repository counter stays 0;format=pdfis reported first; no-filter behaviour unchanged.
E. Sitemap
- Only online articles of the key;
X-Total-Countequals the filtered total; exact item key set;noindexflag included; url from slug and base_url; drafts excluded even with a verified cookie; unpublish removes. - Public: no credentials gives 200; empty site gives
[]and0. - 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.
- Invalid table (
page=0,-1,abc, empty,size=0,10,1000,abc) gives 400apperr.BodyErrorMessageand the repository counter stays 0. - 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. - CORS: with the CORS middleware enabled,
Access-Control-Expose-HeaderslistsX-Total-CountandLink.
F. Quality gates
- 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.gobuilds the new use cases andapi/router.gowires them; validation uses validate tags throughvalidator.Validate.Struct; no comments added. - Scoped mutation run through
task mutateonly, 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 withoutMUTATE_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. Alsotask lint:goandtask fmt.
Non-behavioural deliverables (checked by reading)
docs/api.md: the three new endpoints, theseolist parameter, theseo_statusandseo_suggestionsfields, theX-Total-Countheader 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 forGET /v1/article/{id}/seo,POST /v1/article/{id}/seo/check,GET /v1/site/{key}/sitemap, plus theseolist parameter and the two list fields; its spec andweb/src/views/IntegrationView.spec.tscounts updated. Run undertask 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}/seofor 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-Countis readable when CORS is configured. - Paste a resolved
json_ldinto a schema.org / Rich Results validator. - Not covered here: the SPA (slices 3 and 4).