// ================================================================================== tests for gbld (mwx'2026) package main import ( "context" "crypto/sha256" "encoding/hex" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" "time" ) func inTempDir(t *testing.T) { // ------------------------------------- run a test in an empty working directory t.Helper() old, err := os.Getwd() if err != nil { t.Fatal(err) } dir := t.TempDir() if err := os.Chdir(dir); err != nil { t.Fatal(err) } t.Cleanup(func() { os.Chdir(old) }) } func write(t *testing.T, name, content string) { // ------------------------------------------ write a test file t.Helper() if err := os.WriteFile(name, []byte(content), 0644); err != nil { t.Fatal(err) } } func TestGettargetversion(t *testing.T) { // ------------------------------------------- find 'var version = ..' inTempDir(t) if _, err := gettargetversion(); err == nil { t.Error("expected an error when no version is defined") } write(t, "other.go", "package main\n\n"+`var tbversion = "0.1.0"`+"\n") if _, err := gettargetversion(); err == nil { t.Error("tbversion must not be taken for the target version") } write(t, "app.go", "package main\n\n"+`var version = "2.3.0" // release`+"\n") v, err := gettargetversion() if err != nil { t.Fatal(err) } if v != "2.3.0" { t.Errorf("got %q, want 2.3.0 (a trailing comment must not break the match)", v) } } func TestBumpbuild(t *testing.T) { // ------------------------------------- counter increases, comment survives inTempDir(t) write(t, "build.go", "package main\n\n"+`var build = "41" // counter`+"\n") bumpbuild() c, err := os.ReadFile("build.go") if err != nil { t.Fatal(err) } want := "package main\n\n" + `var build = "42" // counter` + "\n" if string(c) != want { t.Errorf("got %q, want %q", string(c), want) } files, _ := filepath.Glob("tmp/build.go.*") // the backup is what protects the users file if len(files) != 1 { t.Errorf("expected exactly one backup in tmp/, got %v", files) } } func TestBackupPruning(t *testing.T) { // ---------------------------------- tmp/ must not grow without bounds inTempDir(t) write(t, "build.go", "package main\n\n"+`var build = "1"`+"\n") for i := 0; i < KEEPBACKUPS+5; i++ { // same second: force distinct names to test the pruning itself if err := backup("build.go"); err != nil { t.Fatal(err) } write(t, SF("tmp/build.go.2026010112000%02d", i), "old backup") prunebackups("build.go", KEEPBACKUPS) } files, _ := filepath.Glob("tmp/build.go.*") if len(files) > KEEPBACKUPS { t.Errorf("got %d backups, want at most %d", len(files), KEEPBACKUPS) } } func TestBumpbuildKeepsLongLines(t *testing.T) { // -------------- a >64k line must not truncate the file anymore inTempDir(t) long := strings.Repeat("x", 100*1024) write(t, "build.go", "package main\n\n"+`var build = "1"`+"\n\nvar long = \""+long+"\"\n") bumpbuild() c, err := os.ReadFile("build.go") if err != nil { t.Fatal(err) } if !strings.Contains(string(c), `var build = "2"`) { t.Error("counter was not increased") } if !strings.Contains(string(c), long) { t.Error("long line was lost - the file got truncated") } } func TestArtifact(t *testing.T) { // --------------------------------------------- windows artifacts need '.exe' if got := artifact("gbld", "1.2.3", "linux", "amd64"); got != "gbld_1.2.3_linux_amd64" { t.Errorf("got %q", got) } if got := artifact("gbld", "1.2.3", "windows", "amd64"); got != "gbld_1.2.3_windows_amd64.exe" { t.Errorf("got %q", got) } } func TestFileExists(t *testing.T) { // --------------------------------- must not panic on unreadable entries inTempDir(t) write(t, "there.go", "package main\n") if !fileExists("there.go") { t.Error("existing file not found") } if fileExists("missing.go") { t.Error("missing file reported as existing") } os.Mkdir("adir", 0755) if fileExists("adir") { t.Error("a directory is not a file") } os.Mkdir("locked", 0000) // no permission: os.Stat fails with something other than IsNotExist defer os.Chmod("locked", 0755) if fileExists("locked/x.go") { t.Error("unreadable path reported as existing") } } func TestUpdateFindSum(t *testing.T) { // ----------------------------------- checksums are read and validated good := strings.Repeat("ab", 32) // 32 bytes, as a sha256 has body := []byte(good + " gbld-linux-amd64\n" + good + " *gbld-windows-amd64.exe\n" + // binary mode marks the name with '*' "zzzz broken_sum\n") sum, err := updateFindSum(body, "gbld-linux-amd64") if err != nil { t.Fatal(err) } if hex.EncodeToString(sum) != good { t.Errorf("wrong checksum decoded: %x", sum) } if _, err := updateFindSum(body, "gbld-windows-amd64.exe"); err != nil { t.Errorf("the '*' of binary mode belongs to the format, not to the name: %v", err) } if _, err := updateFindSum(body, "gbld-darwin-arm64"); err == nil { t.Error("a missing entry must be an error - never update without a checksum") } if _, err := updateFindSum(body, "broken_sum"); err == nil { t.Error("a malformed checksum must be an error") } if _, err := updateFindSum([]byte(strings.Repeat("ab", 16)+" gbld-linux-amd64\n"), "gbld-linux-amd64"); err == nil { t.Error("a hash of the wrong length must be an error") } } func TestUpdateChecksum(t *testing.T) { // ------------------------- the checksum comes out of the release itself want := strings.Repeat("cd", 32) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(want + " gbld-linux-amd64\n")) })) defer srv.Close() rel := updateRelease{TagName: "v1.2.3", Assets: []updateAsset{ {Name: "gbld-linux-amd64", URL: srv.URL + "/gbld-linux-amd64"}, {Name: updateChecksums, URL: srv.URL + "/" + updateChecksums}, }} sum, err := selfUpdate.checksum(rel, "gbld-linux-amd64") if err != nil { t.Fatal(err) } if hex.EncodeToString(sum) != want { t.Errorf("wrong checksum: %x", sum) } bare := updateRelease{TagName: "v1.2.3", Assets: rel.Assets[:1]} // no checksums.txt published if _, err := selfUpdate.checksum(bare, "gbld-linux-amd64"); err == nil { t.Error("a release without checksums must be refused") } } func TestUpdateDownloadRejectsWrongChecksum(t *testing.T) { // ------ a manipulated download is never installed body := []byte("this is not the binary you asked for") srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write(body) })) defer srv.Close() dir := t.TempDir() exe := filepath.Join(dir, "gbld") a := &updateAsset{Name: "gbld-linux-amd64", Size: int64(len(body)), URL: srv.URL + "/gbld-linux-amd64"} _, err := selfUpdate.download(a, exe, 0755, []byte{0xde, 0xad, 0xbe, 0xef}) if err == nil { t.Fatal("a download with a wrong checksum was accepted") } if !strings.Contains(strings.ToLower(err.Error()), "checksum") { t.Errorf("expected a checksum error, got: %v", err) } left, _ := filepath.Glob(filepath.Join(dir, "*")) if len(left) != 0 { t.Errorf("the rejected download was left behind: %v", left) } sum := sha256.Sum256(body) // the same bytes, now with the checksum that fits tmp, err := selfUpdate.download(a, exe, 0755, sum[:]) if err != nil { t.Fatalf("a matching checksum was refused: %v", err) } os.Remove(tmp) } func TestUpdateGetStatus(t *testing.T) { // ---------------------------------- http errors must not be swallowed srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Error(w, "nope", http.StatusInternalServerError) })) defer srv.Close() ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if _, err := updateGet(ctx, srv.URL); err == nil { t.Error("http 500 was reported as success") } } func TestUpdateCompare(t *testing.T) { // ------------------- versions are numbers, not strings: 1.21.10 > 1.21.9 for _, c := range []struct { a, b string want int }{ {"1.21.10", "1.21.9", 1}, {"v1.21.0", "1.21.0", 0}, {"1.21", "1.21.0", 0}, {"1.21.0-rc1", "1.21.0", -1}, // not finished yet {"1.9.0", "1.10.0", -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) } if got := updateCompare(c.b, c.a); got != -c.want { t.Errorf("updateCompare(%q,%q) = %d, want %d", c.b, c.a, got, -c.want) } } } func TestToolbox(t *testing.T) { // ------------------------------------------------ toolbox edge cases if got := Shortstr("abcdef", 4); got != "ab.." { t.Errorf("Shortstr: got %q", got) } if got := Shortstr("abcdef", 1); got != ".." { // must not panic t.Errorf("Shortstr with a tiny length: got %q", got) } if got := Dec("!!!not valid!!!"); got != "" { // must not panic t.Errorf("Dec of garbage: got %q", got) } if got := Dec(Enc("secret")); got != "secret" { t.Errorf("Enc/Dec roundtrip: got %q", got) } in := []string{"a", "b", "a", "c"} out := RemoveAllMatches(in, "a") if strings.Join(out, "") != "bc" { t.Errorf("RemoveAllMatches: got %v", out) } if strings.Join(in, "") != "abac" { t.Errorf("RemoveAllMatches modified the callers slice: %v", in) } if !Checkip("10.0.0.0/8", "10.1.2.3") || Checkip("10.0.0.0/8", "192.168.0.1") { t.Error("Checkip cidr") } if Checkip("10.0.0.0/8", "not an ip") { t.Error("Checkip accepted an invalid address") } } // ========================================================================================================= END