From f2a417ebc40c518be17bfc797d8022859a2bec50 Mon Sep 17 00:00:00 2001 From: everdrone Date: Mon, 22 Aug 2022 22:39:37 +0200 Subject: [PATCH 1/4] feat: checks for updates using github api --- cmd/version.go | 9 ++- cmd/version_test.go | 76 ++++++++++++++++-------- go.mod | 1 + go.sum | 2 + internal/config/version.go | 2 + internal/update/check.go | 52 +++++++++++++++++ internal/update/check_test.go | 107 ++++++++++++++++++++++++++++++++++ 7 files changed, 225 insertions(+), 24 deletions(-) create mode 100644 internal/update/check.go create mode 100644 internal/update/check_test.go diff --git a/cmd/version.go b/cmd/version.go index a67e722..a950551 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -2,13 +2,14 @@ package cmd import ( "github.com/everdrone/grab/internal/config" + "github.com/everdrone/grab/internal/update" "github.com/spf13/cobra" ) var VersionCmd = &cobra.Command{ Use: "version", - Short: "Print the version number and exit", + Short: "Print the version number and check for updates", Run: func(cmd *cobra.Command, args []string) { cmd.Printf("%s v%s %s/%s (%s)\n", cmd.Root().Name(), @@ -16,6 +17,12 @@ var VersionCmd = &cobra.Command{ config.BuildOS, config.BuildArch, config.CommitHash[:7]) + + newVersion, _ := update.CheckForUpdates() + if newVersion != "" { + cmd.Printf("\nNew version available: %s\n", newVersion) + cmd.Printf("Download it at https://github.com/everdrone/grab/releases/latest\n") + } }, } diff --git a/cmd/version_test.go b/cmd/version_test.go index e290860..b153d2e 100644 --- a/cmd/version_test.go +++ b/cmd/version_test.go @@ -1,7 +1,9 @@ package cmd import ( - "fmt" + "net/http" + "net/http/httptest" + "runtime" "testing" "github.com/everdrone/grab/internal/config" @@ -9,26 +11,54 @@ import ( ) func TestVersionCmd(t *testing.T) { - t.Run("version", func(t *testing.T) { - cmdName := "version" - - c, got, err := tu.ExecuteCommand(RootCmd, cmdName) - if err != nil { - t.Fatal(err) - } - - want := fmt.Sprintf("%s v%s %s/%s (%s)\n", - "grab", - config.Version, - "unknown", - "unknown", - "unknown", - ) - if c.Name() != cmdName { - t.Fatalf("got: '%s', want: '%s'", c.Name(), cmdName) - } - if got != want { - t.Errorf("got: %s, want: %s", got, want) - } - }) + cmdName := "version" + + config.CommitHash = "abcdef0123456789" + config.BuildOS = runtime.GOOS + config.BuildArch = runtime.GOARCH + + tests := []struct { + name string + handler func(w http.ResponseWriter, r *http.Request) + want string + }{ + { + name: "no updates", + handler: func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{"tag_name": "v` + config.Version + `"}`)) + }, + want: "grab v" + config.Version + " " + config.BuildOS + "/" + config.BuildArch + " (" + config.CommitHash[:7] + ")\n", + }, + { + name: "newer version", + handler: func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{"tag_name": "v987.654.321"}`)) + }, + want: "grab v" + config.Version + " " + config.BuildOS + "/" + config.BuildArch + " (" + config.CommitHash[:7] + ")\n\n" + + "New version available: v987.654.321\n" + + "Download it at https://github.com/everdrone/grab/releases/latest\n", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(tc *testing.T) { + // start the test server + ts := httptest.NewServer(http.HandlerFunc(tt.handler)) + + config.LatestReleaseURL = ts.URL + + c, got, err := tu.ExecuteCommand(RootCmd, cmdName) + if err != nil { + tc.Fatal(err) + } + + if c.Name() != cmdName { + tc.Fatalf("got: '%s', want: '%s'", c.Name(), cmdName) + } + + if got != tt.want { + tc.Errorf("got: '%s', want: '%s'", got, tt.want) + } + }) + } } diff --git a/go.mod b/go.mod index 60d9112..087e178 100644 --- a/go.mod +++ b/go.mod @@ -26,6 +26,7 @@ require ( github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.1 // indirect golang.org/x/crypto v0.0.0-20220517005047-85d78b3ac167 // indirect + golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect golang.org/x/sys v0.0.0-20220804214406-8e32c043e418 // indirect golang.org/x/text v0.3.7 // indirect diff --git a/go.sum b/go.sum index b5918fd..7e364ad 100644 --- a/go.sum +++ b/go.sum @@ -236,6 +236,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o= +golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= diff --git a/internal/config/version.go b/internal/config/version.go index fe29b9f..5ad10ed 100644 --- a/internal/config/version.go +++ b/internal/config/version.go @@ -9,3 +9,5 @@ var BuildType = "devel" var BuildOS = "unknown" var BuildArch = "unknown" + +var LatestReleaseURL = "https://api.github.com/repos/everdrone/grab/releases/latest" diff --git a/internal/update/check.go b/internal/update/check.go new file mode 100644 index 0000000..f13f306 --- /dev/null +++ b/internal/update/check.go @@ -0,0 +1,52 @@ +package update + +import ( + "encoding/json" + "fmt" + + "github.com/everdrone/grab/internal/config" + "github.com/everdrone/grab/internal/net" + "golang.org/x/mod/semver" +) + +func CheckForUpdates() (string, error) { + resp, err := net.Fetch(config.LatestReleaseURL, &net.FetchOptions{ + Headers: map[string]string{ + "Accept": "application/vnd.github+json", + }, + Timeout: 1000, + Retries: 1, + }) + + if err != nil { + return "", err + } + + var decoded map[string]interface{} + if err = json.Unmarshal([]byte(resp), &decoded); err != nil { + return "", err + } + + tagName := decoded["tag_name"] + if tagName == "" { + return "", fmt.Errorf("no tag name") + } + + if version, ok := tagName.(string); ok { + if version[0] != 'v' { + version = "v" + version + } + + if !semver.IsValid(version) { + return "", fmt.Errorf("invalid version: %s", version) + } + + if semver.Compare(version, "v"+config.Version) == 1 { + return version, nil + } + } else { + return "", fmt.Errorf("invalid tag name") + } + + return "", nil +} diff --git a/internal/update/check_test.go b/internal/update/check_test.go new file mode 100644 index 0000000..78e39ef --- /dev/null +++ b/internal/update/check_test.go @@ -0,0 +1,107 @@ +package update + +import ( + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/everdrone/grab/internal/config" +) + +func TestCheckForUpdates(t *testing.T) { + tests := []struct { + name string + handler func(w http.ResponseWriter, r *http.Request) + want string + wantErr bool + }{ + { + name: "no updates", + handler: func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{"tag_name": "v` + config.Version + `"}`)) + }, + want: "", + wantErr: false, + }, + { + name: "invalid semver", + handler: func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{"tag_name": "newVersion"}`)) + }, + want: "", + wantErr: true, + }, + { + name: "invalid response", + handler: func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{"something": "else"}`)) + }, + want: "", + wantErr: true, + }, + { + name: "invalid json", + handler: func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{"something`)) + }, + want: "", + wantErr: true, + }, + { + name: "empty tag name", + handler: func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{"tag_name": ""}`)) + }, + want: "", + wantErr: true, + }, + { + name: "network error", + handler: func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + }, + want: "", + wantErr: true, + }, + { + name: "request times out", + handler: func(w http.ResponseWriter, r *http.Request) { + time.Sleep(time.Millisecond * 1500) + + w.Write([]byte(`{"tag_name": "v` + config.Version + `"}`)) + }, + want: "", + wantErr: true, + }, + { + name: "update available", + handler: func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{"tag_name": "v987.654.321"}`)) + }, + want: "v987.654.321", + wantErr: false, + }, + } + + for _, tt := range tests { + tt := tt + + t.Run(tt.name, func(tc *testing.T) { + tc.Parallel() + + // start the test server + ts := httptest.NewServer(http.HandlerFunc(tt.handler)) + + config.LatestReleaseURL = ts.URL + + got, err := CheckForUpdates() + if (err != nil) != tt.wantErr { + tc.Errorf("got error: '%v', want error: '%v'", err, tt.wantErr) + } + if got != tt.want { + tc.Errorf("got: '%s', want: '%s'", got, tt.want) + } + }) + } +} From 9da59bc0003149d69a5fc20591391eb918ebbe32 Mon Sep 17 00:00:00 2001 From: everdrone Date: Mon, 22 Aug 2022 22:47:53 +0200 Subject: [PATCH 2/4] test: tests for RootCmd and ConfigCmd --- cmd/check_test.go | 4 ++-- cmd/config_test.go | 26 ++++++++++++++++++++++++++ cmd/find_test.go | 4 ++-- cmd/generate_test.go | 4 ++-- cmd/get_test.go | 4 ++-- cmd/root_test.go | 26 ++++++++++++++++++++++++++ cmd/version_test.go | 10 ++++------ 7 files changed, 64 insertions(+), 14 deletions(-) create mode 100644 cmd/config_test.go create mode 100644 cmd/root_test.go diff --git a/cmd/check_test.go b/cmd/check_test.go index ced28e1..0ee01dc 100644 --- a/cmd/check_test.go +++ b/cmd/check_test.go @@ -136,8 +136,8 @@ func TestCheckCmd(t *testing.T) { t.Errorf("unexpected error: %v", err) } - if c.Name() != "check" { - t.Errorf("got: '%s', want: 'check'", c.Name()) + if c.Name() != CheckCmd.Name() { + t.Errorf("got: '%s', want: %s", c.Name(), CheckCmd.Name()) } if !strings.Contains(got, tt.WantContains) { t.Errorf("got: %s, does not contain: %s", got, tt.WantContains) diff --git a/cmd/config_test.go b/cmd/config_test.go new file mode 100644 index 0000000..e49fb5b --- /dev/null +++ b/cmd/config_test.go @@ -0,0 +1,26 @@ +package cmd + +import ( + "strings" + "testing" + + tu "github.com/everdrone/grab/testutils" +) + +func TestConfigCmd(t *testing.T) { + t.Run("prints help message", func(tc *testing.T) { + c, got, err := tu.ExecuteCommand(RootCmd, "config") + + if err != nil { + tc.Fatal(err) + } + + if c.Name() != ConfigCmd.Name() { + tc.Fatalf("got: '%s', want: '%s'", c.Name(), ConfigCmd.Name()) + } + + if !strings.HasPrefix(got, ConfigCmd.Short) { + tc.Errorf("got: '%s', want: '%s'", got, ConfigCmd.Short) + } + }) +} diff --git a/cmd/find_test.go b/cmd/find_test.go index 99eddec..46f589d 100644 --- a/cmd/find_test.go +++ b/cmd/find_test.go @@ -89,8 +89,8 @@ func TestFindCmd(t *testing.T) { tc.Errorf("got: %v, want: %v", err, tt.HasErrors) } - if c.Name() != "find" { - tc.Errorf("got: %s, want: 'find", c.Name()) + if c.Name() != FindCmd.Name() { + tc.Errorf("got: %s, want: %s", c.Name(), FindCmd.Name()) } if got != tt.Want { tc.Errorf("got: %s, want: %s", got, tt.Want) diff --git a/cmd/generate_test.go b/cmd/generate_test.go index aafa238..ff8579f 100644 --- a/cmd/generate_test.go +++ b/cmd/generate_test.go @@ -58,8 +58,8 @@ func TestGenerate(t *testing.T) { tc.Errorf("got: %v, want: %v", err, tt.HasErrors) } - if c.Name() != "generate" { - tc.Errorf("got: %s, want: 'generate", c.Name()) + if c.Name() != GenerateCmd.Name() { + tc.Errorf("got: %s, want: %s", c.Name(), GenerateCmd.Name()) } if tt.CheckFile != "" { diff --git a/cmd/get_test.go b/cmd/get_test.go index 7bbf65d..a525ef5 100644 --- a/cmd/get_test.go +++ b/cmd/get_test.go @@ -154,8 +154,8 @@ site "example" { c, _, _, err := tu.ExecuteCommandErr(RootCmd, append([]string{"get"}, tt.Args...)...) - if c.Name() != "get" { - tc.Fatalf("got: %s, want: 'find", c.Name()) + if c.Name() != GetCmd.Name() { + tc.Fatalf("got: %s, want: %s", c.Name(), GetCmd.Name()) } if tt.CheckFiles != nil { diff --git a/cmd/root_test.go b/cmd/root_test.go new file mode 100644 index 0000000..23409db --- /dev/null +++ b/cmd/root_test.go @@ -0,0 +1,26 @@ +package cmd + +import ( + "strings" + "testing" + + tu "github.com/everdrone/grab/testutils" +) + +func TestRootCmd(t *testing.T) { + t.Run("prints help message", func(tc *testing.T) { + c, got, err := tu.ExecuteCommand(RootCmd, "") + + if err != nil { + tc.Fatal(err) + } + + if c.Name() != RootCmd.Name() { + tc.Fatalf("got: '%s', want: '%s'", c.Name(), RootCmd.Name()) + } + + if !strings.HasPrefix(got, RootCmd.Short) { + tc.Errorf("got: '%s', want: '%s'", got, RootCmd.Short) + } + }) +} diff --git a/cmd/version_test.go b/cmd/version_test.go index b153d2e..f9af9e4 100644 --- a/cmd/version_test.go +++ b/cmd/version_test.go @@ -11,8 +11,6 @@ import ( ) func TestVersionCmd(t *testing.T) { - cmdName := "version" - config.CommitHash = "abcdef0123456789" config.BuildOS = runtime.GOOS config.BuildArch = runtime.GOARCH @@ -47,17 +45,17 @@ func TestVersionCmd(t *testing.T) { config.LatestReleaseURL = ts.URL - c, got, err := tu.ExecuteCommand(RootCmd, cmdName) + c, got, err := tu.ExecuteCommand(RootCmd, "version") if err != nil { tc.Fatal(err) } - if c.Name() != cmdName { - tc.Fatalf("got: '%s', want: '%s'", c.Name(), cmdName) + if c.Name() != VersionCmd.Name() { + tc.Fatalf("got: %s, want: %s", c.Name(), VersionCmd.Name()) } if got != tt.want { - tc.Errorf("got: '%s', want: '%s'", got, tt.want) + tc.Errorf("got: %s, want: %s", got, tt.want) } }) } From 6b2f73b4c21ddefa88980fcb29275f1a3f6df061 Mon Sep 17 00:00:00 2001 From: everdrone Date: Mon, 22 Aug 2022 22:55:46 +0200 Subject: [PATCH 3/4] test: disable parallelism for network tests --- internal/update/check_test.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/internal/update/check_test.go b/internal/update/check_test.go index 78e39ef..fb804ea 100644 --- a/internal/update/check_test.go +++ b/internal/update/check_test.go @@ -85,11 +85,7 @@ func TestCheckForUpdates(t *testing.T) { } for _, tt := range tests { - tt := tt - t.Run(tt.name, func(tc *testing.T) { - tc.Parallel() - // start the test server ts := httptest.NewServer(http.HandlerFunc(tt.handler)) From 1c2fe6b9476dc8ed097769adc93b51192ac1567c Mon Sep 17 00:00:00 2001 From: everdrone Date: Tue, 23 Aug 2022 00:07:56 +0200 Subject: [PATCH 4/4] style: better update notifier display --- cmd/version.go | 4 ++-- cmd/version_test.go | 4 ++-- internal/update/check.go | 16 +++++++++------- internal/update/check_test.go | 2 +- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/cmd/version.go b/cmd/version.go index a950551..21007bf 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -20,8 +20,8 @@ var VersionCmd = &cobra.Command{ newVersion, _ := update.CheckForUpdates() if newVersion != "" { - cmd.Printf("\nNew version available: %s\n", newVersion) - cmd.Printf("Download it at https://github.com/everdrone/grab/releases/latest\n") + cmd.Printf("\nNew version available %s → %s\n", config.Version, newVersion) + cmd.Printf("https://github.com/everdrone/grab/releases/latest\n") } }, } diff --git a/cmd/version_test.go b/cmd/version_test.go index f9af9e4..caea4cf 100644 --- a/cmd/version_test.go +++ b/cmd/version_test.go @@ -33,8 +33,8 @@ func TestVersionCmd(t *testing.T) { w.Write([]byte(`{"tag_name": "v987.654.321"}`)) }, want: "grab v" + config.Version + " " + config.BuildOS + "/" + config.BuildArch + " (" + config.CommitHash[:7] + ")\n\n" + - "New version available: v987.654.321\n" + - "Download it at https://github.com/everdrone/grab/releases/latest\n", + "New version available " + config.Version + " → 987.654.321\n" + + "https://github.com/everdrone/grab/releases/latest\n", }, } diff --git a/internal/update/check.go b/internal/update/check.go index f13f306..76ffd7d 100644 --- a/internal/update/check.go +++ b/internal/update/check.go @@ -32,17 +32,19 @@ func CheckForUpdates() (string, error) { return "", fmt.Errorf("no tag name") } - if version, ok := tagName.(string); ok { - if version[0] != 'v' { - version = "v" + version + if latest, ok := tagName.(string); ok { + if latest[0] != 'v' { + latest = "v" + latest } - if !semver.IsValid(version) { - return "", fmt.Errorf("invalid version: %s", version) + if !semver.IsValid(latest) { + return "", fmt.Errorf("invalid version: %s", latest) } - if semver.Compare(version, "v"+config.Version) == 1 { - return version, nil + current := "v" + config.Version + + if semver.Compare(latest, current) == 1 { + return latest[1:], nil } } else { return "", fmt.Errorf("invalid tag name") diff --git a/internal/update/check_test.go b/internal/update/check_test.go index fb804ea..39b32ea 100644 --- a/internal/update/check_test.go +++ b/internal/update/check_test.go @@ -79,7 +79,7 @@ func TestCheckForUpdates(t *testing.T) { handler: func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(`{"tag_name": "v987.654.321"}`)) }, - want: "v987.654.321", + want: "987.654.321", wantErr: false, }, }