From ee64f66913b91c7774faf2a550906abe87c0c9f4 Mon Sep 17 00:00:00 2001 From: Stella Lok Date: Wed, 12 Aug 2020 15:45:53 +0800 Subject: [PATCH 1/3] add commands to list categories and add/remove categorizations --- api/api.go | 196 +++++++++- cmd/orb.go | 112 ++++++ cmd/orb_test.go | 352 +++++++++++++++++- .../gql_orb_category_list/first_response.json | 115 ++++++ .../pretty_json_output.json | 72 ++++ .../second_response.json | 24 ++ 6 files changed, 868 insertions(+), 3 deletions(-) create mode 100644 cmd/testdata/gql_orb_category_list/first_response.json create mode 100644 cmd/testdata/gql_orb_category_list/pretty_json_output.json create mode 100644 cmd/testdata/gql_orb_category_list/second_response.json diff --git a/api/api.go b/api/api.go index 80b32cc6b..e4c341c18 100644 --- a/api/api.go +++ b/api/api.go @@ -18,6 +18,13 @@ import ( "gopkg.in/yaml.v3" ) +type UpdateOrbCategorizationRequestType int + +const ( + Add UpdateOrbCategorizationRequestType = iota + Remove +) + // GQLErrorsCollection is a slice of errors returned by the GraphQL server. // Each error is made up of a GQLResponseError type. type GQLErrorsCollection []GQLResponseError @@ -218,6 +225,43 @@ type OrbsForListing struct { Namespace string `json:"namespace,omitempty"` } +// OrbCategoryIDResponse matches the GQL response for fetching an Orb category's id +type OrbCategoryIDResponse struct { + OrbCategoryByName struct { + ID string + } +} + +// AddOrRemoveOrbCategorizationResponse type matches the data shape of the GQL response for +// adding or removing an orb categorization +type AddOrRemoveOrbCategorizationResponse map[string]AddOrRemoveOrbCategorizationData + +type AddOrRemoveOrbCategorizationData struct { + CategoryId string + OrbId string + Errors GQLErrorsCollection +} + +// OrbListResponse type matches the result from GQL. +// So that we can use mapstructure to convert from nested maps to a strongly typed struct. +type OrbCategoryListResponse struct { + OrbCategories struct { + TotalCount int + Edges []struct { + Cursor string + Node OrbCategory + } + PageInfo struct { + HasNextPage bool + } + } +} + +// OrbCategoriesForListing is a container type for multiple orb categories for deserializing back into JSON. +type OrbCategoriesForListing struct { + OrbCategories []OrbCategory `json:"orbCategories"` +} + // SortBy allows us to sort a collection of orbs by builds, projects, or orgs from the last 30 days of data. func (orbs *OrbsForListing) SortBy(sortBy string) { switch sortBy { @@ -336,6 +380,8 @@ type Orb struct { Jobs map[string]OrbElement Executors map[string]OrbElement Versions []OrbVersion + + Categories []OrbCategory } // OrbVersion wraps the GQL result used by OrbSource and OrbInfo @@ -347,6 +393,11 @@ type OrbVersion struct { CreatedAt string } +type OrbCategory struct { + ID string `json:"id"` + Name string `json:"name"` +} + // #nosec func loadYaml(path string) (string, error) { var err error @@ -950,7 +1001,11 @@ func OrbInfo(cl *client.Client, orbRef string) (*OrbVersion, error) { orb { id createdAt - name + name + categories { + id + name + } statistics { last30DaysBuildCount, last30DaysProjectCount, @@ -1185,3 +1240,142 @@ func IntrospectionQuery(cl *client.Client) (*IntrospectionResponse, error) { return &response, err } + +// OrbCategoryID fetches an orb returning the ID +func OrbCategoryID(cl *client.Client, name string) (*OrbCategoryIDResponse, error) { + var response OrbCategoryIDResponse + + query := ` + query ($name: String!) { + orbCategoryByName(name: $name) { + id + } + } + ` + + request := client.NewRequest(query) + request.SetToken(cl.Token) + + request.Var("name", name) + + err := cl.Run(request, &response) + + // If there is an error, or the request was successful, return now. + if err != nil || response.OrbCategoryByName.ID != "" { + return &response, err + } + + return nil, fmt.Errorf("the '%s' category does not exist. Did you misspell the category name? To see the list of category names, please run 'circleci orb list-categories'.", name) +} + +// OrbAddOrRemoveOrbCategorization adds or removes an orb categorization +func OrbAddOrRemoveOrbCategorization(cl *client.Client, namespace string, orb string, categoryName string, updateType UpdateOrbCategorizationRequestType) error { + orbId, err := OrbID(cl, namespace, orb) + if err != nil { + return err + } + + categoryId, err := OrbCategoryID(cl, categoryName) + if err != nil { + return err + } + + var response AddOrRemoveOrbCategorizationResponse + + var mutationName string + if updateType == Add { + mutationName = "addCategorizationToOrb" + } else if updateType == Remove { + mutationName = "removeCategorizationFromOrb" + } + + if mutationName == "" { + return fmt.Errorf("Internal error - invalid update type %d", updateType) + } + + query := ` + mutation($orbId: UUID!, $categoryId: UUID!) { + ` + mutationName + `( + orbId: $orbId, + categoryId: $categoryId + ) { + orbId + categoryId + errors { + message + type + } + } + } + ` + + request := client.NewRequest(query) + request.SetToken(cl.Token) + + request.Var("orbId", orbId.Orb.ID) + request.Var("categoryId", categoryId.OrbCategoryByName.ID) + + err = cl.Run(request, &response) + + responseData := response[mutationName] + + if len(responseData.Errors) > 0 { + return &responseData.Errors + } + + if err != nil { + return errors.Wrap(err, "Unable to add/remove orb categorization") + } + + return nil +} + +// ListOrbCategories queries the API to find all categories. +// Returns a collection of OrbCategory objects containing their relevant data. +func ListOrbCategories(cl *client.Client) (*OrbCategoriesForListing, error) { + + query := ` + query ListOrbCategories($after: String!) { + orbCategories(first: 20, after: $after) { + totalCount + edges { + cursor + node { + id + name + } + } + pageInfo { + hasNextPage + } + } + } +` + + var orbCategories OrbCategoriesForListing + + var result OrbCategoryListResponse + currentCursor := "" + + for { + request := client.NewRequest(query) + request.Var("after", currentCursor) + + err := cl.Run(request, &result) + if err != nil { + return nil, errors.Wrap(err, "GraphQL query failed") + } + + for i := range result.OrbCategories.Edges { + edge := result.OrbCategories.Edges[i] + currentCursor = edge.Cursor + orbCategories.OrbCategories = append(orbCategories.OrbCategories, edge.Node) + } + + if !result.OrbCategories.PageInfo.HasNextPage { + break + } + + } + return &orbCategories, nil +} diff --git a/cmd/orb.go b/cmd/orb.go index f2c87e551..48dd7e59d 100644 --- a/cmd/orb.go +++ b/cmd/orb.go @@ -256,6 +256,38 @@ Please note that at this time all orbs created in the registry are world-readabl Args: cobra.ExactArgs(1), } + listCategoriesCommand := &cobra.Command{ + Use: "list-categories", + Short: "List categories", + Args: cobra.ExactArgs(0), + RunE: func(_ *cobra.Command, _ []string) error { + return listOrbCategories(opts) + }, + } + + listCategoriesCommand.PersistentFlags().BoolVar(&opts.listJSON, "json", false, "print output as json instead of human-readable") + if err := listCategoriesCommand.PersistentFlags().MarkHidden("json"); err != nil { + panic(err) + } + + addCategorizationToOrbCommand := &cobra.Command{ + Use: "add-to-category / \"\"", + Short: "Add an orb to a category", + Args: cobra.ExactArgs(2), + RunE: func(_ *cobra.Command, _ []string) error { + return addOrRemoveOrbCategorization(opts, api.Add) + }, + } + + removeCategorizationFromOrbCommand := &cobra.Command{ + Use: "remove-from-category / \"\"", + Short: "Remove an orb from a category", + Args: cobra.ExactArgs(2), + RunE: func(_ *cobra.Command, _ []string) error { + return addOrRemoveOrbCategorization(opts, api.Remove) + }, + } + orbCreate.Flags().BoolVar(&opts.integrationTesting, "integration-testing", false, "Enable test mode to bypass interactive UI.") if err := orbCreate.Flags().MarkHidden("integration-testing"); err != nil { panic(err) @@ -286,6 +318,9 @@ Please note that at this time all orbs created in the registry are world-readabl orbCommand.AddCommand(sourceCommand) orbCommand.AddCommand(orbInfoCmd) orbCommand.AddCommand(orbPack) + orbCommand.AddCommand(addCategorizationToOrbCommand) + orbCommand.AddCommand(removeCategorizationFromOrbCommand) + orbCommand.AddCommand(listCategoriesCommand) return orbCommand } @@ -464,6 +499,37 @@ func logOrbs(orbCollection *api.OrbsForListing, opts orbOptions) error { return nil } +func orbCategoryCollectionToString(orbCategoryCollection *api.OrbCategoriesForListing, opts orbOptions) (string, error) { + var result string + + if opts.listJSON { + orbCategoriesJSON, err := json.MarshalIndent(orbCategoryCollection, "", " ") + if err != nil { + return "", errors.Wrapf(err, "Failed to convert to convert to JSON") + } + result = string(orbCategoriesJSON) + } else { + var categories []string = make([]string, 0, len(orbCategoryCollection.OrbCategories)) + for _, orbCategory := range orbCategoryCollection.OrbCategories { + categories = append(categories, orbCategory.Name) + } + result = strings.Join(categories, "\n") + } + + return result, nil +} + +func logOrbCategories(orbCategoryCollection *api.OrbCategoriesForListing, opts orbOptions) error { + result, err := orbCategoryCollectionToString(orbCategoryCollection, opts) + if err != nil { + return err + } + + fmt.Println(result) + + return nil +} + var validSortFlag = map[string]bool{ "builds": true, "projects": true, @@ -748,6 +814,14 @@ func orbInfo(opts orbOptions) error { fmt.Printf("Projects: %d\n", info.Orb.Statistics.Last30DaysProjectCount) fmt.Printf("Orgs: %d\n", info.Orb.Statistics.Last30DaysOrganizationCount) + if len(info.Orb.Categories) > 0 { + fmt.Println("") + fmt.Println("## Categories:") + for _, category := range info.Orb.Categories { + fmt.Printf("%s\n", category.Name) + } + } + orbVersionSplit := strings.Split(ref, "@") orbRef := orbVersionSplit[0] fmt.Printf(` @@ -758,6 +832,44 @@ https://circleci.com/orbs/registry/orb/%s return nil } +func listOrbCategories(opts orbOptions) error { + orbCategories, err := api.ListOrbCategories(opts.cl) + if err != nil { + return errors.Wrapf(err, "Failed to list orb categories") + } + + return logOrbCategories(orbCategories, opts) +} + +func addOrRemoveOrbCategorization(opts orbOptions, updateType api.UpdateOrbCategorizationRequestType) error { + var err error + + namespace, orb, err := references.SplitIntoOrbAndNamespace(opts.args[0]) + + if err != nil { + return err + } + + err = api.OrbAddOrRemoveOrbCategorization(opts.cl, namespace, orb, opts.args[1], updateType) + + if err != nil { + var errorString = "Failed to add orb %s to category %s" + if updateType == api.Remove { + errorString = "Failed to remove orb %s from category %s" + } + return errors.Wrapf(err, errorString, opts.args[0], opts.args[1]) + } + + var output = `%s is successfully added to the "%s" category.` + "\n" + if updateType == api.Remove { + output = `%s is successfully removed from the "%s" category.` + "\n" + } + + fmt.Printf(output, opts.args[0], opts.args[1]) + + return nil +} + type OrbSchema struct { Version float32 `yaml:"version,omitempty"` Description string `yaml:"description,omitempty"` diff --git a/cmd/orb_test.go b/cmd/orb_test.go index 30ce267a6..eb7e11ac1 100644 --- a/cmd/orb_test.go +++ b/cmd/orb_test.go @@ -12,6 +12,7 @@ import ( "gotest.tools/golden" + "github.com/CircleCI-Public/circleci-cli/api" "github.com/CircleCI-Public/circleci-cli/client" "github.com/CircleCI-Public/circleci-cli/clitest" . "github.com/onsi/ginkgo" @@ -2377,7 +2378,11 @@ foo.bar/account/api`)) orb { id createdAt - name + name + categories { + id + name + } statistics { last30DaysBuildCount, last30DaysProjectCount, @@ -2407,7 +2412,17 @@ foo.bar/account/api`)) "orb": { "id": "bb604b45-b6b0-4b81-ad80-796f15eddf87", "createdAt": "2018-09-24T08:53:37.086Z", - "name": "my/orb", + "name": "my/orb", + "categories": [ + { + "id": "cc604b45-b6b0-4b81-ad80-796f15eddf87", + "name": "Infra Automation" + }, + { + "id": "dd604b45-b6b0-4b81-ad80-796f15eddf87", + "name": "Testing" + } + ], "versions": [ { "version": "0.0.1", @@ -2447,6 +2462,10 @@ Builds: 0 Projects: 0 Orgs: 0 +## Categories: +Infra Automation +Testing + Learn more about this orb online in the CircleCI Orb Registry: https://circleci.com/orbs/registry/orb/my/orb `)) @@ -2580,6 +2599,335 @@ https://circleci.com/orbs/registry/orb/my/orb Eventually(session).Should(clitest.ShouldFail()) }) }) + + Describe("orb categories", func() { + Context("with mock server", func() { + DescribeTable("sends multiple requests when there are more than 1 page of orb categories", + func(json bool) { + argList := []string{"orb", "list-categories", + "--skip-update-check", + "--host", tempSettings.TestServer.URL()} + if json { + argList = append(argList, "--json") + } + + command = exec.Command(pathCLI, + argList..., + ) + + query := ` + query ListOrbCategories($after: String!) { + orbCategories(first: 20, after: $after) { + totalCount + edges { + cursor + node { + id + name + } + } + pageInfo { + hasNextPage + } + } + } +` + + firstRequest := client.NewRequest(query) + firstRequest.Variables["after"] = "" + + firstRequestEncoded, err := firstRequest.Encode() + Expect(err).ShouldNot(HaveOccurred()) + + secondRequest := client.NewRequest(query) + secondRequest.Variables["after"] = "Testing" + + secondRequestEncoded, err := secondRequest.Encode() + Expect(err).ShouldNot(HaveOccurred()) + + tmpBytes := golden.Get(GinkgoT(), filepath.FromSlash("gql_orb_category_list/first_response.json")) + firstResponse := string(tmpBytes) + + tmpBytes = golden.Get(GinkgoT(), filepath.FromSlash("gql_orb_category_list/second_response.json")) + secondResponse := string(tmpBytes) + + tmpBytes = golden.Get(GinkgoT(), filepath.FromSlash("gql_orb_category_list/pretty_json_output.json")) + expectedOutput := string(tmpBytes) + + tempSettings.AppendPostHandler("", clitest.MockRequestResponse{ + Status: http.StatusOK, + Request: firstRequestEncoded.String(), + Response: firstResponse, + }) + tempSettings.AppendPostHandler("", clitest.MockRequestResponse{ + Status: http.StatusOK, + Request: secondRequestEncoded.String(), + Response: secondResponse, + }) + + By("running the command") + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session).Should(gexec.Exit(0)) + Expect(tempSettings.TestServer.ReceivedRequests()).Should(HaveLen(2)) + + completeOutput := string(session.Wait().Out.Contents()) + if json { + Expect(completeOutput).Should(MatchJSON(expectedOutput)) + } + }, + Entry("with --json", true), + Entry("without --json", false), + ) + }) + }) + + Describe("Add/remove orb categorization", func() { + var ( + orbId string + orbNamespaceName string + orbFullName string + orbName string + categoryId string + categoryName string + ) + + BeforeEach(func() { + orbId = "bb604b45-b6b0-4b81-ad80-796f15eddf87" + orbNamespaceName = "bar-ns" + orbName = "foo-orb" + orbFullName = orbNamespaceName + "/" + orbName + categoryId = "cc604b45-b6b0-4b81-ad80-796f15eddf87" + categoryName = "Cloud Platform" + }) + + Context("with mock server", func() { + DescribeTable("add/remove orb categorization", + func(mockErrorResponse bool, updateType api.UpdateOrbCategorizationRequestType) { + commandName := "add-to-category" + operationName := "addCategorizationToOrb" + expectedOutputSegment := "added to" + if updateType == api.Remove { + commandName = "remove-from-category" + operationName = "removeCategorizationFromOrb" + expectedOutputSegment = "removed from" + } + + command = exec.Command(pathCLI, + "orb", commandName, + "--skip-update-check", + "--token", token, + "--host", tempSettings.TestServer.URL(), + orbFullName, categoryName) + + gqlOrbIDResponse := fmt.Sprintf(`{ + "orb": { + "id": "%s" + } + }`, orbId) + + expectedOrbIDRequest := fmt.Sprintf(`{ + "query": "\n\tquery ($name: String!, $namespace: String) {\n\t\torb(name: $name) {\n\t\t id\n\t\t}\n\t\tregistryNamespace(name: $namespace) {\n\t\t id\n\t\t}\n\t }\n\t ", + "variables": { + "name": "%s", + "namespace": "%s" + } + }`, orbFullName, orbNamespaceName) + + expectedCategoryIDRequest := fmt.Sprintf(`{ + "query": "\n\tquery ($name: String!) {\n\t\torbCategoryByName(name: $name) {\n\t\t id\n\t\t}\n\t}\n\t ", + "variables": { + "name": "%s" + } + }`, categoryName) + + gqlCategoryIDResponse := fmt.Sprintf(`{ + "orbCategoryByName": { + "id": "%s" + } + }`, categoryId) + + expectedOrbCategorizationRequest := fmt.Sprintf(`{ + "query": "\n\t\tmutation($orbId: UUID!, $categoryId: UUID!) {\n\t%s(\n\t\t\t\torbId: $orbId,\n\t\t\t\tcategoryId: $categoryId\n\t\t\t) {\n\t\t\t\torbId\n\t\t\t\tcategoryId\n\t\t\t\terrors {\n\t\t\t\t\tmessage\n\t\t\t\t\ttype\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t", + "variables": { + "categoryId": "%s", + "orbId": "%s" + } + }`, operationName, categoryId, orbId) + + gqlCategorizationResponse := fmt.Sprintf(`{ + "%s": { + "orbId": "%s", + "categoryId": "%s", + "errors": [] + } + }`, operationName, orbId, categoryId) + + if mockErrorResponse { + gqlCategorizationResponse = fmt.Sprintf(`{ + "%s": { + "orbId": "", + "categoryId": "", + "errors": [{ + "message": "Mock error message", + "type": "Mock error from server" + }] + } + }`, operationName) + } + + tempSettings.AppendPostHandler(token, clitest.MockRequestResponse{ + Status: http.StatusOK, + Request: expectedOrbIDRequest, + Response: gqlOrbIDResponse}) + + tempSettings.AppendPostHandler(token, clitest.MockRequestResponse{ + Status: http.StatusOK, + Request: expectedCategoryIDRequest, + Response: gqlCategoryIDResponse}) + + tempSettings.AppendPostHandler(token, clitest.MockRequestResponse{ + Status: http.StatusOK, + Request: expectedOrbCategorizationRequest, + Response: gqlCategorizationResponse}) + + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + Expect(err).ShouldNot(HaveOccurred()) + + stdout := session.Wait().Out.Contents() + if mockErrorResponse { + Eventually(session).Should(clitest.ShouldFail()) + errorMsg := fmt.Sprintf(`Error: Failed to add orb %s to category %s: Mock error message`, orbFullName, categoryName) + if updateType == api.Remove { + errorMsg = fmt.Sprintf(`Error: Failed to remove orb %s from category %s: Mock error message`, orbFullName, categoryName) + } + Eventually(session.Err).Should(gbytes.Say(errorMsg)) + } else { + Eventually(session).Should(gexec.Exit(0)) + Expect(string(stdout)).To(ContainSubstring(fmt.Sprintf(`%s is successfully %s the "%s" category.`, orbFullName, expectedOutputSegment, categoryName))) + } + }, + Entry("add categorization success", false, api.Add), + Entry("remove categorization success", false, api.Remove), + Entry("server error on adding categorization", true, api.Add), + Entry("server error on removing categorization", true, api.Remove), + ) + }) + Context("with mock server", func() { + DescribeTable("orb does not exist", + func(updateType api.UpdateOrbCategorizationRequestType) { + commandName := "add-to-category" + if updateType == api.Remove { + commandName = "remove-from-category" + } + + command = exec.Command(pathCLI, + "orb", commandName, + "--skip-update-check", + "--token", token, + "--host", tempSettings.TestServer.URL(), + orbFullName, categoryName) + + expectedOrbIDRequest := fmt.Sprintf(`{ + "query": "\n\tquery ($name: String!, $namespace: String) {\n\t\torb(name: $name) {\n\t\t id\n\t\t}\n\t\tregistryNamespace(name: $namespace) {\n\t\t id\n\t\t}\n\t }\n\t ", + "variables": { + "name": "%s", + "namespace": "%s" + } + }`, orbFullName, orbNamespaceName) + + gqlOrbIDResponse := `{ + "orb": null, + "registryNamespace": { + "id": "eac63dee-9960-48c2-b763-612e1683194e" + } + }` + + tempSettings.AppendPostHandler(token, clitest.MockRequestResponse{ + Status: http.StatusOK, + Request: expectedOrbIDRequest, + Response: gqlOrbIDResponse}) + + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session).Should(clitest.ShouldFail()) + errorMsg := fmt.Sprintf(`Error: Failed to add orb %s to category %s: the '%s' orb does not exist in the '%s' namespace. Did you misspell the namespace or the orb name?`, orbFullName, categoryName, orbName, orbNamespaceName) + if updateType == api.Remove { + errorMsg = fmt.Sprintf(`Error: Failed to remove orb %s from category %s: the '%s' orb does not exist in the '%s' namespace. Did you misspell the namespace or the orb name?`, orbFullName, categoryName, orbName, orbNamespaceName) + } + Eventually(session.Err).Should(gbytes.Say(errorMsg)) + }, + Entry("add categorization to non-existent orb", api.Add), + Entry("remove categorization to non-existent orb", api.Remove), + ) + }) + Context("with mock server", func() { + DescribeTable("category does not exist", + func(updateType api.UpdateOrbCategorizationRequestType) { + commandName := "add-to-category" + if updateType == api.Remove { + commandName = "remove-from-category" + } + + command = exec.Command(pathCLI, + "orb", commandName, + "--skip-update-check", + "--token", token, + "--host", tempSettings.TestServer.URL(), + orbFullName, categoryName) + + expectedOrbIDRequest := fmt.Sprintf(`{ + "query": "\n\tquery ($name: String!, $namespace: String) {\n\t\torb(name: $name) {\n\t\t id\n\t\t}\n\t\tregistryNamespace(name: $namespace) {\n\t\t id\n\t\t}\n\t }\n\t ", + "variables": { + "name": "%s", + "namespace": "%s" + } + }`, orbFullName, orbNamespaceName) + + gqlOrbIDResponse := fmt.Sprintf(`{ + "orb": { + "id": "%s" + } + }`, orbId) + + expectedCategoryIDRequest := fmt.Sprintf(`{ + "query": "\n\tquery ($name: String!) {\n\t\torbCategoryByName(name: $name) {\n\t\t id\n\t\t}\n\t}\n\t ", + "variables": { + "name": "%s" + } + }`, categoryName) + + gqlCategoryIDResponse := `{ + "orbCategoryByName": null + }` + + tempSettings.AppendPostHandler(token, clitest.MockRequestResponse{ + Status: http.StatusOK, + Request: expectedOrbIDRequest, + Response: gqlOrbIDResponse}) + + tempSettings.AppendPostHandler(token, clitest.MockRequestResponse{ + Status: http.StatusOK, + Request: expectedCategoryIDRequest, + Response: gqlCategoryIDResponse}) + + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session).Should(clitest.ShouldFail()) + errorCause := fmt.Sprintf(`the '%s' category does not exist. Did you misspell the category name? To see the list of category names, please run 'circleci orb list-categories'.`, categoryName) + errorMsg := fmt.Sprintf(`Error: Failed to add orb %s to category %s: %s`, orbFullName, categoryName, errorCause) + if updateType == api.Remove { + errorMsg = fmt.Sprintf(`Error: Failed to remove orb %s from category %s: %s`, orbFullName, categoryName, errorCause) + } + stderr := session.Wait().Err.Contents() + Expect(string(stderr)).To(ContainSubstring(errorMsg)) + }, + Entry("add orb to non-existent category", api.Add), + Entry("remove orb to non-existent category", api.Remove), + ) + }) + }) }) Describe("Orb pack", func() { diff --git a/cmd/testdata/gql_orb_category_list/first_response.json b/cmd/testdata/gql_orb_category_list/first_response.json new file mode 100644 index 000000000..0925bacae --- /dev/null +++ b/cmd/testdata/gql_orb_category_list/first_response.json @@ -0,0 +1,115 @@ +{ + "orbCategories": { + "totalCount": 22, + "edges": [ + { + "cursor": "Artifacts/Registry", + "node": { + "id": "bd82f99e-cc7a-4cb6-be89-f1e347cfee0c", + "name": "Artifacts/Registry" + } + }, + { + "cursor": "Build", + "node": { + "id": "6b6c1c5f-5e59-48b1-9352-79c54a081391", + "name": "Build" + } + }, + { + "cursor": "Cloud Platform", + "node": { + "id": "85807118-bb81-45c6-99ae-143d4eacc1e6", + "name": "Cloud Platform" + } + }, + { + "cursor": "Code Analysis", + "node": { + "id": "2861f288-eca8-4c46-b3e4-4b242826f880", + "name": "Code Analysis" + } + }, + { + "cursor": "Collaboration", + "node": { + "id": "5e916839-7a4b-4483-97b9-242fd5faf997", + "name": "Collaboration" + } + }, + { + "cursor": "Containers", + "node": { + "id": "ce8e1764-1adc-4b23-9be3-5c31a1064a88", + "name": "Containers" + } + }, + { + "cursor": "Deployment", + "node": { + "id": "c89bb286-f3bc-4e15-a6c1-2618ec41d3e9", + "name": "Deployment" + } + }, + { + "cursor": "Infra Automation", + "node": { + "id": "66d32498-7965-412f-8bf0-a717092f7807", + "name": "Infra Automation" + } + }, + { + "cursor": "Kubernetes", + "node": { + "id": "36fa1dd0-9a9f-46ef-a332-003e17c01bb1", + "name": "Kubernetes" + } + }, + { + "cursor": "Language/Framework", + "node": { + "id": "ca67b176-28d4-42a5-b38f-70e28a644343", + "name": "Language/Framework" + } + }, + { + "cursor": "Monitoring", + "node": { + "id": "7a255857-9bbc-47b6-be2c-0756528f5077", + "name": "Monitoring" + } + }, + { + "cursor": "Notifications", + "node": { + "id": "4486aa87-2fbf-4864-b048-7a0b82838a74", + "name": "Notifications" + } + }, + { + "cursor": "Reporting", + "node": { + "id": "ae3378d5-3b6a-43bb-aa59-9564e919b8b5", + "name": "Reporting" + } + }, + { + "cursor": "Security", + "node": { + "id": "a736f168-ce98-4c3e-afb8-b93a6c8ebeac", + "name": "Security" + } + }, + { + "cursor": "Testing", + "node": { + "id": "50c0078b-447d-49ad-9a8e-f4c1b09c7651", + "name": "Testing" + } + } + ], + "pageInfo": { + "hasNextPage": true + } + } +} diff --git a/cmd/testdata/gql_orb_category_list/pretty_json_output.json b/cmd/testdata/gql_orb_category_list/pretty_json_output.json new file mode 100644 index 000000000..360d1d892 --- /dev/null +++ b/cmd/testdata/gql_orb_category_list/pretty_json_output.json @@ -0,0 +1,72 @@ +{ + "orbCategories": [ + { + "id": "bd82f99e-cc7a-4cb6-be89-f1e347cfee0c", + "name": "Artifacts/Registry" + }, + { + "id": "6b6c1c5f-5e59-48b1-9352-79c54a081391", + "name": "Build" + }, + { + "id": "85807118-bb81-45c6-99ae-143d4eacc1e6", + "name": "Cloud Platform" + }, + { + "id": "2861f288-eca8-4c46-b3e4-4b242826f880", + "name": "Code Analysis" + }, + { + "id": "5e916839-7a4b-4483-97b9-242fd5faf997", + "name": "Collaboration" + }, + { + "id": "ce8e1764-1adc-4b23-9be3-5c31a1064a88", + "name": "Containers" + }, + { + "id": "c89bb286-f3bc-4e15-a6c1-2618ec41d3e9", + "name": "Deployment" + }, + { + "id": "66d32498-7965-412f-8bf0-a717092f7807", + "name": "Infra Automation" + }, + { + "id": "36fa1dd0-9a9f-46ef-a332-003e17c01bb1", + "name": "Kubernetes" + }, + { + "id": "ca67b176-28d4-42a5-b38f-70e28a644343", + "name": "Language/Framework" + }, + { + "id": "7a255857-9bbc-47b6-be2c-0756528f5077", + "name": "Monitoring" + }, + { + "id": "4486aa87-2fbf-4864-b048-7a0b82838a74", + "name": "Notifications" + }, + { + "id": "ae3378d5-3b6a-43bb-aa59-9564e919b8b5", + "name": "Reporting" + }, + { + "id": "a736f168-ce98-4c3e-afb8-b93a6c8ebeac", + "name": "Security" + }, + { + "id": "50c0078b-447d-49ad-9a8e-f4c1b09c7651", + "name": "Testing" + }, + { + "id": "ed82f99e-cc7a-4cb6-be89-f1e347cfee0c", + "name": "Windows Server 2003" + }, + { + "id": "7b6c1c5f-5e59-48b1-9352-79c54a081391", + "name": "Windows Server 2010" + } + ] +} diff --git a/cmd/testdata/gql_orb_category_list/second_response.json b/cmd/testdata/gql_orb_category_list/second_response.json new file mode 100644 index 000000000..69bdc7eb1 --- /dev/null +++ b/cmd/testdata/gql_orb_category_list/second_response.json @@ -0,0 +1,24 @@ +{ + "orbCategories": { + "totalCount": 22, + "edges": [ + { + "cursor": "Windows Server 2003", + "node": { + "id": "ed82f99e-cc7a-4cb6-be89-f1e347cfee0c", + "name": "Windows Server 2003" + } + }, + { + "cursor": "Windows Server 2010", + "node": { + "id": "7b6c1c5f-5e59-48b1-9352-79c54a081391", + "name": "Windows Server 2010" + } + } + ], + "pageInfo": { + "hasNextPage": false + } + } +} From 977d126e583fad2798408f88fb917528e10e00e8 Mon Sep 17 00:00:00 2001 From: Stella Lok Date: Fri, 14 Aug 2020 12:29:16 +0800 Subject: [PATCH 2/3] minor improvements --- api/api.go | 26 +++++++------- cmd/orb.go | 6 ++-- cmd/orb_test.go | 36 +++++++++++-------- .../gql_orb_category_list/text_output.txt | 17 +++++++++ 4 files changed, 53 insertions(+), 32 deletions(-) create mode 100644 cmd/testdata/gql_orb_category_list/text_output.txt diff --git a/api/api.go b/api/api.go index 99f8fbb1d..d9c5dc478 100644 --- a/api/api.go +++ b/api/api.go @@ -242,7 +242,7 @@ type AddOrRemoveOrbCategorizationData struct { Errors GQLErrorsCollection } -// OrbListResponse type matches the result from GQL. +// OrbCategoryListResponse type matches the result from GQL. // So that we can use mapstructure to convert from nested maps to a strongly typed struct. type OrbCategoryListResponse struct { OrbCategories struct { @@ -1001,11 +1001,11 @@ func OrbInfo(cl *graphql.Client, orbRef string) (*OrbVersion, error) { orb { id createdAt - name - categories { - id - name - } + name + categories { + id + name + } statistics { last30DaysBuildCount, last30DaysProjectCount, @@ -1250,11 +1250,9 @@ func OrbCategoryID(cl *graphql.Client, name string) (*OrbCategoryIDResponse, err orbCategoryByName(name: $name) { id } - } - ` + }` request := graphql.NewRequest(query) - request.SetToken(cl.Token) request.Var("name", name) @@ -1268,8 +1266,8 @@ func OrbCategoryID(cl *graphql.Client, name string) (*OrbCategoryIDResponse, err return nil, fmt.Errorf("the '%s' category does not exist. Did you misspell the category name? To see the list of category names, please run 'circleci orb list-categories'.", name) } -// OrbAddOrRemoveOrbCategorization adds or removes an orb categorization -func OrbAddOrRemoveOrbCategorization(cl *graphql.Client, namespace string, orb string, categoryName string, updateType UpdateOrbCategorizationRequestType) error { +// AddOrRemoveOrbCategorization adds or removes an orb categorization +func AddOrRemoveOrbCategorization(cl *graphql.Client, namespace string, orb string, categoryName string, updateType UpdateOrbCategorizationRequestType) error { orbId, err := OrbID(cl, namespace, orb) if err != nil { return err @@ -1293,9 +1291,9 @@ func OrbAddOrRemoveOrbCategorization(cl *graphql.Client, namespace string, orb s return fmt.Errorf("Internal error - invalid update type %d", updateType) } - query := ` + query := fmt.Sprintf(` mutation($orbId: UUID!, $categoryId: UUID!) { - ` + mutationName + `( + %s( orbId: $orbId, categoryId: $categoryId ) { @@ -1307,7 +1305,7 @@ func OrbAddOrRemoveOrbCategorization(cl *graphql.Client, namespace string, orb s } } } - ` + `, mutationName) request := graphql.NewRequest(query) request.SetToken(cl.Token) diff --git a/cmd/orb.go b/cmd/orb.go index 59f7f8d04..567a43435 100644 --- a/cmd/orb.go +++ b/cmd/orb.go @@ -258,7 +258,7 @@ Please note that at this time all orbs created in the registry are world-readabl listCategoriesCommand := &cobra.Command{ Use: "list-categories", - Short: "List categories", + Short: "List orb categories", Args: cobra.ExactArgs(0), RunE: func(_ *cobra.Command, _ []string) error { return listOrbCategories(opts) @@ -505,7 +505,7 @@ func orbCategoryCollectionToString(orbCategoryCollection *api.OrbCategoriesForLi if opts.listJSON { orbCategoriesJSON, err := json.MarshalIndent(orbCategoryCollection, "", " ") if err != nil { - return "", errors.Wrapf(err, "Failed to convert to convert to JSON") + return "", errors.Wrapf(err, "Failed to convert to JSON") } result = string(orbCategoriesJSON) } else { @@ -850,7 +850,7 @@ func addOrRemoveOrbCategorization(opts orbOptions, updateType api.UpdateOrbCateg return err } - err = api.OrbAddOrRemoveOrbCategorization(opts.cl, namespace, orb, opts.args[1], updateType) + err = api.AddOrRemoveOrbCategorization(opts.cl, namespace, orb, opts.args[1], updateType) if err != nil { var errorString = "Failed to add orb %s to category %s" diff --git a/cmd/orb_test.go b/cmd/orb_test.go index fe0d0dd07..e311c0ae5 100644 --- a/cmd/orb_test.go +++ b/cmd/orb_test.go @@ -2378,11 +2378,11 @@ foo.bar/account/api`)) orb { id createdAt - name - categories { - id - name - } + name + categories { + id + name + } statistics { last30DaysBuildCount, last30DaysProjectCount, @@ -2600,7 +2600,7 @@ https://circleci.com/orbs/registry/orb/my/orb }) }) - Describe("orb categories", func() { + Describe("list orb categories", func() { Context("with mock server", func() { DescribeTable("sends multiple requests when there are more than 1 page of orb categories", func(json bool) { @@ -2651,7 +2651,11 @@ https://circleci.com/orbs/registry/orb/my/orb tmpBytes = golden.Get(GinkgoT(), filepath.FromSlash("gql_orb_category_list/second_response.json")) secondResponse := string(tmpBytes) - tmpBytes = golden.Get(GinkgoT(), filepath.FromSlash("gql_orb_category_list/pretty_json_output.json")) + expectedOutputPath := "gql_orb_category_list/text_output.txt" + if json { + expectedOutputPath = "gql_orb_category_list/pretty_json_output.json" + } + tmpBytes = golden.Get(GinkgoT(), filepath.FromSlash(expectedOutputPath)) expectedOutput := string(tmpBytes) tempSettings.AppendPostHandler("", clitest.MockRequestResponse{ @@ -2672,9 +2676,11 @@ https://circleci.com/orbs/registry/orb/my/orb Eventually(session).Should(gexec.Exit(0)) Expect(tempSettings.TestServer.ReceivedRequests()).Should(HaveLen(2)) - completeOutput := string(session.Wait().Out.Contents()) + displayedOutput := string(session.Wait().Out.Contents()) if json { - Expect(completeOutput).Should(MatchJSON(expectedOutput)) + Expect(displayedOutput).Should(MatchJSON(expectedOutput)) + } else { + Expect(displayedOutput).To(Equal(expectedOutput)) } }, Entry("with --json", true), @@ -2703,7 +2709,7 @@ https://circleci.com/orbs/registry/orb/my/orb }) Context("with mock server", func() { - DescribeTable("add/remove orb categorization", + DescribeTable("add/remove a valid orb to/from a valid category", func(mockErrorResponse bool, updateType api.UpdateOrbCategorizationRequestType) { commandName := "add-to-category" operationName := "addCategorizationToOrb" @@ -2736,7 +2742,7 @@ https://circleci.com/orbs/registry/orb/my/orb }`, orbFullName, orbNamespaceName) expectedCategoryIDRequest := fmt.Sprintf(`{ - "query": "\n\tquery ($name: String!) {\n\t\torbCategoryByName(name: $name) {\n\t\t id\n\t\t}\n\t}\n\t ", + "query": "\n\tquery ($name: String!) {\n\t\torbCategoryByName(name: $name) {\n\t\t id\n\t\t}\n\t}", "variables": { "name": "%s" } @@ -2749,7 +2755,7 @@ https://circleci.com/orbs/registry/orb/my/orb }`, categoryId) expectedOrbCategorizationRequest := fmt.Sprintf(`{ - "query": "\n\t\tmutation($orbId: UUID!, $categoryId: UUID!) {\n\t%s(\n\t\t\t\torbId: $orbId,\n\t\t\t\tcategoryId: $categoryId\n\t\t\t) {\n\t\t\t\torbId\n\t\t\t\tcategoryId\n\t\t\t\terrors {\n\t\t\t\t\tmessage\n\t\t\t\t\ttype\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t", + "query": "\n\t\tmutation($orbId: UUID!, $categoryId: UUID!) {\n\t\t\t%s(\n\t\t\t\torbId: $orbId,\n\t\t\t\tcategoryId: $categoryId\n\t\t\t) {\n\t\t\t\torbId\n\t\t\t\tcategoryId\n\t\t\t\terrors {\n\t\t\t\t\tmessage\n\t\t\t\t\ttype\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t", "variables": { "categoryId": "%s", "orbId": "%s" @@ -2782,7 +2788,7 @@ https://circleci.com/orbs/registry/orb/my/orb Request: expectedOrbIDRequest, Response: gqlOrbIDResponse}) - tempSettings.AppendPostHandler(token, clitest.MockRequestResponse{ + tempSettings.AppendPostHandler("", clitest.MockRequestResponse{ Status: http.StatusOK, Request: expectedCategoryIDRequest, Response: gqlCategoryIDResponse}) @@ -2892,7 +2898,7 @@ https://circleci.com/orbs/registry/orb/my/orb }`, orbId) expectedCategoryIDRequest := fmt.Sprintf(`{ - "query": "\n\tquery ($name: String!) {\n\t\torbCategoryByName(name: $name) {\n\t\t id\n\t\t}\n\t}\n\t ", + "query": "\n\tquery ($name: String!) {\n\t\torbCategoryByName(name: $name) {\n\t\t id\n\t\t}\n\t}", "variables": { "name": "%s" } @@ -2907,7 +2913,7 @@ https://circleci.com/orbs/registry/orb/my/orb Request: expectedOrbIDRequest, Response: gqlOrbIDResponse}) - tempSettings.AppendPostHandler(token, clitest.MockRequestResponse{ + tempSettings.AppendPostHandler("", clitest.MockRequestResponse{ Status: http.StatusOK, Request: expectedCategoryIDRequest, Response: gqlCategoryIDResponse}) diff --git a/cmd/testdata/gql_orb_category_list/text_output.txt b/cmd/testdata/gql_orb_category_list/text_output.txt new file mode 100644 index 000000000..5f3f3612d --- /dev/null +++ b/cmd/testdata/gql_orb_category_list/text_output.txt @@ -0,0 +1,17 @@ +Artifacts/Registry +Build +Cloud Platform +Code Analysis +Collaboration +Containers +Deployment +Infra Automation +Kubernetes +Language/Framework +Monitoring +Notifications +Reporting +Security +Testing +Windows Server 2003 +Windows Server 2010 From 4c104ca9844c659233501bd567805b53125dd4e8 Mon Sep 17 00:00:00 2001 From: Stella Lok Date: Fri, 14 Aug 2020 16:04:32 +0800 Subject: [PATCH 3/3] fix test error on windows --- cmd/orb_test.go | 25 ++++++++++++++----- .../gql_orb_category_list/text_output.txt | 17 ------------- 2 files changed, 19 insertions(+), 23 deletions(-) delete mode 100644 cmd/testdata/gql_orb_category_list/text_output.txt diff --git a/cmd/orb_test.go b/cmd/orb_test.go index e311c0ae5..cb182292e 100644 --- a/cmd/orb_test.go +++ b/cmd/orb_test.go @@ -2651,11 +2651,7 @@ https://circleci.com/orbs/registry/orb/my/orb tmpBytes = golden.Get(GinkgoT(), filepath.FromSlash("gql_orb_category_list/second_response.json")) secondResponse := string(tmpBytes) - expectedOutputPath := "gql_orb_category_list/text_output.txt" - if json { - expectedOutputPath = "gql_orb_category_list/pretty_json_output.json" - } - tmpBytes = golden.Get(GinkgoT(), filepath.FromSlash(expectedOutputPath)) + tmpBytes = golden.Get(GinkgoT(), filepath.FromSlash("gql_orb_category_list/pretty_json_output.json")) expectedOutput := string(tmpBytes) tempSettings.AppendPostHandler("", clitest.MockRequestResponse{ @@ -2680,7 +2676,24 @@ https://circleci.com/orbs/registry/orb/my/orb if json { Expect(displayedOutput).Should(MatchJSON(expectedOutput)) } else { - Expect(displayedOutput).To(Equal(expectedOutput)) + Expect(displayedOutput).To(Equal(`Artifacts/Registry +Build +Cloud Platform +Code Analysis +Collaboration +Containers +Deployment +Infra Automation +Kubernetes +Language/Framework +Monitoring +Notifications +Reporting +Security +Testing +Windows Server 2003 +Windows Server 2010 +`)) } }, Entry("with --json", true), diff --git a/cmd/testdata/gql_orb_category_list/text_output.txt b/cmd/testdata/gql_orb_category_list/text_output.txt deleted file mode 100644 index 5f3f3612d..000000000 --- a/cmd/testdata/gql_orb_category_list/text_output.txt +++ /dev/null @@ -1,17 +0,0 @@ -Artifacts/Registry -Build -Cloud Platform -Code Analysis -Collaboration -Containers -Deployment -Infra Automation -Kubernetes -Language/Framework -Monitoring -Notifications -Reporting -Security -Testing -Windows Server 2003 -Windows Server 2010