Files
VeillaxandClaude Sonnet 5.5 73f45fb3ae Add self-update, including in Docker
Sandbar can install new releases itself using Reef's selfupdate, automatically with
SANDBAR_AUTO_UPDATE=1 or from a button on the Account page. In a container the new binary is
written to <data>/bin and takes over from the image at start, until the image catches up.
Tags now publish a Gitea release with archives and SHA256SUMS as well as the image.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 04:22:54 -06:00

110 lines
3.2 KiB
Go

package sandbar
import (
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
)
func TestVersionNewer(t *testing.T) {
for _, c := range []struct {
a, b string
want bool
}{
{"v0.1.4", "v0.1.3", true},
{"v0.2.0", "v0.1.9", true},
{"v1.0.0", "v0.9.9", true},
{"v0.1.3", "v0.1.3", false},
{"v0.1.2", "v0.1.3", false},
{"v0.1.10", "v0.1.9", true},
{"v0.2.0-rc1", "v0.1.0", false}, // pre-releases are never offered
{"v0.2.0", "dev", false}, // a development build never updates
{"dev", "v0.1.0", false},
} {
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)
}
}
}
// The update page asks the release server and reports whether it has something newer.
func TestUpdateStatus(t *testing.T) {
release := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/v1/repos/Veillax/sandbar/releases/latest" {
http.NotFound(w, r)
return
}
w.Write([]byte(`{"tag_name":"v0.2.0","assets":[]}`))
}))
defer release.Close()
s, _, _ := newTestServer(t)
s.opts.Version, s.opts.UpdateServer = "v0.1.0", release.URL
ts := httptest.NewServer(s.Handler())
defer ts.Close()
c := newAPIClient(t, ts)
if code := c.do("GET", "/api/v1/system/update", nil, nil); code != 401 {
t.Fatalf("signed out: %d", code)
}
c.setup()
var st map[string]any
if code := c.do("GET", "/api/v1/system/update", nil, &st); code != 200 {
t.Fatalf("status: %d", code)
}
if st["current"] != "v0.1.0" || st["latest"] != "v0.2.0" || st["newer"] != true {
t.Fatalf("status: %v", st)
}
if st["error"] != "" {
t.Fatalf("unexpected error: %v", st["error"])
}
// A development build can look but not install.
s.opts.Version = "dev"
s.upd.checked = s.upd.checked.AddDate(-1, 0, 0)
if code := c.do("POST", "/api/v1/system/update", map[string]string{}, &st); code != 400 {
t.Fatalf("a development build must not install: %d %v", code, st)
}
}
// A newer binary on the data volume takes over from the image, and a stale one is removed.
func TestOverlay(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("self-update is not supported on Windows")
}
dir := t.TempDir()
bin := filepath.Join(dir, "bin")
os.MkdirAll(bin, 0o755)
overlay := filepath.Join(bin, "sandbar")
script := func(version string) {
os.WriteFile(overlay, []byte("#!/bin/sh\necho 'sandbar "+version+"'\n"), 0o755)
}
var logged []string
logf := func(f string, a ...any) { logged = append(logged, f) }
// An older overlay is deleted once the image is a release that has caught up.
script("v0.1.0")
UseNewerOverlay(dir, "v0.1.5", logf)
if _, err := os.Stat(overlay); err == nil {
t.Fatal("a stale overlay should be removed")
}
// A development image never deletes or switches to anything.
script("v0.9.0")
UseNewerOverlay(dir, "dev", logf)
if _, err := os.Stat(overlay); err != nil {
t.Fatal("a development build must leave the overlay alone")
}
// Something that does not report a version is ignored, not run.
os.WriteFile(overlay, []byte("#!/bin/sh\necho hello\n"), 0o755)
UseNewerOverlay(dir, "v0.1.5", logf)
if len(logged) == 0 || !strings.Contains(logged[len(logged)-1], "did not report a version") {
t.Fatalf("log: %v", logged)
}
}