11 KiB
11 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 extendedmockArticleRepo(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 newNewAuthorRouter, plus the article, auth (/me), media and activity routers. Protected routes sit behindjwtauth.Verifier+jwtauth.Authenticatorwith a testjwtauth.New("HS256", ...). Requests carry signedjwtcookies with theuser_idclaim 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
- Newest-first list with
total,page,sizeandarticle_count. - Credentials table: owner, admin, "editor" and API key give 200; unverified gives 403; none gives 401.
qtable: name, slug, case-insensitive, no match gives total 0.- Pagination table: page 2 of 30, size 5, out-of-range page gives empty items,
size=30,page=0,page=abcgive 400apperr.BodyErrorMessage. - Email visibility: owner and admin see
user_email; "editor" gets a body without anyuser_emailkey or the email string (substring check on the raw body). - Guest author:
is_linkedfalse,user_idnull. Null and empty defaults for avatar, website, bio, links.
B. Creating
- Owner and admin create: 201, full body, stored,
article_count0,is_linkedfalse. - Only the name given: generated slug, defaults.
- Slug generation table (accents folded, punctuation runs,
!!!givesauthor). - Generated and explicit taken slug: 409
author_slug_taken, nothing stored. Repository unique-violation injection gives the same 409. - 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).
- Malformed JSON gives 400
apperr.BodyErrorMessage; "editor" with malformed JSON still gets 403 "Only owners and admins can do this.". user_idon create: success, 404user_not_found, 409user_already_linked.
C. Own author (POST /me)
- Naming table: display name, then username fallback;
user_idis the caller's;is_linkedtrue. - Owner, admin and "editor" all get 201.
- Slug de-duplication:
marta-alves-2taken, result-3; the 100-character slug case stays within 100 and ends with-2. - Already linked: 409
user_already_linked, nothing added; unique-violation injection onuser_idgives the same 409. - API key gives 403 "This action needs a real session."; unverified gives the existing 403; none gives 401.
D. Editing
- Full edit by owner; partial edit keeps absent fields; nullable clear table (avatar, website null and "", links [], bio "").
- Avatar set; social links replaced as a whole.
- Validation table like creation; own slug re-sent is 200; another author's slug is 409; unchanged author after each refusal.
{}, unknown-only and{"name": null}give 400apperr.BodyErrorMessage; missing author 404author_not_found;abcid givesapperr.IdUint32ErrorMessage.- No-op edit: 200 and no audit event.
- "editor" table: own author 200 (also name, slug, avatar, links); another author 403
author_rights; missing id 404;user_idin the body of its own author 403author_rightsand still linked; API key gives 403 "This action needs a real session."
E. Linking and unlinking
- Link a guest author: 200,
user_emailvisible to the owner. - Table: linked user gives 409
user_already_linked; already-linked author gives 409author_already_linked; same user is a 200 with no event; unknown user gives 404user_not_found. - Unlink: 200,
is_linkedfalse, nouser_email; the user can create an author again; the author keeps its article credits; unlinking a guest is a no-op with no event. - After unlinking, an article created by that user has no authors.
F. Deleting
- No articles: 204. With articles and no
reassign_to: 409author_has_articles, unchanged. - 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).
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.- Single repository operation (the fake records one call). Failure injected in it leaves credits and author unchanged.
reassign_totable: 999 gives 404author_not_found; own id,abc,-1, empty give 400invalid_reassign. Unused validreassign_toon an author without articles gives 204 and leaves the destination untouched. Missing author gives 404.- Linked author deleted: user still exists and can create again.
- 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
author_idsPATCH sets, replaces, empties and reorders; reads (GET one, GET list, POST, PATCH, publish, unpublish) carryauthorsin order with only the seven public fields; empty is[].- Callers table: owner, admin, "editor", API key all give 200.
- Combined PATCH with
bodyandsite_id; omission keeps the authors; a refusedauthor_ids(404) leaves body and credits unchanged. - Unusable table:
[999],[5,999]404author_not_found;[5,5],null,"a",[-1]give 400apperr.BodyErrorMessage;[0]gives the validator error text. The article is unchanged in every case. - Unchanged list is 200 with no event. Publishing an article with no authors succeeds.
- Anonymous read of an online article includes
authorsbut nouser_id,user_emailorarticle_count(substring check); an offline article is 404 "Article not found". - Creation default: creator with a linked author gets
[author]; creator without one gets[]; the article is offline. - Deleting an article removes its credits and keeps the author,
article_countdrops;article_countcounts offline articles. - List filters (
status,q,site) andcountsare unchanged apart fromauthorson each item.
H. Current user
/v1/auth/mereturnsauthorfor a linked caller (seven public fields plusarticle_count, nouser_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
- Usage lookup counts an avatar, shape unchanged, image not "Unused". Media list
used_insums article, site and author usage. - Deleting the image (204) nulls
avatar_id. - Merge repoints authors and reports
authors_updated; purge does the same; a failed purge group leaves everyavatar_idpointing at an existing image.
J. Activity log
- Success table: one event per write with action, category "authors", target type "author", actor, source "web", recorded after the change (including
/me). - 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).
- Failed table with the plain reasons; unknown ids, 403, 401, 422 and other validation refusals and malformed requests record nothing; reads record nothing.
- Failing and panicking audit repository leave the response and the change as with a healthy one.
article.editwith an authors change holds{"field":"authors","from":"Inès Duarte, Marta Alves","to":"Marta Alves, Inès Duarte, Zoe Lambert"}; delete with reassignment records oneauthor.deleteand noarticle.edit.GET /v1/activity/?category=authorsreturns only author events for a verified session (an API key gets the route's existing session refusal). Failure reasons use the style ofauditFailures.go: the author sentinels map to the plain sentences of the feature and any other error givesGenericFailureReason.
K. Persistence and migration
- 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.Migratecreates both tables and leaves existing articles without authors;test/sqlite_schema_unchanged_test.gois updated for the new tables.
L. Quality gates
- 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. - Scoped mutation run through
task mutateonly, for exampleMUTATE_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 withoutMUTATE_RUN. Also runtask lint:goandtask fmt. - 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/meauthorfield andauthors_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
/datavolume: 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).