Files
RenewCMS/features/authors_backend.qa.md
T

12 KiB

QA procedure: Authors backend

Spec: features/authors_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 or constant named in the spec.

Setup

  • New files test/author_*_test.go. Fakes have unique names: mockAuthorRepo (authors, slug and user_id uniqueness, article counts, ordered article credits, the reassign-and-delete as one recorded operation, failure and unique-violation injection), mockAuthorUserRepo (users with ids, usernames, display names, emails and roles), mockAuthorImageRepo (images, avatar usage, merge and purge repointing), and an extended mockArticleRepo (ordered author credits, create with default author, delete removes credits). A fake audit repository with fail and panic switches is reused.
  • Routers mirror api/router.go: a new NewAuthorRouter, plus the article, auth (/me), media and activity routers. Protected routes sit behind jwtauth.Verifier + jwtauth.Authenticator with a test jwtauth.New("HS256", ...). Requests carry signed jwt cookies with the user_id claim for: an owner, an admin, a verified account whose role is "editor" (a value other than owner or admin, because no third role exists yet), and an unverified account. An API-key request uses the key authentication path.
  • Helper builders create authors, link users and credit articles directly in the fakes; assertions read the fakes, never the JSON alone.

A. Reading

  1. Newest-first list with total, page, size and article_count.
  2. Credentials table: owner, admin, "editor" and API key give 200; unverified gives 403; none gives 401.
  3. q table: name, slug, case-insensitive, no match gives total 0.
  4. Pagination table: page 2 of 30, size 5, out-of-range page gives empty items, size=30, page=0, page=abc give 400 apperr.BodyErrorMessage.
  5. Email visibility: owner and admin see user_email; "editor" gets a body without any user_email key or the email string (substring check on the raw body).
  6. Guest author: is_linked false, user_id null. Null and empty defaults for avatar, website, bio, links.

B. Creating

  1. Owner and admin create: 201, full body, stored, article_count 0, is_linked false.
  2. Only the name given: generated slug, defaults.
  3. Slug generation table (accents folded, punctuation runs, !!! gives author).
  4. Generated and explicit taken slug: 409 author_slug_taken, nothing stored. Repository unique-violation injection gives the same 409.
  5. Validation table (name, slug, bio, website, links, link type, avatar) with exact status and token; 422 rows assert the token; boundary table (100-character name and slug, 280 characters and 280 multi-byte characters, 5 links, two custom links, empty website).
  6. Malformed JSON gives 400 apperr.BodyErrorMessage; "editor" with malformed JSON still gets 403 "Only owners and admins can do this.".
  7. user_id on create: success, 404 user_not_found, 409 user_already_linked.

C. Own author (POST /me)

  1. Naming table: display name, then username fallback; user_id is the caller's; is_linked true.
  2. Owner, admin and "editor" all get 201.
  3. Slug de-duplication: marta-alves-2 taken, result -3; the 100-character slug case stays within 100 and ends with -2.
  4. Already linked: 409 user_already_linked, nothing added; unique-violation injection on user_id gives the same 409.
  5. API key gives 403 "This action needs a real session."; unverified gives the existing 403; none gives 401.

D. Editing

  1. Full edit by owner; partial edit keeps absent fields; nullable clear table (avatar, website null and "", links [], bio "").
  2. Avatar set; social links replaced as a whole.
  3. Validation table like creation; own slug re-sent is 200; another author's slug is 409; unchanged author after each refusal.
  4. {}, unknown-only and {"name": null} give 400 apperr.BodyErrorMessage; missing author 404 author_not_found; abc id gives apperr.IdUint32ErrorMessage.
  5. No-op edit: 200 and no audit event.
  6. "editor" table: own author 200 (also name, slug, avatar, links); another author 403 author_rights; missing id 404; user_id in the body of its own author 403 author_rights and still linked; API key gives 403 "This action needs a real session."

E. Linking and unlinking

  1. Link a guest author: 200, user_email visible to the owner.
  2. Table: linked user gives 409 user_already_linked; already-linked author gives 409 author_already_linked; same user is a 200 with no event; unknown user gives 404 user_not_found.
  3. Unlink: 200, is_linked false, no user_email; the user can create an author again; the author keeps its article credits; unlinking a guest is a no-op with no event.
  4. After unlinking, an article created by that user has no authors.

F. Deleting

  1. No articles: 204. With articles and no reassign_to: 409 author_has_articles, unchanged.
  2. Reassign: credits move, author gone; position is preserved (5, 2, 6 becomes 5, 3, 6); no duplicate when the target already credits the article (2, 3 becomes 3).
  3. none: only that author is removed from shared articles, order of the rest is kept; articles with only that author end with none and still exist.
  4. Single repository operation (the fake records one call). Failure injected in it leaves credits and author unchanged.
  5. reassign_to table: 999 gives 404 author_not_found; own id, abc, -1, empty give 400 invalid_reassign. Unused valid reassign_to on an author without articles gives 204 and leaves the destination untouched. Missing author gives 404.
  6. Linked author deleted: user still exists and can create again.
  7. Refusals: "editor" gives 403 "Only owners and admins can do this." including on its own author; API key gives 403 "This action needs a real session."; admin gives 204.

G. Articles

  1. author_ids PATCH sets, replaces, empties and reorders; reads (GET one, GET list, POST, PATCH, publish, unpublish) carry authors in order with only the seven public fields; empty is [].
  2. Callers table: owner, admin, "editor", API key all give 200.
  3. Combined PATCH with body and site_id; omission keeps the authors; a refused author_ids (404) leaves body and credits unchanged.
  4. Unusable table: [999], [5,999] 404 author_not_found; [5,5], null, "a", [-1] give 400 apperr.BodyErrorMessage; [0] gives the validator error text. The article is unchanged in every case.
  5. Unchanged list is 200 with no event. Publishing an article with no authors succeeds.
  6. Anonymous read of an online article includes authors but no user_id, user_email or article_count (substring check); an offline article is 404 "Article not found".
  7. Creation default: creator with a linked author gets [author]; creator without one gets []; the article is offline.
  8. Deleting an article removes its credits and keeps the author, article_count drops; article_count counts offline articles.
  9. List filters (status, q, site) and counts are unchanged apart from authors on each item.

H. Current user

  1. /v1/auth/me returns author for a linked caller (seven public fields plus article_count, no user_email) and null otherwise, null again after an unlink; existing fields are unchanged and no secret appears; API key request returns the same shape.

I. Media

  1. Usage lookup counts an avatar, stays a JSON array whose article items keep id, title and published (additive keys are specified in seo_data_backend), image not "Unused". Media list used_in sums article, site and author usage.
  2. Deleting the image (204) nulls avatar_id.
  3. Merge repoints authors and reports authors_updated; purge does the same; a failed purge group leaves every avatar_id pointing at an existing image.

J. Activity log

  1. Success table: one event per write with action, category "authors", target type "author", actor, source "web", recorded after the change (including /me).
  2. Change fields are limited to name, slug, bio, website, social_links, avatar, user; the user change shows usernames and no email (substring check on the stored event).
  3. Failed table with the plain reasons; unknown ids, 403, 401, 422 and other validation refusals and malformed requests record nothing; reads record nothing.
  4. Failing and panicking audit repository leave the response and the change as with a healthy one.
  5. article.edit with an authors change holds {"field":"authors","from":"Inès Duarte, Marta Alves","to":"Marta Alves, Inès Duarte, Zoe Lambert"}; delete with reassignment records one author.delete and no article.edit.
  6. GET /v1/activity/?category=authors returns only author events for a verified session (an API key gets the route's existing session refusal). Failure reasons use the style of auditFailures.go: the author sentinels map to the plain sentences of the feature and any other error gives GenericFailureReason.

K. Persistence and migration

  1. Repository-contract harness (temp-dir SQLite; MySQL and Postgres opt-in through the existing env switches): author repository contract (CRUD, slug and user uniqueness with many NULL users, search, newest-first pagination, ordered credits and stable read-back, unique (article, author) pair, transactional reassign-and-delete incl. the no-duplicate and position rules). database.Migrate creates both tables and leaves existing articles without authors; test/sqlite_schema_unchanged_test.go is updated for the new tables.

L. Quality gates

  1. Layering guard: use cases import no persistence, mailer, models, gorm, net/http or chi; the domain author package imports no GORM or HTTP; handlers import no persistence; validation uses validate tags and validator.Validate.Struct; no comments added.
  2. Scoped mutation run through task mutate only, for example MUTATE_FILES=<author and article use case files> MUTATE_RUN='TestAuthor' 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, checked by reading the files: docs/api.md documents the five /v1/author routes (GET, POST, POST /me, PATCH, DELETE) with auth and roles, error tokens, the author and article-author shapes, author_ids, the /auth/me author field and authors_updated; docs/architecture.md names the author aggregate, repository and use cases.

M. X-Total-Count (Go, test/, task test)

  1. On the same mockAuthorRepo and router as item 4, assert X-Total-Count equals the decoded body total for: size=5 of 30, q=mar with 7 matches, page=3&size=100 (items empty, total 30), q=nobody (0), and for owner, admin, "editor" and API key. Assert the header is absent for size=30, page=0, page=abc, unverified (403) and none (401). The body assertions of item 4 stay unchanged.
  2. On the SQLite contract harness, the author list total (and so the header) follows q and does not depend on page/size; task test:mysql and task test:postgres run the same case.
  3. CORS preflight from an allowed origin exposes X-Total-Count.
  4. Scoped task mutate on the author list use case and handler files only. docs/api.md author row and web/src/data/apiCatalogue.ts (X-Total-Count response header on GET /v1/author/) with its spec, read by hand. No comment added.

Manual

  • MySQL and Postgres: run the contract suite with the opt-in env vars against real servers.
  • Real boot with an old database copy: both new tables appear, articles keep working with no authors, second boot changes nothing.
  • Docker with the /data volume: same boot check.
  • Read the activity log screen and the CSV export: author events show names, failure reasons and no emails.
  • Not covered here: the SPA (later slices).