Files
mgsh/selfupdate_test.go
T
2026-08-11 15:07:21 +02:00

191 lines
6.7 KiB
Go

package main
// selfupdate_test.go — the parts of selfupdate.go that can be checked without
// replacing the running binary: the version arithmetic, the URL it derives, and
// what it makes of a Gitea release.
//
// selfupdate.go itself is a file copied between programs and stays as it is;
// the tests live here so the copy keeps working when it lands in the next one.
import (
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
)
// cacheInTemp points os.UserCacheDir at a temporary directory, so the note
// selfupdate writes never touches the real one.
func cacheInTemp(t *testing.T) {
t.Helper()
dir := t.TempDir()
t.Setenv("HOME", dir) // darwin: ~/Library/Caches
t.Setenv("XDG_CACHE_HOME", filepath.Join(dir, "xdg")) // linux
}
// fakeGitea serves one /releases/latest answer, the way Gitea does.
func fakeGitea(t *testing.T, body string) *httptest.Server {
t.Helper()
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/v1/repos/mike/mgsh/releases/latest" {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(body))
}))
t.Cleanup(srv.Close)
return srv
}
func testUpdater(srv *httptest.Server, current string) selfUpdater {
return selfUpdater{
repo: srv.URL + "/mike/mgsh",
asset: "mgsh",
current: current,
every: 0, // no background run from a test
quietEnv: "MGSH_NO_UPDATE_CHECK",
}
}
// TestUpdateCompare is the one piece of arithmetic in the file, and the reason
// it exists: a plain string comparison puts 4.0.9 after 4.0.10 and would offer
// an update backwards forever.
func TestUpdateCompare(t *testing.T) {
for _, c := range []struct {
a, b string
want int
}{
{"4.0.10", "4.0.9", 1}, // numerically, not alphabetically
{"4.0.9", "4.0.10", -1},
{"4.0.64", "4.0.64", 0},
{"v4.1.0", "4.0.64", 1}, // a leading v does not count
{"4.1", "4.1.0", 0}, // missing places are zeroes
{"4.2", "4.1.9", 1},
{"4.0.64-rc1", "4.0.64", -1}, // a suffix is not yet the release
{"4.0.64", "4.0.64-rc1", 1},
{"4.0.64-rc2", "4.0.64-rc1", 1},
{"5.0.0", "4.99.99", 1},
} {
if got := updateCompare(c.a, c.b); got != c.want {
t.Errorf("updateCompare(%q, %q) = %d, want %d", c.a, c.b, got, c.want)
}
}
}
// TestUpdateAPIBase: the browser URL of the repo is all the file is configured
// with, so everything depends on it becoming the right API root.
func TestUpdateAPIBase(t *testing.T) {
for _, c := range []struct{ repo, want string }{
{"https://git.micw.org/mike/mgsh", "https://git.micw.org/api/v1/repos/mike/mgsh"},
{"https://git.micw.org/mike/mgsh/", "https://git.micw.org/api/v1/repos/mike/mgsh"},
{"https://git.micw.org/mike/mgsh.git", "https://git.micw.org/api/v1/repos/mike/mgsh"},
{"http://localhost:3000/mike/mgsh", "http://localhost:3000/api/v1/repos/mike/mgsh"},
} {
got, err := selfUpdater{repo: c.repo}.apiBase()
if err != nil || got != c.want {
t.Errorf("apiBase(%q) = %q, %v, want %q", c.repo, got, err, c.want)
}
}
// anything that is not host/owner/repo has to say so rather than build a
// URL that 404s later
for _, bad := range []string{"", "git.micw.org/mike/mgsh", "https://git.micw.org/mike", "https://git.micw.org/a/b/c"} {
if _, err := (selfUpdater{repo: bad}).apiBase(); err == nil {
t.Errorf("apiBase(%q) accepted a malformed repo URL", bad)
}
}
}
// TestSelfUpdateCheck covers both answers of `update -c`, and that a successful
// look leaves the note behind that the daily hint later reads.
func TestSelfUpdateCheck(t *testing.T) {
cacheInTemp(t)
srv := fakeGitea(t, `{"tag_name":"4.1.0","html_url":"https://git.micw.org/mike/mgsh/releases/tag/4.1.0",
"assets":[{"name":"mgsh-darwin-arm64","size":9,"browser_download_url":"x"}]}`)
var out strings.Builder
if err := testUpdater(srv, "4.0.64").check(&out); err != nil {
t.Fatalf("check: %v", err)
}
if !strings.Contains(out.String(), "4.1.0 is available") {
t.Errorf("check output = %q, want the new version offered", out.String())
}
out.Reset()
if err := testUpdater(srv, "4.1.0").check(&out); err != nil {
t.Fatalf("check: %v", err)
}
if !strings.Contains(out.String(), "up to date") {
t.Errorf("check output = %q, want 'up to date'", out.String())
}
// the same version must not be offered as an update to itself
out.Reset()
if err := testUpdater(srv, "4.2.0").check(&out); err != nil {
t.Fatalf("check: %v", err)
}
if strings.Contains(out.String(), "available") {
t.Errorf("a newer running version was offered an update: %q", out.String())
}
cache, err := os.UserCacheDir()
if err != nil {
t.Fatal(err)
}
note := filepath.Join(cache, "mgsh", "update.json")
b, err := os.ReadFile(note)
if err != nil {
t.Fatalf("no note written to %s: %v", note, err)
}
if !strings.Contains(string(b), `"latest":"4.1.0"`) {
t.Errorf("note = %s, want the looked-up version in it", b)
}
}
// TestSelfUpdateNoReleases: an empty repository must produce a plain message,
// not a nil release that gets compared against the running version.
func TestSelfUpdateNoReleases(t *testing.T) {
cacheInTemp(t)
srv := fakeGitea(t, `{}`)
err := testUpdater(srv, "4.0.64").check(&strings.Builder{})
if err == nil || !strings.Contains(err.Error(), "no releases") {
t.Errorf("check against a release-less repo = %v, want a 'no releases' error", err)
}
}
// TestSelfUpdateWithoutMatchingAsset: a release built for other platforms must
// not be installed, and the message has to name what was looked for — that is
// what tells you the release is incomplete rather than the machine unsupported.
func TestSelfUpdateWithoutMatchingAsset(t *testing.T) {
cacheInTemp(t)
srv := fakeGitea(t, `{"tag_name":"4.1.0","assets":[{"name":"mgsh-plan9-mips","size":1,"browser_download_url":"x"}]}`)
err := testUpdater(srv, "4.0.64").install(&strings.Builder{})
if err == nil {
t.Fatal("install accepted a release without a binary for this platform")
}
if !strings.Contains(err.Error(), "mgsh-") || !strings.Contains(err.Error(), "mgsh-plan9-mips") {
t.Errorf("error = %v, want the wanted and the available asset names", err)
}
}
// TestUpdateDailyStaysQuiet: the automatic look is for the person sitting
// there. Under MGSH_NO_UPDATE_CHECK, and with `every` at zero, it says nothing
// and starts nothing.
func TestUpdateDailyStaysQuiet(t *testing.T) {
cacheInTemp(t)
u := selfUpdater{asset: "mgsh", current: "4.0.0", every: 0, quietEnv: "MGSH_NO_UPDATE_CHECK"}
if hint := u.daily(); hint != "" {
t.Errorf("daily with every=0 = %q, want silence", hint)
}
t.Setenv("MGSH_NO_UPDATE_CHECK", "1")
u.every = 1
if hint := u.daily(); hint != "" {
t.Errorf("daily under MGSH_NO_UPDATE_CHECK = %q, want silence", hint)
}
}