Skip to content

Commit

Permalink
Support for “yank PACKAGE@VERSION”
Browse files Browse the repository at this point in the history
  • Loading branch information
rykov committed Nov 25, 2021
1 parent ef430cc commit cf9b02f
Show file tree
Hide file tree
Showing 6 changed files with 181 additions and 72 deletions.
24 changes: 14 additions & 10 deletions cli/git_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,13 @@ func TestGitDestroyCommandSuccess(t *testing.T) {

// Destroy without reset
path := "/git/repos/me/repo-name"
serverDestroy := testutil.APIServerCustom(t, "DELETE", path, func(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Has("reset") {
t.Errorf("Has extraneous reset=1 URL query")
}
w.Write([]byte("{}"))
serverDestroy := testutil.APIServerCustom(t, func(mux *http.ServeMux) {
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Has("reset") {
t.Errorf("Has extraneous reset=1 URL query")
}
w.Write([]byte("{}"))
})
})
defer serverDestroy.Close()

Expand All @@ -128,11 +130,13 @@ func TestGitDestroyCommandSuccess(t *testing.T) {
}

// Reset without destroying repo
serverReset := testutil.APIServerCustom(t, "DELETE", path, func(w http.ResponseWriter, r *http.Request) {
if q := r.URL.Query(); q.Get("reset") != "1" {
t.Errorf("Missing reset=1 URL query")
}
w.Write([]byte("{}"))
serverReset := testutil.APIServerCustom(t, func(mux *http.ServeMux) {
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
if q := r.URL.Query(); q.Get("reset") != "1" {
t.Errorf("Missing reset=1 URL query")
}
w.Write([]byte("{}"))
})
})
defer serverReset.Close()

Expand Down
41 changes: 28 additions & 13 deletions cli/yank.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,27 @@ package cli

import (
"github.com/gemfury/cli/internal/ctx"
"github.com/hashicorp/go-multierror"
"github.com/spf13/cobra"

"fmt"
"strings"
)

// NewCmdYank generates the Cobra command for "yank"
func NewCmdYank() *cobra.Command {
var versionFlag string

yankCmd := &cobra.Command{
Use: "yank PACKAGE VERSION",
Use: "yank PACKAGE@VERSION",
Short: "Remove a package version",
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return fmt.Errorf("Please specify at least one package")
}

var pkg string = args[0]
var ver string = ""

if versionFlag != "" {
ver = versionFlag
} else if len(args) > 1 {
ver = args[1]
} else {
return fmt.Errorf("No version specified")
if versionFlag != "" && len(args) > 1 {
return fmt.Errorf("Use PACKAGE@VERSION for multiple yanks")
}

cc := cmd.Context()
Expand All @@ -37,12 +32,32 @@ func NewCmdYank() *cobra.Command {
return err
}

err = c.Yank(cc, pkg, ver)
if err == nil {
var multiErr *multierror.Error
for _, pkg := range args {
var ver string = ""

if versionFlag != "" {
ver = versionFlag
} else if at := strings.LastIndex(pkg, "@"); at > 0 {
pkg, ver = pkg[0:at], pkg[at+1:]
}

if pkg == "" || ver == "" {
err := fmt.Errorf("Invalid package/version specified")
multiErr = multierror.Append(multiErr, err)
continue
}

err = c.Yank(cc, pkg, ver)
if err != nil {
multiErr = multierror.Append(multiErr, err)
continue
}

term.Printf("Removed package %q version %q\n", pkg, ver)
}

return err
return multiErr.Unwrap()
},
}

Expand Down
79 changes: 78 additions & 1 deletion cli/yank_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import (
"github.com/gemfury/cli/internal/ctx"
"github.com/gemfury/cli/internal/testutil"
"github.com/gemfury/cli/pkg/terminal"

"net/http"
"strings"
"testing"
)

// ==== YANK ====

func TestYankCommandSuccess(t *testing.T) {
func TestYankCommandOnePackage(t *testing.T) {
auth := terminal.TestAuther("user", "abc123", nil)
term := terminal.NewForTest()

Expand All @@ -24,6 +26,7 @@ func TestYankCommandSuccess(t *testing.T) {
flags := ctx.GlobalFlags(cc)
flags.Endpoint = server.URL

// Removing using version flag
err := runCommandNoErr(cc, []string{"yank", "foo", "-v", "0.0.1"})
if err != nil {
t.Fatal(err)
Expand All @@ -33,6 +36,80 @@ func TestYankCommandSuccess(t *testing.T) {
if outStr := string(term.OutBytes()); !strings.HasSuffix(outStr, exp) {
t.Errorf("Expected output to include %q, got %q", exp, outStr)
}

// Removing using PACKAGE@VERSION
err = runCommandNoErr(cc, []string{"yank", "[email protected]"})
if err != nil {
t.Fatal(err)
}

exp = "Removed package \"foo\" version \"0.0.1\"\n"
if outStr := string(term.OutBytes()); !strings.HasSuffix(outStr, exp) {
t.Errorf("Expected output to include %q, got %q", exp, outStr)
}

// Fail if no version specified
err = runCommandNoErr(cc, []string{"yank", "foo"})
if err == nil || !strings.Contains(err.Error(), "Invalid package/version") {
t.Errorf("Expected invalid error, got %q", err)
}
}

func TestYankCommandMultiPackage(t *testing.T) {
auth := terminal.TestAuther("user", "abc123", nil)
term := terminal.NewForTest()

// Fire up test server
server := testutil.APIServerCustom(t, func(mux *http.ServeMux) {
mux.HandleFunc("/packages/foo/versions/0.0.1", func(w http.ResponseWriter, r *http.Request) {
if method := r.Method; method != "DELETE" {
t.Errorf("Invalid request: %s %s", method, r.URL.Path)
}
w.Write([]byte("{}"))
})
mux.HandleFunc("/packages/foo/versions/0.0.2", func(w http.ResponseWriter, r *http.Request) {
if method := r.Method; method != "DELETE" {
t.Errorf("Invalid request: %s %s", method, r.URL.Path)
}
http.NotFound(w, r)
})
})

defer server.Close()

cc := cli.TestContext(term, auth)
flags := ctx.GlobalFlags(cc)
flags.Endpoint = server.URL

// Expected successful output
exp := "Removed package \"foo\" version \"0.0.1\"\n"

// Failure for multiple packages without version
err := runCommandNoErr(cc, []string{"yank", "foo", "bar"})
if err == nil || !strings.Contains(err.Error(), "Invalid package/version") {
t.Errorf("Expected invalid error, got %q", err)
}

// Failure for multiple packages with version flag
err = runCommandNoErr(cc, []string{"yank", "foo", "bar", "-v", "0.0.1"})
if err == nil || !strings.Contains(err.Error(), "Use PACKAGE@VERSION") {
t.Errorf("Expected invalid error, got %q", err)
}

// Partial failure for multiple packages
err = runCommandNoErr(cc, []string{"yank", "[email protected]", "[email protected]"})
if err == nil || !strings.Contains(err.Error(), "Doesn't look like this exists") {
t.Errorf("Expected invalid error, got %q", err)
}
if outStr := string(term.OutBytes()); !strings.Contains(outStr, exp) {
t.Errorf("Expected output to include %q, got %q", exp, outStr)
}

// Success all around (reusing the same test package URL)
err = runCommandNoErr(cc, []string{"yank", "[email protected]", "[email protected]"})
if outStr := string(term.OutBytes()); !strings.HasSuffix(outStr, exp) {
t.Errorf("Expected output to include %q, got %q", exp, outStr)
}
}

func TestYankCommandUnauthorized(t *testing.T) {
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ require (
github.com/fatih/color v1.13.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/mattn/go-colorable v0.1.11 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 // indirect
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881 // indirect
)
7 changes: 4 additions & 3 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -196,8 +196,8 @@ github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaO
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs=
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
Expand Down Expand Up @@ -420,8 +420,9 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 h1:foEbQz/B0Oz6YIqu/69kfXPYeFQAuuMYFkjaqXzl5Wo=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881 h1:TyHqChC80pFkXWraUUf6RuB5IqFdQieMLwwCJokV2pc=
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
Expand Down
98 changes: 55 additions & 43 deletions internal/testutil/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,74 +21,86 @@ const loginResponse = `{
}`

func APIServer(t *testing.T, method, path, resp string, code int) *httptest.Server {
return APIServerCustom(t, method, path, func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(code)
w.Write([]byte(resp))
return APIServerCustom(t, func(mux *http.ServeMux) {
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
t.Logf("API Request: %s %s", r.Method, r.URL.String())

if r.Method != method {
w.WriteHeader(http.StatusNotImplemented)
return
}

w.WriteHeader(code)
w.Write([]byte(resp))
})
})
}

// Allow responses to be paginated forward. Page param is just a string of "p" characters to
// simplify implementation (without parsing), and prevent parsing page number as an integer
func APIServerPaginated(t *testing.T, method, path string, resps []string, code int) *httptest.Server {
return APIServerCustom(t, method, path, func(w http.ResponseWriter, r *http.Request) {
return APIServerCustom(t, func(mux *http.ServeMux) {
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
t.Logf("API Request: %s %s", r.Method, r.URL.String())

// Page from JSON body or query
pageReq := api.PaginationRequest{}
page := len(r.URL.Query().Get("page"))
if err := json.NewDecoder(r.Body).Decode(&pageReq); err == nil && pageReq.Page != "" {
page = len(pageReq.Page)
}
if r.Method != method {
w.WriteHeader(http.StatusNotImplemented)
return
}

// Out of bounds empty response
if page > len(resps) {
w.WriteHeader(code)
w.Write([]byte("[]"))
return
}
// Page from JSON body or query
pageReq := api.PaginationRequest{}
page := len(r.URL.Query().Get("page"))
if err := json.NewDecoder(r.Body).Decode(&pageReq); err == nil && pageReq.Page != "" {
page = len(pageReq.Page)
}

// Populate "Link" header
if page < len(resps)-1 {
newURL := *r.URL // Copy incoming URL
newURL.Scheme, newURL.Host = "", ""
// Out of bounds empty response
if page > len(resps) {
w.WriteHeader(code)
w.Write([]byte("[]"))
return
}

query := newURL.Query()
query.Set("page", strings.Repeat("p", page+1))
newURL.RawQuery = query.Encode()
linkStr := linkheader.Links{
{URL: newURL.String(), Rel: "next"},
}.String()
// Populate "Link" header
if page < len(resps)-1 {
newURL := *r.URL // Copy incoming URL
newURL.Scheme, newURL.Host = "", ""

t.Logf("Next page Link: %s", linkStr)
w.Header().Set("Link", linkStr)
}
query := newURL.Query()
query.Set("page", strings.Repeat("p", page+1))
newURL.RawQuery = query.Encode()
linkStr := linkheader.Links{
{URL: newURL.String(), Rel: "next"},
}.String()

t.Logf("Next page Link: %s", linkStr)
w.Header().Set("Link", linkStr)
}

w.WriteHeader(code)
w.Write([]byte(resps[page]))
w.WriteHeader(code)
w.Write([]byte(resps[page]))
})
})
}

func APIServerCustom(t *testing.T, method, path string, hf http.HandlerFunc) *httptest.Server {
func APIServerCustom(t *testing.T, custom func(*http.ServeMux)) *httptest.Server {
h := http.NewServeMux()

h.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
t.Logf("API Request: %s %s", r.Method, r.URL.String())

if r.Method != method {
w.WriteHeader(http.StatusNotImplemented)
return
}

hf(w, r)
})
// Add custom path handlers
custom(h)

// Default handler for auth
h.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
w.WriteHeader(http.StatusNotImplemented)
}
w.Write([]byte(loginResponse))
})

if path != "/" {
// Check if mux has a handler for "/"
rootRequest := httptest.NewRequest("GET", "/", nil)
if _, pattern := h.Handler(rootRequest); pattern == "" {
h.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
t.Errorf("Unexpected: %s %s", r.Method, r.URL.String())
http.NotFound(w, r)
Expand Down

0 comments on commit cf9b02f

Please sign in to comment.