Files
RenewCMS/features/authors_backend.qa.md
T

112 lines
11 KiB
Markdown

# 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
7. Owner and admin create: 201, full body, stored, `article_count` 0, `is_linked` false.
8. Only the name given: generated slug, defaults.
9. Slug generation table (accents folded, punctuation runs, `!!!` gives `author`).
10. Generated and explicit taken slug: 409 `author_slug_taken`, nothing stored. Repository unique-violation injection gives the same 409.
11. 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).
12. Malformed JSON gives 400 `apperr.BodyErrorMessage`; "editor" with malformed JSON still gets 403 "Only owners and admins can do this.".
13. `user_id` on create: success, 404 `user_not_found`, 409 `user_already_linked`.
## C. Own author (`POST /me`)
14. Naming table: display name, then username fallback; `user_id` is the caller's; `is_linked` true.
15. Owner, admin and "editor" all get 201.
16. Slug de-duplication: `marta-alves-2` taken, result `-3`; the 100-character slug case stays within 100 and ends with `-2`.
17. Already linked: 409 `user_already_linked`, nothing added; unique-violation injection on `user_id` gives the same 409.
18. API key gives 403 "This action needs a real session."; unverified gives the existing 403; none gives 401.
## D. Editing
19. Full edit by owner; partial edit keeps absent fields; nullable clear table (avatar, website null and "", links [], bio "").
20. Avatar set; social links replaced as a whole.
21. Validation table like creation; own slug re-sent is 200; another author's slug is 409; unchanged author after each refusal.
22. `{}`, unknown-only and `{"name": null}` give 400 `apperr.BodyErrorMessage`; missing author 404 `author_not_found`; `abc` id gives `apperr.IdUint32ErrorMessage`.
23. No-op edit: 200 and no audit event.
24. "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
25. Link a guest author: 200, `user_email` visible to the owner.
26. 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`.
27. 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.
28. After unlinking, an article created by that user has no authors.
## F. Deleting
29. No articles: 204. With articles and no `reassign_to`: 409 `author_has_articles`, unchanged.
30. 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).
31. `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.
32. Single repository operation (the fake records one call). Failure injected in it leaves credits and author unchanged.
33. `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.
34. Linked author deleted: user still exists and can create again.
35. 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
36. `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 `[]`.
37. Callers table: owner, admin, "editor", API key all give 200.
38. Combined PATCH with `body` and `site_id`; omission keeps the authors; a refused `author_ids` (404) leaves body and credits unchanged.
39. 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.
40. Unchanged list is 200 with no event. Publishing an article with no authors succeeds.
41. 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".
42. Creation default: creator with a linked author gets `[author]`; creator without one gets `[]`; the article is offline.
43. Deleting an article removes its credits and keeps the author, `article_count` drops; `article_count` counts offline articles.
44. List filters (`status`, `q`, `site`) and `counts` are unchanged apart from `authors` on each item.
## H. Current user
45. `/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
46. Usage lookup counts an avatar, shape unchanged, image not "Unused". Media list `used_in` sums article, site and author usage.
47. Deleting the image (204) nulls `avatar_id`.
48. 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
49. Success table: one event per write with action, category "authors", target type "author", actor, source "web", recorded after the change (including `/me`).
50. 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).
51. Failed table with the plain reasons; unknown ids, 403, 401, 422 and other validation refusals and malformed requests record nothing; reads record nothing.
52. Failing and panicking audit repository leave the response and the change as with a healthy one.
53. `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`.
54. `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
55. 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
56. 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.
57. 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`.
58. 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.
## 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).