diff --git a/Makefile b/Makefile index ff96f0d..087f22d 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,8 @@ clean: .PHONY: mocks mocks: # Ref: https://github.com/golang/mock - mockgen -source=client/client.go -destination=mocks/client.go -package=mocks + mockgen -source=internal/client/client.go -destination=internal/mocks/client.go -package=mocks + mockgen -source=internal/terminal/terminal.go -destination=internal/mocks/terminal.go -package=mocks .PHONY: test test: diff --git a/cmd/configure/configure.go b/cmd/configure/configure.go index 36ae331..82d3691 100644 --- a/cmd/configure/configure.go +++ b/cmd/configure/configure.go @@ -7,7 +7,7 @@ import ( "github.com/spf13/cobra" ) -func NewConfigureCmd() *cobra.Command { +func NewConfigureCmd(term terminal.Terminal) *cobra.Command { var ClientID string var ClientSecret string var err error @@ -20,7 +20,7 @@ func NewConfigureCmd() *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { if ClientID == "" { - ClientID, err = terminal.PromptPassword("Personal Access Token Client ID:") + ClientID, err = term.PromptPassword("Personal Access Token Client ID:") if err != nil { return err } @@ -29,7 +29,7 @@ func NewConfigureCmd() *cobra.Command { config.SetPatClientID(ClientID) if ClientSecret == "" { - ClientSecret, err = terminal.PromptPassword("Personal Access Token Client Secret:") + ClientSecret, err = term.PromptPassword("Personal Access Token Client Secret:") if err != nil { return err } diff --git a/cmd/connector/conn.go b/cmd/connector/conn.go index 78fbb19..f81646d 100644 --- a/cmd/connector/conn.go +++ b/cmd/connector/conn.go @@ -9,6 +9,7 @@ import ( "github.com/sailpoint-oss/sailpoint-cli/internal/client" "github.com/sailpoint-oss/sailpoint-cli/internal/config" + "github.com/sailpoint-oss/sailpoint-cli/internal/terminal" "github.com/spf13/cobra" "github.com/spf13/pflag" "gopkg.in/yaml.v2" @@ -18,7 +19,7 @@ const ( connectorsEndpoint = "/beta/platform-connectors" ) -func NewConnCmd() *cobra.Command { +func NewConnCmd(term terminal.Terminal) *cobra.Command { conn := &cobra.Command{ Use: "connectors", Short: "manage connectors", @@ -45,7 +46,7 @@ func NewConnCmd() *cobra.Command { newConnCreateCmd(Client), newConnCreateVersionCmd(Client), newConnVersionsCmd(Client), - newConnInvokeCmd(Client), + newConnInvokeCmd(Client, term), newConnValidateCmd(Client), newConnTagCmd(Client), newConnValidateSourcesCmd(Client), diff --git a/cmd/connector/conn_invoke.go b/cmd/connector/conn_invoke.go index 0c18137..1a2387a 100644 --- a/cmd/connector/conn_invoke.go +++ b/cmd/connector/conn_invoke.go @@ -9,6 +9,7 @@ import ( connclient "github.com/sailpoint-oss/sailpoint-cli/cmd/connector/client" "github.com/sailpoint-oss/sailpoint-cli/internal/client" + "github.com/sailpoint-oss/sailpoint-cli/internal/terminal" "github.com/spf13/cobra" ) @@ -23,7 +24,7 @@ const ( stdTestConnection = "std:test-connection" ) -func newConnInvokeCmd(client client.Client) *cobra.Command { +func newConnInvokeCmd(client client.Client, term terminal.Terminal) *cobra.Command { cmd := &cobra.Command{ Use: "invoke", Short: "Invoke Command on a connector", @@ -42,7 +43,7 @@ func newConnInvokeCmd(client client.Client) *cobra.Command { cmd.AddCommand( newConnInvokeTestConnectionCmd(client), - newConnInvokeChangePasswordCmd(client), + newConnInvokeChangePasswordCmd(client, term), newConnInvokeAccountCreateCmd(client), newConnInvokeAccountDiscoverSchemaCmd(client), newConnInvokeAccountListCmd(client), diff --git a/cmd/connector/conn_invoke_change-password.go b/cmd/connector/conn_invoke_change_password.go similarity index 86% rename from cmd/connector/conn_invoke_change-password.go rename to cmd/connector/conn_invoke_change_password.go index 9181fd5..0dedca2 100644 --- a/cmd/connector/conn_invoke_change-password.go +++ b/cmd/connector/conn_invoke_change_password.go @@ -10,7 +10,7 @@ import ( ) // newConnInvokeChangePasswordCmd defines a command to perform change password operation -func newConnInvokeChangePasswordCmd(spClient client.Client) *cobra.Command { +func newConnInvokeChangePasswordCmd(spClient client.Client, term terminal.Terminal) *cobra.Command { cmd := &cobra.Command{ Use: "change-password", Short: "Invoke a change-password command", @@ -24,7 +24,7 @@ func newConnInvokeChangePasswordCmd(spClient client.Client) *cobra.Command { return err } - password, err := terminal.PromptPassword("Enter Password:") + password, err := term.PromptPassword("Enter Password:") if err != nil { return err } diff --git a/cmd/connector/conn_invoke_change_password_test.go b/cmd/connector/conn_invoke_change_password_test.go new file mode 100644 index 0000000..7edc127 --- /dev/null +++ b/cmd/connector/conn_invoke_change_password_test.go @@ -0,0 +1,92 @@ +// Copyright (c) 2023, SailPoint Technologies, Inc. All rights reserved. +package connector + +import ( + "bytes" + "io" + "net/http" + "testing" + + "github.com/golang/mock/gomock" + "github.com/sailpoint-oss/sailpoint-cli/internal/mocks" +) + +func TestChangePasswordWithoutInput(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + client := mocks.NewMockClient(ctrl) + term := mocks.NewMockTerm(ctrl) + + cmd := newConnInvokeChangePasswordCmd(client, term) + addRequiredFlagsFromParentCmd(cmd) + + b := new(bytes.Buffer) + cmd.SetOut(b) + cmd.SetArgs([]string{"-c", "test-connector", "--config-json", "{}"}) + + err := cmd.Execute() + if err == nil { + t.Errorf("failed to detect error: reading account without identity") + } +} + +func TestChangePasswordWithIdentityAndPassword(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + i := `{"connectorRef":"test-connector","tag":"latest","type":"std:change-password","config":{},` + + `"input":{"identity":"john.doe","key":{"simple":{"id":"john.doe"}},"password":"password"}}` + + client := mocks.NewMockClient(ctrl) + client.EXPECT(). + Post(gomock.Any(), gomock.Any(), "application/json", bytes.NewReader([]byte(i))). + Return(&http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewReader([]byte("{}")))}, nil) + + term := mocks.NewMockTerm(ctrl) + term.EXPECT(). + PromptPassword(gomock.Any()). + Return("password", nil) + + cmd := newConnInvokeChangePasswordCmd(client, term) + addRequiredFlagsFromParentCmd(cmd) + + b := new(bytes.Buffer) + cmd.SetOut(b) + cmd.SetArgs([]string{"john.doe", "-c", "test-connector", "--config-json", "{}"}) + + err := cmd.Execute() + if err != nil { + t.Errorf("command failed with err: %s", err) + } +} + +func TestChangePasswordWithIdentityAndPasswordAndUniqueId(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + i := `{"connectorRef":"test-connector","tag":"latest","type":"std:change-password","config":{},` + + `"input":{"identity":"john.doe","key":{"compound":{"lookupId":"john.doe","uniqueId":"12345"}},"password":"password"}}` + + client := mocks.NewMockClient(ctrl) + client.EXPECT(). + Post(gomock.Any(), gomock.Any(), "application/json", bytes.NewReader([]byte(i))). + Return(&http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewReader([]byte("{}")))}, nil) + + term := mocks.NewMockTerm(ctrl) + term.EXPECT(). + PromptPassword(gomock.Any()). + Return("password", nil) + + cmd := newConnInvokeChangePasswordCmd(client, term) + addRequiredFlagsFromParentCmd(cmd) + + b := new(bytes.Buffer) + cmd.SetOut(b) + cmd.SetArgs([]string{"john.doe", "12345", "-c", "test-connector", "--config-json", "{}"}) + + err := cmd.Execute() + if err != nil { + t.Errorf("command failed with err: %s", err) + } +} diff --git a/cmd/connector/conn_invoke_test.go b/cmd/connector/conn_invoke_test.go index 7d6c891..ac0661d 100644 --- a/cmd/connector/conn_invoke_test.go +++ b/cmd/connector/conn_invoke_test.go @@ -24,7 +24,7 @@ func TestNewConnInvokeCmd_noArgs(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - cmd := newConnInvokeCmd(mocks.NewMockClient(ctrl)) + cmd := newConnInvokeCmd(mocks.NewMockClient(ctrl), mocks.NewMockTerm(ctrl)) if len(cmd.Commands()) != numConnInvokeSubcommands { t.Fatalf("expected: %d, actual: %d", len(cmd.Commands()), numConnInvokeSubcommands) } diff --git a/cmd/root/root.go b/cmd/root/root.go index 74591b8..b61be1c 100644 --- a/cmd/root/root.go +++ b/cmd/root/root.go @@ -13,6 +13,7 @@ import ( "github.com/sailpoint-oss/sailpoint-cli/cmd/spconfig" "github.com/sailpoint-oss/sailpoint-cli/cmd/transform" "github.com/sailpoint-oss/sailpoint-cli/cmd/va" + "github.com/sailpoint-oss/sailpoint-cli/internal/terminal" "github.com/spf13/cobra" ) @@ -34,13 +35,15 @@ func NewRootCmd() *cobra.Command { }, } + t := &terminal.Term{} + root.AddCommand( set.NewSetCommand(), environment.NewEnvironmentCommand(), - configure.NewConfigureCmd(), - connector.NewConnCmd(), + configure.NewConfigureCmd(t), + connector.NewConnCmd(t), transform.NewTransformCmd(), - va.NewVACmd(), + va.NewVACmd(t), search.NewSearchCmd(), spconfig.NewSPConfigCmd(), report.NewReportCommand(), diff --git a/cmd/va/collect.go b/cmd/va/collect.go index f6e29c7..a4daebe 100644 --- a/cmd/va/collect.go +++ b/cmd/va/collect.go @@ -11,7 +11,7 @@ import ( "github.com/spf13/cobra" ) -func newCollectCmd() *cobra.Command { +func newCollectCmd(term terminal.Terminal) *cobra.Command { var output string var logs bool var config bool @@ -37,7 +37,7 @@ func newCollectCmd() *cobra.Command { } for credential := 0; credential < len(args); credential++ { - password, _ := terminal.PromptPassword(fmt.Sprintf("Enter Password for %v:", args[credential])) + password, _ := term.PromptPassword(fmt.Sprintf("Enter Password for %v:", args[credential])) credentials = append(credentials, password) } diff --git a/cmd/va/troubleshoot.go b/cmd/va/troubleshoot.go index 47ac2f0..3d71977 100644 --- a/cmd/va/troubleshoot.go +++ b/cmd/va/troubleshoot.go @@ -13,7 +13,7 @@ import ( "github.com/spf13/cobra" ) -func NewTroubleshootCmd() *cobra.Command { +func NewTroubleshootCmd(term terminal.Terminal) *cobra.Command { var output string cmd := &cobra.Command{ Use: "troubleshoot", @@ -29,7 +29,7 @@ func NewTroubleshootCmd() *cobra.Command { var credentials []string for credential := 0; credential < len(args); credential++ { - password, _ := terminal.PromptPassword(fmt.Sprintf("Enter Password for %v:", args[credential])) + password, _ := term.PromptPassword(fmt.Sprintf("Enter Password for %v:", args[credential])) credentials = append(credentials, password) } diff --git a/cmd/va/update.go b/cmd/va/update.go index 6df0474..6db7351 100644 --- a/cmd/va/update.go +++ b/cmd/va/update.go @@ -9,7 +9,7 @@ import ( "github.com/spf13/cobra" ) -func newUpdateCmd() *cobra.Command { +func newUpdateCmd(term terminal.Terminal) *cobra.Command { cmd := &cobra.Command{ Use: "update", Short: "Perform Update Operations on a SailPoint Virtual Appliance", @@ -19,7 +19,7 @@ func newUpdateCmd() *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { var credentials []string for credential := 0; credential < len(args); credential++ { - password, _ := terminal.PromptPassword(fmt.Sprintf("Enter Password for %v:", args[credential])) + password, _ := term.PromptPassword(fmt.Sprintf("Enter Password for %v:", args[credential])) credentials = append(credentials, password) } for i := 0; i < len(args); i++ { diff --git a/cmd/va/va.go b/cmd/va/va.go index 7f0a9ad..6d34450 100644 --- a/cmd/va/va.go +++ b/cmd/va/va.go @@ -4,10 +4,11 @@ package va import ( "fmt" + "github.com/sailpoint-oss/sailpoint-cli/internal/terminal" "github.com/spf13/cobra" ) -func NewVACmd() *cobra.Command { +func NewVACmd(term terminal.Terminal) *cobra.Command { cmd := &cobra.Command{ Use: "va", Short: "Interact with SailPoint Virtual Appliances", @@ -19,9 +20,9 @@ func NewVACmd() *cobra.Command { } cmd.AddCommand( - newCollectCmd(), + newCollectCmd(term), // newTroubleshootCmd(), - newUpdateCmd(), + newUpdateCmd(term), newParseCmd(), ) diff --git a/internal/mocks/client.go b/internal/mocks/client.go index f3efdd0..5cdcd38 100644 --- a/internal/mocks/client.go +++ b/internal/mocks/client.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: client/client.go +// Source: internal/client/client.go // Package mocks is a generated GoMock package. package mocks @@ -95,17 +95,3 @@ func (mr *MockClientMockRecorder) Put(ctx, url, contentType, body interface{}) * mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Put", reflect.TypeOf((*MockClient)(nil).Put), ctx, url, contentType, body) } - -// VerifyToken mocks base method. -func (m *MockClient) VerifyToken(ctx context.Context, tokenUrl, clientID, secret string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "VerifyToken", ctx, tokenUrl, clientID, secret) - ret0, _ := ret[0].(error) - return ret0 -} - -// VerifyToken indicates an expected call of VerifyToken. -func (mr *MockClientMockRecorder) VerifyToken(ctx, tokenUrl, clientID, secret interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VerifyToken", reflect.TypeOf((*MockClient)(nil).VerifyToken), ctx, tokenUrl, clientID, secret) -} diff --git a/internal/mocks/terminal.go b/internal/mocks/terminal.go new file mode 100644 index 0000000..36b80e1 --- /dev/null +++ b/internal/mocks/terminal.go @@ -0,0 +1,49 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: internal/terminal/terminal.go + +// Package mocks is a generated GoMock package. +package mocks + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" +) + +// MockTerm is a mock of Term interface. +type MockTerm struct { + ctrl *gomock.Controller + recorder *MockTermMockRecorder +} + +// MockTermMockRecorder is the mock recorder for MockTerm. +type MockTermMockRecorder struct { + mock *MockTerm +} + +// NewMockTerm creates a new mock instance. +func NewMockTerm(ctrl *gomock.Controller) *MockTerm { + mock := &MockTerm{ctrl: ctrl} + mock.recorder = &MockTermMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockTerm) EXPECT() *MockTermMockRecorder { + return m.recorder +} + +// PromptPassword mocks base method. +func (m *MockTerm) PromptPassword(promptMsg string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PromptPassword", promptMsg) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PromptPassword indicates an expected call of PromptPassword. +func (mr *MockTermMockRecorder) PromptPassword(promptMsg interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PromptPassword", reflect.TypeOf((*MockTerm)(nil).PromptPassword), promptMsg) +} diff --git a/internal/terminal/terminal.go b/internal/terminal/terminal.go index dcde77e..d2f4b19 100644 --- a/internal/terminal/terminal.go +++ b/internal/terminal/terminal.go @@ -11,8 +11,14 @@ import ( "golang.org/x/term" ) +type Term struct{} + +type Terminal interface { + PromptPassword(promptMsg string) (string, error) +} + // PromptPassword prompts user to enter password and then returns it -func PromptPassword(promptMsg string) (string, error) { +func (c *Term) PromptPassword(promptMsg string) (string, error) { fmt.Print(promptMsg) bytePassword, err := term.ReadPassword(int(syscall.Stdin)) if err != nil {