8.3 KiB
8.3 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, shape unchanged, 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.
Manual
- 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).