112 lines
11 KiB
Markdown
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).
|