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) } }