Files
RenewCMS/features/activity-log-producers.qa.md
T
2026-10-06 22:25:23 +02:00

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

  1. validator.InitValidator() once. jwtauth.New("HS256", []byte("test-secret"), nil) stands in for JWT_SECRET.
  2. One auditing.NewRecorder(mockAuditRepo, fakeAuditClock.Now, logger) shared by every use case. logger writes to a bytes.Buffer (restore with t.Cleanup).
  3. 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.
  4. A fresh chi.NewRouter() per test that mirrors api/router.go for the article, user, media, system and auth routers, with middleware.AuditContext(getUser, trustedProxies) placed exactly where the router wires it (after apiKeyAuth() on the article, user and media routers). Cookies are real signed jwt cookies with user_id and token_version. httptest.NewRecorder per request.
  5. Helpers: eventsOf(repo), onlyEvent(t, repo) (fails unless exactly one event), assertNoEvent(t, repo).
  6. Each mutating use case method takes ctx context.Context first. A method called with a context holding no origin still works.

A. Origin and routing

  1. 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 AuditContext is mounted on the article, user, media and system routers.
  2. A route without credentials, or answered 401/403/429, records nothing.

B. Articles

  1. Create: one article.create event, target id string, name as typed, no changes, no body anywhere (marshal the event and assert absence).
  2. Edit: table (before, after, delta): 100 to 412 (312), 412 to 100 (-312), 5 to 5 with different content (0, still recorded), héllo to héllo wörld (6, characters not bytes). Identical body records nothing. Change kind body, Delta set, Before and After nil.
  3. Publish and unpublish: online text change ("false"/"true"). Repeating the same state records nothing and returns 200.
  4. Delete: event keeps the name snapshot. After it, GET /v1/activity shows target.exists false.
  5. Title diff: unit-test the change builder with title, online and body only; any other field name is rejected.
  6. 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.
  7. Reads (list, get, public get) record nothing.

C. Media

  1. 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.
  2. Rename: filename text change banner.png to spring-banner.png. Same name records nothing. Invalid stem records a failed event keeping the target. Missing image records nothing (404).
  3. Delete: event with name snapshot. Missing image records nothing.
  4. Merge: three images with one hash, two articles referencing the copies. One media.merge event with {"merged":2,"articles_updated":2}, target the kept image. Failure table: ErrInvalidMergeRequest, ErrMergeCopyMissing, ErrMergeHashMismatch.
  5. Manual purge: plan with 2 groups, 3 copies, 5 articles. Exactly one media.purge event with {"merged":3,"articles_updated":5} and zero media.merge events. A plan with one failing group: outcome success with failed_groups 1. ErrPurgeConflict and ErrPurgeBusy give failed events. Preview, usage, list and settings GETs record nothing.
  6. Scheduled purge: drive PurgeSchedulerUseCase.Tick(now) with the fake purge repo and clock. Due and merging 3: one system.media_purge event with system actor, source system, ip nil, user agent nil. Due with a failed group: success with failed_groups. Not due, or no duplicates: none. A fake SaveLastRun failure: failed event with the generic reason.
  7. Manual purge after the scheduler in the same test yields two events with sources web and system.

D. Users and invites

  1. Register: open registration records user.register with 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.
  2. Verify: success, invalid code, expired code.
  3. 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.
  4. 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.
  5. Accept: actor is the new account, source web; failed accept actor is anonymous with the attempted username.
  6. Reads (GET /v1/user, /v1/auth/me, /v1/auth/invite, /v1/auth/registration) record nothing.

E. System tasks

  1. Check: success event with {"update_available":true} and a release target. ErrFeedUnavailable and ErrNoRelease give failed events without target. Status GET records nothing.
  2. Download: fakes complete the job through a hand-fed channel (no time.Sleep). No event before the job ends, one after ready; actor, ip and user agent are those captured at request time even though the request context was cancelled. Failure kinds download and verification give the two reasons, no URL or path. Start-time refusals give failed events. Run with -race.
  3. Restart: {"kind":"restart"} and {"kind":"update","tag":"v1.4.0"}. Refusals for ErrNotPrepared, ErrJobInProgress, ErrRestartPending, ErrRestartKindMismatch.

F. Reasons, secrets and recorder faults

  1. 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.
  2. 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.
  3. Secret sweep: after exercising every producer, marshal every stored event, the /v1/activity response and the /v1/activity/export file; 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.
  4. Allowlist: every Change.Field is in {title, online, body, filename, display_name} and every kind is text or body.
  5. Read-back: list by category=articles|media|users|system and outcome=failed; compare action sets with the feature tables.

G. Mutation guard (gremlins stays clean)

  1. task mutate scope 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); every errors.Is row of the reason map plus the generic fallback; every ctx forwarding; the system-actor defaults. Bare-bool conditions produce no mutant, so cover both branches with tests regardless.

H. Layering and hygiene

  1. Touched use cases import no gorm, persistence, mailer, net/http, chi or api/...; handlers only pass r.Context(); internal/domain/audit imports only stdlib and domain packages.
  2. No comments in new or edited Go files, test files and docs, except tool directives.
  3. 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)

  1. Real boot with task dev:api and 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/activity and the CSV export and read every sentence.
  2. 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.
  3. Leave the scheduled purge due and wait for the hourly tick: a single system.media_purge event, source system, no ip or user agent.
  4. Remove the database file's write access during one article edit: edit result unchanged, process log names the audit write failure.