Files
RenewCMS/features/updates_release_feed.qa.md

8.9 KiB

QA: Updates part 1 (signed releases, release feed, notification)

Automated Go (task test, package test, black-box)

Files: test/update_test.go (domain, use cases), test/update_http_test.go (HTTP), test/update_feed_test.go (Gitea adapter, JSON store), test/releasesign_test.go (signing tool). Fakes with unique names: fakeReleaseFeed (returns a list or an error, counts calls), fakeUpdateStore; reuse mockUserRepo from test/user_test.go.

HTTP setup: validator.InitValidator(); token := jwtauth.New("HS256", []byte("test-secret"), nil); mount a fresh chi.NewRouter() at /v1/system mirroring NewSystemRouter (Verifier, Authenticator, RequireCurrentToken, RequireVerifiedUser; the check route also RequireOwner and the per-user limiter, 5 per minute); cookies via token.Encode({"user_id","token_version"}) as in test/stale_token_invalidation_http_test.go. httptest only: no live server, no SQLite, no SMTP.

  1. Version comparison, table-driven over the Examples in the feature (domain; errors.Is for sentinels).
  2. Feed adapter against httptest.NewServer serving a Gitea JSON list: path is /api/v1/repos/fsylvain/RenewCMS/releases; newest by numeric order regardless of order; drafts skipped; final beats beta; notes markdown converted to items; html_url carried; a 500 and invalid JSON return an error.
  3. GET /v1/system/update as owner and as admin: 200, exactly the 12 keys, can_apply false, state "idle", values from the stored result, release_url from the stored html_url; the fake feed call count is 0.
  4. GET before any check: latest "", available false, release_url, notes, checked_at, progress, error, started_at null.
  5. GET: no cookie 401; stale token 401 with apperr.UnauthorizedErrorMessage; unverified 403 with apperr.VerificationRequiredErrorMessage.
  6. POST /v1/system/update/check as owner: 200, store updated with latest, release_url and checked_at, feed called once, a later GET equals it. Nothing newer: available false. Running version "dev": available false.
  7. POST check as admin: 403, body apperr.OwnerRequiredErrorMessage+"\n", feed not called.
  8. Feed error: 500, store unchanged (latest and checked_at as before), body contains no feed error text.
  9. Rate limit: owner A sends 6 checks: first 5 are 200, the 6th is 429 with apperr.TooManyRequestsErrorMessage+"\n" and an integer Retry-After of at least 1; feed called 5 times; owner B's first check still 200; a login-limiter instance built in the same test is unaffected.
  10. JSON file store in t.TempDir(): save, reopen, same values; missing file and corrupt file read as empty without error.
  11. Signing tool, run through go run ./cmd/releasesign (or its exported package) with a throwaway key pair generated in t.TempDir() at test time, always with --pub; never read the committed release.pub key except in case 13: keygen creates the key; sign then verify passes; one flipped byte fails verify; a different public key fails verify; keygen into a path inside the repo tree is refused and writes nothing; sign with a private key that does not match --pub fails, naming the mismatch, and writes no .sig.
  12. Placeholder: a --pub file holding base64 of 32 zero bytes makes sign and verify fail with the placeholder message.
  13. The committed internal/infrastructure/update/release.pub is checked only for shape: a valid base64 string that decodes to 32 bytes. Do not assert that it is or is not the placeholder, since the user replaces it later.
  14. Review items, not Go tests: internal/domain/update imports only the stdlib; use cases import no net/http, gorm, adapter or config; the handler imports no adapter; UPDATE_FEED_URL is in requiredEnvVars/envDefaults, .env.example and docs/configuration.md; no private key or secret value is in the repo.

Automated SPA (Vitest + Vue Test Utils, task test:web)

Mock useApi.request for GET /v1/system/update and POST /v1/system/update/check. Fake timers for the countdown and the 2 s "Copied". No test freezes the system date and none asserts a relative date or "Last checked today". Reuse the board's sample data: running v2.2.0-beta, latest v2.3.0-beta, released 24 Sep 2026, items from the frame's script.

One test per state:

  1. Up to date (owner): "You're up to date", only "Check for updates" rendered.
  2. Checking: text, polite live region, "Checking…" disabled, one POST.
  3. Check failed (500, then NetworkError): copy, only "Try again", last result kept.
  4. Not prepared (available, can_apply false): copy, command line, note, "How to update" link with href equal to release_url and an external target; no button other than Copy.
  5. Notes open: collapsed by default, aria-expanded toggles, "Released 24 Sep 2026" and the plain-text list.
  6. Command copied: Copy writes the command to the clipboard mock, reads "Copied" for 2 s, then "Copy".
  7. Admin view: both texts, no button; no auto check.
  8. Role changed (403): toast "Only the owner can do this" / "Ask the owner of this site to update RenewCMS.", then a reload of the GET and the admin view.
  9. 429: "Too many tries", button "Try again in 0:42" counting down to enabled.
  10. Auto check: sent once when checked_at is null or older than 24 h, not when fresh, never for admin.
  11. Home banner: shown for the owner when available, copy and "Go to Settings" navigates to /settings, dismiss writes the version to localStorage and hides it, same version stays hidden after remount, a newer latest shows it again, absent for admin and when not available.
  12. Sidebar pill and mobile dot: owner and available only, pill remains after the banner is dismissed; absent for admin.
  13. No Update, Restart or progressbar element exists in any of the states above; the shown version drops the leading "v" ("Version 2.3.0-beta") while the Version card keeps v2.2.0-beta.

Visual (task visual:check -- settings and -- home; desktop and mobile, light and dark; every capture within 10 %)

Add the setups to web/visual/states.ts using the board's own sample data. Exact values from docs/design/source/AppFrame.dc.html (Settings and Home blocks); layout as drawn, not from the prose.

  • Settings: Checking (mock a never-resolving check after the click setup), Not prepared, Admin view (available), Too many requests (429, Retry-After 42).
  • Settings, skip: 'clock-sensitive' (the meta line is relative: "Last checked today at 09:41", "yesterday at 18:12"): Up to date, Check failed, Admin view (up to date).
  • Settings, Not prepared: command copied (the "Copied" state is time-bound; Vitest only), listed as not visual.
  • Home: Update available (owner), with the sidebar pill and the mobile dot as drawn.
  • Pending, never passing (slices 2 and 3): Update available with its Update button, Update available with notes open (its frame draws the Update button), Downloading, Verifying, Ready to install, Restart confirm, Restart request failed, Restarting, Restarting taking longer, Update done, Rolled back, Download failed, Verification failed, Plain restart (confirm, restarting, done), Update already running (409), Home "ready to install" banner.
  • Not covered by screenshots: keyboard and focus order, focus ring on the "Updates" heading (id="updates-h"), hover and motion, the Copy announcement, the dismiss button focus.

Manual verification

  • Replace the placeholder key: run task release:keygen, store the private key outside the repo, set RELEASE_SIGNING_KEY to its path, commit the printed public key as internal/infrastructure/update/release.pub, and rebuild. Never commit or paste the private key. Until this is done, task release must fail with the placeholder message.
  • Run task release with RELEASE_SIGNING_KEY unset, pointing to a missing file, and pointing to a key that does not match release.pub: each fails before any binary is built, with a message naming the cause. With the correct key, checksums.txt and checksums.txt.sig appear next to the six binaries and cmd/releasesign verify passes on them.
  • Against the real Gitea feed (default UPDATE_FEED_URL), run a dev API with a lower fake version and press "Check for updates" in the SPA: it reports the newest release; the "How to update" link opens that release's page.
  • Real release notes (the markdown body) render as a sensible plain-text list.
  • Restart the API and confirm the last result and checked_at survive, in update-status.json beside DB_FILE.
  • Embedded SPA build (no -tags nofrontend) serves /settings and /home with the card, banner and pill.
  • Confirm docs/release-notes.md (publish step: attach checksums.txt and checksums.txt.sig with the six binaries), docs/deployment.md and docs/configuration.md are updated, and that nothing was published.
  • Release gate: slices 1 to 3 ship together, so no released build shows renewcms enable-browser-updates before slice 3 implements it.