package selfupdate import ( "context" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" ) func TestCompareMatchesInstaller(t *testing.T) { older := []([2]string){ {"v0.1.0-69-g9c4f9df", "v0.1.0-107-gad88a48"}, {"v0.1.0", "v0.1.0-1-gabc1234"}, {"v0.1.0-107", "v0.2.0"}, {"v0.9.9-999", "v1.0.0"}, {"v0.1.9", "v0.1.10"}, } for _, p := range older { if got := Compare(p[0], p[1]); got != -1 { t.Errorf("Compare(%q, %q) = %d, want -1", p[0], p[1], got) } if got := Compare(p[1], p[0]); got != 1 { t.Errorf("Compare(%q, %q) = %d, want 1", p[1], p[0], got) } } same := []string{"v0.1.0-107-gad88a48", "v1.2.3", "dev"} for _, v := range same { if got := Compare(v, v); got != 0 { t.Errorf("Compare(%q, %q) = %d, want 0", v, v, got) } } // the hash is ignored: the same build described two ways is the same build if got := Compare("v0.1.0-107-gad88a48", "v0.1.0-107-gffffff1"); got != 0 { t.Errorf("hash should not count: %d", got) } // "dev" is not a version; anything numbered beats it if got := Compare("dev", "v0.1.0"); got != -1 { t.Errorf("Compare(dev, v0.1.0) = %d, want -1", got) } } // serve puts a release behind both shapes of source and returns the two base URLs. func serve(t *testing.T, version string, body []byte, token string) (static, api string, srv *httptest.Server) { t.Helper() sum := sha256.Sum256(body) sums := fmt.Sprintf("%s %s\n%s install.sh\n", hex.EncodeToString(sum[:]), AssetName("amd64"), strings.Repeat("0", 64)) mux := http.NewServeMux() guard := func(h http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if token != "" && r.Header.Get("Authorization") != "token "+token { w.WriteHeader(401) return } h(w, r) } } mux.HandleFunc("/dl/VERSION", guard(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, version) })) mux.HandleFunc("/dl/SHA256SUMS", guard(func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, sums) })) mux.HandleFunc("/dl/"+AssetName("amd64"), guard(func(w http.ResponseWriter, r *http.Request) { w.Write(body) })) mux.HandleFunc("/api/v1/repos/m/hamlab/releases/latest", guard(func(w http.ResponseWriter, r *http.Request) { base := "http://" + r.Host + "/dl/" _ = json.NewEncoder(w).Encode(map[string]any{ "tag_name": version, "body": "notes here", "assets": []map[string]string{ {"name": AssetName("amd64"), "browser_download_url": base + AssetName("amd64")}, {"name": "SHA256SUMS", "browser_download_url": base + "SHA256SUMS"}, }, }) })) srv = httptest.NewServer(mux) return srv.URL + "/dl", srv.URL + "/api/v1/repos/m/hamlab", srv } func TestLatestAndFetch(t *testing.T) { body := make([]byte, 2<<20) copy(body, "ELF fake hamlab") static, api, srv := serve(t, "v0.9.0-3-gdeadbee", body, "") defer srv.Close() for _, url := range []string{static, api} { s := Source{URL: url} rel, err := s.Latest(context.Background(), "amd64") if err != nil { t.Fatalf("%s: %v", url, err) } if rel.Version != "v0.9.0-3-gdeadbee" { t.Fatalf("%s: version %q", url, rel.Version) } dest := filepath.Join(t.TempDir(), "hamlab.new") if err := s.Fetch(context.Background(), rel, "amd64", dest); err != nil { t.Fatalf("%s: fetch: %v", url, err) } got, _ := os.ReadFile(dest) if len(got) != len(body) { t.Fatalf("%s: got %d bytes, want %d", url, len(got), len(body)) } } } func TestFetchRefusesBadChecksum(t *testing.T) { body := make([]byte, 2<<20) static, _, srv := serve(t, "v1.0.0", body, "") defer srv.Close() s := Source{URL: static} rel, err := s.Latest(context.Background(), "amd64") if err != nil { t.Fatal(err) } rel.Sums = static + "/VERSION" // sums that name nothing dest := filepath.Join(t.TempDir(), "hamlab.new") if err := s.Fetch(context.Background(), rel, "amd64", dest); err == nil { t.Fatal("a release whose sums do not name the binary must be refused") } if _, err := os.Stat(dest); !os.IsNotExist(err) { t.Fatal("a refused download must not be left on disk") } // and an arch nobody published if _, err := s.Latest(context.Background(), "riscv64"); err == nil { t.Log("static sources cannot know an arch is missing until the fetch; that is fine") } } func TestTokenIsSentAndMissingTokenSaysSo(t *testing.T) { body := make([]byte, 2<<20) _, api, srv := serve(t, "v1.0.0", body, "s3cret") defer srv.Close() if _, err := (Source{URL: api}).Latest(context.Background(), "amd64"); err == nil || !strings.Contains(err.Error(), "update.token") { t.Fatalf("a walled source should say a token is needed, got %v", err) } rel, err := (Source{URL: api, Token: "s3cret"}).Latest(context.Background(), "amd64") if err != nil || rel.Version != "v1.0.0" { t.Fatalf("with the token: %v %+v", err, rel) } dest := filepath.Join(t.TempDir(), "hamlab.new") if err := (Source{URL: api, Token: "s3cret"}).Fetch(context.Background(), rel, "amd64", dest); err != nil { t.Fatalf("fetch with token: %v", err) } }