From 1c38bc40782e5099068def51da6262cd0ec7e137 Mon Sep 17 00:00:00 2001 From: Andrey Pozolotin Date: Fri, 12 Mar 2021 19:59:19 +0300 Subject: [PATCH] Added tests --- internal/pkg/config/prompt.go | 7 + internal/pkg/controllers/commands.go | 7 +- internal/pkg/controllers/commands_test.go | 299 ++++++++++++++++++++++ internal/pkg/controllers/init_test.go | 28 -- internal/pkg/controllers/mocks.go | 29 +++ internal/pkg/output/client_test.go | 219 +++++++++++++--- internal/pkg/output/format_test.go | 71 +++++ 7 files changed, 596 insertions(+), 64 deletions(-) create mode 100644 internal/pkg/controllers/commands_test.go create mode 100644 internal/pkg/controllers/mocks.go create mode 100644 internal/pkg/output/format_test.go diff --git a/internal/pkg/config/prompt.go b/internal/pkg/config/prompt.go index 538200a..8a1032e 100644 --- a/internal/pkg/config/prompt.go +++ b/internal/pkg/config/prompt.go @@ -9,6 +9,8 @@ import ( "github.com/fatih/color" ) +const maxPromptIterations = 100 + type PromptReader interface { ReadString() (string, error) ReadPassword() (string, error) @@ -40,6 +42,7 @@ func PromptRequiredValues( } err = missedReqP.Validate(missedReqP.Field, readValue) + promptCount := 0 for err != nil { readValue, err = promptValue(missedReqP, promptReader) if err != nil { @@ -50,6 +53,10 @@ func PromptRequiredValues( if err != nil { color.Red(err.Error()) } + promptCount++ + if promptCount > maxPromptIterations { + return fmt.Errorf("max prompt attempts %d elapsed", maxPromptIterations) + } } targetKV[missedReqP.Field] = readValue } diff --git a/internal/pkg/controllers/commands.go b/internal/pkg/controllers/commands.go index f6237d9..8a33451 100644 --- a/internal/pkg/controllers/commands.go +++ b/internal/pkg/controllers/commands.go @@ -3,6 +3,7 @@ package controllers import ( "context" "encoding/json" + "fmt" "io" "os" "os/signal" @@ -224,11 +225,11 @@ func (icm *InteractiveCommandsController) processRawMessage(msg []byte) error { var errResp models.ErrorResp err = json.Unmarshal(msg, &errResp) if err != nil { - logrus.Errorf("cannot recognize command output message: %s, reason: %v", string(msg), err) - return err + e := fmt.Errorf("cannot recognize command output message: %s, reason: %v", string(msg), err) + return e } icm.Spinner.StopError(errResp.Error()) - return nil + return errResp } logrus.Debugf("received message: '%s'", string(msg)) diff --git a/internal/pkg/controllers/commands_test.go b/internal/pkg/controllers/commands_test.go new file mode 100644 index 0000000..4595db7 --- /dev/null +++ b/internal/pkg/controllers/commands_test.go @@ -0,0 +1,299 @@ +package controllers + +import ( + "context" + "encoding/json" + "io" + "testing" + "time" + + "github.com/cloudradar-monitoring/rportcli/internal/pkg/models" + "github.com/stretchr/testify/assert" +) + +type ReadChunk struct { + Output []byte + Err error +} + +type ReadWriterMock struct { + itemsToRead []ReadChunk + itemReadIndex int + writtenItems []string + writeError error + isClosed bool + closeError error +} + +func (rwm *ReadWriterMock) Read() (msg []byte, err error) { + item := rwm.itemsToRead[rwm.itemReadIndex] + + msg = item.Output + err = item.Err + + rwm.itemReadIndex++ + + return +} + +func (rwm *ReadWriterMock) Write(inputMsg []byte) (n int, err error) { + rwm.writtenItems = append(rwm.writtenItems, string(inputMsg)) + return 0, rwm.writeError +} + +func (rwm *ReadWriterMock) Close() error { + rwm.isClosed = true + return rwm.closeError +} + +type SpinnerMock struct { + startMsgs []string + updateMsgs []string + stopSuccessMsgs []string + stopErrorMsgs []string +} + +func (sm *SpinnerMock) Start(msg string) { + sm.startMsgs = append(sm.startMsgs, msg) +} + +func (sm *SpinnerMock) Update(msg string) { + sm.updateMsgs = append(sm.updateMsgs, msg) +} + +func (sm *SpinnerMock) StopSuccess(msg string) { + sm.stopSuccessMsgs = append(sm.stopSuccessMsgs, msg) +} + +func (sm *SpinnerMock) StopError(msg string) { + sm.stopErrorMsgs = append(sm.stopErrorMsgs, msg) +} + +type JobRendererMock struct { + jobToRender *models.Job + err error +} + +func (jrm *JobRendererMock) RenderJob(j *models.Job) error { + jrm.jobToRender = j + return jrm.err +} + +func TestInteractiveCommandExecutionSuccess(t *testing.T) { + jobResp := models.Job{ + Jid: "123", + Status: "done", + FinishedAt: time.Now(), + ClientID: "123", + Command: "ls", + Shell: "sh", + Pid: 12, + StartedAt: time.Now(), + CreatedBy: "admin", + TimeoutSec: 1, + Result: models.JobResult{ + Stdout: "some out", + Stderr: "some err", + }, + } + jobRespBytes, err := json.Marshal(jobResp) + assert.NoError(t, err) + if err != nil { + return + } + + rw := &ReadWriterMock{ + itemsToRead: []ReadChunk{ + { + Output: jobRespBytes, + }, + { + Err: io.EOF, + }, + }, + writtenItems: []string{}, + isClosed: false, + } + + pr := &PromptReaderMock{ + ReadOutputs: []string{}, + PasswordReadOutputs: []string{}, + } + + s := &SpinnerMock{ + startMsgs: []string{}, + updateMsgs: []string{}, + stopSuccessMsgs: []string{}, + stopErrorMsgs: []string{}, + } + + jr := &JobRendererMock{} + + ic := &InteractiveCommandsController{ + ReadWriter: rw, + PromptReader: pr, + Spinner: s, + JobRenderer: jr, + } + + cids := "1235" + cmd := "cmd" + to := "1" + gi := "333" + ec := "1" + err = ic.Start(context.Background(), map[string]*string{ + clientIDs: &cids, + command: &cmd, + timeout: &to, + groupIDs: &gi, + execConcurrently: &ec, + }) + + assert.NoError(t, err) + + assert.Equal(t, pr.PasswordReadCount, 0) + assert.Equal(t, pr.ReadCount, 0) + + assert.Len(t, rw.writtenItems, 1) + expectedCommandInput := `{"command":"cmd","client_ids":["1235"],"group_ids":["333"],"timeout_sec":1,"execute_concurrently":true}` + assert.Equal(t, expectedCommandInput, rw.writtenItems[0]) + + assert.NotNil(t, jr.jobToRender) + actualJobRenderResult, err := json.Marshal(jr.jobToRender) + assert.NoError(t, err) + assert.Equal(t, string(jobRespBytes), string(actualJobRenderResult)) + assert.True(t, rw.isClosed) + assert.Len(t, s.stopErrorMsgs, 0) + assert.Len(t, s.stopSuccessMsgs, 2) +} + +func TestInteractiveCommandExecutionWithPromptParams(t *testing.T) { + jobResp := models.Job{ + Jid: "123", + Status: "done", + } + jobRespBytes, err := json.Marshal(jobResp) + assert.NoError(t, err) + if err != nil { + return + } + + rw := &ReadWriterMock{ + itemsToRead: []ReadChunk{ + { + Output: jobRespBytes, + }, + { + Err: io.EOF, + }, + }, + writtenItems: []string{}, + isClosed: false, + } + + pr := &PromptReaderMock{ + ReadOutputs: []string{ + "123", + "dir", + }, + PasswordReadOutputs: []string{}, + } + + s := &SpinnerMock{ + startMsgs: []string{}, + updateMsgs: []string{}, + stopSuccessMsgs: []string{}, + stopErrorMsgs: []string{}, + } + + jr := &JobRendererMock{} + + ic := &InteractiveCommandsController{ + ReadWriter: rw, + PromptReader: pr, + Spinner: s, + JobRenderer: jr, + } + + err = ic.Start(context.Background(), map[string]*string{}) + + assert.NoError(t, err) + + assert.Equal(t, pr.PasswordReadCount, 0) + assert.Equal(t, pr.ReadCount, 2) + + assert.Len(t, rw.writtenItems, 1) + expectedCommandInput := `{"command":"dir","client_ids":["123"],"timeout_sec":30,"execute_concurrently":false}` + assert.Equal(t, expectedCommandInput, rw.writtenItems[0]) + + assert.NotNil(t, jr.jobToRender) + actualJobRenderResult, err := json.Marshal(jr.jobToRender) + assert.NoError(t, err) + assert.Equal(t, string(jobRespBytes), string(actualJobRenderResult)) + assert.True(t, rw.isClosed) + assert.Len(t, s.stopErrorMsgs, 0) + assert.Len(t, s.stopSuccessMsgs, 2) +} + +func TestInteractiveCommandExecutionWithInvalidResponse(t *testing.T) { + resp := models.ErrorResp{ + Errors: []models.Error{ + { + Code: "500", + Title: "some error", + Detail: "some error detail", + }, + }, + } + jobRespBytes, err := json.Marshal(resp) + assert.NoError(t, err) + if err != nil { + return + } + rw := &ReadWriterMock{ + itemsToRead: []ReadChunk{ + { + Output: jobRespBytes, + }, + { + Err: io.EOF, + }, + }, + writtenItems: []string{}, + isClosed: false, + } + + pr := &PromptReaderMock{ + ReadOutputs: []string{}, + PasswordReadOutputs: []string{}, + } + + s := &SpinnerMock{ + startMsgs: []string{}, + updateMsgs: []string{}, + stopSuccessMsgs: []string{}, + stopErrorMsgs: []string{}, + } + + jr := &JobRendererMock{} + + ic := &InteractiveCommandsController{ + ReadWriter: rw, + PromptReader: pr, + Spinner: s, + JobRenderer: jr, + } + + cids := "123" + cmd := "ls" + err = ic.Start(context.Background(), map[string]*string{ + clientIDs: &cids, + command: &cmd, + }) + + assert.Error(t, err) + if err == nil { + return + } + assert.Contains(t, err.Error(), "some error, code: 500, details: some error detail") +} diff --git a/internal/pkg/controllers/init_test.go b/internal/pkg/controllers/init_test.go index 2b4e41a..018cc76 100644 --- a/internal/pkg/controllers/init_test.go +++ b/internal/pkg/controllers/init_test.go @@ -12,34 +12,6 @@ import ( "github.com/stretchr/testify/assert" ) -type PromptReaderMock struct { - ReadCount int - PasswordReadCount int - ReadOutputs []string - PasswordReadOutputs []string - ErrToGive error -} - -func (prm *PromptReaderMock) ReadString() (string, error) { - prm.ReadCount++ - - if len(prm.ReadOutputs) < prm.ReadCount { - return "", prm.ErrToGive - } - - return prm.ReadOutputs[prm.ReadCount-1], prm.ErrToGive -} - -func (prm *PromptReaderMock) ReadPassword() (string, error) { - prm.PasswordReadCount++ - - if len(prm.PasswordReadOutputs) < prm.PasswordReadCount { - return "", prm.ErrToGive - } - - return prm.PasswordReadOutputs[prm.PasswordReadCount-1], prm.ErrToGive -} - func TestInitSuccess(t *testing.T) { statusRequested := false srv := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { diff --git a/internal/pkg/controllers/mocks.go b/internal/pkg/controllers/mocks.go new file mode 100644 index 0000000..85a7361 --- /dev/null +++ b/internal/pkg/controllers/mocks.go @@ -0,0 +1,29 @@ +package controllers + +type PromptReaderMock struct { + ReadCount int + PasswordReadCount int + ReadOutputs []string + PasswordReadOutputs []string + ErrToGive error +} + +func (prm *PromptReaderMock) ReadString() (string, error) { + prm.ReadCount++ + + if len(prm.ReadOutputs) < prm.ReadCount { + return "", prm.ErrToGive + } + + return prm.ReadOutputs[prm.ReadCount-1], prm.ErrToGive +} + +func (prm *PromptReaderMock) ReadPassword() (string, error) { + prm.PasswordReadCount++ + + if len(prm.PasswordReadOutputs) < prm.PasswordReadCount { + return "", prm.ErrToGive + } + + return prm.PasswordReadOutputs[prm.PasswordReadCount-1], prm.ErrToGive +} diff --git a/internal/pkg/output/client_test.go b/internal/pkg/output/client_test.go index c0014cb..f8e77b7 100644 --- a/internal/pkg/output/client_test.go +++ b/internal/pkg/output/client_test.go @@ -9,6 +9,91 @@ import ( ) func TestRenderClients(t *testing.T) { + testCases := []struct { + Format string + ExpectedOutput string + }{ + { + Format: FormatHuman, + ExpectedOutput: `Clients +ID NAME NUM TUNNELS REMOTE ADDRESS HOSTNAME OS KERNEL +123 SomeName 0 +124 SomeOtherName 0 +`, + }, + { + Format: FormatJSON, + ExpectedOutput: `[{"id":"123","name":"SomeName","os":"","os_arch":"","os_family":"","os_kernel":"","hostname":"","ipv4":null,"ipv6":null,"tags":null,"version":"","address":"","Tunnels":null},{"id":"124","name":"SomeOtherName","os":"","os_arch":"","os_family":"","os_kernel":"","hostname":"","ipv4":null,"ipv6":null,"tags":null,"version":"","address":"","Tunnels":null}] +`, + }, + { + Format: FormatJSONPretty, + ExpectedOutput: `[ + { + "id": "123", + "name": "SomeName", + "os": "", + "os_arch": "", + "os_family": "", + "os_kernel": "", + "hostname": "", + "ipv4": null, + "ipv6": null, + "tags": null, + "version": "", + "address": "", + "Tunnels": null + }, + { + "id": "124", + "name": "SomeOtherName", + "os": "", + "os_arch": "", + "os_family": "", + "os_kernel": "", + "hostname": "", + "ipv4": null, + "ipv6": null, + "tags": null, + "version": "", + "address": "", + "Tunnels": null + } +] +`, + }, + { + Format: FormatYAML, + ExpectedOutput: `- id: "123" + name: SomeName + os: "" + osarch: "" + osfamily: "" + oskernel: "" + hostname: "" + ipv4: [] + ipv6: [] + tags: [] + version: "" + address: "" + tunnels: [] +- id: "124" + name: SomeOtherName + os: "" + osarch: "" + osfamily: "" + oskernel: "" + hostname: "" + ipv4: [] + ipv6: [] + tags: [] + version: "" + address: "" + tunnels: [] +`, + }, + } + clients := []*models.Client{ { ID: "123", @@ -20,49 +105,117 @@ func TestRenderClients(t *testing.T) { }, } - buf := &bytes.Buffer{} - cr := &ClientRenderer{ - ColCountCalculator: func() int { - return 150 - }, - Writer: buf, - } + for _, testCase := range testCases { + buf := &bytes.Buffer{} + cr := &ClientRenderer{ + ColCountCalculator: func() int { + return 150 + }, + Writer: buf, + Format: testCase.Format, + } - err := cr.RenderClients(clients) - assert.NoError(t, err) - if err != nil { - return - } + err := cr.RenderClients(clients) + assert.NoError(t, err) + if err != nil { + return + } - actualRenderResult := RemoveEmptySpaces(buf.String()) - assert.Equal( - t, - "Clients ID NAME NUM TUNNELS REMOTE ADDRESS HOSTNAME OS KERNEL 123 SomeName 0 124 SomeOtherName 0", - actualRenderResult, - ) + assert.Equal( + t, + testCase.ExpectedOutput, + buf.String(), + ) + } } func TestRenderClient(t *testing.T) { + testCases := []struct { + Format string + ExpectedOutput string + }{ + { + Format: FormatHuman, + ExpectedOutput: `Client [123] + +KEY VALUE +ID: 123 +Name: SomeName +Os: +OsArch: +OsFamily: +OsKernel: +Hostname: +Ipv4: +Ipv6: +Tags: +Version: +Address: +`, + }, + { + Format: FormatJSON, + ExpectedOutput: `{"id":"123","name":"SomeName","os":"","os_arch":"","os_family":"","os_kernel":"","hostname":"","ipv4":null,"ipv6":null,"tags":null,"version":"","address":"","Tunnels":null} +`, + }, + { + Format: FormatJSONPretty, + ExpectedOutput: `{ + "id": "123", + "name": "SomeName", + "os": "", + "os_arch": "", + "os_family": "", + "os_kernel": "", + "hostname": "", + "ipv4": null, + "ipv6": null, + "tags": null, + "version": "", + "address": "", + "Tunnels": null +} +`, + }, + { + Format: FormatYAML, + ExpectedOutput: `id: "123" +name: SomeName +os: "" +osarch: "" +osfamily: "" +oskernel: "" +hostname: "" +ipv4: [] +ipv6: [] +tags: [] +version: "" +address: "" +tunnels: [] +`, + }, + } client := &models.Client{ ID: "123", Name: "SomeName", } - buf := &bytes.Buffer{} - cr := &ClientRenderer{ - ColCountCalculator: func() int { - return 150 - }, - Writer: buf, - } + for _, testCase := range testCases { + buf := &bytes.Buffer{} + cr := &ClientRenderer{ + ColCountCalculator: func() int { + return 150 + }, + Writer: buf, + Format: testCase.Format, + } - err := cr.RenderClient(client) - assert.NoError(t, err) - if err != nil { - return - } + err := cr.RenderClient(client) + assert.NoError(t, err) + if err != nil { + return + } - actualRenderResult := RemoveEmptySpaces(buf.String()) - expectedResult := `Client [123] KEY VALUE ID: 123 Name: SomeName Os: OsArch: OsFamily: OsKernel: Hostname: Ipv4: Ipv6: Tags: Version: Address:` - assert.Equal(t, expectedResult, actualRenderResult) + assert.Equal(t, testCase.ExpectedOutput, buf.String()) + } } diff --git a/internal/pkg/output/format_test.go b/internal/pkg/output/format_test.go new file mode 100644 index 0000000..af7da8e --- /dev/null +++ b/internal/pkg/output/format_test.go @@ -0,0 +1,71 @@ +package output + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRenderByFormat(t *testing.T) { + testCases := []struct { + inputFormat string + source interface{} + expectedResult string + expectedError string + }{ + { + inputFormat: "", + source: map[string]int{"one": 1, "two": 2, "three": 3}, + expectedResult: "default render result", + expectedError: "", + }, + { + inputFormat: FormatJSON, + source: []string{"one", "two", "three"}, + expectedResult: `["one","two","three"] +`, + expectedError: "", + }, + { + inputFormat: FormatYAML, + source: []string{"one", "two", "three"}, + expectedResult: `- one +- two +- three +`, + expectedError: "", + }, + { + inputFormat: FormatJSONPretty, + source: []string{"one", "two", "three"}, + expectedResult: `[ + "one", + "two", + "three" +] +`, + expectedError: "", + }, + { + inputFormat: "some unknown format", + source: []string{"one", "two", "three"}, + expectedError: "unknown rendering format: some unknown format", + }, + } + + for _, testCase := range testCases { + buf := &bytes.Buffer{} + err := RenderByFormat(testCase.inputFormat, buf, testCase.source, func() error { + _, e := buf.WriteString("default render result") + return e + }) + + if testCase.expectedError != "" { + assert.EqualError(t, err, testCase.expectedError) + continue + } + + assert.Equal(t, testCase.expectedResult, buf.String()) + } +}