From db34270ecb0ff83a978bb993a5185e9d7849ee45 Mon Sep 17 00:00:00 2001 From: Deepak Dahiya <59823596+t-dedah@users.noreply.github.com> Date: Thu, 14 Jul 2022 02:14:03 +0530 Subject: [PATCH] Tests for CLI (#6) * Completed List cmd and added API calls * Minor comments and add delete code to pass linting * Typo in descriptions * Minor comments * Validations * Validations-1 * improved branch flag validation * removed build * working after refactory with bad names * Command working, test not working * Corrected creation of service * Finalized structure using service * Deleted tests * cleanup * cleanup * cleanup * removed space with tab * aligned types in model.go * Update model.go * resolved comments * Refactor * removed long descriptions * Working incomplete tests * Completed tests * cleanup * checks * PR comments * PR comments * minor comment issue * minor comment issue * updated tests to work with workflow * Updated tests to support new option service * Improved eror handling for list * Improved error handling * Upgraded go-gh * reusing rest client error --- cmd/delete.go | 49 +++++--- cmd/list.go | 42 +++++-- cmd/list_test.go | 220 ++++++++++++++++++++++++++++++++++ go.mod | 10 +- go.sum | 10 ++ internal/test_utils.go | 16 +++ internal/utils.go | 4 +- internal/utils_test.go | 55 +++++++++ service/actions_cache.go | 55 ++++----- service/actions_cache_test.go | 199 ++++++++++++++++++++++++++++++ types/errors.go | 16 +++ 11 files changed, 615 insertions(+), 61 deletions(-) create mode 100644 cmd/list_test.go create mode 100644 internal/test_utils.go create mode 100644 internal/utils_test.go create mode 100644 service/actions_cache_test.go create mode 100644 types/errors.go diff --git a/cmd/delete.go b/cmd/delete.go index 4979e4a..6fd645f 100644 --- a/cmd/delete.go +++ b/cmd/delete.go @@ -17,31 +17,38 @@ func NewCmdDelete() *cobra.Command { f := types.DeleteOptions{} var deleteCmd = &cobra.Command{ - Use: "delete", + Use: "delete ", Short: "Delete cache by key", - Run: func(cmd *cobra.Command, args []string) { + RunE: func(cmd *cobra.Command, args []string) error { if len(args) != 1 { - fmt.Printf("accepts 1 arg(s), received %d\n", len(args)) - return + return fmt.Errorf(fmt.Sprintf("accepts 1 arg(s), received %d", len(args))) } f.Key = args[0] repo, err := internal.GetRepo(f.Repo) if err != nil { - fmt.Println(err) - return + return err } - artifactCache := service.NewArtifactCache(repo, COMMAND, VERSION) + + artifactCache, err := service.NewArtifactCache(repo, COMMAND, VERSION) + if err != nil { + fmt.Printf("error connecting to %s\n", repo.Host()) + fmt.Println("check your internet connection or https://githubstatus.com") + return nil + } + queryParams := url.Values{} f.GenerateBaseQueryParams(queryParams) if !f.Confirm { - var matchedCaches = getCacheListWithExactMatch(f, artifactCache) + matchedCaches, err := getCacheListWithExactMatch(f, artifactCache) + if err != nil { + return err + } matchedCachesLen := len(matchedCaches) if matchedCachesLen == 0 { - fmt.Printf("Cache with input key '%s' does not exist\n", f.Key) - return + return fmt.Errorf(fmt.Sprintf("Cache with input key '%s' does not exist\n", f.Key)) } fmt.Printf("You're going to delete %s", internal.PrintSingularOrPlural(matchedCachesLen, "cache entry\n\n", "cache entries\n\n")) internal.PrettyPrintTrimmedCacheList(matchedCaches) @@ -50,22 +57,26 @@ func NewCmdDelete() *cobra.Command { Message: "Are you sure you want to delete the cache entries?", Options: []string{"Delete", "Cancel"}, } - err := survey.AskOne(prompt, &choice) + err = survey.AskOne(prompt, &choice) if err != nil { - fmt.Println("Error occured while taking input from user while trying to delete cache") - return + return fmt.Errorf("Error occured while taking input from user while trying to delete cache") } f.Confirm = choice == "Delete" fmt.Println() } if f.Confirm { - cachesDeleted := artifactCache.DeleteCaches(queryParams) + cachesDeleted, err := artifactCache.DeleteCaches(queryParams) + if err != nil { + return err + } + if cachesDeleted > 0 { fmt.Printf("%s Deleted %s with key '%s'\n", internal.RedTick(), internal.PrintSingularOrPlural(cachesDeleted, "cache entry", "cache entries"), f.Key) } else { fmt.Printf("Cache with input key '%s' does not exist\n", f.Key) } } + return nil }, } deleteCmd.Flags().StringVarP(&f.Repo, "repo", "R", "", "Select another repository for finding actions cache.") @@ -99,18 +110,20 @@ EXAMPLES: ` } -func getCacheListWithExactMatch(f types.DeleteOptions, artifactCache service.ArtifactCacheService) []types.ActionsCache { +func getCacheListWithExactMatch(f types.DeleteOptions, artifactCache service.ArtifactCacheService) ([]types.ActionsCache, error) { listOption := types.ListOptions{BaseOptions: types.BaseOptions{Repo: f.Repo, Branch: f.Branch, Key: f.Key}, Limit: 100, Order: "", Sort: ""} queryParams := url.Values{} listOption.GenerateBaseQueryParams(queryParams) - caches := artifactCache.ListAllCaches(queryParams, f.Key) - + caches, err := artifactCache.ListAllCaches(queryParams, f.Key) + if err != nil { + return nil, err + } var exactMatchedKeys []types.ActionsCache for _, cache := range caches { if strings.EqualFold(f.Key, cache.Key) { exactMatchedKeys = append(exactMatchedKeys, cache) } } - return exactMatchedKeys + return exactMatchedKeys, nil } diff --git a/cmd/list.go b/cmd/list.go index 502d30b..fabfa69 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -1,13 +1,14 @@ package cmd import ( + "errors" "fmt" - "log" "net/url" "github.com/actions/gh-actions-cache/internal" "github.com/actions/gh-actions-cache/service" "github.com/actions/gh-actions-cache/types" + "github.com/cli/go-gh/pkg/api" "github.com/spf13/cobra" ) @@ -16,42 +17,59 @@ func NewCmdList() *cobra.Command { f := types.ListOptions{} - var listCmd = &cobra.Command{ + var listCmd = &cobra.Command { Use: "list", Short: "Lists the actions cache", - Run: func(cmd *cobra.Command, args []string) { + RunE: func(cmd *cobra.Command, args []string) error { if len(args) != 0 { - fmt.Printf("Invalid argument(s). Expected 0 received %d\n", len(args)) - fmt.Println(getListHelp()) - return + return fmt.Errorf(fmt.Sprintf("Invalid argument(s). Expected 0 received %d", len(args))) } repo, err := internal.GetRepo(f.Repo) if err != nil { - log.Fatal(err) + return err } + // This will silence the usage (help) message as they are not needed for errors beyond this point + cmd.SilenceUsage = true + err = f.Validate() if err != nil { - log.Fatal(err) + return err } - artifactCache := service.NewArtifactCache(repo, COMMAND, VERSION) + artifactCache, err := service.NewArtifactCache(repo, COMMAND, VERSION) + if err != nil { + return types.HandledError{Message: err.Error(), InnerError: err} + } if f.Branch == "" && f.Key == "" { - totalCacheSize := artifactCache.GetCacheUsage() - fmt.Printf("Total caches size %s\n\n", internal.FormatCacheSize(totalCacheSize)) + totalCacheSize, err := artifactCache.GetCacheUsage() + if err == nil { + fmt.Printf("Total caches size %s\n\n", internal.FormatCacheSize(totalCacheSize)) + } } queryParams := url.Values{} f.GenerateQueryParams(queryParams) - listCacheResponse := artifactCache.ListCaches(queryParams) + listCacheResponse, err := artifactCache.ListCaches(queryParams) + if err != nil { + var httpError api.HTTPError + if errors.As(err, &httpError) && httpError.StatusCode == 404 { + return types.HandledError{Message: "The given repo does not exist.", InnerError: err} + } else if errors.As(err, &httpError) && httpError.StatusCode >= 400 && httpError.StatusCode < 500 { + return types.HandledError{Message: httpError.Message, InnerError: err} + } else { + return types.HandledError{Message: "We could not process your request due to internal error.", InnerError: err} + } + } totalCaches := listCacheResponse.TotalCount caches := listCacheResponse.ActionsCaches fmt.Printf("Showing %d of %d cache entries in %s/%s\n\n", displayedEntriesCount(len(caches), f.Limit), totalCaches, repo.Owner(), repo.Name()) internal.PrettyPrintCacheList(caches) + return nil }, } diff --git a/cmd/list_test.go b/cmd/list_test.go new file mode 100644 index 0000000..66475e1 --- /dev/null +++ b/cmd/list_test.go @@ -0,0 +1,220 @@ +package cmd + +import ( + "errors" + "fmt" + "testing" + "reflect" + + "github.com/actions/gh-actions-cache/internal" + "github.com/stretchr/testify/assert" + "github.com/actions/gh-actions-cache/types" + "gopkg.in/h2non/gock.v1" +) + +func TestListWithIncorrectArguments(t *testing.T) { + t.Cleanup(gock.Off) + + cmd := NewCmdList() + cmd.SetArgs([]string{"keyValue"}) + err := cmd.Execute() + + assert.NotNil(t, err) + assert.Equal(t, err, fmt.Errorf("Invalid argument(s). Expected 0 received 1")) + assert.True(t, gock.IsDone(), internal.PrintPendingMocks(gock.Pending())) +} + +func TestListWithIncorrectRepo(t *testing.T) { + t.Cleanup(gock.Off) + + cmd := NewCmdList() + cmd.SetArgs([]string{"--repo", "testOrg/testRepo/123/123"}) + err := cmd.Execute() + + assert.NotNil(t, err) + assert.Equal(t, err, fmt.Errorf("expected the \"[HOST/]OWNER/REPO\" format, got \"testOrg/testRepo/123/123\"")) + assert.True(t, gock.IsDone(), internal.PrintPendingMocks(gock.Pending())) +} + +func TestListWithNegativeLimit(t *testing.T) { + t.Cleanup(gock.Off) + + cmd := NewCmdList() + cmd.SetArgs([]string{"--limit", "-1", "--repo", "testOrg/testRepo"}) + err := cmd.Execute() + + assert.NotNil(t, err) + assert.Equal(t, err, fmt.Errorf("-1 is not a valid value for limit flag. Allowed values: 1-100")) + assert.True(t, gock.IsDone(), internal.PrintPendingMocks(gock.Pending())) +} + +func TestListWithIncorrectLimit(t *testing.T) { + t.Cleanup(gock.Off) + + cmd := NewCmdList() + cmd.SetArgs([]string{"--limit", "101", "--repo", "testOrg/testRepo"}) + err := cmd.Execute() + + assert.NotNil(t, err) + assert.Equal(t, err, fmt.Errorf("101 is not a valid value for limit flag. Allowed values: 1-100")) + assert.True(t, gock.IsDone(), internal.PrintPendingMocks(gock.Pending())) +} + +func TestListWithIncorrectOrder(t *testing.T) { + t.Cleanup(gock.Off) + + cmd := NewCmdList() + cmd.SetArgs([]string{"--order", "incorrectOrderValue", "--repo", "testOrg/testRepo"}) + err := cmd.Execute() + + assert.NotNil(t, err) + assert.Equal(t, err, fmt.Errorf("incorrectOrderValue is not a valid value for order flag. Allowed values: asc/desc")) + assert.True(t, gock.IsDone(), internal.PrintPendingMocks(gock.Pending())) +} + +func TestListWithIncorrectSort(t *testing.T) { + t.Cleanup(gock.Off) + + cmd := NewCmdList() + cmd.SetArgs([]string{"--sort", "incorrectSortValue", "--repo", "testOrg/testRepo"}) + err := cmd.Execute() + + assert.NotNil(t, err) + assert.Equal(t, err, fmt.Errorf("incorrectSortValue is not a valid value for sort flag. Allowed values: last-used/size/created-at")) + assert.True(t, gock.IsDone(), internal.PrintPendingMocks(gock.Pending())) +} + +func TestListWithIncorrectRepoForListCaches(t *testing.T) { + t.Cleanup(gock.Off) + gock.New("https://api.github.com"). + Get("/repos/testOrg/testRepo/actions/cache/usage"). + Reply(200). + JSON(`{ + "full_name": "t-dedah/vipul-bugbash", + "active_caches_size_in_bytes": 291205, + "active_caches_count": 12 + }`) + + gock.New("https://api.github.com"). + Get("/repos/testOrg/testRepo/actions/caches"). + Reply(404). + JSON(`{ + "message": "Not Found", + "documentation_url": "https://docs.github.com/rest/reference/actions#get-github-actions-cache-list-for-a-repository" + }`) + + cmd := NewCmdList() + cmd.SetArgs([]string{"--repo", "testOrg/testRepo"}) + err := cmd.Execute() + + assert.NotNil(t, err) + assert.Equal(t, reflect.TypeOf(err), reflect.TypeOf(types.HandledError{})) + + var customError types.HandledError + errors.As(err, &customError) + assert.Equal(t, customError.Message, "The given repo does not exist.") + + assert.True(t, gock.IsDone(), internal.PrintPendingMocks(gock.Pending())) +} + +func TestListWithUnauthorizedRequestForListCaches(t *testing.T) { + t.Cleanup(gock.Off) + gock.New("https://api.github.com"). + Get("/repos/testOrg/testRepo/actions/cache/usage"). + Reply(200). + JSON(`{ + "full_name": "t-dedah/vipul-bugbash", + "active_caches_size_in_bytes": 291205, + "active_caches_count": 12 + }`) + + gock.New("https://api.github.com"). + Get("/repos/testOrg/testRepo/actions/caches"). + Reply(401). + JSON(`{ + "message": "Must have admin rights to Repository.", + "documentation_url": "https://docs.github.com/rest/actions/cache#delete-a-github-actions-cache-for-a-repository-using-a-cache-id" + }`) + + cmd := NewCmdList() + cmd.SetArgs([]string{"--repo", "testOrg/testRepo"}) + err := cmd.Execute() + + assert.NotNil(t, err) + assert.Equal(t, reflect.TypeOf(err), reflect.TypeOf(types.HandledError{})) + + var customError types.HandledError + errors.As(err, &customError) + assert.Equal(t, customError.Message, "Must have admin rights to Repository.") + + assert.True(t, gock.IsDone(), internal.PrintPendingMocks(gock.Pending())) +} + +func TestListWithInternalServerErrorForListCaches(t *testing.T) { + t.Cleanup(gock.Off) + gock.New("https://api.github.com"). + Get("/repos/testOrg/testRepo/actions/cache/usage"). + Reply(200). + JSON(`{ + "full_name": "t-dedah/vipul-bugbash", + "active_caches_size_in_bytes": 291205, + "active_caches_count": 12 + }`) + + gock.New("https://api.github.com"). + Get("/repos/testOrg/testRepo/actions/caches"). + Reply(500). + JSON(`{ + "message": "Internal Server Error", + "documentation_url": "https://docs.github.com/rest/reference/actions#get-github-actions-cache-list-for-a-repository" + }`) + + cmd := NewCmdList() + cmd.SetArgs([]string{"--repo", "testOrg/testRepo"}) + err := cmd.Execute() + + assert.NotNil(t, err) + assert.Equal(t, reflect.TypeOf(err), reflect.TypeOf(types.HandledError{})) + + var customError types.HandledError + errors.As(err, &customError) + assert.Equal(t, customError.Message, "We could not process your request due to internal error.") + + assert.True(t, gock.IsDone(), internal.PrintPendingMocks(gock.Pending())) +} + +func TestListSuccess(t *testing.T) { + t.Cleanup(gock.Off) + gock.New("https://api.github.com"). + Get("/repos/testOrg/testRepo/actions/cache/usage"). + Reply(200). + JSON(`{ + "full_name": "t-dedah/vipul-bugbash", + "active_caches_size_in_bytes": 2432967, + "active_caches_count": 1 + }`) + + gock.New("https://api.github.com"). + Get("/repos/testOrg/testRepo/actions/caches"). + Reply(200). + JSON(`{ + "total_count": 1, + "actions_caches": [ + { + "id": 29, + "ref": "refs/heads/master", + "key": "Linux-build-cache-node-modules-3fd22dd3a926d576e2562e8b76a5ff157cd3b986f3d44195acfe7efa6bc05919-8", + "version": "7fcda33c1e1d849a13bcc06f49b9ab64efc01ca9dabe4d7a8d0d387feef4fc88", + "last_accessed_at": "2022-06-22T20:32:45.550000000Z", + "created_at": "2022-06-22T20:32:45.550000000Z", + "size_in_bytes": 2432967 + }] + }`) + + cmd := NewCmdList() + cmd.SetArgs([]string{"--repo", "testOrg/testRepo"}) + err := cmd.Execute() + + assert.Nil(t, err) + assert.True(t, gock.IsDone(), internal.PrintPendingMocks(gock.Pending())) +} diff --git a/go.mod b/go.mod index f41a2d0..4900dbb 100644 --- a/go.mod +++ b/go.mod @@ -5,15 +5,20 @@ go 1.18 require ( github.com/AlecAivazis/survey/v2 v2.3.5 github.com/TwiN/go-color v1.1.0 - github.com/cli/go-gh v0.0.3 + github.com/cli/go-gh v0.0.4-0.20220623035622-91ca4ef447d4 github.com/nleeper/goment v1.4.4 github.com/spf13/cobra v1.4.0 + github.com/stretchr/testify v1.7.0 ) +require github.com/pmezard/go-difflib v1.0.0 // indirect + require ( github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/cli/safeexec v1.0.0 // indirect github.com/cli/shurcooL-graphql v0.0.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect github.com/henvic/httpretty v0.0.6 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect @@ -27,5 +32,6 @@ require ( golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 // indirect golang.org/x/term v0.0.0-20210503060354-a79de5458b56 // indirect golang.org/x/text v0.3.6 // indirect - gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect + gopkg.in/h2non/gock.v1 v1.1.2 + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 9fb0992..fc09715 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,8 @@ github.com/TwiN/go-color v1.1.0 h1:yhLAHgjp2iAxmNjDiVb6Z073NE65yoaPlcki1Q22yyQ= github.com/TwiN/go-color v1.1.0/go.mod h1:aKVf4e1mD4ai2FtPifkDPP5iyoCwiK08YGzGwerjKo0= github.com/cli/go-gh v0.0.3 h1:GcVgUa7q0SeauIRbch3VSUXVij6+c49jtAHv7WuWj5c= github.com/cli/go-gh v0.0.3/go.mod h1:J1eNgrPJYAUy7TwPKj7GW1ibqI+WCiMndtyzrCyZIiQ= +github.com/cli/go-gh v0.0.4-0.20220623035622-91ca4ef447d4 h1:6WrekNBE2Y+Xl9OCl7vsg49SSN68hwaVryfEawQevaQ= +github.com/cli/go-gh v0.0.4-0.20220623035622-91ca4ef447d4/go.mod h1:Y/QFb/VxnXQH0W4VlP+507HVxMzQ430x8kdjUuVcono= github.com/cli/safeexec v1.0.0 h1:0VngyaIyqACHdcMNWfo6+KdUYnqEr2Sg+bSP1pdF+dI= github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q= github.com/cli/shurcooL-graphql v0.0.1 h1:/9J3t9O6p1B8zdBBtQighq5g7DQRItBwuwGh3SocsKM= @@ -24,6 +26,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= github.com/henvic/httpretty v0.0.6 h1:JdzGzKZBajBfnvlMALXXMVQWxWMF/ofTy8C3/OSUTxs= github.com/henvic/httpretty v0.0.6/go.mod h1:X38wLjWXHkXT7r2+uK8LjCMne9rsuNaBLJ+5cU2/Pmo= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= @@ -45,6 +49,8 @@ github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1f github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc= github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw= +github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4= +github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= github.com/nleeper/goment v1.4.4 h1:GlMTpxvhueljArSunzYjN9Ri4SOmpn0Vh2hg2z/IIl8= github.com/nleeper/goment v1.4.4/go.mod h1:zDl5bAyDhqxwQKAvkSXMRLOdCowrdZz53ofRJc4VhTo= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -89,9 +95,13 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY= +gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.0.2 h1:kG1BFyqVHuQoVQiR1bWGnfz/fmHvvuiSPIV7rvl360E= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= diff --git a/internal/test_utils.go b/internal/test_utils.go new file mode 100644 index 0000000..bbdaaef --- /dev/null +++ b/internal/test_utils.go @@ -0,0 +1,16 @@ +package internal + +import ( + "fmt" + "strings" + + "gopkg.in/h2non/gock.v1" +) + +func PrintPendingMocks(mocks []gock.Mock) string { + paths := []string{} + for _, mock := range mocks { + paths = append(paths, mock.Request().URLStruct.String()) + } + return fmt.Sprintf("%d unmatched mocks: %s", len(paths), strings.Join(paths, ", ")) +} \ No newline at end of file diff --git a/internal/utils.go b/internal/utils.go index 19e9b21..dbc0ce8 100644 --- a/internal/utils.go +++ b/internal/utils.go @@ -47,7 +47,7 @@ func FormatCacheSize(size_in_bytes float64) string { func PrettyPrintCacheList(caches []types.ActionsCache) { fd := os.Stdout.Fd() ws, _ := term.GetWinsize(fd) - width := math.Min(float64(ws.Width), 180) + width := math.Max(math.Min(float64(ws.Width), 180), 100) sizeWidth := SIZE_COLUMN_WIDTH // hard-coded size as the content is scoped timeWidth := LAST_ACCESSED_AT_COLUMN_WIDTH // hard-coded size as the content is scoped @@ -90,7 +90,7 @@ func getFormattedCacheInfo(cache types.ActionsCache, keyWidth int, sizeWidth int size := trimOrPad(fmt.Sprintf("[%s]", FormatCacheSize(cache.SizeInBytes)), sizeWidth) ref := trimOrPad(cache.Ref, refWidth) time := trimOrPad(lastAccessedTime(cache.LastAccessedAt), timeWidth) - return fmt.Sprintf(" %s %s %s %s", key, size, ref, time) + return fmt.Sprintf("%s %s %s %s", key, size, ref, time) } func RedTick() string { diff --git a/internal/utils_test.go b/internal/utils_test.go new file mode 100644 index 0000000..d2e2717 --- /dev/null +++ b/internal/utils_test.go @@ -0,0 +1,55 @@ +package internal + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetRepo_IncorrectRepoString(t *testing.T) { + r := "testOrg/testRepo/123/123" + repo, err := GetRepo(r) + + assert.Error(t, err) + assert.Nil(t, repo) + assert.Equal(t, err.Error(), fmt.Sprintf("expected the \"[HOST/]OWNER/REPO\" format, got \"%s\"", r)) +} + +func TestGetRepo_CorrectRepoString(t *testing.T) { + r := "testOrg/testRepo" + repo, err := GetRepo(r) + + assert.NotNil(t, repo) + assert.Nil(t, err) + assert.Equal(t, repo.Host(), "github.com") + assert.Equal(t, repo.Owner(), "testOrg") + assert.Equal(t, repo.Name(), "testRepo") +} + +func TestGetRepo_CorrectRepoStringWithCustomHost(t *testing.T) { + r := "api.testEnterprise.com/testOrg/testRepo" + repo, err := GetRepo(r) + + assert.NotNil(t, repo) + assert.Nil(t, err) + assert.Equal(t, repo.Host(), "api.testEnterprise.com") + assert.Equal(t, repo.Owner(), "testOrg") + assert.Equal(t, repo.Name(), "testRepo") +} + +func TestFormatCacheSize_MB(t *testing.T) { + cacheSizeInBytes := 1024 * 1024 * 1.5 + cacheSizeDetailString := FormatCacheSize(cacheSizeInBytes) + + assert.NotNil(t, cacheSizeDetailString) + assert.Equal(t, cacheSizeDetailString, "1.50 MB") +} + +func TestFormatCacheSize_GB(t *testing.T) { + cacheSizeInBytes := 1024 * 1024 * 1024 * 1.5 + cacheSizeDetailString := FormatCacheSize(cacheSizeInBytes) + + assert.NotNil(t, cacheSizeDetailString) + assert.Equal(t, cacheSizeDetailString, "1.50 GB") +} \ No newline at end of file diff --git a/service/actions_cache.go b/service/actions_cache.go index 2bf3616..9ce74b7 100644 --- a/service/actions_cache.go +++ b/service/actions_cache.go @@ -1,9 +1,7 @@ package service import ( - "errors" "fmt" - "log" "math" "net/url" "strconv" @@ -15,10 +13,10 @@ import ( ) type ArtifactCacheService interface { - GetCacheUsage() float64 - ListCaches(queryParams url.Values) types.ListApiResponse - DeleteCaches(queryParams url.Values) int - ListAllCaches(queryParams url.Values, key string) []types.ActionsCache + GetCacheUsage() (float64, error) + ListCaches(queryParams url.Values) (types.ListApiResponse, error) + DeleteCaches(queryParams url.Values) (int, error) + ListAllCaches(queryParams url.Values, key string) ([]types.ActionsCache, error) } type ArtifactCache struct { @@ -26,66 +24,69 @@ type ArtifactCache struct { repo ghRepo.Repository } -func NewArtifactCache(repo ghRepo.Repository, command string, version string) ArtifactCacheService { +func NewArtifactCache(repo ghRepo.Repository, command string, version string) (ArtifactCacheService, error) { opts := api.ClientOptions{ Host: repo.Host(), Headers: map[string]string{"User-Agent": fmt.Sprintf("gh-actions-cache/%s/%s", version, command)}, } restClient, err := gh.RESTClient(&opts) if err != nil { - log.Fatal(err) + return nil, err } - return &ArtifactCache{HttpClient: restClient, repo: repo} + return &ArtifactCache{HttpClient: restClient, repo: repo}, nil } -func (a *ArtifactCache) GetCacheUsage() float64 { +func (a *ArtifactCache) GetCacheUsage() (float64, error) { pathComponent := fmt.Sprintf("repos/%s/%s/actions/cache/usage", a.repo.Owner(), a.repo.Name()) var apiResults types.RepoLevelUsageApiResponse err := a.HttpClient.Get(pathComponent, &apiResults) if err != nil { - log.Fatal(err) + return -1, err } - return apiResults.ActiveCacheSizeInBytes + return apiResults.ActiveCacheSizeInBytes, nil } -func (a *ArtifactCache) ListCaches(queryParams url.Values) types.ListApiResponse { +func (a *ArtifactCache) ListCaches(queryParams url.Values) (types.ListApiResponse, error) { pathComponent := fmt.Sprintf("repos/%s/%s/actions/caches", a.repo.Owner(), a.repo.Name()) var apiResults types.ListApiResponse err := a.HttpClient.Get(pathComponent+"?"+queryParams.Encode(), &apiResults) + if err != nil { - log.Fatal(err) + return types.ListApiResponse{}, err } - return apiResults + return apiResults, nil } -func (a *ArtifactCache) DeleteCaches(queryParams url.Values) int { +func (a *ArtifactCache) DeleteCaches(queryParams url.Values) (int, error) { pathComponent := fmt.Sprintf("repos/%s/%s/actions/caches", a.repo.Owner(), a.repo.Name()) var apiResults types.DeleteApiResponse err := a.HttpClient.Delete(pathComponent+"?"+queryParams.Encode(), &apiResults) if err != nil { - var httpError api.HTTPError - if errors.As(err, &httpError) && httpError.StatusCode == 404 { - return 0 - } else { - log.Fatal(err) - } + return 0, err } - return apiResults.TotalCount + return apiResults.TotalCount, nil } -func (a *ArtifactCache) ListAllCaches(queryParams url.Values, key string) []types.ActionsCache { +func (a *ArtifactCache) ListAllCaches(queryParams url.Values, key string) ([]types.ActionsCache, error) { var listApiResponse types.ListApiResponse - listApiResponse = a.ListCaches(queryParams) + listApiResponse, err := a.ListCaches(queryParams) + if err != nil { + return nil, err + } + caches := listApiResponse.ActionsCaches totalCaches := listApiResponse.TotalCount if totalCaches > 100 { for page := 2; page <= int(math.Ceil(float64(listApiResponse.TotalCount)/100)); page++ { queryParams.Set("page", strconv.Itoa(page)) - listApiResponse = a.ListCaches(queryParams) + listApiResponse, err := a.ListCaches(queryParams) + if err != nil { + return nil, err + } caches = append(caches, listApiResponse.ActionsCaches...) } } - return caches + return caches, nil } diff --git a/service/actions_cache_test.go b/service/actions_cache_test.go new file mode 100644 index 0000000..fba2c85 --- /dev/null +++ b/service/actions_cache_test.go @@ -0,0 +1,199 @@ +package service + +import ( + "errors" + "net/url" + "testing" + + "github.com/actions/gh-actions-cache/internal" + "github.com/actions/gh-actions-cache/types" + "github.com/cli/go-gh/pkg/api" + "github.com/stretchr/testify/assert" + "gopkg.in/h2non/gock.v1" +) + +const VERSION string = "0.0.1" + +func TestGetCacheUsage_CorrectRepo(t *testing.T) { + t.Cleanup(gock.Off) + + gock.New("https://api.github.com"). + Get("/repos/testOrg/testRepo/actions/cache/usage"). + Reply(200). + JSON(`{ + "full_name": "testOrg/testRepo", + "active_caches_size_in_bytes": 291205, + "active_caches_count": 12 + }`) + + repo, err := internal.GetRepo("testOrg/testRepo") + assert.Nil(t, err) + + artifactCache, err := NewArtifactCache(repo, "list", VERSION) + assert.Nil(t, err) + totalCacheSize, err := artifactCache.GetCacheUsage() + + assert.Equal(t, totalCacheSize, float64(291205)) + assert.Nil(t, err) + assert.True(t, gock.IsDone(), internal.PrintPendingMocks(gock.Pending())) +} + +func TestGetCacheUsage_IncorrectRepo(t *testing.T) { + t.Cleanup(gock.Off) + + gock.New("https://api.github.com"). + Get("/repos/testOrg/wrongRepo/actions/cache/usage"). + Reply(404). + JSON(`{ + "message": "Not Found", + "documentation_url": "https://docs.github.com/rest/reference/actions#get-github-actions-cache-usage-for-a-repository" + }`) + + repo, err := internal.GetRepo("testOrg/wrongRepo") + assert.Nil(t, err) + + artifactCache, err := NewArtifactCache(repo, "list", VERSION) + assert.Nil(t, err) + totalCacheSize, err := artifactCache.GetCacheUsage() + var httpError api.HTTPError + errors.As(err, &httpError) + + assert.NotNil(t, err) + assert.Equal(t, httpError.StatusCode, 404) + assert.Equal(t, httpError.Message, "Not Found") + assert.Equal(t, totalCacheSize, float64(-1)) + assert.True(t, gock.IsDone(), internal.PrintPendingMocks(gock.Pending())) +} + +func TestListCaches_Success(t *testing.T) { + t.Cleanup(gock.Off) + + gock.New("https://api.github.com"). + Get("/repos/testOrg/testRepo/actions/caches"). + Reply(200). + JSON(`{ + "total_count": 1, + "actions_caches": [ + { + "id": 29, + "ref": "refs/heads/master", + "key": "Linux-build-cache-node-modules-3fd22dd3a926d576e2562e8b76a5ff157cd3b986f3d44195acfe7efa6bc05919-8", + "version": "7fcda33c1e1d849a13bcc06f49b9ab64efc01ca9dabe4d7a8d0d387feef4fc88", + "last_accessed_at": "2022-06-22T20:32:45.550000000Z", + "created_at": "2022-06-22T20:32:45.550000000Z", + "size_in_bytes": 2432967 + }] + }`) + + repo, err := internal.GetRepo("testOrg/testRepo") + assert.Nil(t, err) + + f := types.ListOptions{BaseOptions: types.BaseOptions{Repo: "testOrg/testRepo"}, Limit: 30} + queryParams := url.Values{} + f.GenerateQueryParams(queryParams) + + artifactCache, err := NewArtifactCache(repo, "list", VERSION) + assert.Nil(t, err) + listCacheResponse, err := artifactCache.ListCaches(queryParams) + + assert.Nil(t, err) + assert.NotNil(t, listCacheResponse) + assert.Equal(t, listCacheResponse.TotalCount, 1) + assert.Equal(t, len(listCacheResponse.ActionsCaches), 1) + assert.Equal(t, listCacheResponse.ActionsCaches[0].Id, 29) + assert.True(t, gock.IsDone(), internal.PrintPendingMocks(gock.Pending())) +} + +func TestListCaches_Failure(t *testing.T) { + t.Cleanup(gock.Off) + + gock.New("https://api.github.com"). + Get("/repos/testOrg/testRepo/actions/caches"). + Reply(404). + JSON(`{ + "message": "Not Found", + "documentation_url": "https://docs.github.com/rest/reference/actions#get-github-actions-cache-list-for-a-repository" + }`) + + repo, err := internal.GetRepo("testOrg/testRepo") + assert.Nil(t, err) + + f := types.ListOptions{BaseOptions: types.BaseOptions{Repo: "testOrg/testRepo"}, Limit: 30} + queryParams := url.Values{} + f.GenerateQueryParams(queryParams) + + artifactCache, err := NewArtifactCache(repo, "list", VERSION) + assert.Nil(t, err) + listCacheResponse, err := artifactCache.ListCaches(queryParams) + var httpError api.HTTPError + errors.As(err, &httpError) + + assert.NotNil(t, err) + assert.Equal(t, httpError.StatusCode, 404) + assert.Equal(t, httpError.Message, "Not Found") + assert.Equal(t, listCacheResponse, types.ListApiResponse{}) + assert.True(t, gock.IsDone(), internal.PrintPendingMocks(gock.Pending())) +} + +func TestDeleteCaches_Success(t *testing.T) { + t.Cleanup(gock.Off) + + gock.New("https://api.github.com"). + Delete("/repos/testOrg/testRepo/actions/caches"). + Reply(200). + JSON(`{ + "total_count": 1, + "actions_caches": [ + { + "id": 29, + "ref": "refs/heads/master", + "key": "Linux-build-cache-node-modules-3fd22dd3a926d576e2562e8b76a5ff157cd3b986f3d44195acfe7efa6bc05919-8", + "version": "7fcda33c1e1d849a13bcc06f49b9ab64efc01ca9dabe4d7a8d0d387feef4fc88", + "last_accessed_at": "2022-06-22T20:32:45.550000000Z", + "created_at": "2022-06-22T20:32:45.550000000Z", + "size_in_bytes": 2432967 + }] + }`) + + repo, err := internal.GetRepo("testOrg/testRepo") + assert.Nil(t, err) + + f := types.DeleteOptions{BaseOptions: types.BaseOptions{Repo: "testOrg/testRepo"}} + queryParams := url.Values{} + f.GenerateBaseQueryParams(queryParams) + + artifactCache, err := NewArtifactCache(repo, "delete", VERSION) + assert.Nil(t, err) + deletedCache, err := artifactCache.DeleteCaches(queryParams) + + assert.Nil(t, err) + assert.Equal(t, deletedCache, 1) + assert.True(t, gock.IsDone(), internal.PrintPendingMocks(gock.Pending())) +} + +func TestDeleteCaches_Failure(t *testing.T) { + t.Cleanup(gock.Off) + + gock.New("https://api.github.com"). + Delete("/repos/testOrg/testRepo/actions/caches"). + Reply(404). + JSON(`{ + "message": "Not Found", + "documentation_url": "https://docs.github.com/rest/reference/actions#get-github-actions-cache-list-for-a-repository" + }`) + + repo, err := internal.GetRepo("testOrg/testRepo") + assert.Nil(t, err) + + f := types.DeleteOptions{BaseOptions: types.BaseOptions{Repo: "testOrg/testRepo"}} + queryParams := url.Values{} + f.GenerateBaseQueryParams(queryParams) + + artifactCache, err := NewArtifactCache(repo, "delete", VERSION) + assert.Nil(t, err) + deletedCache, err := artifactCache.DeleteCaches(queryParams) + + assert.NotNil(t, err) + assert.Equal(t, deletedCache, 0) + assert.True(t, gock.IsDone(), internal.PrintPendingMocks(gock.Pending())) +} diff --git a/types/errors.go b/types/errors.go new file mode 100644 index 0000000..634e151 --- /dev/null +++ b/types/errors.go @@ -0,0 +1,16 @@ +package types + +import ( + "fmt" +) + +type HandledError struct { + Message string + InnerError error +} + +// Allow HandledError to satisfy error interface. +func (err HandledError) Error() string { + return fmt.Sprintf(err.Message) +} +