Files
BrowserOS/packages/browseros-agent/apps/cli/update/manager_test.go
Nikhil ace9307878 feat: add browseros-cli self-updater (#605)
* feat: add browseros-cli self-updater

* fix: address review comments for 0327-cli_self_updater

* fix: address PR review comments for 0327-cli_self_updater

* fix: replace goreleaser with Makefile-based release build

Remove .goreleaser.yml (required Pro license for monorepo field) and
consolidate cross-compilation into `make release`. CI now uses the same
Makefile target, fixing a bug where POSTHOG_API_KEY was missing from
release ldflags.

* fix: address critical self-updater bugs from code review

- Fix SHA256 checksum mismatch: verify archive checksum before extraction
  instead of verifying extracted binary against archive hash (was always
  failing). Add VerifyChecksum() and integration test.
- Fix JSON field name mismatch: TypeScript was emitting camelCase
  (publishedAt, archiveFormat) but Go expected snake_case
  (published_at, archive_format). Manifest parsing was silently broken.
- Add decompression size limit (256 MB) to prevent zip/gzip bombs.
- Don't update LastCheckedAt on transient errors so retry happens on
  next CLI invocation instead of waiting 24h.
2026-03-27 14:52:54 -07:00

163 lines
3.9 KiB
Go

package update
import (
"context"
"net/http"
"net/http/httptest"
"runtime"
"testing"
"time"
)
func TestManagerCachedNotice(t *testing.T) {
manager := NewManager(Options{
CurrentVersion: "1.0.0",
Automatic: true,
})
manager.state = &State{LatestVersion: "1.2.0"}
notice := manager.CachedNotice()
if notice == "" {
t.Fatal("CachedNotice() returned empty notice")
}
}
func TestManagerShouldCheck(t *testing.T) {
manager := NewManager(Options{
CurrentVersion: "1.0.0",
Automatic: true,
CheckTTL: time.Minute,
Now: func() time.Time {
return time.Unix(1000, 0).UTC()
},
})
manager.state = &State{LastCheckedAt: time.Unix(0, 0).UTC()}
if !manager.ShouldCheck() {
t.Fatal("ShouldCheck() = false, want true")
}
}
func TestManagerCheckNow(t *testing.T) {
configRoot := t.TempDir()
t.Setenv("XDG_CONFIG_HOME", configRoot)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{
"version":"9.9.9",
"published_at":"2026-03-27T19:00:00Z",
"tag":"browseros-cli-v9.9.9",
"assets":{
"` + runtimePlatformKey(t) + `":{
"filename":"browseros-cli_9.9.9_test.tar.gz",
"url":"https://cdn.example.com/cli/v9.9.9/browseros-cli_9.9.9_test.tar.gz",
"archive_format":"tar.gz",
"sha256":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
}
}
}`))
}))
defer server.Close()
manager := NewManager(Options{
CurrentVersion: "1.0.0",
ManifestURL: server.URL,
Automatic: false,
HTTPClient: server.Client(),
Now: func() time.Time {
return time.Unix(100, 0).UTC()
},
})
result, err := manager.CheckNow(context.Background())
if err != nil {
t.Fatalf("CheckNow() error = %v", err)
}
if !result.UpdateAvailable {
t.Fatal("CheckNow() UpdateAvailable = false, want true")
}
if result.LatestPublishedAt != "2026-03-27T19:00:00Z" {
t.Fatalf(
"CheckNow() LatestPublishedAt = %q, want %q",
result.LatestPublishedAt,
"2026-03-27T19:00:00Z",
)
}
if manager.state.LatestPublishedAt != "2026-03-27T19:00:00Z" {
t.Fatalf(
"state LatestPublishedAt = %q, want %q",
manager.state.LatestPublishedAt,
"2026-03-27T19:00:00Z",
)
}
}
func TestCloneHTTPClientClearsTimeout(t *testing.T) {
base := &http.Client{Timeout: time.Second}
cloned := cloneHTTPClient(base)
if cloned == base {
t.Fatal("cloneHTTPClient() returned the original client")
}
if cloned.Timeout != 0 {
t.Fatalf("cloneHTTPClient() Timeout = %s, want 0", cloned.Timeout)
}
if base.Timeout != time.Second {
t.Fatalf("base Timeout = %s, want %s", base.Timeout, time.Second)
}
}
func TestManagerSaveAppliedState(t *testing.T) {
configRoot := t.TempDir()
t.Setenv("XDG_CONFIG_HOME", configRoot)
now := time.Unix(200, 0).UTC()
manager := NewManager(Options{
CurrentVersion: "1.0.0",
Now: func() time.Time {
return now
},
})
manager.state = &State{
LastCheckedAt: time.Unix(100, 0).UTC(),
CheckError: "manifest fetch failed",
}
manager.saveAppliedState(&CheckResult{
LatestVersion: "9.9.9",
LatestPublishedAt: "2026-03-27T19:00:00Z",
Asset: &Asset{
URL: "https://cdn.example.com/cli/v9.9.9/browseros-cli_9.9.9_test.tar.gz",
},
})
if manager.state.LastCheckedAt != now {
t.Fatalf("LastCheckedAt = %v, want %v", manager.state.LastCheckedAt, now)
}
if manager.state.CheckError != "" {
t.Fatalf("CheckError = %q, want empty", manager.state.CheckError)
}
if manager.state.LatestPublishedAt != "2026-03-27T19:00:00Z" {
t.Fatalf("LatestPublishedAt = %q", manager.state.LatestPublishedAt)
}
}
func runtimePlatformKey(t *testing.T) string {
t.Helper()
key, err := PlatformKey(runtimeGOOS(), runtimeGOARCH())
if err != nil {
t.Fatalf("PlatformKey() error = %v", err)
}
return key
}
func runtimeGOOS() string {
return runtime.GOOS
}
func runtimeGOARCH() string {
return runtime.GOARCH
}