From 54c834aa2571faf90383158484866e01bf7e2660 Mon Sep 17 00:00:00 2001 From: Michael Rykov Date: Mon, 1 Jul 2024 19:43:58 +0800 Subject: [PATCH] Multi-version yank with confirmation --- Makefile | 5 ++- api/packages.go | 24 ++++++++++- cli/packages.go | 13 ++++-- cli/packages_test.go | 14 +++++- cli/yank.go | 65 ++++++++++++++++++++++++++-- cli/yank_test.go | 92 +++++++++++++++++++++++++++++----------- go.mod | 16 +++---- go.sum | 30 ++++++------- internal/testutil/api.go | 61 ++++++++++++++------------ 9 files changed, 233 insertions(+), 87 deletions(-) diff --git a/Makefile b/Makefile index 8fc9248..ffc07b4 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ GIT_DESCRIBE=$$(git describe --tags --always --match "v*") GOLDFLAGS="-X main.Version=$(GIT_DESCRIBE)" GO_CMD?=go -bin: # creates the Fury CLI binaries for current platform +fury: # creates the Fury CLI binaries for current platform $(GO_CMD) build -ldflags $(GOLDFLAGS) -o ./fury ./cmd/fury bin/linux: # creates the Fury CLI binaries for Linux (AMD64) @@ -10,3 +10,6 @@ bin/linux: # creates the Fury CLI binaries for Linux (AMD64) bin/windows: # create windows binaries GOOS=windows GOARCH=amd64 $(GO_CMD) build -ldflags $(GOLDFLAGS) -o ./fury.exe ./cmd/fury + +clean: # remove binary + rm -f ./fury ./fury.exe diff --git a/api/packages.go b/api/packages.go index fb5995d..6141820 100644 --- a/api/packages.go +++ b/api/packages.go @@ -22,7 +22,7 @@ func (c *Client) Packages(cc context.Context, body *PaginationRequest) (*Package } // Versions returns the details of the versions listing for a package -func (c *Client) Versions(cc context.Context, pkg string, body *PaginationRequest) (*VersionsResponse, error) { +func (c *Client) PackageVersions(cc context.Context, pkg string, body *PaginationRequest) (*VersionsResponse, error) { req := c.newRequest(cc, "GET", "/packages/"+url.PathEscape(pkg)+"/versions?expand=package", true) if body != nil { @@ -36,6 +36,21 @@ func (c *Client) Versions(cc context.Context, pkg string, body *PaginationReques return &resp, err } +// Versions returns the details of the versions listing for specified filters +func (c *Client) Versions(cc context.Context, filter url.Values, body *PaginationRequest) (*VersionsResponse, error) { + req := c.newRequest(cc, "GET", "/versions?expand=package&"+filter.Encode(), true) + + if body != nil { + c.prepareJSONBody(req, body) + } + + resp := VersionsResponse{} + pagination, err := req.doPaginatedJSON(&resp.Versions) + resp.Pagination = pagination + + return &resp, err +} + // Version returns the details of a specific version of a package func (c *Client) Version(cc context.Context, pkg, ver string) (*Version, error) { path := "/packages/" + url.PathEscape(pkg) + "/versions/" + url.PathEscape(ver) @@ -108,3 +123,10 @@ func (v Version) DisplayCreatedBy() string { } return "N/A" } + +func (v Version) Kind() string { + if p := v.Package; p != nil && p.Kind != "" { + return p.Kind + } + return "N/A" +} diff --git a/cli/packages.go b/cli/packages.go index cd86b81..6ad8f98 100644 --- a/cli/packages.go +++ b/cli/packages.go @@ -4,6 +4,7 @@ import ( "github.com/briandowns/spinner" "github.com/gemfury/cli/api" "github.com/gemfury/cli/internal/ctx" + "github.com/gemfury/cli/pkg/terminal" "github.com/spf13/cobra" "context" @@ -89,7 +90,7 @@ func listVersions(cmd *cobra.Command, args []string) error { // Paginate over package listings until no more pages err = iterateAllPages(cc, func(pageReq *api.PaginationRequest) (*api.PaginationResponse, error) { - resp, err := c.Versions(cc, args[0], pageReq) + resp, err := c.PackageVersions(cc, args[0], pageReq) if err != nil { return nil, err } @@ -100,16 +101,20 @@ func listVersions(cmd *cobra.Command, args []string) error { // Print results term.Printf("\n*** %s versions ***\n\n", args[0]) + termPrintVersions(term, versions) + return err +} + +func termPrintVersions(term terminal.Terminal, versions []*api.Version) { w := tabwriter.NewWriter(term.IOOut(), 0, 0, 2, ' ', 0) - fmt.Fprintf(w, "version\tuploaded_by\tuploaded_at\tfilename\n") + fmt.Fprintf(w, "version\tuploaded_by\tuploaded_at\tkind\tfilename\n") for _, v := range versions { uploadedAt := timeStringWithAgo(v.CreatedAt) - fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", v.Version, v.DisplayCreatedBy(), uploadedAt, v.Filename) + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", v.Version, v.DisplayCreatedBy(), uploadedAt, v.Kind(), v.Filename) } w.Flush() - return err } func iterateAllPages(cc context.Context, fn func(req *api.PaginationRequest) (*api.PaginationResponse, error)) error { diff --git a/cli/packages_test.go b/cli/packages_test.go index 199cc29..0d98d2f 100644 --- a/cli/packages_test.go +++ b/cli/packages_test.go @@ -68,13 +68,23 @@ var versionsResponses = []string{`[{ "id": "ver_a1b2c3", "version": "1.2.3", "created_at": "2011-05-27T00:39:07+00:00", + "filename": "foo-1.2.3.tgz", "created_by": { "name": "user1" + }, + "package": { + "id": "pkg_x9y8z7", + "kind": "js" } }]`, `[{ "id": "ver_z1y2x3", "version": "3.2.1", - "created_at": "2011-01-27T00:44:00+00:00" + "created_at": "2011-01-27T00:44:00+00:00", + "filename": "foo-3.2.1.tgz", + "package": { + "id": "pkg_x9y8z7", + "kind": "js" + } }]`} func TestVersionsCommandSuccess(t *testing.T) { @@ -95,7 +105,7 @@ func TestVersionsCommandSuccess(t *testing.T) { t.Fatal(err) } - exp := "1.2.3 user1 2011-05-26 17:39 3.2.1 N/A 2011-01-26 16:44" + exp := "1.2.3 user1 2011-05-26 17:39 js foo-1.2.3.tgz 3.2.1 N/A 2011-01-26 16:44 js foo-3.2.1.tgz" if outStr := compactString(term.OutBytes()); !strings.HasSuffix(outStr, exp) { t.Errorf("Expected output to include %q, got %q", exp, outStr) } diff --git a/cli/yank.go b/cli/yank.go index c67796b..5b55e5a 100644 --- a/cli/yank.go +++ b/cli/yank.go @@ -1,17 +1,23 @@ package cli import ( + "github.com/gemfury/cli/api" "github.com/gemfury/cli/internal/ctx" "github.com/hashicorp/go-multierror" + "github.com/manifoldco/promptui" "github.com/spf13/cobra" + "context" + "errors" "fmt" + "net/url" "strings" ) // NewCmdYank generates the Cobra command for "yank" func NewCmdYank() *cobra.Command { var versionFlag string + var forceFlag bool yankCmd := &cobra.Command{ Use: "yank PACKAGE@VERSION", @@ -32,6 +38,7 @@ func NewCmdYank() *cobra.Command { return err } + versions := make([]*api.Version, 0, len(args)) var multiErr *multierror.Error for _, pkg := range args { var ver string = "" @@ -48,13 +55,39 @@ func NewCmdYank() *cobra.Command { continue } - err = c.Yank(cc, pkg, ver) + pkgVersions, err := filterVersions(cc, c, pkg, ver) + versions = append(versions, pkgVersions...) + multiErr = multierror.Append(multiErr, err) + } + + if err := multiErr.Unwrap(); err != nil { + return err + } else if len(versions) == 0 { + term.Printf("No matching versions found\n") + return nil + } + + if !forceFlag { + termPrintVersions(term, versions) + prompt := promptui.Prompt{ + Label: "Are you sure you want to delete these files? [y/N]", + IsConfirm: true, + } + _, err := term.RunPrompt(&prompt) + if errors.Is(err, promptui.ErrAbort) { + return nil + } else if err != nil { + return err + } + } + + for _, v := range versions { + err = c.Yank(cc, v.Package.ID, v.ID) if err != nil { multiErr = multierror.Append(multiErr, err) continue } - - term.Printf("Removed package %q version %q\n", pkg, ver) + term.Printf("Removed %q\n", v.Filename) } return multiErr.Unwrap() @@ -62,7 +95,33 @@ func NewCmdYank() *cobra.Command { } // Flags and options + yankCmd.Flags().BoolVarP(&forceFlag, "force", "f", false, "Skip confirmation") yankCmd.Flags().StringVarP(&versionFlag, "version", "v", "", "Version") return yankCmd } + +func filterVersions(cc context.Context, c *api.Client, pkg, ver string) ([]*api.Version, error) { + versions := []*api.Version{} + + // Default search filters for listed versions + filter := url.Values(map[string][]string{"name": {pkg}, "version": {ver}}) + + // Extract "kind:" from package name, if present + if at := strings.Index(pkg, ":"); at > 0 { + filter["name"] = []string{pkg[at+1:]} + filter["kind"] = []string{pkg[0:at]} + } + + // Paginate over package listings until no more pages + err := iterateAllPages(cc, func(pageReq *api.PaginationRequest) (*api.PaginationResponse, error) { + resp, err := c.Versions(cc, filter, pageReq) + if err != nil { + return nil, err + } + versions = append(versions, resp.Versions...) + return resp.Pagination, nil + }) + + return versions, err +} diff --git a/cli/yank_test.go b/cli/yank_test.go index c91bb4f..488ef9c 100644 --- a/cli/yank_test.go +++ b/cli/yank_test.go @@ -18,8 +18,25 @@ func TestYankCommandOnePackage(t *testing.T) { term := terminal.NewForTest() // Fire up test server - path := "/packages/foo/versions/0.0.1" - server := testutil.APIServer(t, "DELETE", path, "{}", 200) + server := testutil.APIServerCustom(t, func(mux *http.ServeMux) { + mux.HandleFunc("/versions", func(w http.ResponseWriter, r *http.Request) { + if q := r.URL.Query(); q.Get("name") != "foo" || q.Get("version") != "0.0.1" { + t.Errorf("Invalid request: %s %s", r.Method, r.URL.Path) + } else if k := q.Get("kind"); k == "js" { + w.Write([]byte(versionsResponses[0])) // One page + } else if method := r.Method; method != "GET" { + t.Errorf("Invalid method: %s %s", method, r.URL.Path) + } + testutil.APIPaginatedResponse(t, w, r, versionsResponses, 200) + }) + mux.HandleFunc("/packages/{pid}/versions/{vid}", func(w http.ResponseWriter, r *http.Request) { + if method := r.Method; method != "DELETE" { + t.Errorf("Invalid request: %s %s", method, r.URL.Path) + w.WriteHeader(500) + } + w.Write([]byte("{}")) + }) + }) defer server.Close() cc := cli.TestContext(term, auth) @@ -27,24 +44,30 @@ func TestYankCommandOnePackage(t *testing.T) { flags.Endpoint = server.URL // Removing using version flag - err := runCommandNoErr(cc, []string{"yank", "foo", "-v", "0.0.1"}) + err := runCommandNoErr(cc, []string{"yank", "foo", "-v", "0.0.1", "--force"}) if err != nil { t.Fatal(err) } - exp := "Removed package \"foo\" version \"0.0.1\"\n" + exp := "Removed \"foo-1.2.3.tgz\"\nRemoved \"foo-3.2.1.tgz\"\n" 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", "foo@0.0.1"}) + err = runCommandNoErr(cc, []string{"yank", "foo@0.0.1", "--force"}) if err != nil { t.Fatal(err) + } else if outStr := string(term.OutBytes()); !strings.HasSuffix(outStr, exp) { + t.Errorf("Expected output to include %q, got %q", exp, outStr) } - exp = "Removed package \"foo\" version \"0.0.1\"\n" - if outStr := string(term.OutBytes()); !strings.HasSuffix(outStr, exp) { + // Removing using KIND:PACKAGE@VERSION + exp = "Removed \"foo-1.2.3.tgz\"\n" // JS kind returns one Version + err = runCommandNoErr(cc, []string{"yank", "js:foo@0.0.1", "--force"}) + if err != nil { + t.Fatal(err) + } else if outStr := string(term.OutBytes()); !strings.HasSuffix(outStr, exp) { t.Errorf("Expected output to include %q, got %q", exp, outStr) } @@ -61,20 +84,27 @@ func TestYankCommandMultiPackage(t *testing.T) { // 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) + mux.HandleFunc("/versions", func(w http.ResponseWriter, r *http.Request) { + if q := r.URL.Query(); q.Get("name") != "foo" { + t.Errorf("Invalid name: %s %s", r.Method, r.URL.Path) + } else if v := q.Get("version"); v == "0.0.2" { + w.Write([]byte("[]")) // Nothing found + } else if v != "0.0.1" { + t.Errorf("Invalid version: %s %s", r.Method, r.URL.Path) + } else if method := r.Method; method != "GET" { + t.Errorf("Invalid method: %s %s", method, r.URL.Path) + } else { + testutil.APIPaginatedResponse(t, w, r, versionsResponses, 200) } - w.Write([]byte("{}")) }) - mux.HandleFunc("/packages/foo/versions/0.0.2", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/packages/{pid}/versions/{vid}", func(w http.ResponseWriter, r *http.Request) { if method := r.Method; method != "DELETE" { t.Errorf("Invalid request: %s %s", method, r.URL.Path) + w.WriteHeader(500) } - http.NotFound(w, r) + w.Write([]byte("{}")) }) }) - defer server.Close() cc := cli.TestContext(term, auth) @@ -82,7 +112,7 @@ func TestYankCommandMultiPackage(t *testing.T) { flags.Endpoint = server.URL // Expected successful output - exp := "Removed package \"foo\" version \"0.0.1\"\n" + exp := "Removed \"foo-1.2.3.tgz\"\nRemoved \"foo-3.2.1.tgz\"\n" // Failure for multiple packages without version err := runCommandNoErr(cc, []string{"yank", "foo", "bar"}) @@ -96,32 +126,44 @@ func TestYankCommandMultiPackage(t *testing.T) { t.Errorf("Expected invalid error, got %q", err) } - // Partial failure for multiple packages - err = runCommandNoErr(cc, []string{"yank", "foo@0.0.1", "foo@0.0.2"}) - if err == nil || !strings.Contains(err.Error(), "Doesn't look like this exists") { - t.Errorf("Expected invalid error, got %q", err) + // When nothing is found, we expect "nothing found" error message + expNone := "No matching versions found\n" + err = runCommandNoErr(cc, []string{"yank", "foo@0.0.2", "--force"}) + if outStr := string(term.OutBytes()); !strings.HasSuffix(outStr, expNone) { + t.Errorf("Expected output to include %q, got %q", expNone, outStr) } - if outStr := string(term.OutBytes()); !strings.Contains(outStr, exp) { + + // No partial failure for multiple packages when some return nothing + err = runCommandNoErr(cc, []string{"yank", "foo@0.0.1", "foo@0.0.2", "--force"}) + if outStr := string(term.OutBytes()); !strings.HasSuffix(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", "foo@0.0.1", "foo@0.0.1"}) + err = runCommandNoErr(cc, []string{"yank", "foo@0.0.1", "foo@0.0.1", "--force"}) + if outStr := string(term.OutBytes()); !strings.HasSuffix(outStr, exp) { + t.Errorf("Expected output to include %q, got %q", exp, outStr) + } + + // Success all around with confirmation prompt + term.SetPromptResponses(map[string]string{ + "Are you sure you want to delete these files? [y/N]": "Y", + }) + + err = runCommandNoErr(cc, []string{"yank", "foo@0.0.1"}) 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) { - path := "/packages/foo/versions/0.0.1" - server := testutil.APIServer(t, "DELETE", path, "{}", 200) + server := testutil.APIServer(t, "GET", "/versions", "[]", 200) testCommandLoginPreCheck(t, []string{"yank", "foo", "-v", "0.0.1"}, server) server.Close() } func TestYankCommandForbidden(t *testing.T) { - path := "/packages/foo/versions/0.0.1" - server := testutil.APIServer(t, "DELETE", path, "", 403) + server := testutil.APIServer(t, "GET", "/versions", "", 403) testCommandForbiddenResponse(t, []string{"yank", "foo", "-v", "0.0.1"}, server) server.Close() } diff --git a/go.mod b/go.mod index 53df284..2869632 100644 --- a/go.mod +++ b/go.mod @@ -1,14 +1,14 @@ module github.com/gemfury/cli -go 1.21 +go 1.22 require ( github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d - github.com/briandowns/spinner v1.23.0 - github.com/cheggaaa/pb/v3 v3.1.4 + github.com/briandowns/spinner v1.23.1 + github.com/cheggaaa/pb/v3 v3.1.5 github.com/hashicorp/go-multierror v1.1.1 github.com/manifoldco/promptui v0.9.0 - github.com/spf13/cobra v1.8.0 + github.com/spf13/cobra v1.8.1 github.com/spf13/pflag v1.0.5 github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 github.com/yosida95/uritemplate/v3 v3.0.2 @@ -17,13 +17,13 @@ require ( require ( github.com/VividCortex/ewma v1.2.0 // indirect github.com/chzyer/readline v1.5.1 // indirect - github.com/fatih/color v1.16.0 // indirect + github.com/fatih/color v1.17.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect - github.com/rivo/uniseg v0.4.4 // indirect - golang.org/x/sys v0.14.0 // indirect - golang.org/x/term v0.14.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/term v0.21.0 // indirect ) diff --git a/go.sum b/go.sum index 4353d33..b2f1b74 100644 --- a/go.sum +++ b/go.sum @@ -2,10 +2,10 @@ github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1o github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas= github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= -github.com/briandowns/spinner v1.23.0 h1:alDF2guRWqa/FOZZYWjlMIx2L6H0wyewPxo/CH4Pt2A= -github.com/briandowns/spinner v1.23.0/go.mod h1:rPG4gmXeN3wQV/TsAY4w8lPdIM6RX3yqeBQJSrbXjuE= -github.com/cheggaaa/pb/v3 v3.1.4 h1:DN8j4TVVdKu3WxVwcRKu0sG00IIU6FewoABZzXbRQeo= -github.com/cheggaaa/pb/v3 v3.1.4/go.mod h1:6wVjILNBaXMs8c21qRiaUM8BR82erfgau1DQ4iUXmSA= +github.com/briandowns/spinner v1.23.1 h1:t5fDPmScwUjozhDj4FA46p5acZWIPXYE30qW2Ptu650= +github.com/briandowns/spinner v1.23.1/go.mod h1:LaZeM4wm2Ywy6vO571mvhQNRcWfRUnXOs0RcKV0wYKM= +github.com/cheggaaa/pb/v3 v3.1.5 h1:QuuUzeM2WsAqG2gMqtzaWithDJv0i+i6UlnwSCI4QLk= +github.com/cheggaaa/pb/v3 v3.1.5/go.mod h1:CrxkeghYTXi1lQBEI7jSn+3svI3cuc19haAj6jM60XI= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= @@ -15,9 +15,9 @@ github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObk github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= -github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= -github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= +github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -35,11 +35,11 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= -github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= -github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 h1:nrZ3ySNYwJbSpD6ce9duiP+QkD3JuLCcWkdaehUS/3Y= @@ -50,9 +50,9 @@ golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= -golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.14.0 h1:LGK9IlZ8T9jvdy6cTdfKUCltatMFOehAQo9SRC46UQ8= -golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= +golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/testutil/api.go b/internal/testutil/api.go index 774f7e0..06d810c 100644 --- a/internal/testutil/api.go +++ b/internal/testutil/api.go @@ -49,39 +49,44 @@ func APIServerPaginated(t *testing.T, method, path string, resps []string, code } // 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) - } + APIPaginatedResponse(t, w, r, resps, code) + }) + }) +} - // Out of bounds empty response - if page > len(resps) { - w.WriteHeader(code) - w.Write([]byte("[]")) - return - } +func APIPaginatedResponse(t *testing.T, w http.ResponseWriter, r *http.Request, resps []string, code int) { + // 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() - w.WriteHeader(code) - w.Write([]byte(resps[page])) - }) - }) + t.Logf("Next page Link: %s", linkStr) + w.Header().Set("Link", linkStr) + } + + w.WriteHeader(code) + w.Write([]byte(resps[page])) } func APIServerCustom(t *testing.T, custom func(*http.ServeMux)) *httptest.Server {