diff --git a/tools/cli/admin_cluster_commands_test.go b/tools/cli/admin_cluster_commands_test.go index 37323c1ab2c..21d47e0f915 100644 --- a/tools/cli/admin_cluster_commands_test.go +++ b/tools/cli/admin_cluster_commands_test.go @@ -376,80 +376,95 @@ func TestIntValTypeToString(t *testing.T) { func TestAdminRebalanceList(t *testing.T) { tests := []struct { - name string - prepareEnv func() *cli.Context - expectedError string + name string + mockSetup func(mockCtrl *gomock.Controller) *MockClientFactory + contextSetup func(app *cli.App) *cli.Context + expectedOutput string + expectedError string }{ { name: "Success", - prepareEnv: func() *cli.Context { + mockSetup: func(mockCtrl *gomock.Controller) *MockClientFactory { // Initialize the mock client factory and frontend client - mockFrontClient := frontend.NewMockClient(gomock.NewController(t)) - mockClientFactory := NewMockClientFactory(gomock.NewController(t)) + mockFrontClient := frontend.NewMockClient(mockCtrl) + mockClientFactory := NewMockClientFactory(mockCtrl) // Mock successful ListWorkflow call mockClientFactory.EXPECT().ServerFrontendClient(gomock.Any()).Return(mockFrontClient, nil).Times(1) mockFrontClient.EXPECT().CountWorkflowExecutions(gomock.Any(), gomock.Any()).Return(&types.CountWorkflowExecutionsResponse{}, nil).Times(1) mockFrontClient.EXPECT().ListClosedWorkflowExecutions(gomock.Any(), gomock.Any()).Return(&types.ListClosedWorkflowExecutionsResponse{}, nil).Times(1) - // Create CLI app and set up flag set - app := cli.NewApp() - app.Metadata = map[string]interface{}{ - "deps": &deps{ - ClientFactory: mockClientFactory, - }, - } + return mockClientFactory + }, + contextSetup: func(app *cli.App) *cli.Context { + // Set flags for workflow ID and domain set := flag.NewFlagSet("test", 0) set.String(FlagWorkflowID, "", "workflow ID flag") set.String(FlagDomain, "", "domain flag") c := cli.NewContext(app, set, nil) - - // Set flags for workflow ID and domain _ = c.Set(FlagWorkflowID, failovermanager.RebalanceWorkflowID) _ = c.Set(FlagDomain, common.SystemLocalDomainName) return c }, - expectedError: "", + expectedOutput: "\n", // Example output for success case + expectedError: "", }, { name: "SetWorkflowIDError", - prepareEnv: func() *cli.Context { - // Create CLI app and set up flag set without FlagWorkflowID - app := cli.NewApp() + mockSetup: func(mockCtrl *gomock.Controller) *MockClientFactory { + // No mock setup required for this test case + return nil + }, + contextSetup: func(app *cli.App) *cli.Context { + // Set only the domain flag, so setting FlagWorkflowID should trigger an error set := flag.NewFlagSet("test", 0) - set.String(FlagDomain, "", "domain flag") // Only Domain flag is set + set.String(FlagDomain, "", "domain flag") c := cli.NewContext(app, set, nil) - - // Set only the domain flag, so setting FlagWorkflowID should trigger an error _ = c.Set(FlagDomain, common.SystemLocalDomainName) return c }, - expectedError: "no such flag -workflow_id", + expectedOutput: "", + expectedError: "no such flag -workflow_id", }, { name: "SetDomainError", - prepareEnv: func() *cli.Context { - // Create CLI app and set up flag set without FlagDomain - app := cli.NewApp() + mockSetup: func(mockCtrl *gomock.Controller) *MockClientFactory { + // No mock setup required for this test case + return nil + }, + contextSetup: func(app *cli.App) *cli.Context { + // Set workflow ID flag, but not the domain flag set := flag.NewFlagSet("test", 0) - set.String(FlagWorkflowID, "", "workflow ID flag") // Only Workflow ID flag is set + set.String(FlagWorkflowID, "", "workflow ID flag") c := cli.NewContext(app, set, nil) - - // Set workflow ID flag, but not the domain flag _ = c.Set(FlagWorkflowID, failovermanager.RebalanceWorkflowID) return c }, - expectedError: "no such flag -domain", + expectedOutput: "", + expectedError: "no such flag -domain", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // Prepare the test environment for the specific test case - c := tt.prepareEnv() + // Initialize mock controller + mockCtrl := gomock.NewController(t) + + // Set up mock based on the specific test case + mockClientFactory := tt.mockSetup(mockCtrl) + + // Create test IO handler to capture output + ioHandler := &testIOHandler{} + + // Set up the CLI app and mock dependencies + var app *cli.App + app = NewCliApp(mockClientFactory, WithIOHandler(ioHandler)) + + // Set up the context for the specific test case + c := tt.contextSetup(app) // Call AdminRebalanceList err := AdminRebalanceList(c) @@ -460,6 +475,8 @@ func TestAdminRebalanceList(t *testing.T) { assert.Contains(t, err.Error(), tt.expectedError) } else { assert.NoError(t, err) + // Validate the output captured by testIOHandler + assert.Equal(t, tt.expectedOutput, ioHandler.outputBytes.String()) } }) } @@ -467,9 +484,10 @@ func TestAdminRebalanceList(t *testing.T) { func TestAdminAddSearchAttribute_errors(t *testing.T) { tests := []struct { - name string - setupContext func(app *cli.App) *cli.Context - expectedError string + name string + setupContext func(app *cli.App) *cli.Context + expectedOutput string + expectedError string }{ { name: "MissingSearchAttributesKey", @@ -479,7 +497,8 @@ func TestAdminAddSearchAttribute_errors(t *testing.T) { // No FlagSearchAttributesKey set return cli.NewContext(app, set, nil) }, - expectedError: "Required flag not present:", + expectedOutput: "", // In this case, likely no output + expectedError: "Required flag not present:", }, { name: "InvalidSearchAttributeKey", @@ -489,14 +508,18 @@ func TestAdminAddSearchAttribute_errors(t *testing.T) { set.String(FlagSearchAttributesKey, "123_invalid_key", "Key flag") // Invalid key, starts with number return cli.NewContext(app, set, nil) }, - expectedError: "Invalid search-attribute key.", + expectedOutput: "", // In this case, likely no output + expectedError: "Invalid search-attribute key.", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + // Create test IO handler to capture output + ioHandler := &testIOHandler{} + // Create CLI app - app := cli.NewApp() + app := NewCliApp(nil, WithIOHandler(ioHandler)) // Set up the CLI context for the specific test case c := tt.setupContext(app) @@ -511,6 +534,9 @@ func TestAdminAddSearchAttribute_errors(t *testing.T) { } else { assert.NoError(t, err) } + + // Validate the output captured by testIOHandler + assert.Equal(t, tt.expectedOutput, ioHandler.outputBytes.String()) }) } } diff --git a/tools/cli/admin_elastic_search_commands_test.go b/tools/cli/admin_elastic_search_commands_test.go new file mode 100644 index 00000000000..927677676b1 --- /dev/null +++ b/tools/cli/admin_elastic_search_commands_test.go @@ -0,0 +1,695 @@ +// The MIT License (MIT) + +// Copyright (c) 2017-2020 Uber Technologies Inc. + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package cli + +import ( + "bufio" + "flag" + "fmt" + "net/http" + "net/http/httptest" + "os" + "regexp" + "testing" + "time" + + "github.com/golang/mock/gomock" + "github.com/olivere/elastic" + "github.com/stretchr/testify/assert" + "github.com/urfave/cli/v2" + + "github.com/uber/cadence/.gen/go/indexer" + "github.com/uber/cadence/common/elasticsearch" +) + +// Tests for timeKeyFilter function +func TestTimeKeyFilter(t *testing.T) { + tests := []struct { + name string + key string + expected bool + }{ + { + name: "ValidTimeKeyStartTime", + key: "StartTime", + expected: true, + }, + { + name: "ValidTimeKeyCloseTime", + key: "CloseTime", + expected: true, + }, + { + name: "ValidTimeKeyExecutionTime", + key: "ExecutionTime", + expected: true, + }, + { + name: "InvalidTimeKey", + key: "SomeOtherKey", + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := timeKeyFilter(tt.key) + assert.Equal(t, tt.expected, result) + }) + } +} + +// Tests for timeValProcess function +func TestTimeValProcess(t *testing.T) { + tests := []struct { + name string + timeStr string + expected string + expectError bool + }{ + { + name: "ValidInt64TimeString", + timeStr: "1630425600000000000", // Already in int64 format + expected: "1630425600000000000", + expectError: false, + }, + { + name: "ValidDateTimeString", + timeStr: "2021-09-01T00:00:00Z", // A valid time string + expected: fmt.Sprintf("%v", time.Date(2021, 9, 1, 0, 0, 0, 0, time.UTC).UnixNano()), + expectError: false, + }, + { + name: "InvalidTimeString", + timeStr: "invalid-time", + expected: "", + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := timeValProcess(tt.timeStr) + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected, result) + } + }) + } +} + +// Helper function to remove ANSI color codes from the output +func removeANSIColors(text string) string { + ansiEscapePattern := `\x1b\[[0-9;]*m` + re := regexp.MustCompile(ansiEscapePattern) + return re.ReplaceAllString(text, "") +} + +func TestAdminCatIndices(t *testing.T) { + tests := []struct { + name string + handler http.HandlerFunc + expectedOutput string + expectedError string + handlerCalled bool + }{ + { + name: "Success", + handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Simulate a successful response from Elasticsearch CatIndices API + if r.URL.Path == "/_cat/indices" && r.Method == "GET" { + w.WriteHeader(http.StatusOK) + w.Write([]byte(`[{"health":"green","status":"open","index":"test-index","pri":"5","rep":"1","docs.count":"1000","docs.deleted":"50","store.size":"10gb","pri.store.size":"5gb"}]`)) + } else { + w.WriteHeader(http.StatusNotFound) + } + }), + expectedOutput: `+--------+--------+------------+-----+-----+------------+--------------+------------+----------------+ +| HEALTH | STATUS | INDEX | PRI | REP | DOCS COUNT | DOCS DELETED | STORE SIZE | PRI STORE SIZE | ++--------+--------+------------+-----+-----+------------+--------------+------------+----------------+ +| green | open | test-index | 5 | 1 | 1000 | 50 | 10gb | 5gb | ++--------+--------+------------+-----+-----+------------+--------------+------------+----------------+ + +`, + expectedError: "", + handlerCalled: true, + }, + { + name: "CatIndices Error", + handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Simulate an error response + w.WriteHeader(http.StatusInternalServerError) + }), + expectedOutput: "", + expectedError: "Unable to cat indices", + handlerCalled: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + handlerCalled := false + + // Wrap the test case's handler to track if it was called + wrappedHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + handlerCalled = true + tt.handler.ServeHTTP(w, r) + }) + + // Create mock Elasticsearch client and server + esClient, testServer := getMockClient(t, wrappedHandler) + defer testServer.Close() + + // Initialize mock controller + mockCtrl := gomock.NewController(t) + + // Create mock Cadence client factory + mockClientFactory := NewMockClientFactory(mockCtrl) + + // Create test IO handler to capture output + ioHandler := &testIOHandler{} + + // Set up the CLI app + app := NewCliApp(mockClientFactory, WithIOHandler(ioHandler)) + + // Expect ElasticSearchClient to return the mock client created by getMockClient + mockClientFactory.EXPECT().ElasticSearchClient(gomock.Any()).Return(esClient, nil).Times(1) + + // Create a mock CLI context + c := setContextMock(app) + + // Call AdminCatIndices + err := AdminCatIndices(c) + + // Validate handler was called + assert.Equal(t, tt.handlerCalled, handlerCalled, "Expected handler to be called") + + // Check for expected error or success + if tt.expectedError != "" { + assert.Error(t, err) + assert.Contains(t, err.Error(), tt.expectedError) + } else { + assert.NoError(t, err) + // Remove ANSI color codes from the captured output + actualOutput := removeANSIColors(ioHandler.outputBytes.String()) + + // Validate the output captured by testIOHandler + assert.Equal(t, tt.expectedOutput, actualOutput) + } + }) + } +} + +// getMockClient creates a mock elastic.Client using the provided HTTP handler and returns the client and the test server +func getMockClient(t *testing.T, handler http.HandlerFunc) (*elastic.Client, *httptest.Server) { + // Create a mock HTTP test server + testServer := httptest.NewTLSServer(handler) + + // Create an Elasticsearch client using the test server's URL + mockClient, err := elastic.NewClient( + elastic.SetURL(testServer.URL), + elastic.SetSniff(false), + elastic.SetHealthcheck(false), + elastic.SetHttpClient(testServer.Client()), + ) + // Ensure no error occurred while creating the mock client + assert.NoError(t, err) + + // Return the elastic.Client and the test server + return mockClient, testServer +} + +func TestAdminIndex(t *testing.T) { + tests := []struct { + name string + handler http.HandlerFunc + createInputFile bool + messageType indexer.MessageType + expectedOutput string + expectedError string + }{ + { + name: "SuccessIndexMessage", + handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Simulate a successful Bulk request + if r.URL.Path == "/_bulk" && r.Method == "POST" { + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"took": 30, "errors": false}`)) + } else { + w.WriteHeader(http.StatusNotFound) + } + }), + createInputFile: true, + messageType: indexer.MessageTypeIndex, + expectedOutput: "", // Example output for success case + expectedError: "", + }, + { + name: "SuccessCreateMessage", + handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Simulate a successful Bulk request + if r.URL.Path == "/_bulk" && r.Method == "POST" { + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"took": 30, "errors": false}`)) + } else { + w.WriteHeader(http.StatusNotFound) + } + }), + createInputFile: true, + messageType: indexer.MessageTypeCreate, + expectedOutput: "", // Example output for create case + expectedError: "", + }, + { + name: "SuccessDeleteMessage", + handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Simulate a successful Bulk request + if r.URL.Path == "/_bulk" && r.Method == "POST" { + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"took": 30, "errors": false}`)) + } else { + w.WriteHeader(http.StatusNotFound) + } + }), + createInputFile: true, + messageType: indexer.MessageTypeDelete, + expectedOutput: "", // Example output for delete case + expectedError: "", + }, + { + name: "UnknownMessageType", + handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // No bulk request needed for this case + w.WriteHeader(http.StatusOK) + }), + createInputFile: true, + messageType: indexer.MessageType(9999), + expectedOutput: "", + expectedError: "Unknown message type", + }, + { + name: "BulkRequestFailure", + handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Simulate a Bulk request failure + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(`{"error": "Bulk request failed"}`)) + }), + createInputFile: true, + messageType: indexer.MessageTypeIndex, + expectedOutput: "", + expectedError: "Bulk failed", + }, + { + name: "ParseIndexerMessageError", + handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // In this test case, we are simulating a parse error, so no bulk request needed. + w.WriteHeader(http.StatusOK) + }), + createInputFile: false, // No valid input file created + messageType: indexer.MessageTypeIndex, + expectedOutput: "", + expectedError: "Unable to parse indexer message", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var inputFileName string + var err error + if tt.createInputFile { + // Create a temporary input file with a valid message + inputFileName, err = createTempIndexerInputFileWithMessageType(tt.messageType, false) + assert.NoError(t, err) + defer os.Remove(inputFileName) // Clean up after test + } + + // Create mock Elasticsearch client and server + esClient, testServer := getMockClient(t, tt.handler) + defer testServer.Close() + + // Initialize mock controller + mockCtrl := gomock.NewController(t) + + // Create mock client factory + mockClientFactory := NewMockClientFactory(mockCtrl) + + // Create test IO handler to capture output + ioHandler := &testIOHandler{} + + // Set up the CLI app + app := NewCliApp(mockClientFactory, WithIOHandler(ioHandler)) + + // Expect ElasticSearchClient to return the mock client created by getMockClient + mockClientFactory.EXPECT().ElasticSearchClient(gomock.Any()).Return(esClient, nil).Times(1) + + // Setup flag values for the CLI context + set := flag.NewFlagSet("test", 0) + set.String(FlagIndex, "test-index", "Index flag") + if tt.createInputFile { + set.String(FlagInputFile, inputFileName, "Input file flag") + } else { + set.String(FlagInputFile, "invalid-input-file", "Input file flag") + } + set.Int(FlagBatchSize, 1, "Batch size flag") + + // Create a mock CLI context + c := cli.NewContext(app, set, nil) + + // Call AdminIndex + err = AdminIndex(c) + + // Validate results + if tt.expectedError != "" { + assert.Error(t, err) + assert.Contains(t, err.Error(), tt.expectedError) + } else { + assert.NoError(t, err) + // Validate the output captured by testIOHandler + assert.Equal(t, tt.expectedOutput, ioHandler.outputBytes.String()) + } + }) + } +} + +// Helper function to create a temporary input file for AdminIndex or AdminDelete with valid data +func createTempIndexerInputFileWithMessageType(messageType indexer.MessageType, forDelete bool) (string, error) { + file, err := os.CreateTemp("", "indexer_input_*.txt") + if err != nil { + return "", err + } + defer file.Close() + + writer := bufio.NewWriter(file) + + if forDelete { + // For AdminDelete, we need to simulate workflow-id|run-id format + _, err = writer.WriteString("Header\n") // First line is skipped in AdminDelete + if err != nil { + return "", err + } + _, err = writer.WriteString("some-value|workflow-id|run-id\n") // Simulate document deletion data + if err != nil { + return "", err + } + } else { + // For AdminIndex, we need to generate a JSON message format + message := `{"WorkflowID": "test-workflow-id", "RunID": "test-run-id", "Version": 1, "MessageType": ` + fmt.Sprintf("%d", messageType) + `}` + _, err = writer.WriteString(message + "\n") + if err != nil { + return "", err + } + } + + writer.Flush() + + return file.Name(), nil +} + +func TestAdminDelete(t *testing.T) { + tests := []struct { + name string + handler http.HandlerFunc + createInputFile bool + expectedOutput string + expectedError string + }{ + { + name: "SuccessDelete", + handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Simulate a successful Bulk delete request + if r.URL.Path == "/_bulk" && r.Method == "POST" { + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"took": 30, "errors": false}`)) + } else { + w.WriteHeader(http.StatusNotFound) + } + }), + createInputFile: true, + expectedOutput: "", // Example output for delete case + expectedError: "", + }, + { + name: "BulkRequestFailure", + handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Simulate an error in the Bulk delete request + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(`{"error": "Bulk request failed"}`)) + }), + createInputFile: true, + expectedOutput: "", + expectedError: "Bulk failed", + }, + { + name: "ParseFileError", + handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // No bulk request needed in this case, just simulating a file parsing error + w.WriteHeader(http.StatusOK) + }), + createInputFile: false, // No valid input file created + expectedOutput: "", + expectedError: "Cannot open input file", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var inputFileName string + var err error + if tt.createInputFile { + // Reuse the temp input file creation helper from previous tests + inputFileName, err = createTempIndexerInputFileWithMessageType(indexer.MessageTypeDelete, true) + assert.NoError(t, err) + defer os.Remove(inputFileName) // Clean up after test + } + + // Create mock Elasticsearch client and server + esClient, testServer := getMockClient(t, tt.handler) + defer testServer.Close() + + // Initialize mock controller + mockCtrl := gomock.NewController(t) + + // Create mock client factory + mockClientFactory := NewMockClientFactory(mockCtrl) + + // Expect ElasticSearchClient to return the mock client created by getMockClient + mockClientFactory.EXPECT().ElasticSearchClient(gomock.Any()).Return(esClient, nil).Times(1) + + // Create test IO handler to capture output + ioHandler := &testIOHandler{} + + // Set up the CLI app + app := NewCliApp(mockClientFactory, WithIOHandler(ioHandler)) + + // Setup flag values for the CLI context + set := flag.NewFlagSet("test", 0) + set.String(FlagIndex, "test-index", "Index flag") + if tt.createInputFile { + set.String(FlagInputFile, inputFileName, "Input file flag") + } else { + set.String(FlagInputFile, "invalid-input-file", "Input file flag") + } + set.Int(FlagBatchSize, 1, "Batch size flag") + set.Int(FlagRPS, 10, "RPS flag") + + // Create a mock CLI context + c := cli.NewContext(app, set, nil) + + // Call AdminDelete + err = AdminDelete(c) + + // Validate results + if tt.expectedError != "" { + if err != nil { + assert.Contains(t, err.Error(), tt.expectedError) + } else { + t.Errorf("Expected error: %s, but got no error", tt.expectedError) + } + } else { + assert.NoError(t, err) + // Validate the output captured by testIOHandler + assert.Equal(t, tt.expectedOutput, ioHandler.outputBytes.String()) + } + }) + } +} + +func TestParseIndexerMessage(t *testing.T) { + workflowID := "test-workflow-id" + runID := "test-run-id" + version := int64(1) + messageType := indexer.MessageTypeIndex + + tests := []struct { + name string + messageType indexer.MessageType + createInputFile bool + expectedError string + expectedResult []*indexer.Message + }{ + { + name: "SuccessParse", + messageType: indexer.MessageTypeIndex, + createInputFile: true, + expectedError: "", + expectedResult: []*indexer.Message{ + { + WorkflowID: &workflowID, + RunID: &runID, + Version: &version, + MessageType: &messageType, + }, + }, + }, + { + name: "FileNotExist", + messageType: 0, + createInputFile: false, // No file created + expectedError: "open nonexistent-file.txt: no such file or directory", + expectedResult: nil, + }, + { + name: "SkipEmptyLines", + messageType: indexer.MessageTypeIndex, + createInputFile: true, + expectedError: "", + expectedResult: []*indexer.Message{ + { + WorkflowID: &workflowID, + RunID: &runID, + Version: &version, + MessageType: &messageType, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var fileName string + var err error + if tt.createInputFile { + // Use the existing createTempIndexerInputFileWithMessageType function + fileName, err = createTempIndexerInputFileWithMessageType(tt.messageType, false) // forDelete=false for AdminIndex + assert.NoError(t, err) + defer os.Remove(fileName) // Clean up after test + } else { + // Simulate file not found + fileName = "nonexistent-file.txt" + } + + // Call the function being tested + messages, err := parseIndexerMessage(fileName) + + // Validate results + if tt.expectedError != "" { + assert.Error(t, err) + assert.Contains(t, err.Error(), tt.expectedError) + assert.Nil(t, messages) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectedResult, messages) + } + }) + } +} + +func TestGenerateESDoc(t *testing.T) { + tests := []struct { + name string + message *indexer.Message + expectedDoc map[string]interface{} + expectedError string + }{ + { + name: "SuccessWithAllFieldTypes", + message: &indexer.Message{ + DomainID: &[]string{"domain1"}[0], + WorkflowID: &[]string{"workflow1"}[0], + RunID: &[]string{"run1"}[0], + Fields: map[string]*indexer.Field{ + "field_string": { + Type: &[]indexer.FieldType{indexer.FieldTypeString}[0], + StringData: &[]string{"string_value"}[0], + }, + "field_int": { + Type: &[]indexer.FieldType{indexer.FieldTypeInt}[0], + IntData: &[]int64{123}[0], + }, + "field_bool": { + Type: &[]indexer.FieldType{indexer.FieldTypeBool}[0], + BoolData: &[]bool{true}[0], + }, + "field_binary": { + Type: &[]indexer.FieldType{indexer.FieldTypeBinary}[0], + BinaryData: []byte("binary_value"), + }, + }, + }, + expectedDoc: map[string]interface{}{ + elasticsearch.DomainID: "domain1", + elasticsearch.WorkflowID: "workflow1", + elasticsearch.RunID: "run1", + "field_string": "string_value", + "field_int": int64(123), + "field_bool": true, + "field_binary": []byte("binary_value"), + }, + expectedError: "", + }, + { + name: "UnknownFieldType", + message: &indexer.Message{ + DomainID: &[]string{"domain1"}[0], + WorkflowID: &[]string{"workflow1"}[0], + RunID: &[]string{"run1"}[0], + Fields: map[string]*indexer.Field{ + "unknown_field": { + Type: &[]indexer.FieldType{9999}[0], // Invalid field type + }, + }, + }, + expectedDoc: nil, + expectedError: "Unknown field type", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Call the function being tested + doc, err := generateESDoc(tt.message) + + // Validate results + if tt.expectedError != "" { + assert.Error(t, err) + assert.Contains(t, err.Error(), tt.expectedError) + assert.Nil(t, doc) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectedDoc, doc) + } + }) + } +} diff --git a/tools/cli/render.go b/tools/cli/render.go index 36910de5f64..2dbcbd71213 100644 --- a/tools/cli/render.go +++ b/tools/cli/render.go @@ -24,7 +24,6 @@ import ( "encoding/json" "fmt" "io" - "os" "reflect" "sort" "strconv" @@ -82,7 +81,7 @@ func Render(c *cli.Context, data interface{}, opts RenderOptions) (err error) { }() // For now always output to stdout - w := os.Stdout + w := getDeps(c).Output() template := opts.DefaultTemplate diff --git a/tools/cli/workflow_commands.go b/tools/cli/workflow_commands.go index aabfbaed46e..d74c17ac339 100644 --- a/tools/cli/workflow_commands.go +++ b/tools/cli/workflow_commands.go @@ -1464,6 +1464,7 @@ func displayAllWorkflows(c *cli.Context, getWorkflowsPage getWorkflowPageFn) err func displayWorkflows(c *cli.Context, workflows []*types.WorkflowExecutionInfo) error { printJSON := c.Bool(FlagPrintJSON) printDecodedRaw := c.Bool(FlagPrintFullyDetail) + if printJSON || printDecodedRaw { fmt.Println("[") printListResults(workflows, printJSON, false)