diff --git a/cmd/cmd_suite_test.go b/cmd/cmd_suite_test.go index 4c2a4f02d..7ad0862f2 100644 --- a/cmd/cmd_suite_test.go +++ b/cmd/cmd_suite_test.go @@ -1,11 +1,16 @@ package cmd_test import ( + "io/ioutil" + "net/http" + "os" + "path/filepath" "testing" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "github.com/onsi/gomega/gexec" + "github.com/onsi/gomega/ghttp" ) var pathCLI string @@ -24,3 +29,78 @@ func TestCmd(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "Cmd Suite") } + +// Test helpers + +func appendPostHandler(server *ghttp.Server, authToken string, statusCode int, expectedRequestJson string, responseBody string) { + server.AppendHandlers( + ghttp.CombineHandlers( + ghttp.VerifyRequest("POST", "/"), + ghttp.VerifyHeader(http.Header{ + "Authorization": []string{authToken}, + }), + ghttp.VerifyContentType("application/json; charset=utf-8"), + // From Gomegas ghttp.VerifyJson to avoid the + // VerifyContentType("application/json") check + // that fails with "application/json; charset=utf-8" + func(w http.ResponseWriter, req *http.Request) { + body, err := ioutil.ReadAll(req.Body) + req.Body.Close() + Expect(err).ShouldNot(HaveOccurred()) + Expect(body).Should(MatchJSON(expectedRequestJson), "JSON Mismatch") + }, + ghttp.RespondWith(statusCode, `{ "data": `+responseBody+`}`), + ), + ) +} + +type tmpFile struct { + RootDir string + Path string + File *os.File +} + +func (f tmpFile) close() { + f.File.Close() + os.RemoveAll(f.RootDir) +} + +func (f tmpFile) write(fileContent string) error { + _, err := f.File.Write([]byte(fileContent)) + + return err +} + +func openTmpFile(path string) (tmpFile, error) { + var ( + config tmpFile = tmpFile{} + err error + ) + + tmpDir, err := ioutil.TempDir("", "circleci-cli-test-") + if err != nil { + return config, err + } + + config.RootDir = tmpDir + config.Path = filepath.Join(tmpDir, path) + + err = os.MkdirAll(filepath.Dir(config.Path), 0700) + if err != nil { + return config, err + } + + var file *os.File + file, err = os.OpenFile( + config.Path, + os.O_RDWR|os.O_CREATE, + 0600, + ) + if err != nil { + return config, err + } + + config.File = file + + return config, nil +} diff --git a/cmd/config_test.go b/cmd/config_test.go index 0becff835..ad7636f16 100644 --- a/cmd/config_test.go +++ b/cmd/config_test.go @@ -1,9 +1,7 @@ package cmd_test import ( - "io/ioutil" "net/http" - "os" "os/exec" "path/filepath" @@ -14,91 +12,23 @@ import ( "github.com/onsi/gomega/ghttp" ) -func appendPostHandler(server *ghttp.Server, authToken string, statusCode int, expectedRequestJson string, responseBody string) { - server.AppendHandlers( - ghttp.CombineHandlers( - ghttp.VerifyRequest("POST", "/"), - ghttp.VerifyHeader(http.Header{ - "Authorization": []string{authToken}, - }), - ghttp.VerifyContentType("application/json; charset=utf-8"), - // From Gomegas ghttp.VerifyJson to avoid the - // VerifyContentType("application/json") check - // that fails with "application/json; charset=utf-8" - func(w http.ResponseWriter, req *http.Request) { - body, err := ioutil.ReadAll(req.Body) - req.Body.Close() - Expect(err).ShouldNot(HaveOccurred()) - Expect(body).Should(MatchJSON(expectedRequestJson), "JSON Mismatch") - }, - ghttp.RespondWith(statusCode, `{ "data": `+responseBody+`}`), - ), - ) -} - -type configYaml struct { - TempHome string - Path string - YamlFile *os.File -} - -func openConfigYaml() (configYaml, error) { - var ( - config configYaml = configYaml{} - err error - ) - - const ( - configDir = ".circleci" - configFile = "config.yaml" - ) - - tempHome, err := ioutil.TempDir("", "circleci-cli-test-") - if err != nil { - return config, err - } - - err = os.Mkdir(filepath.Join(tempHome, configDir), 0700) - if err != nil { - return config, err - } - - config.Path = filepath.Join(tempHome, configDir, configFile) - - var file *os.File - file, err = os.OpenFile( - config.Path, - os.O_RDWR|os.O_CREATE, - 0600, - ) - if err != nil { - return config, err - } - - config.YamlFile = file - - return config, nil -} - var _ = Describe("Config", func() { Describe("with an api and config.yml", func() { var ( testServer *ghttp.Server - config configYaml + config tmpFile ) BeforeEach(func() { var err error - config, err = openConfigYaml() + config, err = openTmpFile(filepath.Join(".circleci", "config.yaml")) Expect(err).ToNot(HaveOccurred()) testServer = ghttp.NewServer() }) AfterEach(func() { - config.YamlFile.Close() - os.RemoveAll(config.TempHome) - + config.close() testServer.Close() }) @@ -119,7 +49,7 @@ var _ = Describe("Config", func() { }) It("works", func() { - _, err := config.YamlFile.Write([]byte(`some config`)) + err := config.write(`some config`) Expect(err).ToNot(HaveOccurred()) gqlResponse := `{ @@ -147,7 +77,7 @@ var _ = Describe("Config", func() { }) It("prints errors if invalid", func() { - _, err := config.YamlFile.Write([]byte(`some config`)) + err := config.write(`some config`) Expect(err).ToNot(HaveOccurred()) gqlResponse := `{ @@ -174,8 +104,7 @@ var _ = Describe("Config", func() { Expect(err).ShouldNot(HaveOccurred()) Eventually(session.Err).Should(gbytes.Say("Error:")) Eventually(session.Err).Should(gbytes.Say("-- invalid_config")) - Eventually(session).Should(gexec.Exit(255)) - + Eventually(session).ShouldNot(gexec.Exit(0)) }) }) @@ -196,7 +125,7 @@ var _ = Describe("Config", func() { }) It("works", func() { - _, err := config.YamlFile.Write([]byte(`some config`)) + err := config.write(`some config`) Expect(err).ToNot(HaveOccurred()) gqlResponse := `{ @@ -224,7 +153,7 @@ var _ = Describe("Config", func() { }) It("prints errors if invalid", func() { - _, err := config.YamlFile.Write([]byte(`some config`)) + err := config.write(`some config`) Expect(err).ToNot(HaveOccurred()) gqlResponse := `{ @@ -253,8 +182,7 @@ var _ = Describe("Config", func() { Eventually(session.Err).Should(gbytes.Say("Error:")) Eventually(session.Err).Should(gbytes.Say("-- error1,")) Eventually(session.Err).Should(gbytes.Say("-- error2,")) - Eventually(session).Should(gexec.Exit(255)) - + Eventually(session).ShouldNot(gexec.Exit(0)) }) }) }) diff --git a/cmd/orb.go b/cmd/orb.go index 01cd58201..e6c2359cb 100644 --- a/cmd/orb.go +++ b/cmd/orb.go @@ -1,7 +1,9 @@ package cmd import ( + "bytes" "context" + "io/ioutil" "github.com/CircleCI-Public/circleci-cli/client" "github.com/pkg/errors" @@ -11,6 +13,20 @@ import ( "github.com/spf13/viper" ) +var orbPath string + +type orbConfigResponse struct { + OrbConfig struct { + Valid bool + SourceYaml string + OutputYaml string + + Errors []struct { + Message string + } + } +} + func newOrbCommand() *cobra.Command { orbListCommand := &cobra.Command{ @@ -19,6 +35,18 @@ func newOrbCommand() *cobra.Command { RunE: listOrbs, } + orbValidateCommand := &cobra.Command{ + Use: "validate", + Short: "validate an orb.yml", + RunE: validateOrb, + } + + orbExpandCommand := &cobra.Command{ + Use: "expand", + Short: "expand an orb.yml", + RunE: expandOrb, + } + orbCommand := &cobra.Command{ Use: "orb", Short: "Operate on orbs", @@ -26,6 +54,12 @@ func newOrbCommand() *cobra.Command { orbCommand.AddCommand(orbListCommand) + orbValidateCommand.PersistentFlags().StringVarP(&orbPath, "path", "p", "orb.yml", "path to orb file") + orbCommand.AddCommand(orbValidateCommand) + + orbExpandCommand.PersistentFlags().StringVarP(&orbPath, "path", "p", "orb.yml", "path to orb file") + orbCommand.AddCommand(orbExpandCommand) + return orbCommand } @@ -97,3 +131,90 @@ query ListOrbs ($after: String!) { return nil } + +func loadOrbYaml(path string) (string, error) { + + orb, err := ioutil.ReadFile(path) + + if err != nil { + return "", errors.Wrapf(err, "Could not load orb file at %s", path) + } + + return string(orb), nil +} + +func (response orbConfigResponse) processErrors() error { + var buffer bytes.Buffer + + buffer.WriteString("\n") + for i := range response.OrbConfig.Errors { + buffer.WriteString("-- ") + buffer.WriteString(response.OrbConfig.Errors[i].Message) + buffer.WriteString(",\n") + } + + return errors.New(buffer.String()) +} + +func orbValidateQuery(ctx context.Context) (*orbConfigResponse, error) { + + query := ` + query ValidateOrb ($orb: String!) { + orbConfig(orbYaml: $orb) { + valid, + errors { message }, + sourceYaml, + outputYaml + } + }` + + orb, err := loadOrbYaml(orbPath) + if err != nil { + return nil, err + } + + variables := map[string]string{ + "orb": orb, + } + + var response orbConfigResponse + err = queryAPI(ctx, query, variables, &response) + if err != nil { + return nil, errors.Wrap(err, "Unable to validate orb") + } + + return &response, nil +} + +func validateOrb(cmd *cobra.Command, args []string) error { + ctx := context.Background() + response, err := orbValidateQuery(ctx) + + if err != nil { + return err + } + + if !response.OrbConfig.Valid { + return response.processErrors() + } + + Logger.Infof("Orb at %s is valid", orbPath) + return nil +} + +func expandOrb(cmd *cobra.Command, args []string) error { + ctx := context.Background() + + response, err := orbValidateQuery(ctx) + + if err != nil { + return err + } + + if !response.OrbConfig.Valid { + return response.processErrors() + } + + Logger.Info(response.OrbConfig.OutputYaml) + return nil +} diff --git a/cmd/orb_test.go b/cmd/orb_test.go new file mode 100644 index 000000000..a1615587a --- /dev/null +++ b/cmd/orb_test.go @@ -0,0 +1,198 @@ +package cmd_test + +import ( + "net/http" + "os/exec" + "path/filepath" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gbytes" + "github.com/onsi/gomega/gexec" + "github.com/onsi/gomega/ghttp" +) + +var _ = Describe("Orb", func() { + Describe("with an api and orb.yml", func() { + var ( + testServer *ghttp.Server + orb tmpFile + ) + + BeforeEach(func() { + var err error + orb, err = openTmpFile(filepath.Join("myorb", "orb.yml")) + Expect(err).ToNot(HaveOccurred()) + + testServer = ghttp.NewServer() + }) + + AfterEach(func() { + orb.close() + testServer.Close() + }) + + Describe("when validating orb", func() { + var ( + token string + command *exec.Cmd + ) + + BeforeEach(func() { + token = "testtoken" + command = exec.Command(pathCLI, + "orb", "validate", + "-t", token, + "-e", testServer.URL(), + "-p", orb.Path, + ) + }) + + It("works", func() { + By("setting up a mock server") + err := orb.write(`{}`) + Expect(err).ToNot(HaveOccurred()) + + gqlResponse := `{ + "orbConfig": { + "sourceYaml": "{}", + "valid": true, + "errors": [] + } + }` + + expectedRequestJson := ` { + "query": "\n\t\tquery ValidateOrb ($orb: String!) {\n\t\t\torbConfig(orbYaml: $orb) {\n\t\t\t\tvalid,\n\t\t\t\terrors { message },\n\t\t\t\tsourceYaml,\n\t\t\t\toutputYaml\n\t\t\t}\n\t\t}", + "variables": { + "orb": "{}" + } + }` + + appendPostHandler(testServer, token, http.StatusOK, expectedRequestJson, gqlResponse) + + By("running the command") + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + + Expect(err).ShouldNot(HaveOccurred()) + // the .* is because the full path with temp dir is printed + Eventually(session.Out).Should(gbytes.Say("Orb at .*myorb/orb.yml is valid")) + Eventually(session).Should(gexec.Exit(0)) + }) + + It("prints errors if invalid", func() { + By("setting up a mock server") + err := orb.write(`some orb`) + Expect(err).ToNot(HaveOccurred()) + + gqlResponse := `{ + "orbConfig": { + "sourceYaml": "hello world", + "valid": false, + "errors": [ + {"message": "invalid_orb"} + ] + } + }` + + expectedRequestJson := ` { + "query": "\n\t\tquery ValidateOrb ($orb: String!) {\n\t\t\torbConfig(orbYaml: $orb) {\n\t\t\t\tvalid,\n\t\t\t\terrors { message },\n\t\t\t\tsourceYaml,\n\t\t\t\toutputYaml\n\t\t\t}\n\t\t}", + "variables": { + "orb": "some orb" + } + }` + appendPostHandler(testServer, token, http.StatusOK, expectedRequestJson, gqlResponse) + + By("running the command") + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session.Err).Should(gbytes.Say("Error:")) + Eventually(session.Err).Should(gbytes.Say("-- invalid_orb")) + Eventually(session).ShouldNot(gexec.Exit(0)) + }) + }) + + Describe("when expanding orb", func() { + var ( + token string + command *exec.Cmd + ) + + BeforeEach(func() { + token = "testtoken" + command = exec.Command(pathCLI, + "orb", "expand", + "-t", token, + "-e", testServer.URL(), + "-p", orb.Path, + ) + }) + + It("works", func() { + By("setting up a mock server") + err := orb.write(`some orb`) + Expect(err).ToNot(HaveOccurred()) + + gqlResponse := `{ + "orbConfig": { + "outputYaml": "hello world", + "valid": true, + "errors": [] + } + }` + + expectedRequestJson := ` { + "query": "\n\t\tquery ValidateOrb ($orb: String!) {\n\t\t\torbConfig(orbYaml: $orb) {\n\t\t\t\tvalid,\n\t\t\t\terrors { message },\n\t\t\t\tsourceYaml,\n\t\t\t\toutputYaml\n\t\t\t}\n\t\t}", + "variables": { + "orb": "some orb" + } + }` + + appendPostHandler(testServer, token, http.StatusOK, expectedRequestJson, gqlResponse) + + By("running the command") + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session.Out).Should(gbytes.Say("hello world")) + Eventually(session).Should(gexec.Exit(0)) + }) + + It("prints errors if invalid", func() { + By("setting up a mock server") + err := orb.write(`some orb`) + Expect(err).ToNot(HaveOccurred()) + + gqlResponse := `{ + "orbConfig": { + "outputYaml": "hello world", + "valid": false, + "errors": [ + {"message": "error1"}, + {"message": "error2"} + ] + } + }` + + expectedRequestJson := ` { + "query": "\n\t\tquery ValidateOrb ($orb: String!) {\n\t\t\torbConfig(orbYaml: $orb) {\n\t\t\t\tvalid,\n\t\t\t\terrors { message },\n\t\t\t\tsourceYaml,\n\t\t\t\toutputYaml\n\t\t\t}\n\t\t}", + "variables": { + "orb": "some orb" + } + }` + + appendPostHandler(testServer, token, http.StatusOK, expectedRequestJson, gqlResponse) + + By("running the command") + session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) + + Expect(err).ShouldNot(HaveOccurred()) + Eventually(session.Err).Should(gbytes.Say("Error:")) + Eventually(session.Err).Should(gbytes.Say("-- error1,")) + Eventually(session.Err).Should(gbytes.Say("-- error2,")) + Eventually(session).ShouldNot(gexec.Exit(0)) + + }) + }) + }) +})