bastion 0.53.2
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
package main
|
||||
|
||||
// "Is there a newer version?" — the question the box never asked.
|
||||
// fetchManifest had exactly one caller, maybeSelfUpdate, which runs only when
|
||||
// the HOST directs an update (update.go), so a machine could sit months behind
|
||||
// and say nothing about it to the person sitting at it.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// freshVersionState isolates the package-level answer and the build stamp.
|
||||
func freshVersionState(t *testing.T) {
|
||||
t.Helper()
|
||||
latestMu.Lock()
|
||||
old := latestPublished
|
||||
latestPublished = ""
|
||||
latestMu.Unlock()
|
||||
oldClient, oldURL := clientVersion, manifestURL
|
||||
t.Cleanup(func() {
|
||||
latestMu.Lock()
|
||||
latestPublished = old
|
||||
latestMu.Unlock()
|
||||
clientVersion, manifestURL = oldClient, oldURL
|
||||
})
|
||||
}
|
||||
|
||||
// The trap this function exists to avoid is a string compare. "0.9.0" sorts
|
||||
// ABOVE "0.24.3" lexically, so a plain > would have told every 0.24 machine to
|
||||
// go back to a release from months earlier — and every 0.9 machine that it was
|
||||
// ahead of the world.
|
||||
func TestVersionNewerIsNotAStringCompare(t *testing.T) {
|
||||
cases := []struct {
|
||||
a, b string
|
||||
want bool
|
||||
}{
|
||||
{"0.24.4", "0.24.3", true},
|
||||
{"0.24.3", "0.24.4", false},
|
||||
{"0.24.3", "0.24.3", false},
|
||||
{"0.9.0", "0.24.3", false}, // the lexical trap, in the direction that matters
|
||||
{"0.24.3", "0.9.0", true}, // ...and in the other
|
||||
{"1.0.0", "0.99.99", true},
|
||||
{"0.24", "0.24.0", false}, // a missing component is zero, not "older"
|
||||
{"0.24.0", "0.24", false},
|
||||
{"0.24.1", "0.24", true},
|
||||
{"v0.25.0", "0.24.3", true}, // a stray v is not a different release
|
||||
// Unanswerable, in both directions: a `dev` build (run.go:32) and a
|
||||
// manifest this code cannot parse must produce NO claim rather than a
|
||||
// guessed ordering.
|
||||
{"0.24.4", "dev", false},
|
||||
{"dev", "0.24.3", false},
|
||||
{"0.24.4", "", false},
|
||||
{"", "0.24.3", false},
|
||||
{"0.24.4", "0.24.3-rc1", false},
|
||||
{"0.24.-1", "0.24.3", false},
|
||||
{"0.24.+4", "0.24.3", false},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := versionNewer(c.a, c.b); got != c.want {
|
||||
t.Errorf("versionNewer(%q, %q) = %v, want %v", c.a, c.b, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateAvailableOnlyClaimsWhatItCanStandBehind(t *testing.T) {
|
||||
freshVersionState(t)
|
||||
clientVersion = "0.24.3"
|
||||
|
||||
if got := updateAvailable(); got != "" {
|
||||
t.Errorf("nothing has been checked yet, so there is nothing to say: %q", got)
|
||||
}
|
||||
recordLatestPublished("0.24.4")
|
||||
if got := updateAvailable(); got != "0.24.4" {
|
||||
t.Errorf("a newer published version must reach the page: %q", got)
|
||||
}
|
||||
recordLatestPublished("0.24.3")
|
||||
if got := updateAvailable(); got != "" {
|
||||
t.Errorf("the version we are already on is not an update: %q", got)
|
||||
}
|
||||
recordLatestPublished("0.9.9")
|
||||
if got := updateAvailable(); got != "" {
|
||||
t.Errorf("an OLDER published version must never read as available: %q", got)
|
||||
}
|
||||
// A local build cannot be compared, so it claims nothing — rather than
|
||||
// telling every developer their machine is out of date.
|
||||
recordLatestPublished("0.24.4")
|
||||
clientVersion = "dev"
|
||||
if got := updateAvailable(); got != "" {
|
||||
t.Errorf("an unparseable running version must produce no claim: %q", got)
|
||||
}
|
||||
// An empty or blank manifest version never overwrites a real answer.
|
||||
clientVersion = "0.24.3"
|
||||
recordLatestPublished(" ")
|
||||
if got := updateAvailable(); got != "0.24.4" {
|
||||
t.Errorf("a blank manifest version must not erase what is known: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// manifestServer serves a version.json and counts every hit, including the hit
|
||||
// on the BINARY the manifest points at — which the availability check must
|
||||
// never make.
|
||||
func manifestServer(t *testing.T, version string) (url string, manifestHits, binaryHits *atomic.Int64) {
|
||||
t.Helper()
|
||||
manifestHits, binaryHits = &atomic.Int64{}, &atomic.Int64{}
|
||||
mux := http.NewServeMux()
|
||||
var srv *httptest.Server
|
||||
mux.HandleFunc("/version.json", func(w http.ResponseWriter, r *http.Request) {
|
||||
manifestHits.Add(1)
|
||||
bin := map[string]any{"url": srv.URL + "/dl", "sha256": "00"}
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"version": version,
|
||||
"binaries": map[string]any{
|
||||
"linux-amd64": bin, "linux-arm64": bin,
|
||||
"darwin-amd64": bin, "darwin-arm64": bin,
|
||||
"windows-amd64.exe": bin, "windows-arm64.exe": bin,
|
||||
},
|
||||
})
|
||||
})
|
||||
mux.HandleFunc("/dl", func(w http.ResponseWriter, r *http.Request) {
|
||||
binaryHits.Add(1)
|
||||
_, _ = w.Write([]byte("this must never be fetched by a status page"))
|
||||
})
|
||||
srv = httptest.NewServer(signedStub(releaseKindBastion, mux))
|
||||
t.Cleanup(srv.Close)
|
||||
return srv.URL + "/version.json", manifestHits, binaryHits
|
||||
}
|
||||
|
||||
// The check READS. It never downloads, never verifies, never swaps — the
|
||||
// directed path is the only thing allowed to touch the binary, and a local UI
|
||||
// that could start one would be across the line in ui.go's header.
|
||||
func TestTheAvailabilityCheckOnlyReadsTheManifest(t *testing.T) {
|
||||
freshVersionState(t)
|
||||
clientVersion = "0.24.3"
|
||||
url, manifestHits, binaryHits := manifestServer(t, "0.24.4")
|
||||
manifestURL = url
|
||||
|
||||
checkLatestPublished()
|
||||
|
||||
if got := updateAvailable(); got != "0.24.4" {
|
||||
t.Fatalf("the check did not record what was published: %q", got)
|
||||
}
|
||||
if n := manifestHits.Load(); n != 1 {
|
||||
t.Errorf("one check is one manifest fetch, got %d", n)
|
||||
}
|
||||
if n := binaryHits.Load(); n != 0 {
|
||||
t.Fatalf("the availability check downloaded the binary %d time(s) — it is a "+
|
||||
"status page, not an updater", n)
|
||||
}
|
||||
|
||||
// An unreachable manifest leaves the last known answer alone and says
|
||||
// nothing: running with no route to privatae.ai is a supported way to use
|
||||
// this, and it must not be reported as a fault.
|
||||
manifestURL = "http://127.0.0.1:1/version.json"
|
||||
checkLatestPublished()
|
||||
if got := updateAvailable(); got != "0.24.4" {
|
||||
t.Errorf("a failed check must not erase the answer: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// A host-directed update reads the same manifest, and on the mismatch path it
|
||||
// does NOT exit — so what it learned is the freshest thing the page can have.
|
||||
func TestADirectedUpdateAlsoRecordsWhatIsPublished(t *testing.T) {
|
||||
uiTestConfigDir(t)
|
||||
freshVersionState(t)
|
||||
t.Cleanup(func() { clearDirectiveRefusal("self-update") })
|
||||
clientVersion = "0.24.3"
|
||||
url, _, binaryHits := manifestServer(t, "0.24.4")
|
||||
manifestURL = url
|
||||
|
||||
// The host asks for a version the manifest does not carry: maybeSelfUpdate
|
||||
// fetches, finds the mismatch, and returns without swapping anything.
|
||||
maybeSelfUpdate("member.host.privatae.ai", "", []byte(`{"update":{"version":"9.9.9"}}`))
|
||||
|
||||
if n := binaryHits.Load(); n != 0 {
|
||||
t.Fatalf("a target the manifest does not carry must download nothing (%d hits)", n)
|
||||
}
|
||||
if got := updateAvailable(); got != "0.24.4" {
|
||||
t.Fatalf("the directed path must record what the manifest actually said: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// The cadence, as a constraint rather than a restatement of the number: this is
|
||||
// the only repeating request in the binary that leaves for privatae.ai, so it
|
||||
// must stay far slower than everything that talks to the user's own host or
|
||||
// reads local disk. The slowest of those is faReconcileEvery (10 min,
|
||||
// freeagent_managed.go:114). Breaking this is what "the banner feels slow"
|
||||
// turns into if nobody writes down why it is slow on purpose.
|
||||
func TestTheAvailabilityCheckStaysSlow(t *testing.T) {
|
||||
if updateCheckEvery < 20*faReconcileEvery {
|
||||
t.Errorf("updateCheckEvery = %v — a public poll must stay an order of magnitude "+
|
||||
"slower than the slowest local refresh (%v); update.go carries the measurement",
|
||||
updateCheckEvery, faReconcileEvery)
|
||||
}
|
||||
if updateCheckFirst < 10*time.Second || updateCheckFirst > updateCheckEvery {
|
||||
t.Errorf("updateCheckFirst = %v — it must clear daemon startup and still be the "+
|
||||
"first of the series", updateCheckFirst)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user