9.3 KiB
9.3 KiB
QA procedure: activity-log-producers
Go only, no SPA. Package test (black-box). New files: test/activity_producers_articles_test.go, test/activity_producers_media_test.go, test/activity_producers_users_test.go, test/activity_producers_system_test.go, test/activity_producers_reasons_test.go. Reuse the foundation helpers in test/activity_support_test.go (mockAuditRepo, fakeAuditClock, seedEvents). Extend the existing hand-written fakes (mockArticleRepo, the image, user, invite, purge, update and status fakes) with failure switches and call counters. New fake type names must be unique across the package. No live server, no real SMTP, no real SQLite file, no real sleeping, no time.Ticker. Secrets in tests are fixed test strings only.
Setup shared by every case
validator.InitValidator()once.jwtauth.New("HS256", []byte("test-secret"), nil)stands in forJWT_SECRET.- One
auditing.NewRecorder(mockAuditRepo, fakeAuditClock.Now, logger)shared by every use case.loggerwrites to abytes.Buffer(restore witht.Cleanup). - Users: owner 1, admin 2, verified user 3 "Marta", unverified user 4 "lea" (with a hashed code). API key 7 "Deploy bot" in the fake key repo.
- A fresh
chi.NewRouter()per test that mirrorsapi/router.gofor the article, user, media, system and auth routers, withmiddleware.AuditContext(getUser, trustedProxies)placed exactly where the router wires it (afterapiKeyAuth()on the article, user and media routers). Cookies are real signedjwtcookies withuser_idandtoken_version.httptest.NewRecorderper request. - Helpers:
eventsOf(repo),onlyEvent(t, repo)(fails unless exactly one event),assertNoEvent(t, repo). - Each mutating use case method takes
ctx context.Contextfirst. A method called with a context holding no origin still works.
A. Origin and routing
- Table (feature rule 1): each request with the cookie or the API key yields the expected actor type, id, name and source, plus the request ip and user agent. Proves
AuditContextis mounted on the article, user, media and system routers. - A route without credentials, or answered 401/403/429, records nothing.
B. Articles
- Create: one
article.createevent, target id string, name as typed, no changes, no body anywhere (marshal the event and assert absence). - Edit: table
(before, after, delta): 100 to 412 (312), 412 to 100 (-312), 5 to 5 with different content (0, still recorded),héllotohéllo wörld(6, characters not bytes). Identical body records nothing. Change kindbody,Deltaset,BeforeandAfternil. - Publish and unpublish:
onlinetext change ("false"/"true"). Repeating the same state records nothing and returns 200. - Delete: event keeps the name snapshot. After it,
GET /v1/activityshowstarget.existsfalse. - Title diff: unit-test the change builder with title, online and body only; any other field name is rejected.
- Failure table: 2 character title, repository failing with
errors.New("db is locked")(generic reason). 404 on delete, edit, publish, unpublish of id 99 and 400 on malformed id or body record nothing. - Reads (list, get, public get) record nothing.
C. Media
- Upload: in-memory PNG, target name equals the normalised filename, no bytes and no path. Failed upload table: GIF, oversize, corrupt PNG, each with its mapped reason.
- Rename:
filenametext changebanner.pngtospring-banner.png. Same name records nothing. Invalid stem records a failed event keeping the target. Missing image records nothing (404). - Delete: event with name snapshot. Missing image records nothing.
- Merge: three images with one hash, two articles referencing the copies. One
media.mergeevent with{"merged":2,"articles_updated":2}, target the kept image. Failure table:ErrInvalidMergeRequest,ErrMergeCopyMissing,ErrMergeHashMismatch. - Manual purge: plan with 2 groups, 3 copies, 5 articles. Exactly one
media.purgeevent with{"merged":3,"articles_updated":5}and zeromedia.mergeevents. A plan with one failing group: outcome success withfailed_groups1.ErrPurgeConflictandErrPurgeBusygive failed events. Preview, usage, list and settings GETs record nothing. - Scheduled purge: drive
PurgeSchedulerUseCase.Tick(now)with the fake purge repo and clock. Due and merging 3: onesystem.media_purgeevent with system actor, source system, ip nil, user agent nil. Due with a failed group: success withfailed_groups. Not due, or no duplicates: none. A fakeSaveLastRunfailure: failed event with the generic reason. - Manual purge after the scheduler in the same test yields two events with sources
webandsystem.
D. Users and invites
- Register: open registration records
user.registerwith the new account as actor and target. Closed registration and a failing mailer (error carrying a fake SMTP password) record failed events with the mapped reason, anonymous actor named by the attempted username, no raw text. Marshal the event and assert password, hash and code are absent. - Verify: success, invalid code, expired code.
- Display name: before/after diff, trimmed no-op records nothing, empty to empty records nothing, 61 characters records a failed event with no changes. Actor snapshot name is the pre-change name.
- Invites: create, cancel, renew, accept each record the documented event with the invited email as target name. Failure table covers every error in the feature. A renew whose mail fails is rolled back and records one failed event only. Unknown ids (404) record nothing.
- Accept: actor is the new account, source
web; failed accept actor is anonymous with the attempted username. - Reads (
GET /v1/user,/v1/auth/me,/v1/auth/invite,/v1/auth/registration) record nothing.
E. System tasks
- Check: success event with
{"update_available":true}and a release target.ErrFeedUnavailableandErrNoReleasegive failed events without target. StatusGETrecords nothing. - Download: fakes complete the job through a hand-fed channel (no
time.Sleep). No event before the job ends, one afterready; actor, ip and user agent are those captured at request time even though the request context was cancelled. Failure kindsdownloadandverificationgive the two reasons, no URL or path. Start-time refusals give failed events. Run with-race. - Restart:
{"kind":"restart"}and{"kind":"update","tag":"v1.4.0"}. Refusals forErrNotPrepared,ErrJobInProgress,ErrRestartPending,ErrRestartKindMismatch.
F. Reasons, secrets and recorder faults
- Reason table: wrap each sentinel with
fmt.Errorf("%w: %w", sentinel, errors.New("secret detail")), assert the sentence equals the table and the extra text is absent. Unmapped errors give the generic sentence. Every sentence is at most 255 characters. - Recorder fault: for each producer row of the fault table, run with failing, panicking and healthy
mockAuditRepo. Identical status and body, change applied, log line naming the failed write. - Secret sweep: after exercising every producer, marshal every stored event, the
/v1/activityresponse and the/v1/activity/exportfile; assert none contains the password, hash, verification code, invite code, raw API key, article body, file bytes, asset URL or staging path. The log buffer is free of them too. - Allowlist: every
Change.Fieldis in{title, online, body, filename, display_name}and every kind istextorbody. - Read-back: list by
category=articles|media|users|systemandoutcome=failed; compare action sets with the feature tables.
G. Mutation guard (gremlins stays clean)
task mutatescope is./internal/infrastructure/useCases. Each new branch needs a killing assertion: every no-op guard (body, online, filename, display name) with equal and differing values; delta sign and rune-versus-byte count; success versus failed outcome and recorded once after the change (event count and order against the repository call); everyerrors.Isrow of the reason map plus the generic fallback; everyctxforwarding; the system-actor defaults. Bare-bool conditions produce no mutant, so cover both branches with tests regardless.
H. Layering and hygiene
- Touched use cases import no
gorm,persistence,mailer,net/http, chi orapi/...; handlers only passr.Context();internal/domain/auditimports only stdlib and domain packages. - No comments in new or edited Go files, test files and docs, except tool directives.
- Docs (
docs/api.md,docs/architecture.md) list the action catalogue, target types, reason sentences and the rule that no read is recorded.
Manual (not automated)
- Real boot with
task dev:apiand SMTP: register, verify, invite, renew, accept and cancel an invite, change display name, create/edit/publish/unpublish/delete an article, upload/rename/merge/delete images, manual purge; read/v1/activityand the CSV export and read every sentence. - Real update helper on Linux systemd (or the Docker QA flow): check, download until
ready, restart; one event each, real asset names absent. Skip in a container. - Leave the scheduled purge due and wait for the hourly tick: a single
system.media_purgeevent, sourcesystem, no ip or user agent. - Remove the database file's write access during one article edit: edit result unchanged, process log names the audit write failure.