CI / test (push) Successful in 1m56s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
138 lines
4.8 KiB
Go
138 lines
4.8 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)
|
|
}
|
|
}
|
|
}
|