diff --git a/.circleci/config.yml b/.circleci/config.yml index b94c2e90c..82efff2bf 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -115,7 +115,7 @@ jobs: name: Horus Security Analysis command: | curl -fsSL https://horusec.io/bin/install.sh | bash -s v1-5-0 - horusec start -p ./ -a "$HORUSEC_CLI_REPOSITORY_AUTHORIZATION" -G "true" -u "https://api-horus.zup.com.br" -n "ritchie-cli" + horusec start -p ./ -a "$HORUSEC_CLI_REPOSITORY_AUTHORIZATION" -G "true" -u "https://api-horusec.zup.com.br" -n "ritchie-cli" unit_test: executor: ritchie-tests-and-static-analisys-executor diff --git a/internal/mocks/mocks.go b/internal/mocks/mocks.go index 89891158d..d5d8c8a3e 100644 --- a/internal/mocks/mocks.go +++ b/internal/mocks/mocks.go @@ -93,7 +93,6 @@ type InputURLMock struct { func (i *InputURLMock) URL(name, defaultValue string) (string, error) { args := i.Called(name, defaultValue) - return args.String(0), args.Error(1) } diff --git a/pkg/cmd/cmd.go b/pkg/cmd/cmd.go index b51df9664..e019fe80f 100644 --- a/pkg/cmd/cmd.go +++ b/pkg/cmd/cmd.go @@ -17,18 +17,48 @@ package cmd import ( + "fmt" + "reflect" + "github.com/spf13/cobra" + "github.com/spf13/pflag" "github.com/ZupIT/ritchie-cli/pkg/api" "github.com/ZupIT/ritchie-cli/pkg/prompt" ) const stdinWarning = "stdin commands are deprecated and will no longer be supported in future versions. Please use" + - "flags for programatic formula execution" + " flags for programatic formula execution" + +type flag struct { + name string + shortName string + kind reflect.Kind + defValue interface{} + description string +} + +type flags []flag // CommandRunnerFunc represents that runner func for commands. type CommandRunnerFunc func(cmd *cobra.Command, args []string) error +func addReservedFlags(flags *pflag.FlagSet, flagsToAdd flags) { + for _, flag := range flagsToAdd { + switch flag.kind { //nolint:exhaustive + case reflect.String: + flags.StringP(flag.name, flag.shortName, flag.defValue.(string), flag.description) + case reflect.Bool: + flags.BoolP(flag.name, flag.shortName, flag.defValue.(bool), flag.description) + case reflect.Int: + flags.IntP(flag.name, flag.shortName, flag.defValue.(int), flag.description) + default: + warning := fmt.Sprintf("The %q type is not supported for the %q flag", flag.kind.String(), flag.name) + prompt.Warning(warning) + } + } +} + // RunFuncE delegates to stdinFunc if --stdin flag is passed otherwise delegates to promptFunc. func RunFuncE(stdinFunc, promptFunc CommandRunnerFunc) CommandRunnerFunc { return func(cmd *cobra.Command, args []string) error { @@ -45,6 +75,10 @@ func RunFuncE(stdinFunc, promptFunc CommandRunnerFunc) CommandRunnerFunc { } } +func IsFlagInput(cmd *cobra.Command) bool { + return cmd.Flags().NFlag() > 0 +} + func DeprecateCmd(parentCmd *cobra.Command, deprecatedCmd, deprecatedMsg string) { command := &cobra.Command{ Use: deprecatedCmd, diff --git a/pkg/cmd/delete_credential.go b/pkg/cmd/delete_credential.go index d12f1955b..a2cbd7716 100644 --- a/pkg/cmd/delete_credential.go +++ b/pkg/cmd/delete_credential.go @@ -1,8 +1,10 @@ package cmd import ( + "errors" "fmt" "io" + "reflect" "github.com/spf13/cobra" @@ -12,6 +14,15 @@ import ( "github.com/ZupIT/ritchie-cli/pkg/stdin" ) +const ( + providerFlagName = "provider" + providerFlagDescription = "Provider name to delete" +) + +type inputConfig struct { + provider string +} + // deleteCredentialCmd type for set credential command type deleteCredentialCmd struct { credential.CredDelete @@ -26,6 +37,15 @@ type deleteCredential struct { Provider string `json:"provider"` } +var deleteCredentialFlags = flags{ + { + name: providerFlagName, + kind: reflect.String, + defValue: "", + description: providerFlagDescription, + }, +} + // NewDeleteCredentialCmd creates a new cmd instance func NewDeleteCredentialCmd( credDelete credential.CredDelete, @@ -46,49 +66,32 @@ func NewDeleteCredentialCmd( Use: "credential", Short: "Delete credential", Long: `Delete credential from current env`, - RunE: RunFuncE(s.runStdin(), s.runPrompt()), + RunE: RunFuncE(s.runStdin(), s.runFormula()), ValidArgs: []string{""}, Args: cobra.OnlyValidArgs, } - cmd.LocalFlags() + + addReservedFlags(cmd.Flags(), deleteCredentialFlags) + return cmd } -func (d deleteCredentialCmd) runPrompt() CommandRunnerFunc { +func (d deleteCredentialCmd) runFormula() CommandRunnerFunc { return func(cmd *cobra.Command, args []string) error { - env, err := d.currentEnv() + curEnv, err := d.currentEnv() if err != nil { return err } - prompt.Info(fmt.Sprintf("Current env: %s", env)) + prompt.Info(fmt.Sprintf("Current env: %s", curEnv)) - data, err := d.ReadCredentialsValueInEnv(d.CredentialsPath(), env) + inputParams, err := d.resolveInput(cmd, curEnv) if err != nil { return err - } - - if len(data) <= 0 { - prompt.Error("You have no defined credentials in this env") - return nil - } - - var providers []string - for _, c := range data { - providers = append(providers, c.Provider) - } - - cred, err := d.List("Credentials: ", providers) - if err != nil { - return err - } - - if b, err := d.Bool("Are you sure want to delete this credential?", []string{"yes", "no"}); err != nil { - return err - } else if !b { + } else if inputParams.provider == "" { return nil } - if err := d.Delete(cred); err != nil { + if err := d.Delete(inputParams.provider); err != nil { return err } @@ -97,6 +100,52 @@ func (d deleteCredentialCmd) runPrompt() CommandRunnerFunc { } } +func (d *deleteCredentialCmd) resolveInput(cmd *cobra.Command, context string) (inputConfig, error) { + if IsFlagInput(cmd) { + return d.resolveFlags(cmd) + } + return d.resolvePrompt(context) +} + +func (d *deleteCredentialCmd) resolvePrompt(context string) (inputConfig, error) { + data, err := d.ReadCredentialsValueInEnv(d.CredentialsPath(), context) + if err != nil { + return inputConfig{}, err + } + + if len(data) == 0 { + return inputConfig{}, errors.New("you have no defined credentials in this env") + } + + providers := make([]string, 0, len(data)) + for _, c := range data { + providers = append(providers, c.Provider) + } + + provider, err := d.List("Credentials: ", providers) + if err != nil { + return inputConfig{}, err + } + + if b, err := d.Bool("Are you sure want to delete this credential?", []string{"yes", "no"}); err != nil { + return inputConfig{}, err + } else if !b { + return inputConfig{}, nil + } + return inputConfig{provider}, nil +} + +func (d *deleteCredentialCmd) resolveFlags(cmd *cobra.Command) (inputConfig, error) { + provider, err := cmd.Flags().GetString(providerFlagName) + if err != nil { + return inputConfig{}, err + } else if provider == "" { + return inputConfig{}, errors.New("please provide a value for 'provider'") + } + return inputConfig{provider}, nil +} + +// TODO: remove upon stdin deprecation func (d deleteCredentialCmd) runStdin() CommandRunnerFunc { return func(cmd *cobra.Command, args []string) error { dc, err := d.stdinResolver(cmd.InOrStdin()) @@ -104,12 +153,12 @@ func (d deleteCredentialCmd) runStdin() CommandRunnerFunc { return err } - env, err := d.currentEnv() + curEnv, err := d.currentEnv() if err != nil { return err } - data, err := d.ReadCredentialsValueInEnv(d.CredentialsPath(), env) + data, err := d.ReadCredentialsValueInEnv(d.CredentialsPath(), curEnv) if err != nil { return err } diff --git a/pkg/cmd/delete_credential_test.go b/pkg/cmd/delete_credential_test.go index a459257b3..6486f90a6 100644 --- a/pkg/cmd/delete_credential_test.go +++ b/pkg/cmd/delete_credential_test.go @@ -1,13 +1,23 @@ package cmd import ( + "encoding/json" "errors" + "io/ioutil" + "os" + "path/filepath" "strings" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/ZupIT/ritchie-cli/internal/mocks" "github.com/ZupIT/ritchie-cli/pkg/credential" "github.com/ZupIT/ritchie-cli/pkg/env" "github.com/ZupIT/ritchie-cli/pkg/prompt" + "github.com/ZupIT/ritchie-cli/pkg/stream" ) type credDeleteMock struct { @@ -26,6 +36,9 @@ type fieldsTestDeleteCredentialCmd struct { inputList prompt.InputList } +const provider = "github" + +// TODO: remove upon stdin deprecation, reduce dependencies func TestDeleteCredential(t *testing.T) { stdinTest := &deleteCredential{ Provider: "github", @@ -39,13 +52,13 @@ func TestDeleteCredential(t *testing.T) { tests := []struct { name string - wantErr bool + wantErr string fields fieldsTestDeleteCredentialCmd inputStdin string }{ { name: "execute with success", - wantErr: false, + wantErr: "", fields: fieldsTestDeleteCredentialCmd{ credDelete: deleteSuccess, reader: credSettingsCustomMock{ @@ -68,7 +81,7 @@ func TestDeleteCredential(t *testing.T) { }, { name: "error on find env", - wantErr: true, + wantErr: "some error on find env", fields: fieldsTestDeleteCredentialCmd{ credDelete: credDeleteMock{}, reader: credSettingsMock{}, @@ -84,7 +97,7 @@ func TestDeleteCredential(t *testing.T) { }, { name: "error to read credentials value", - wantErr: true, + wantErr: "ReadCredentialsValue error", fields: fieldsTestDeleteCredentialCmd{ credDelete: credDeleteMock{}, reader: credSettingsCustomMock{ @@ -107,7 +120,7 @@ func TestDeleteCredential(t *testing.T) { }, { name: "error when there are no credentials in the env", - wantErr: false, + wantErr: "you have no defined credentials in this env", fields: fieldsTestDeleteCredentialCmd{ credDelete: credDeleteMock{}, reader: credSettingsCustomMock{ @@ -130,7 +143,7 @@ func TestDeleteCredential(t *testing.T) { }, { name: "error on input list", - wantErr: true, + wantErr: "some error", fields: fieldsTestDeleteCredentialCmd{ credDelete: credDeleteMock{}, reader: credSettingsCustomMock{ @@ -153,7 +166,7 @@ func TestDeleteCredential(t *testing.T) { }, { name: "error on input bool", - wantErr: true, + wantErr: "error on boolean list", fields: fieldsTestDeleteCredentialCmd{ credDelete: credDeleteMock{}, reader: credSettingsCustomMock{ @@ -176,7 +189,7 @@ func TestDeleteCredential(t *testing.T) { }, { name: "cancel when input bool is false", - wantErr: false, + wantErr: "", fields: fieldsTestDeleteCredentialCmd{ credDelete: credDeleteMock{}, reader: credSettingsCustomMock{ @@ -199,7 +212,7 @@ func TestDeleteCredential(t *testing.T) { }, { name: "error on Delete", - wantErr: true, + wantErr: "some error on Delete", fields: fieldsTestDeleteCredentialCmd{ credDelete: credDeleteMock{ deleteMock: func() error { @@ -226,7 +239,7 @@ func TestDeleteCredential(t *testing.T) { }, { name: "error different provider", - wantErr: false, + wantErr: "", fields: fieldsTestDeleteCredentialCmd{ credDelete: deleteSuccess, reader: credSettingsCustomMock{ @@ -256,17 +269,118 @@ func TestDeleteCredential(t *testing.T) { deleteCredentialCmd.PersistentFlags().Bool("stdin", false, "input by stdin") deleteCredentialStdin.PersistentFlags().Bool("stdin", true, "input by stdin") + deleteCredentialCmd.SetArgs([]string{}) + deleteCredentialStdin.SetArgs([]string{}) newReader := strings.NewReader(tt.inputStdin) deleteCredentialStdin.SetIn(newReader) - if err := deleteCredentialCmd.Execute(); (err != nil) != tt.wantErr { - t.Errorf("delete credential command error = %v, wantErr %v", err, tt.wantErr) + err := deleteCredentialCmd.Execute() + if err != nil { + require.Equal(t, err.Error(), tt.wantErr) + } else { + require.Empty(t, tt.wantErr) } itsTestCaseWithStdin := tt.inputStdin != "" - if err := deleteCredentialStdin.Execute(); (err != nil) != tt.wantErr && itsTestCaseWithStdin { - t.Errorf("delete credential stdin command error = %v, wantErr %v", err, tt.wantErr) + err = deleteCredentialStdin.Execute() + if itsTestCaseWithStdin { + if err != nil { + require.Equal(t, err.Error(), tt.wantErr) + } else { + require.Empty(t, tt.wantErr) + } + } + }) + } +} + +func TestDeleteCredentialFormula(t *testing.T) { + homeDir := os.TempDir() + ritHomeDir := filepath.Join(homeDir, ".rit") + credentialPath := filepath.Join(ritHomeDir, "credentials", env.Default) + credentialFile := filepath.Join(credentialPath, provider) + _ = os.MkdirAll(credentialPath, os.ModePerm) + defer os.RemoveAll(ritHomeDir) + + fileManager := stream.NewFileManager() + dirManager := stream.NewDirManager(fileManager) + + ctxFinder := env.NewFinder(ritHomeDir, fileManager) + credDeleter := credential.NewCredDelete(ritHomeDir, ctxFinder) + credSettings := credential.NewSettings(fileManager, dirManager, homeDir) + + tests := []struct { + name string + inputBoolResult bool + inputListError error + fileShouldExist bool + args string + wantErr string + }{ + { + name: "execute prompt with success", + inputBoolResult: true, + }, + { + name: "execute flag with success", + args: "--provider=github", + }, + { + name: "execute flag with empty provider fail", + args: "--provider=", + wantErr: "please provide a value for 'provider'", + fileShouldExist: true, + }, + { + name: "fail on input list error", + wantErr: "some error", + inputListError: errors.New("some error"), + fileShouldExist: true, + }, + { + name: "do nothing on input bool refusal", + inputBoolResult: false, + fileShouldExist: true, + }, + } + + cred := credential.Detail{ + Username: "", + Credential: credential.Credential{ + "username": "my user", + }, + Service: provider, + Type: "text", + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + jsonData, _ := json.Marshal(cred) + err := ioutil.WriteFile(credentialFile, jsonData, os.ModePerm) + assert.NoError(t, err) + + listMock := &mocks.InputListMock{} + listMock.On("List", mock.Anything, mock.Anything, mock.Anything).Return(provider, tt.inputListError) + boolMock := &mocks.InputBoolMock{} + boolMock.On("Bool", mock.Anything, mock.Anything, mock.Anything).Return(tt.inputBoolResult, nil) + + cmd := NewDeleteCredentialCmd(credDeleter, credSettings, ctxFinder, boolMock, listMock) + // TODO: remove stdin flag after deprecation + cmd.PersistentFlags().Bool("stdin", false, "input by stdin") + cmd.SetArgs([]string{tt.args}) + + err = cmd.Execute() + if err != nil { + assert.Equal(t, err.Error(), tt.wantErr) + } else { + assert.Empty(t, tt.wantErr) + } + + if tt.fileShouldExist { + assert.FileExists(t, credentialFile) + } else { + assert.NoFileExists(t, credentialFile) } }) } diff --git a/pkg/cmd/delete_env_test.go b/pkg/cmd/delete_env_test.go index 9a72fc951..4793d707d 100644 --- a/pkg/cmd/delete_env_test.go +++ b/pkg/cmd/delete_env_test.go @@ -17,24 +17,111 @@ package cmd import ( + "encoding/json" + "errors" + "io/ioutil" + "os" + "path/filepath" "testing" + "github.com/ZupIT/ritchie-cli/internal/mocks" "github.com/ZupIT/ritchie-cli/pkg/env" + "github.com/ZupIT/ritchie-cli/pkg/stream" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" ) -func TestNewDeleteEnvCmd(t *testing.T) { - findRemoverMock := envFindRemoverMock{holder: env.Holder{ - Current: "", - All: []string{"prod", "qa"}, - }} - cmd := NewDeleteEnvCmd(findRemoverMock, inputTrueMock{}, inputListMock{}) - cmd.PersistentFlags().Bool("stdin", false, "input by stdin") - if cmd == nil { - t.Errorf("NewDeleteEnvCmd got %v", cmd) +func TestNewDeleteEnv(t *testing.T) { + homeDir := os.TempDir() + ritHomeDir := filepath.Join(homeDir, ".rit") + envFile := filepath.Join(ritHomeDir, env.FileName) + _ = os.MkdirAll(ritHomeDir, os.ModePerm) + defer os.RemoveAll(ritHomeDir) + fileManager := stream.NewFileManager() + + envFinder := env.NewFinder(ritHomeDir, fileManager) + envRemover := env.NewRemover(ritHomeDir, envFinder, fileManager) + envFindRemover := env.NewFindRemover(envFinder, envRemover) + + envEmpty := env.Holder{Current: "", All: []string{}} + envCompleted := env.Holder{Current: "prod", All: []string{"prod", "qa", "stg"}} + + tests := []struct { + name string + env env.Holder + envFileNil bool + inputBoolError error + inputBoolResult bool + inputListString string + inputListError error + wantErr string + envResultInFile env.Holder + }{ + { + name: "execute with success", + inputBoolResult: true, + inputListString: "qa", + env: envCompleted, + envResultInFile: env.Holder{Current: "prod", All: []string{"prod", "stg"}}, + }, + { + name: "execute with success when not envs defined", + env: envEmpty, + envResultInFile: envEmpty, + }, + { + name: "fail on input list error", + wantErr: "some error", + inputListError: errors.New("some error"), + env: envCompleted, + }, + { + name: "fail on input bool error", + wantErr: "some error", + inputBoolError: errors.New("some error"), + env: envCompleted, + envResultInFile: envEmpty, + }, + { + name: "do nothing on input bool refusal", + inputBoolResult: false, + env: envCompleted, + envResultInFile: envCompleted, + }, } - if err := cmd.Execute(); err != nil { - t.Errorf("%s = %v, want %v", cmd.Use, err, nil) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if !tt.envFileNil { + jsonData, _ := json.Marshal(tt.env) + err := ioutil.WriteFile(envFile, jsonData, os.ModePerm) + assert.NoError(t, err) + } + + listMock := &mocks.InputListMock{} + boolMock := &mocks.InputBoolMock{} + listMock.On("List", mock.Anything, mock.Anything, mock.Anything).Return(tt.inputListString, tt.inputListError) + boolMock.On("Bool", mock.Anything, mock.Anything, mock.Anything).Return(tt.inputBoolResult, tt.inputBoolError) + + cmd := NewDeleteEnvCmd(envFindRemover, boolMock, listMock) + // TODO: remove stdin flag after deprecation + cmd.PersistentFlags().Bool("stdin", false, "input by stdin") + cmd.SetArgs([]string{}) + + err := cmd.Execute() + if err != nil { + assert.Equal(t, err.Error(), tt.wantErr) + } else { + assert.Empty(t, tt.wantErr) + + assert.FileExists(t, envFile) + + envResult, err := envFinder.Find() + assert.NoError(t, err) + assert.Equal(t, tt.envResultInFile, envResult) + + } + }) } } diff --git a/pkg/cmd/formula.go b/pkg/cmd/formula.go index 0bfe8499a..33723b63c 100644 --- a/pkg/cmd/formula.go +++ b/pkg/cmd/formula.go @@ -31,7 +31,6 @@ import ( "github.com/ZupIT/ritchie-cli/pkg/api" "github.com/ZupIT/ritchie-cli/pkg/formula" "github.com/ZupIT/ritchie-cli/pkg/formula/input" - "github.com/ZupIT/ritchie-cli/pkg/prompt" "github.com/ZupIT/ritchie-cli/pkg/stream" ) @@ -74,16 +73,6 @@ var ( } ) -type flag struct { - name string - shortName string - kind reflect.Kind - defValue interface{} - description string -} - -type flags []flag - var ErrRunFormulaWithTwoFlag = errors.New("you cannot run formula with --docker and --local flags together") type FormulaCommand struct { @@ -165,7 +154,7 @@ func (f FormulaCommand) newFormulaCmd(cmd api.Command) *cobra.Command { } flags := formulaCmd.Flags() - addReservedFlags(flags) + addReservedFlags(flags, reservedFlags) f.addInputFlags(def, flags) return formulaCmd @@ -223,22 +212,6 @@ func (f FormulaCommand) execFormulaFunc(repo, path string) func(cmd *cobra.Comma } } -func addReservedFlags(flags *pflag.FlagSet) { - for _, flag := range reservedFlags { - switch flag.kind { //nolint:exhaustive - case reflect.String: - flags.StringP(flag.name, flag.shortName, flag.defValue.(string), flag.description) - case reflect.Bool: - flags.BoolP(flag.name, flag.shortName, flag.defValue.(bool), flag.description) - case reflect.Int: - flags.IntP(flag.name, flag.shortName, flag.defValue.(int), flag.description) - default: - warning := fmt.Sprintf("The %q type is not supported for the %q flag", flag.kind.String(), flag.name) - prompt.Warning(warning) - } - } -} - func (f FormulaCommand) addInputFlags(def formula.Definition, flags *pflag.FlagSet) { s := def.FormulaPath(api.RitchieHomeDir()) configPath := def.ConfigPath(s) diff --git a/pkg/commands/builder.go b/pkg/commands/builder.go index 1577821bb..794322e61 100644 --- a/pkg/commands/builder.go +++ b/pkg/commands/builder.go @@ -212,7 +212,8 @@ func Build() *cobra.Command { inputText, inputBool, inputList, - inputPassword) + inputPassword, + ) listCredentialCmd := cmd.NewListCredentialCmd(credSettings) deleteCredentialCmd := cmd.NewDeleteCredentialCmd( credDeleter,