Files
RenewCMS/features/sites_backend.qa.md
T

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 extended mockArticleRepo (site_id, site filter, move). A fake audit repository with fail and panic switches is reused.
  • Routers mirror api/router.go: a new NewSiteRouter, plus the article and media routers. Protected routes sit behind jwtauth.Verifier + jwtauth.Authenticator with a test jwtauth.New("HS256", ...). Requests carry real signed jwt cookies carrying the user_id claim 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)

  1. GET /v1/site/ with three sites: 200, order default first then name, article_count on each.
  2. GET /v1/site/ credentials table: owner, admin, API key give 200; unverified gives 403; none gives 401.
  3. A site without image and locale returns default_social_image_id null and locale null.

B. Creating

  1. POST creates a site: 201, full body, stored, is_default false, article_count 0.
  2. Optional fields default to default_robots "index", locale null, image null.
  3. POST with is_default true: the new site is the only default.
  4. Base URL normalisation table (trailing slash, host case, loopback http).
  5. Base URL rule table (all rows, invalid_site_url).
  6. Key format table (valid and invalid rows) and site_key_taken 409 with nothing stored.
  7. Other fields table: name, robots, locale (invalid_locale), unknown image (image_not_found), malformed JSON gives 400 and nothing stored.
  8. Locale table: valid values stored, empty and null stored as null.

C. Editing

  1. PATCH edits name, base_url, robots, locale and returns the site with the unchanged key.
  2. Absent fields stay untouched.
  3. Image set to 5 then cleared with null.
  4. Key table: same key 200, different key 400 key_immutable, key unchanged.
  5. A rejected key change leaves the name unchanged.
  6. is_default in the body: 400 use_set_default, default unchanged.
  7. Validation table (base_url, locale, image, name, robots), site unchanged.
  8. Missing site 404 site_not_found; malformed id gives the uint32 format error.
  9. No-op edit: 200 and no audit event.

D. Default site

  1. PATCH /{id}/default swaps the default; exactly one default remains.
  2. Current default: 200, no event. Missing site: 404.
  3. Fault injected on the second step of the swap: the previous default is still the only default (the fake mimics the transaction rollback).
  4. A subsequently created article gets the new default's id.

E. Deleting

  1. Empty site: 204, gone.
  2. Site with articles and no reassign_to: 409 site_has_articles, unchanged.
  3. With a valid reassign_to: 204, articles moved, site gone, via a single repository operation (assert the fake records one call).
  4. Failure injected in that operation: articles and site unchanged.
  5. reassign_to table: 999 gives 404 site_not_found; the site's own id and a non-numeric value give 400 invalid_reassign.
  6. Default site: 403 default_site, also when it has articles and no reassign_to. Missing site: 404. Unused reassign_to on an empty site: 204, destination untouched.

F. Owner-only writes

  1. 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.
  2. Admin with a malformed body still gets 403 "Only the owner can do this.".

G. Articles

  1. New article is offline and on the default site; reads (GET one, GET list, publish, PATCH) carry site_id.
  2. GET /v1/article/?site=shop filters; composes with status (online, draft), q, sort (for example title:asc), size and page; ?site=nope gives 200 empty with global counts; ?site= gives 400 apperr.BodyErrorMessage.
  3. Without a verified account, GET /v1/article/?site=shop returns online articles only (there is no separate public route); GET /v1/article/{offline id} is 404 "Article not found".
  4. PATCH site_id moves the article for owner, admin and an API key; title plus site_id change together; omission keeps the site; is_online unaffected; rename of the site leaves the article readable.
  5. Unusable site_id table, one case each: 999 gives 404 site_not_found; 0 gives 400 with the validator error text; -1 and "a" give 400 apperr.BodyErrorMessage (decode failure); null gives 400 apperr.BodyErrorMessage; {} and an unknown-only object give 400 apperr.BodyErrorMessage; in every case the article is unchanged and no event is recorded. A same-site move returns 200 and records nothing.

H. Media

  1. Usage lookup counts a site's default social image, shape unchanged, image not "Unused".
  2. Deleting the image succeeds (204) and nulls the site's default_social_image_id.
  3. 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

  1. Success table: one event per write with action, category "sites", target, actor, source, recorded after the change.
  2. Failed table with the plain reasons; unknown ids, 403, 401, malformed requests record nothing; reads record nothing.
  3. Failing and panicking audit repository leave the response and the change as with a healthy one.
  4. 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 gives GenericFailureReason. 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

  1. 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.Migrate is called with PUBLIC_URL at every caller, including test/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

  1. 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.
  2. Scoped mutation run through task mutate only, for example MUTATE_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 without MUTATE_RUN. Also run task lint:go and task fmt.
  3. 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_updated on 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 /data volume: 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).