106 lines
8.9 KiB
Markdown
106 lines
8.9 KiB
Markdown
# 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.
|