feat: signed releases, release feed, notification (read-only)

This commit is contained in:
fsylvain committed 2026-10-01 15:01:53 +02:00
1 parent a49eec909c
commit 834493a4f5
49 files changed
+4191 -18

No files matched your search

+105
View File
@@ -0,0 +1,105 @@
# 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.