feat: update button (download, verify, stage)
This commit is contained in:
1 parent
834493a4f5
commit
70460e4170
41 files changed
+5345
-97
No files matched your search
@@ -0,0 +1,334 @@
|
||||
Feature: Updates part 2: the Update button (download, verify, stage)
|
||||
|
||||
As the owner of a RenewCMS site
|
||||
I want one button that downloads the new release, proves it is genuine, backs up the database and stages it
|
||||
So that the later Restart (slice 3) installs a file that was already checked, while the site stays online
|
||||
|
||||
Background:
|
||||
Given slices 1 to 3 of Updates ship in one release
|
||||
And this slice changes nothing that is running: it never replaces the running binary and never restarts the site
|
||||
And the domain package internal/domain/update (stdlib only) gains these interfaces, each faked in tests:
|
||||
AssetFetcher (downloads one asset to a path, reporting bytes done and total),
|
||||
SignatureVerifier (ed25519 check of checksums.txt against checksums.txt.sig with the embedded release.pub),
|
||||
StagedBinaryRunner (runs <path> --version and returns its output),
|
||||
DatabaseBackup (consistent copy of the SQLite file with VACUUM INTO, prune to the last 3),
|
||||
HelperDetector (reports whether the slice 3 helper is installed),
|
||||
JobStore (persists the update job record)
|
||||
And update.Release gains Assets (name, download URL, size) read by the Gitea adapter, and the stored CheckResult keeps
|
||||
them, so POST /v1/system/update/download never contacts the feed
|
||||
And the use cases depend only on domain interfaces; handlers in api/handlers/update receive them through NewHandler;
|
||||
only api/dependecyInjection.go builds the adapters
|
||||
And the job runs in one background goroutine owned by the use case; the HTTP handler returns as soon as it is started
|
||||
And POST /v1/system/update/download is mounted in NewSystemRouter behind jwtauth.Verifier, jwtauth.Authenticator,
|
||||
RequireCurrentToken, RequireVerifiedUser, RequireOwner and its own limiter of 5 starts per minute per user_id
|
||||
(constants, separate from the check limiter and the auth limiter) answering 429 with an integer Retry-After
|
||||
And the checks on POST run in this order: no cookie or stale token (401), unverified (403), not owner (403),
|
||||
limiter (429), can_apply false (409), a job running or already ready (409), available false (400),
|
||||
pre-flight refusal (422), then 202
|
||||
And errors are plain text from http.Error
|
||||
And the data directory is the directory that holds DB_FILE; no new env var is added
|
||||
And the job record is update-job.json in the data directory, shared by all owners and surviving a restart
|
||||
And a job record found as "downloading" or "verifying" at startup (no goroutine owns it) reads as failed with
|
||||
error "download"; a record "ready" whose staged binary is gone reads as idle
|
||||
And the staged files live in <data dir>/updates/<tag>/: the binary, checksums.txt and checksums.txt.sig
|
||||
And the asset to download is named renewcms_<tag>_<runtime.GOOS>_<runtime.GOARCH>, plus ".exe" on windows
|
||||
And the job runs these steps in order: download checksums.txt, checksums.txt.sig and the binary; verify the
|
||||
ed25519 signature of checksums.txt; verify the SHA-256 of the binary against its checksums.txt line; run the
|
||||
staged binary with --version and require the output to equal the tag; back up the database; mark "ready"
|
||||
And state is "downloading" during the downloads and "verifying" from the signature check until "ready"
|
||||
(the version run and the backup happen in "verifying")
|
||||
And the download policy, enforced in the production adapter: the URL must be https, its host (with port) must equal
|
||||
the host of UPDATE_FEED_URL, and every redirect must satisfy the same rule; there is a size cap per file
|
||||
(binary 200 MiB, checksums.txt 64 KiB, checksums.txt.sig 1 KiB) and an overall timeout of 10 minutes (named constants)
|
||||
And in tests the policy is relaxed only through the adapter constructor, which takes the allowed host, an
|
||||
"allow insecure scheme" option, a smaller cap and a shorter timeout; there is no env var or build flag that relaxes it
|
||||
And GET /v1/system/update keeps its 12 keys; state is one of idle, downloading, verifying, ready, failed;
|
||||
progress is {percent, downloaded_bytes, total_bytes} while downloading and null otherwise;
|
||||
error is null, "download" or "verification" (a kind only, never technical text); started_at is the job start
|
||||
And can_apply is true only for the owner when HelperDetector reports the helper installed; it is false for admins
|
||||
And the real HelperDetector reads <data dir>/update-helper.json (the status file the slice 3 helper writes); nothing
|
||||
writes it in this slice, so can_apply is false on real builds until slice 3
|
||||
And disk errors and database backup failures end the job as failed with error "download"
|
||||
And on any failure the staged files are deleted, the technical reason is logged server side, and no response carries it
|
||||
And a failed record stays until the next job starts; ready stays until slice 3 consumes it
|
||||
And the SPA shows the newer release without its leading "v" ("Version 2.3.0-beta") and the running version exactly as reported
|
||||
And the Restart button in "Ready to install" is rendered but disabled in this slice: the board frame draws it as the
|
||||
primary action, it has no handler, and slice 3 enables it; plain Restart (no update waiting) stays not rendered until slice 3
|
||||
|
||||
Scenario: Start the update, happy path
|
||||
Given the owner, can_apply true, a stored result with latest "v2.3.0-beta" (running "v2.2.0-beta", available true)
|
||||
and assets for the host platform, all served by the fake fetcher and signed with a throwaway key
|
||||
When the owner sends POST /v1/system/update/download
|
||||
Then the status is 202 and the body is the status JSON with state "downloading" and started_at set
|
||||
And GET /v1/system/update then reports downloading with progress.total_bytes equal to the asset size
|
||||
And when the job finishes GET reports state "ready", error null, progress null, latest "v2.3.0-beta"
|
||||
And the staged binary, checksums.txt and checksums.txt.sig are in <data dir>/updates/v2.3.0-beta/
|
||||
And a database backup exists in <data dir>/backups/ before state became "ready"
|
||||
And the running binary and the database are unchanged
|
||||
|
||||
Scenario Outline: The asset name follows the platform
|
||||
Given the runtime is <os>/<arch> and the release lists all six binaries
|
||||
When the update starts
|
||||
Then the fetcher is asked for "<asset>", "checksums.txt" and "checksums.txt.sig" only
|
||||
|
||||
Examples:
|
||||
| os | arch | asset |
|
||||
| linux | amd64 | renewcms_v2.3.0-beta_linux_amd64 |
|
||||
| linux | arm64 | renewcms_v2.3.0-beta_linux_arm64 |
|
||||
| darwin | arm64 | renewcms_v2.3.0-beta_darwin_arm64 |
|
||||
| windows | amd64 | renewcms_v2.3.0-beta_windows_amd64.exe |
|
||||
|
||||
Scenario: Progress is reported in bytes
|
||||
Given the fake fetcher has delivered 11 of 26 MiB
|
||||
When any verified user sends GET /v1/system/update
|
||||
Then state is "downloading" and progress is {percent: 42, downloaded_bytes: 11534336, total_bytes: 27262976}
|
||||
And percent is the floor of done * 100 / total
|
||||
|
||||
Scenario: Every owner sees the same job
|
||||
Given owner A started the job
|
||||
When owner B and an admin send GET /v1/system/update
|
||||
Then both read state "downloading" with the same progress and started_at
|
||||
And the owner's can_apply is true and the admin's can_apply is false
|
||||
|
||||
Scenario: The job record survives a restart
|
||||
Given the job is "ready" for "v2.3.0-beta" with its staged binary present
|
||||
When a new status use case is built over the same data directory
|
||||
Then GET reports state "ready"
|
||||
And a record "downloading" at startup reads as state "failed", error "download"
|
||||
And a record "ready" with the staged binary deleted reads as "idle"
|
||||
And a missing or corrupt job file reads as "idle" and is not fatal
|
||||
|
||||
Scenario: Only the owner
|
||||
When an admin sends POST /v1/system/update/download
|
||||
Then the status is 403 and the body is apperr.OwnerRequiredErrorMessage
|
||||
And no job starts and the fetcher is not called
|
||||
|
||||
Scenario Outline: POST is guarded like the other system routes
|
||||
When <caller> sends POST /v1/system/update/download
|
||||
Then the status is <status>
|
||||
And no job starts
|
||||
|
||||
Examples:
|
||||
| caller | status |
|
||||
| no jwt cookie | 401 |
|
||||
| a stale token version | 401 |
|
||||
| an unverified owner | 403 |
|
||||
|
||||
Scenario: Nothing to update
|
||||
Given a stored result with available false, or no stored result
|
||||
When the owner sends POST /v1/system/update/download
|
||||
Then the status is 400 and no job starts and the fetcher is not called
|
||||
|
||||
Scenario: The server is not prepared
|
||||
Given HelperDetector reports no helper
|
||||
When the owner sends POST /v1/system/update/download
|
||||
Then the status is 409 and no job starts
|
||||
And GET reports can_apply false
|
||||
And this holds even when available is false (409 before 400)
|
||||
|
||||
Scenario Outline: A second start while a job exists
|
||||
Given the job state is <state>
|
||||
When the owner sends POST /v1/system/update/download
|
||||
Then the status is 409 and the existing job is untouched
|
||||
|
||||
Examples:
|
||||
| state |
|
||||
| downloading |
|
||||
| verifying |
|
||||
| ready |
|
||||
|
||||
Scenario: Two starts at once run one job
|
||||
When two owners send POST /v1/system/update/download concurrently
|
||||
Then exactly one answers 202 and the other 409
|
||||
And the fetcher downloads each asset once
|
||||
|
||||
Scenario: Try again after a failure
|
||||
Given the job state is "failed"
|
||||
When the owner sends POST /v1/system/update/download
|
||||
Then the status is 202, error returns to null, started_at is new and the old staged files are replaced
|
||||
|
||||
Scenario: Start is rate limited per user
|
||||
When the owner sends 6 starts within one minute
|
||||
Then the first 5 are answered by the checks above and the 6th is 429 with apperr.TooManyRequestsErrorMessage
|
||||
and an integer Retry-After of at least 1
|
||||
And a different owner is not limited, and the check limiter and auth limiter counters are untouched
|
||||
|
||||
Scenario Outline: 422 is only a pre-flight refusal
|
||||
Given a stored result where <condition>
|
||||
When the owner sends POST /v1/system/update/download
|
||||
Then the status is 422 and the body is plain text without technical detail
|
||||
And no job starts, the fetcher is not called and the technical reason is logged
|
||||
|
||||
Examples:
|
||||
| condition |
|
||||
| no asset is named for the host platform |
|
||||
| checksums.txt or checksums.txt.sig is missing from the assets |
|
||||
| an asset URL is http, not https |
|
||||
| an asset URL is on a host other than UPDATE_FEED_URL's host |
|
||||
|
||||
Scenario Outline: Verification failures end the job as failed, kind "verification"
|
||||
Given a started job and <fault>
|
||||
When the job reaches the check
|
||||
Then GET reports state "failed", error "verification", progress null
|
||||
And <data dir>/updates/<tag>/ is deleted
|
||||
And the technical reason is logged and appears in no response body
|
||||
|
||||
Examples:
|
||||
| fault |
|
||||
| the binary was changed after signing (SHA-256 mismatch) |
|
||||
| checksums.txt was changed after signing |
|
||||
| the signature is from a different key |
|
||||
| checksums.txt has no line for the asset |
|
||||
| the staged binary prints "v2.2.9-beta" for --version |
|
||||
| the staged binary cannot be run |
|
||||
|
||||
Scenario Outline: Download and disk failures end the job as failed, kind "download"
|
||||
Given a started job and <fault>
|
||||
Then GET reports state "failed", error "download"
|
||||
And <data dir>/updates/<tag>/ is deleted and the technical reason is logged, not returned
|
||||
|
||||
Examples:
|
||||
| fault |
|
||||
| the connection drops after half the bytes (truncated download) |
|
||||
| the bytes received differ from the size the release declares |
|
||||
| a file exceeds its size cap |
|
||||
| the download passes the overall timeout |
|
||||
| the feed host answers 404 or 500 for an asset |
|
||||
| the updates directory cannot be created or written (disk error) |
|
||||
| the disk fills during the write |
|
||||
| the database backup fails |
|
||||
|
||||
Scenario: The backup comes before ready, and the last 3 are kept
|
||||
Given the backups directory holds 3 earlier backups
|
||||
When a job completes
|
||||
Then a new consistent copy exists (opened read-only it holds the same article rows as the live file)
|
||||
And only the 3 newest backups remain, the oldest being deleted
|
||||
And no job is "ready" until the new backup exists
|
||||
And a failed backup leaves state "failed", error "download" and does not prune the earlier backups
|
||||
|
||||
Scenario: The production download policy
|
||||
Given the production adapter with UPDATE_FEED_URL "https://git.example"
|
||||
Then an http:// asset URL, an https URL on another host, and a redirect to another host or to http are refused
|
||||
And an https URL on git.example is accepted
|
||||
|
||||
Scenario: A server --version flag
|
||||
When the server binary is run with --version
|
||||
Then it prints the build version and exits 0
|
||||
And it does not read .env, open the database or start listening
|
||||
And with no -ldflags version it prints "dev"
|
||||
|
||||
Scenario: Settings, owner, Update available with the helper detected (route /settings)
|
||||
Given the owner and GET /v1/system/update resolves available true, can_apply true, state "idle", latest "v2.3.0-beta"
|
||||
Then the block shows icon circle-arrow-up (info), "Version 2.3.0-beta is available" /
|
||||
"You have v2.2.0-beta. Update downloads the new version and checks it. Nothing changes until you restart."
|
||||
and the meta "Last checked ..."
|
||||
And the notes button "What's new in 2.3.0-beta" is collapsed, and when open shows "Released 24 Sep 2026" and the list
|
||||
And the one button is "Update" (download, primary) and the hint is "Takes about 2 minutes. Your site stays online."
|
||||
And no server command, no "How to update" link, no Restart and no "Check for updates" is rendered
|
||||
|
||||
Scenario: Settings, owner, Downloading
|
||||
When the owner presses "Update" and POST /v1/system/update/download answers 202
|
||||
Then the block shows icon download (info), "Downloading version 2.3.0-beta…" /
|
||||
"You can leave this page. The download continues and your site stays online."
|
||||
And a progressbar labelled "Downloading version 2.3.0-beta" has aria-valuenow equal to progress.percent
|
||||
and the text "42% · 11 of 26 MB"
|
||||
And the button reads "Updating…" (loader-circle, secondary) and is disabled
|
||||
And GET /v1/system/update is polled every second while state is downloading or verifying
|
||||
|
||||
Scenario: Settings, progress is announced at 25 percent steps only
|
||||
Given the polled percent goes 3, 9, 24, 25, 26, 49, 50, 74, 75, 99, 100
|
||||
Then the announcer text changes only at 25, 50, 75 and the step change, reading
|
||||
"Downloading version 2.3.0-beta, 25 percent" and so on
|
||||
And the visible text and aria-valuenow still update on every poll
|
||||
And the live region does not announce the per-tick text
|
||||
|
||||
Scenario: Settings, owner, Verifying
|
||||
Given the poll answers state "verifying"
|
||||
Then the block shows icon shield-check (info), "Checking the download…" /
|
||||
"Making sure the file is complete and safe to install. This takes a few seconds."
|
||||
And the progressbar is indeterminate (no aria-valuenow, a 35% wide moving bar) labelled "Checking the download"
|
||||
with the text "Almost there"
|
||||
And the button reads "Updating…" and is disabled
|
||||
And "Checking the download" is announced once, when the step changes
|
||||
|
||||
Scenario: Settings, owner, Ready to install
|
||||
Given the poll answers state "ready"
|
||||
Then polling stops
|
||||
And the block shows icon circle-check (success), "Version 2.3.0-beta is ready to install" /
|
||||
"The download is checked. Restart to install it. Your site will be unavailable for about 10 seconds." /
|
||||
"Nothing has changed yet."
|
||||
And the notes button is shown collapsed
|
||||
And the Restart button (power, primary) is rendered with disabled set and 40% opacity, and activating it does nothing
|
||||
And the hint is "Not now? Leave this page. The update waits here, and the banner on Home reminds you."
|
||||
And no dialog, overlay or POST /v1/system/update/restart exists
|
||||
|
||||
Scenario: Settings, Download failed
|
||||
Given the poll answers state "failed" with error "download", or POST download answers 500 or never reaches the server
|
||||
Then focus moves to the "Updates" heading (id="updates-h")
|
||||
And the block shows icon circle-x (error), "The download didn't finish" /
|
||||
"The connection dropped before the file was complete. Nothing was changed and your site is running as before."
|
||||
And the one button is "Try again" (rotate-cw, primary), which sends POST /v1/system/update/download
|
||||
|
||||
Scenario: Settings, Verification failed
|
||||
Given the poll answers state "failed" with error "verification", or POST download answers 422
|
||||
Then the block shows icon triangle-alert (warning), "We didn't use this update" /
|
||||
"The file didn't pass our safety check, so it was not installed. Nothing was changed. Try downloading it again. If this keeps happening, contact RenewCMS support."
|
||||
And the one button is "Try again" (rotate-cw, primary), as the board frame draws it
|
||||
And focus does not move to the heading
|
||||
And no technical detail is on screen
|
||||
|
||||
Scenario: Settings, 409 already running
|
||||
Given POST /v1/system/update/download answers 409 and the reload answers state "downloading" or "verifying"
|
||||
Then an info toast "An update is already running" / "Another owner started it. Progress shows here." shows
|
||||
And the card switches to that run's progress and polls
|
||||
When the reload instead answers can_apply false
|
||||
Then the card shows the "Not prepared" state and no toast
|
||||
When the reload instead answers state "ready"
|
||||
Then the card shows "Ready to install" and no toast
|
||||
|
||||
Scenario: Settings, 400 nothing to update
|
||||
Given POST /v1/system/update/download answers 400
|
||||
Then an info toast "Nothing to update" / "You already have the latest version." shows
|
||||
And the card reloads GET /v1/system/update
|
||||
|
||||
Scenario: Settings, 403 role changed on Update
|
||||
Given POST /v1/system/update/download answers 403
|
||||
Then an error toast "Only the owner can do this" / "Ask the owner of this site to update RenewCMS." shows
|
||||
And the card reloads and renders the admin view
|
||||
|
||||
Scenario: Settings, 429 on Update
|
||||
Given POST /v1/system/update/download answers 429 with Retry-After 42
|
||||
Then the block shows icon info "Too many tries" / "Wait a moment before trying again."
|
||||
And the button reads "Try again in 0:42" (clock, disabled) and counts down each second through useRetryAfter
|
||||
And at zero it is re-enabled as "Try again" and pressing it sends POST /v1/system/update/download again
|
||||
|
||||
Scenario: Settings, reload keeps progress
|
||||
Given a job is downloading and the owner leaves /settings
|
||||
Then polling stops and the job continues on the server
|
||||
When the owner opens /settings again, from this or another device
|
||||
Then GET /v1/system/update is read on open and the card shows the job's current state and progress
|
||||
And polling resumes while state is downloading or verifying
|
||||
|
||||
Scenario: Settings, a poll that fails keeps the card
|
||||
Given a poll answers 500 or never reaches the server while downloading
|
||||
Then the card keeps its last progress, no failure card shows, and polling continues each second
|
||||
|
||||
Scenario: Settings, admin view during a job
|
||||
Given the session role is "admin" and state is "downloading"
|
||||
Then the admin view of slice 1 shows ("Version 2.3.0-beta is available" / "You have v2.2.0-beta. Only the owner can update RenewCMS. Ask the owner of this site.")
|
||||
And no button, progressbar or poll is rendered
|
||||
|
||||
Scenario: Home banner, ready to install (owner)
|
||||
Given the owner is on /home and state is "ready"
|
||||
Then a region labelled "Update available" shows above the content in info tone (as the board frame draws it)
|
||||
with icon circle-check, title "Version 2.3.0-beta is ready to install",
|
||||
text "Restart from Settings to finish. The site is unavailable for about 10 seconds.",
|
||||
a "Go to Settings" button that navigates to /settings, and a Dismiss button
|
||||
And it shows although the "available" banner for this version was dismissed earlier
|
||||
When the owner dismisses it
|
||||
Then it hides for this visit only and shows again on the next visit, and nothing is written to localStorage
|
||||
And an admin sees no banner; with state "downloading" or "verifying" the "available" banner shows as before
|
||||
|
||||
Scenario: Pending states for slice 3
|
||||
Then these remain pending in the visual gate, never passing: Restart confirm, Restart request failed, Restarting to install,
|
||||
Restarting taking longer, Update done, Rolled back, Plain restart (confirm, restarting, done)
|
||||
@@ -0,0 +1,113 @@
|
||||
# QA: Updates part 2 (download, verify, stage)
|
||||
|
||||
## Automated Go (`task test`, package `test`, black-box, no live network, no real SMTP)
|
||||
|
||||
Files: `test/update_download_test.go` (domain, use case), `test/update_download_http_test.go` (HTTP),
|
||||
`test/update_assets_test.go` (production download adapter against a local server). Fakes with unique names:
|
||||
`fakeAssetFetcher` (serves bytes, can truncate, stall, fail, counts calls), `fakeHelperDetector`, `fakeJobStore`,
|
||||
`fakeStagedRunner` (returns a configured --version output or error), `fakeDatabaseBackup`; reuse `fakeReleaseFeed`,
|
||||
`fakeUpdateStore` and `mockUserRepo` from the slice 1 tests.
|
||||
|
||||
HTTP setup: `validator.InitValidator()`; `token := jwtauth.New("HS256", []byte("test-secret"), nil)`; a fresh `chi.NewRouter()` at
|
||||
`/v1/system` mirroring `NewSystemRouter` (Verifier, Authenticator, RequireCurrentToken, RequireVerifiedUser; the download
|
||||
route also RequireOwner and its limiter, 5 per minute); cookies as in `test/stale_token_invalidation_http_test.go`.
|
||||
Job state, staged files and backups go in `t.TempDir()`. Wait for the job with a poll loop on GET (deadline 5 s), never a bare
|
||||
`time.Sleep`. Use a throwaway ed25519 key generated in the test and sign `checksums.txt` with it; never read the committed
|
||||
`release.pub`.
|
||||
|
||||
Local asset server: `httptest.NewServer` (plain HTTP) and `httptest.NewTLSServer` for the policy cases. The production adapter is
|
||||
built through its constructor with the test's allowed host, allow-insecure true for the plain server, the TLS server's client for
|
||||
the TLS cases, a small size cap and a short timeout. Never relax the policy by env var or build flag.
|
||||
|
||||
1. Happy path: owner POST gives 202 with state "downloading"; poll GET until "ready"; files in `updates/<tag>/`, one backup in
|
||||
`backups/`; running binary unchanged; the fetcher was asked for exactly the three asset names.
|
||||
2. Asset naming table over GOOS/GOARCH (including the `.exe` case). Inject the platform into the use case; never assert on the
|
||||
test machine's own platform.
|
||||
3. Progress: a fetcher paused at 11 of 26 MiB gives `{percent:42, downloaded_bytes, total_bytes}` on GET; `progress` is null in
|
||||
verifying, ready and failed; `error` is null until a failure.
|
||||
4. Shared state: GET as owner A, owner B and admin agree; admin `can_apply` false, owner true with the fake detector on.
|
||||
5. Persistence: JSON job store round trip in a temp dir; "downloading" at startup reads failed/"download"; "ready" with the staged
|
||||
binary deleted reads idle; a missing or corrupt file reads idle without error. The real helper detector reads
|
||||
`<data dir>/update-helper.json`: absent file gives false, a present valid file gives true.
|
||||
6. Guards: admin 403 with `apperr.OwnerRequiredErrorMessage+"\n"`; no cookie 401; stale token 401; unverified 403; none start a job.
|
||||
7. Status codes: available false or nothing stored gives 400; detector off gives 409; state downloading, verifying, ready each give
|
||||
409; after "failed" a new POST gives 202 with a new started_at. Order check: detector off and available false gives 409.
|
||||
8. Concurrency: two simultaneous POSTs (goroutines) give one 202 and one 409; each asset fetched once (run with `-race`).
|
||||
9. Rate limit: owner A sends 6 POSTs within a minute; the 6th is 429 with `apperr.TooManyRequestsErrorMessage+"\n"` and an integer
|
||||
Retry-After >= 1; owner B is unaffected; the check limiter and a login limiter built in the test are untouched.
|
||||
10. 422 pre-flight: each of the four conditions in the feature gives 422, the body is plain text with no URL, host or file name from
|
||||
the fixture, no job record is created, fetcher count is 0, and the reason is captured from an injected logger.
|
||||
11. Verification failures, one subtest per row, each ending with GET `state` "failed", `error` "verification", `updates/<tag>/`
|
||||
removed, a body free of the fault text, and the fault text present in the injected logger: tampered binary, tampered
|
||||
checksums.txt, signature from another key, no checksum line for the asset, wrong `--version` output, runner error.
|
||||
12. Download failures, one subtest per row, each ending "failed", `error` "download", directory removed: truncated body, size
|
||||
mismatch with the release, over the size cap, timeout (server stalls; adapter timeout shortened), 404/500, unwritable updates
|
||||
directory (a file placed where the directory should be), disk full (a writer fake returning an error mid-write), backup failure
|
||||
(earlier backups not pruned).
|
||||
13. Backup: with the real SQLite backup adapter against a SQLite file created in `t.TempDir()` (pure-Go driver, `CGO_ENABLED=0`):
|
||||
the copy opens and holds the same rows, four jobs leave only the 3 newest files, and a job is not "ready" before the copy exists.
|
||||
14. Production policy: http URL, another host, redirect to another host and redirect to http are refused (no request reaches the
|
||||
second server); the TLS server on the allowed host is accepted.
|
||||
15. `--version`: build with `go build -tags nofrontend -ldflags "-X main.version=v9.9.9" ./cmd/server` into `t.TempDir()` and run
|
||||
it with `--version` and an empty environment: stdout `v9.9.9`, exit 0, no `data/` created; without ldflags it prints `dev`.
|
||||
16. Review items, not Go tests: `internal/domain/update` imports only the stdlib; use cases import no `net/http`, `os/exec`, `gorm`
|
||||
or adapter; the handler imports no adapter; no secret value is in the repo; the helper detector reads only its status file.
|
||||
|
||||
## Automated SPA (Vitest + Vue Test Utils, `task test:web`)
|
||||
|
||||
Mock `useApi.request` for `GET /v1/system/update`, `POST /v1/system/update/check` and `POST /v1/system/update/download`. Fake timers
|
||||
drive the 1 s poll and the countdown. No test freezes the system date and none asserts a relative date. Reuse the board's data:
|
||||
running `v2.2.0-beta`, latest `v2.3.0-beta`, 26 MB total, 11 MB done (42 %), released 24 Sep 2026.
|
||||
|
||||
One test per state:
|
||||
1. Update available, can_apply true: copy, notes collapsed, only "Update", hint; no command, no link, no Restart.
|
||||
2. Update available, notes open (`aria-expanded`, "Released 24 Sep 2026").
|
||||
3. Downloading: 202 then the card copy, progressbar label, `aria-valuenow` 42, "42% · 11 of 26 MB", "Updating…" disabled; GET called once per second.
|
||||
4. Announcements: percents 3, 24, 25, 26, 49, 50, 75, 100; the announcer text changes only at 25, 50, 75 and the step change;
|
||||
`aria-valuenow` changes on each tick.
|
||||
5. Verifying: indeterminate bar (no `aria-valuenow`), "Almost there", announced once.
|
||||
6. Ready: copy, meta, hint, Restart present with `disabled` and no request on click, notes button, polling stopped.
|
||||
7. Download failed (state failed/download; a 500; a `NetworkError`): copy, focus on `#updates-h`, "Try again" posts download again.
|
||||
8. Verification failed (state failed/verification; a 422): copy, "Try again", focus not moved, no technical text.
|
||||
9. 409: reload downloading gives the info toast and progress; reload ready gives no toast and the Ready card; reload can_apply false
|
||||
gives the Not prepared card.
|
||||
10. 400: info toast "Nothing to update" and one GET.
|
||||
11. 403: error toast and the admin view after reload.
|
||||
12. 429: "Too many tries", "Try again in 0:42" counting down through `useRetryAfter`, re-enabled as "Try again" and it re-posts
|
||||
download (not check).
|
||||
13. Reload: unmount stops the poll; mount reads GET and resumes the poll for a running state; no poll for idle, ready or failed.
|
||||
14. A failed poll keeps the last progress and keeps polling.
|
||||
15. Admin with state downloading: admin copy, no button, no progressbar, no poll.
|
||||
16. Home banner ready: info tone, copy, `role="region"` labelled "Update available", "Go to Settings" navigates, shows although the
|
||||
available banner was dismissed, Dismiss hides until remount and writes nothing to localStorage; admin sees none.
|
||||
17. Sidebar pill and mobile dot stay for ready, downloading and verifying (owner only, `available` true).
|
||||
|
||||
## 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` with the board's sample data (v2.2.0-beta, v2.3.0-beta, 42 % / "11 of 26 MB"). Exact values
|
||||
from `docs/design/source/AppFrame.dc.html` and the "Settings" and "Home" groups of the Screens board; layout as drawn.
|
||||
|
||||
- [ ] Settings: Update available, Update available (notes open), Downloading (42 %), Verifying, Ready to install,
|
||||
Download failed, Verification failed. The frame draws an enabled Restart; the disabled one (40 % opacity) must still be
|
||||
within budget. If it is not, stop and report; do not widen the budget or add an exception without the user's approval.
|
||||
- [ ] Home: Update ready to install (owner), with the sidebar pill and the mobile dot as drawn.
|
||||
- [ ] Settings: Update already running (409): the frame shows the card in progress; set up with the mocked 409 then the
|
||||
downloading reload (the toast only if the frame draws it; check the markup).
|
||||
- [ ] `skip: 'clock-sensitive'`: any state whose meta line is relative ("Last checked today at 09:41"), as in slice 1.
|
||||
- [ ] Pending, never passing (slice 3): Restart confirm, Restart request failed, Restarting, Restarting taking longer,
|
||||
Update done, Rolled back, Plain restart (confirm, restarting, done).
|
||||
- [ ] Not covered by screenshots: keyboard and focus order, the focus ring on `#updates-h` after Download failed, the 25 %
|
||||
announcements (screen reader), hover and motion of the indeterminate bar.
|
||||
|
||||
## Manual verification
|
||||
|
||||
- `can_apply` stays false on real builds until slice 3 writes `<data dir>/update-helper.json`: use the fake in tests, or place the
|
||||
file by hand to see the Update button.
|
||||
- Against a real signed release on the real feed host (needs `release.pub` replaced and `task release` signing): press Update, watch
|
||||
progress, the Verifying step and the Ready card; check `<data dir>/updates/<tag>/` and one file in `<data dir>/backups/`.
|
||||
- The real staged binary on the target OS runs `--version` and prints the tag.
|
||||
- Flip one byte of a asset on a test mirror: the card shows "We didn't use this update" and the folder is gone.
|
||||
- Kill the server mid-download and restart it: the card shows "The download didn't finish".
|
||||
- Open the card in two browsers (two owners) and confirm the same progress; the second press shows the 409 toast.
|
||||
- Embedded SPA build (no `-tags nofrontend`) serves /settings and /home with the new states.
|
||||
- Confirm no secret or feed credential appears in logs, and that technical failure reasons are in the log only.
|
||||
Reference in new issue
Block a user