11 KiB
11 KiB
QA procedure: Sites backend
Spec: features/sites_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. Every error assertion compares the plain-text body to the exact code token.
Setup
- New files
test/site_*_test.go. Fakes have unique names:mockSiteRepo(sites, default flag, article counts, transactional reassign-and-delete as one operation, failure injection),mockSiteImageRepo(images and usage), and an extendedmockArticleRepo(site_id, site filter, move). A fake audit repository with fail and panic switches is reused. - Routers mirror
api/router.go: a newNewSiteRouter, plus the article and media routers. Protected routes sit behindjwtauth.Verifier+jwtauth.Authenticatorwith a testjwtauth.New("HS256", ...). Requests carry real signedjwtcookies carrying theuser_idclaim for an owner, an admin and an unverified account (roles are only owner and admin); an API key request uses the key authentication path.
A. Reading (Rule: Reading sites)
- GET /v1/site/ with three sites: 200, order default first then name,
article_counton each. - GET /v1/site/ credentials table: owner, admin, API key give 200; unverified gives 403; none gives 401.
- A site without image and locale returns
default_social_image_idnull andlocalenull.
B. Creating
- POST creates a site: 201, full body, stored,
is_defaultfalse,article_count0. - Optional fields default to
default_robots"index", locale null, image null. - POST with
is_defaulttrue: the new site is the only default. - Base URL normalisation table (trailing slash, host case, loopback http).
- Base URL rule table (all rows,
invalid_site_url). - Key format table (valid and invalid rows) and
site_key_taken409 with nothing stored. - Other fields table: name, robots, locale (
invalid_locale), unknown image (image_not_found), malformed JSON gives 400 and nothing stored. - Locale table: valid values stored, empty and null stored as null.
C. Editing
- PATCH edits name, base_url, robots, locale and returns the site with the unchanged key.
- Absent fields stay untouched.
- Image set to 5 then cleared with null.
- Key table: same key 200, different key 400
key_immutable, key unchanged. - A rejected key change leaves the name unchanged.
is_defaultin the body: 400use_set_default, default unchanged.- Validation table (base_url, locale, image, name, robots), site unchanged.
- Missing site 404
site_not_found; malformed id gives the uint32 format error. - No-op edit: 200 and no audit event.
D. Default site
- PATCH /{id}/default swaps the default; exactly one default remains.
- Current default: 200, no event. Missing site: 404.
- Fault injected on the second step of the swap: the previous default is still the only default (the fake mimics the transaction rollback).
- A subsequently created article gets the new default's id.
E. Deleting
- Empty site: 204, gone.
- Site with articles and no
reassign_to: 409site_has_articles, unchanged. - With a valid
reassign_to: 204, articles moved, site gone, via a single repository operation (assert the fake records one call). - Failure injected in that operation: articles and site unchanged.
reassign_totable: 999 gives 404site_not_found; the site's own id and a non-numeric value give 400invalid_reassign.- Default site: 403
default_site, also when it has articles and noreassign_to. Missing site: 404. Unusedreassign_toon an empty site: 204, destination untouched.
F. Owner-only writes
- Role table for POST, PATCH, PATCH default, DELETE: admin gives 403 "Only the owner can do this."; API key gives 403 "This action needs a real session."; nothing changes.
- Admin with a malformed body still gets 403 "Only the owner can do this.".
G. Articles
- New article is offline and on the default site; reads (GET one, GET list, publish, PATCH) carry
site_id. GET /v1/article/?site=shopfilters; composes with status (online, draft), q, sort (for exampletitle:asc), size and page;?site=nopegives 200 empty with globalcounts;?site=gives 400apperr.BodyErrorMessage.- Without a verified account,
GET /v1/article/?site=shopreturns online articles only (there is no separate public route);GET /v1/article/{offline id}is 404 "Article not found". - PATCH
site_idmoves the article for owner, admin and an API key; title plussite_idchange together; omission keeps the site;is_onlineunaffected; rename of the site leaves the article readable. - Unusable
site_idtable, one case each: 999 gives 404site_not_found; 0 gives 400 with the validator error text; -1 and "a" give 400apperr.BodyErrorMessage(decode failure); null gives 400apperr.BodyErrorMessage;{}and an unknown-only object give 400apperr.BodyErrorMessage; in every case the article is unchanged and no event is recorded. A same-site move returns 200 and records nothing.
H. Media
- Usage lookup counts a site's default social image, stays a JSON array whose article items keep id, title and published (additive keys are specified in seo_data_backend), image not "Unused".
- Deleting the image succeeds (204) and nulls the site's
default_social_image_id. - Merge repoints sites and reports
sites_updated; purge does the same; a failed purge group leaves every site pointing at an existing image.
I. Activity log
- Success table: one event per write with action, category "sites", target, actor, source, recorded after the change.
- Failed table with the plain reasons; unknown ids, 403, 401, malformed requests record nothing; reads record nothing.
- Failing and panicking audit repository leave the response and the change as with a healthy one.
- After recording site events, a verified session GET /v1/activity/?category=sites returns the create, edit, set default and delete events with category "sites" and no events of other categories (an API key gets the route's existing session refusal). Failure reasons use the style of
auditFailures.go: the four site sentinels map to the plain sentences of the feature and any other error givesGenericFailureReason. The change fields are limited to name, base_url, default_robots, locale, default_social_image, site and never "key"; article.edit with a site change holds{"field":"site","from":"Main","to":"Shop"}.
J. Persistence and migration
- Repository-contract harness (temp-dir SQLite, MySQL and Postgres opt-in through the existing env switches): site repository contract, transactional default swap, reassign-and-delete, backfill.
database.Migrateis called with PUBLIC_URL at every caller, includingtest/sqlite_schema_unchanged_test.go, which is updated for the new table and column. Seed table (path dropped, http rewritten to https, loopback kept), unparseable value returns an error naming PUBLIC_URL and creates no site, backfill of existing articles, second run changes nothing, owner-edited base URL survives a restart.
K. Quality gates
- Layering guard: the use cases import no persistence, mailer, models, gorm, net/http or chi; the domain site package imports no GORM or HTTP; handlers import no persistence; no comments added.
- Scoped mutation run through
task mutateonly, for exampleMUTATE_FILES=<site and article use case files> MUTATE_RUN='TestSite' MUTATE_WORKERS=2 task mutate -- <use case package path>; report LIVED and NOT COVERED mutants; re-check filtered suspects withoutMUTATE_RUN. Also runtask lint:goandtask fmt. - Docs (Rule: Documentation), checked by reading the files: docs/api.md documents the five /v1/site routes with auth, error tokens, article
site_id, the?site=filter,sites_updatedon media merge and purge; docs/configuration.md says PUBLIC_URL seeds the first site once at migration; docs/architecture.md names the site aggregate, repository and use cases. No comment is added to any file.
L. Pagination and search of the list (Go, test/, task test)
Build site.NewHandler(...) over mockSiteRepo extended with q, paging and a total, mounted on a chi.NewRouter() mirroring NewSiteRouter, with the same jwtauth setup and cookies as above. Call validator.InitValidator(). Add the CORS options of enableCorsIfConfigured to one test router to check the exposed header (pattern: test/seo_output_sitemap_http_test.go).
- 61 sites, no parameter: 61 items, default first,
X-Total-Count"61".q=shopalone returns every match with the matching count. - Page/size table (page=1/2/3, size=5/25, size=50&page=2, size=100): count of items and
X-Total-Count"61" each time. - Pages 1-3 have distinct ids; their concatenation equals the unpaged list; the default site is first on page 1.
page=99gives 200, body exactly[](raw-bytes check, notnull) and the real total.q=nomatchgives[]and "0".- The
qtable (name, key, base URL, uppercase, accent folding, partial host, no match), search plus paging, the default site not forced into a non-matching result, and%and_inqmatched literally. - The invalid table (page 0, -1, abc, empty; size 0, 30, 101, abc, empty; q of 201 characters): 400
apperr.BodyErrorMessageand noX-Total-Count. q of 200 characters is 200 with[]. - Credentials table: owner, admin, API key give 200 with the header; unverified 403 and none 401 give no header. The existing section F owner-only write table is rerun and stays green.
- CORS preflight from an allowed origin exposes
X-Total-Count. - Repository (real SQLite in the temp-dir harness, same file as the contract suite): ordering across the page boundary,
qover the three fields, total equals matches,article_countcorrect on each page. MySQL and Postgres run the same cases throughtask test:mysqlandtask test:postgres, which use the opt-in switchesRENEWCMS_TEST_MYSQL_DSNandRENEWCMS_TEST_POSTGRES_DSN. - Docs, read by hand:
docs/api.md(theGET /v1/site/row),web/src/data/apiCatalogue.ts(query parameters q/page/size and theX-Total-Countresponse header, no-paging default) withapiCatalogue.spec.ts, and the Integration reference counts inIntegrationView.spec.ts. No comment added anywhere. - Scoped
task mutateonly:MUTATE_FILES=<site list use case and repository files> MUTATE_RUN='TestSite' MUTATE_WORKERS=2 task mutate -- ./internal/infrastructure/useCases. Report LIVED and NOT COVERED, and re-check filtered suspects withoutMUTATE_RUN. Thentask lint:goandtask fmt.
Manual
curl -i -b <cookie> 'http://localhost:8080/v1/site/?q=ren&size=5&page=2'againsttask dev; a browser cross-origin read withCORS_ALLOWED_ORIGINSset; the editor's Site select and the Articles Site filter still list all sites (they send no paging parameter).- MySQL and Postgres: run the contract suite with the opt-in env vars against real servers.
- Real boot with an old database copy and PUBLIC_URL set: one "Default" site appears, articles backfilled, second boot changes nothing.
- Docker with the
/datavolume: boot an existing database and confirm the same. - Read the activity log screen and the CSV export: site events show names and failure reasons, and no key change.
- Not covered here: the SPA (a later slice).