Files
Reef/selfupdate/selfupdate_test.go
2026-10-26 17:56:55 -06:00

198 lines
7.0 KiB
Go

package selfupdate
import (
"archive/tar"
"bytes"
"compress/gzip"
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
)
func makeArchive(t *testing.T, pkg, app, content string) []byte {
t.Helper()
var buf bytes.Buffer
gz := gzip.NewWriter(&buf)
tw := tar.NewWriter(gz)
for name, body := range map[string]string{pkg + "/README.md": "hi", pkg + "/" + app: content} {
if err := tw.WriteHeader(&tar.Header{Name: name, Mode: 0o755, Size: int64(len(body)), Typeflag: tar.TypeReg}); err != nil {
t.Fatal(err)
}
tw.Write([]byte(body))
}
tw.Close()
gz.Close()
return buf.Bytes()
}
// fakeGitea serves one release with the given tag.
func fakeGitea(t *testing.T, tag string, prerelease bool, corrupt bool) *httptest.Server {
archive := fmt.Sprintf("demo_%s_linux_amd64.tar.gz", tag)
data := makeArchive(t, strings.TrimSuffix(archive, ".tar.gz"), "demo", "NEW-BINARY "+tag)
sum := sha256.Sum256(data)
sums := hex.EncodeToString(sum[:]) + " " + archive + "\n"
if corrupt {
sums = strings.Repeat("0", 64) + " " + archive + "\n"
}
var srv *httptest.Server
mux := http.NewServeMux()
mux.HandleFunc("/api/v1/repos/Veillax/demo/releases/latest", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, `{"tag_name":%q,"prerelease":%v,"assets":[{"name":%q,"browser_download_url":"%s/dl/%s"},{"name":"SHA256SUMS","browser_download_url":"%s/dl/SHA256SUMS"}]}`,
tag, prerelease, archive, srv.URL, archive, srv.URL)
})
mux.HandleFunc("/dl/SHA256SUMS", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, sums) })
mux.HandleFunc("/dl/"+archive, func(w http.ResponseWriter, r *http.Request) { w.Write(data) })
srv = httptest.NewServer(mux)
t.Cleanup(srv.Close)
return srv
}
func setup(t *testing.T, srv *httptest.Server, current string) (Config, string, *string) {
exe := filepath.Join(t.TempDir(), "demo")
os.WriteFile(exe, []byte("OLD-BINARY"), 0o755)
restarted := new(string)
return Config{Server: srv.URL, Repo: "Veillax/demo", App: "demo", Current: current, GOOS: "linux", GOARCH: "amd64", ExePath: exe,
Verify: func(p, tag string) error { return nil },
Restart: func(e string) error { *restarted = e; return nil }}, exe, restarted
}
func TestInstallsANewerRelease(t *testing.T) {
cfg, exe, restarted := setup(t, fakeGitea(t, "v0.2.0", false, false), "v0.1.5")
tag, err := CheckOnce(context.Background(), cfg)
if err != nil || tag != "v0.2.0" {
t.Fatalf("tag=%q err=%v", tag, err)
}
if b, _ := os.ReadFile(exe); string(b) != "NEW-BINARY v0.2.0" {
t.Fatalf("binary is %q", b)
}
if *restarted != exe {
t.Fatalf("restart called with %q", *restarted)
}
if _, err := os.Stat(filepath.Join(filepath.Dir(exe), ".demo.new")); err == nil {
t.Fatal("temporary file left behind")
}
}
func TestLeavesThingsAloneWhenItShould(t *testing.T) {
for _, c := range []struct {
name, current, tag string
pre bool
}{
{"same version", "v0.2.0", "v0.2.0", false},
{"never downgrades", "v0.3.0", "v0.2.0", false},
{"pre-release is skipped", "v0.1.0", "v0.2.0", true},
{"dev build", "dev", "v0.2.0", false},
{"local build", "v0.2.0-local", "v0.9.0", false},
} {
cfg, exe, restarted := setup(t, fakeGitea(t, c.tag, c.pre, false), c.current)
tag, _ := CheckOnce(context.Background(), cfg)
if b, _ := os.ReadFile(exe); tag != "" || string(b) != "OLD-BINARY" || *restarted != "" {
t.Errorf("%s: changed things (tag=%q binary=%q)", c.name, tag, b)
}
}
}
func TestRefusesABadChecksum(t *testing.T) {
cfg, exe, restarted := setup(t, fakeGitea(t, "v0.2.0", false, true), "v0.1.0")
if _, err := CheckOnce(context.Background(), cfg); err == nil || !strings.Contains(err.Error(), "checksum") {
t.Fatalf("err=%v", err)
}
if b, _ := os.ReadFile(exe); string(b) != "OLD-BINARY" || *restarted != "" {
t.Fatal("installed a binary that failed its checksum")
}
}
func TestKeepsTheOldBinaryWhenTheNewOneFailsItsCheck(t *testing.T) {
cfg, exe, restarted := setup(t, fakeGitea(t, "v0.2.0", false, false), "v0.1.0")
cfg.Verify = func(p, tag string) error { return fmt.Errorf("segfault") }
if _, err := CheckOnce(context.Background(), cfg); err == nil {
t.Fatal("expected an error")
}
if b, _ := os.ReadFile(exe); string(b) != "OLD-BINARY" || *restarted != "" {
t.Fatal("replaced the binary despite a failed check")
}
if _, err := os.Stat(filepath.Join(filepath.Dir(exe), ".demo.new")); err == nil {
t.Fatal("temporary file left behind")
}
}
func TestVersionOrdering(t *testing.T) {
for _, c := range []struct {
a, b string
want bool
}{{"v0.1.10", "v0.1.9", true}, {"v1.0.0", "v0.99.99", true}, {"v0.1.2", "v0.1.2", false}, {"v0.1.1", "v0.2.0", false}} {
a, _ := parseVersion(c.a)
b, _ := parseVersion(c.b)
if a.newerThan(b) != c.want {
t.Errorf("%s newer than %s: want %v", c.a, c.b, c.want)
}
}
}
func TestCheckReportsWithoutInstalling(t *testing.T) {
srv := fakeGitea(t, "v0.2.0", false, false)
for _, c := range []struct {
current string
newer bool
}{{"v0.1.0", true}, {"v0.2.0", false}, {"v0.3.0", false}, {"dev", false}, {"v0.1.0-local", false}} {
cfg, exe, restarted := setup(t, srv, c.current)
tag, newer, err := Check(context.Background(), cfg)
if err != nil || tag != "v0.2.0" || newer != c.newer {
t.Errorf("%s: tag=%q newer=%v err=%v", c.current, tag, newer, err)
}
if b, _ := os.ReadFile(exe); string(b) != "OLD-BINARY" || *restarted != "" {
t.Errorf("%s: Check changed things", c.current)
}
}
}
func TestReportsProgressThroughTheWholeUpdate(t *testing.T) {
cfg, _, _ := setup(t, fakeGitea(t, "v0.2.0", false, false), "v0.1.0")
var events []Event
cfg.Progress = func(e Event) { events = append(events, e) }
if _, err := CheckOnce(context.Background(), cfg); err != nil {
t.Fatal(err)
}
var stages []string
last, lines := 0, ""
for _, e := range events {
if len(stages) == 0 || stages[len(stages)-1] != e.Stage {
stages = append(stages, e.Stage)
}
if e.Pct >= 0 {
if e.Pct < last {
t.Errorf("progress went backwards: %d after %d (%+v)", e.Pct, last, e)
}
last = e.Pct
}
lines += e.Line + "\n"
}
if got := strings.Join(stages, ","); got != "checking,downloading,verifying,installing,restarting" {
t.Errorf("stages: %s", got)
}
if last != 100 || !strings.Contains(lines, "v0.2.0 is available") || !strings.Contains(lines, "checksum matches") {
t.Errorf("progress ended at %d with log:\n%s", last, lines)
}
}
func TestProgressReportsFailureAndNothingToDo(t *testing.T) {
cfg, _, _ := setup(t, fakeGitea(t, "v0.2.0", false, true), "v0.1.0")
var last Event
cfg.Progress = func(e Event) { last = e }
if _, err := CheckOnce(context.Background(), cfg); err == nil || last.Stage != "failed" || !strings.Contains(last.Line, "checksum") {
t.Errorf("err=%v last=%+v", err, last)
}
cfg, _, _ = setup(t, fakeGitea(t, "v0.2.0", false, false), "v0.2.0")
cfg.Progress = func(e Event) { last = e }
if tag, err := CheckOnce(context.Background(), cfg); err != nil || tag != "" || last.Stage != "done" || last.Pct != 100 {
t.Errorf("tag=%q err=%v last=%+v", tag, err, last)
}
}