Skip to content

Commit

Permalink
Merge pull request #9 from everdrone/feat/new_update_available
Browse files Browse the repository at this point in the history
Add new version update check
  • Loading branch information
everdrone authored Aug 23, 2022
2 parents c5dc60d + 1c2fe6b commit ff0b638
Show file tree
Hide file tree
Showing 13 changed files with 281 additions and 32 deletions.
4 changes: 2 additions & 2 deletions cmd/check_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
26 changes: 26 additions & 0 deletions cmd/config_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
4 changes: 2 additions & 2 deletions cmd/find_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions cmd/generate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 != "" {
Expand Down
4 changes: 2 additions & 2 deletions cmd/get_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
26 changes: 26 additions & 0 deletions cmd/root_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
9 changes: 8 additions & 1 deletion cmd/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,27 @@ 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(),
config.Version,
config.BuildOS,
config.BuildArch,
config.CommitHash[:7])

newVersion, _ := update.CheckForUpdates()
if newVersion != "" {
cmd.Printf("\nNew version available %s → %s\n", config.Version, newVersion)
cmd.Printf("https://github.com/everdrone/grab/releases/latest\n")
}
},
}

Expand Down
74 changes: 51 additions & 23 deletions cmd/version_test.go
Original file line number Diff line number Diff line change
@@ -1,34 +1,62 @@
package cmd

import (
"fmt"
"net/http"
"net/http/httptest"
"runtime"
"testing"

"github.com/everdrone/grab/internal/config"
tu "github.com/everdrone/grab/testutils"
)

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)
}
})
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 " + config.Version + " → 987.654.321\n" +
"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, "version")
if err != nil {
tc.Fatal(err)
}

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)
}
})
}
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
2 changes: 2 additions & 0 deletions internal/config/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ var BuildType = "devel"

var BuildOS = "unknown"
var BuildArch = "unknown"

var LatestReleaseURL = "https://api.github.com/repos/everdrone/grab/releases/latest"
54 changes: 54 additions & 0 deletions internal/update/check.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
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 latest, ok := tagName.(string); ok {
if latest[0] != 'v' {
latest = "v" + latest
}

if !semver.IsValid(latest) {
return "", fmt.Errorf("invalid version: %s", latest)
}

current := "v" + config.Version

if semver.Compare(latest, current) == 1 {
return latest[1:], nil
}
} else {
return "", fmt.Errorf("invalid tag name")
}

return "", nil
}
103 changes: 103 additions & 0 deletions internal/update/check_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
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: "987.654.321",
wantErr: false,
},
}

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

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

0 comments on commit ff0b638

Please sign in to comment.