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.
- Version comparison, table-driven over the Examples in the feature (domain;
errors.Isfor sentinels). - Feed adapter against
httptest.NewServerserving 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_urlcarried; a 500 and invalid JSON return an error. GET /v1/system/updateas owner and as admin: 200, exactly the 12 keys,can_applyfalse,state"idle", values from the stored result,release_urlfrom the storedhtml_url; the fake feed call count is 0.- GET before any check:
latest"",availablefalse,release_url,notes,checked_at,progress,error,started_atnull. - GET: no cookie 401; stale token 401 with
apperr.UnauthorizedErrorMessage; unverified 403 withapperr.VerificationRequiredErrorMessage. POST /v1/system/update/checkas owner: 200, store updated with latest,release_urlandchecked_at, feed called once, a later GET equals it. Nothing newer:availablefalse. Running version "dev":availablefalse.- POST check as admin: 403, body
apperr.OwnerRequiredErrorMessage+"\n", feed not called. - Feed error: 500, store unchanged (latest and
checked_atas before), body contains no feed error text. - Rate limit: owner A sends 6 checks: first 5 are 200, the 6th is 429 with
apperr.TooManyRequestsErrorMessage+"\n"and an integerRetry-Afterof 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. - JSON file store in
t.TempDir(): save, reopen, same values; missing file and corrupt file read as empty without error. - Signing tool, run through
go run ./cmd/releasesign(or its exported package) with a throwaway key pair generated int.TempDir()at test time, always with--pub; never read the committedrelease.pubkey 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--pubfails, naming the mismatch, and writes no.sig. - Placeholder: a
--pubfile holding base64 of 32 zero bytes makes sign and verify fail with the placeholder message. - The committed
internal/infrastructure/update/release.pubis 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. - Review items, not Go tests:
internal/domain/updateimports only the stdlib; use cases import nonet/http,gorm, adapter orconfig; the handler imports no adapter;UPDATE_FEED_URLis inrequiredEnvVars/envDefaults,.env.exampleanddocs/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:
- Up to date (owner): "You're up to date", only "Check for updates" rendered.
- Checking: text, polite live region, "Checking…" disabled, one POST.
- Check failed (500, then
NetworkError): copy, only "Try again", last result kept. - Not prepared (available,
can_applyfalse): copy, command line, note, "How to update" link withhrefequal torelease_urland an external target; no button other than Copy. - Notes open: collapsed by default,
aria-expandedtoggles, "Released 24 Sep 2026" and the plain-text list. - Command copied: Copy writes the command to the clipboard mock, reads "Copied" for 2 s, then "Copy".
- Admin view: both texts, no button; no auto check.
- 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.
- 429: "Too many tries", button "Try again in 0:42" counting down to enabled.
- Auto check: sent once when
checked_atis null or older than 24 h, not when fresh, never for admin. - 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
latestshows it again, absent for admin and when not available. - Sidebar pill and mobile dot: owner and available only, pill remains after the banner is dismissed; absent for admin.
- 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, setRELEASE_SIGNING_KEYto its path, commit the printed public key asinternal/infrastructure/update/release.pub, and rebuild. Never commit or paste the private key. Until this is done,task releasemust fail with the placeholder message. - Run
task releasewithRELEASE_SIGNING_KEYunset, pointing to a missing file, and pointing to a key that does not matchrelease.pub: each fails before any binary is built, with a message naming the cause. With the correct key,checksums.txtandchecksums.txt.sigappear next to the six binaries andcmd/releasesign verifypasses 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_atsurvive, inupdate-status.jsonbesideDB_FILE. - Embedded SPA build (no
-tags nofrontend) serves /settings and /home with the card, banner and pill. - Confirm
docs/release-notes.md(publish step: attachchecksums.txtandchecksums.txt.sigwith the six binaries),docs/deployment.mdanddocs/configuration.mdare updated, and that nothing was published. - Release gate: slices 1 to 3 ship together, so no released build shows
renewcms enable-browser-updatesbefore slice 3 implements it.