diff --git a/cmd/mattermost/commands/command.go b/cmd/mattermost/commands/command.go index 2d9efb18c1..2ed931bf6d 100644 --- a/cmd/mattermost/commands/command.go +++ b/cmd/mattermost/commands/command.go @@ -28,6 +28,15 @@ var CommandCreateCmd = &cobra.Command{ RunE: createCommandCmdF, } +var CommandShowCmd = &cobra.Command{ + Use: "show", + Short: "Show a custom slash command", + Long: `Show a custom slash command. Commands can be specified by command ID.`, + Args: cobra.ExactArgs(1), + Example: ` command show commandID`, + RunE: showCommandCmdF, +} + var CommandMoveCmd = &cobra.Command{ Use: "move", Short: "Move a slash command to a different team", @@ -71,6 +80,7 @@ func init() { CommandCmd.AddCommand( CommandCreateCmd, + CommandShowCmd, CommandMoveCmd, CommandListCmd, CommandDeleteCmd, @@ -148,6 +158,24 @@ func createCommandCmdF(command *cobra.Command, args []string) error { return nil } +func showCommandCmdF(command *cobra.Command, args []string) error { + a, err := InitDBCommandContextCobra(command) + if err != nil { + return err + } + defer a.Shutdown() + + slashCommand := getCommandFromCommandArg(a, args[0]) + if slashCommand == nil { + command.SilenceUsage = true + return errors.New("Unable to find command '" + args[0] + "'") + } + // pretty print + fmt.Printf("%s", prettyPrintStruct(*slashCommand)) + + return nil +} + func moveCommandCmdF(command *cobra.Command, args []string) error { a, err := InitDBCommandContextCobra(command) if err != nil { @@ -156,7 +184,7 @@ func moveCommandCmdF(command *cobra.Command, args []string) error { defer a.Shutdown() if len(args) < 2 { - return errors.New("Enter the destination team and at least one comamnd to move.") + return errors.New("Enter the destination team and at least one command to move.") } team := getTeamFromTeamArg(a, args[0]) diff --git a/cmd/mattermost/commands/command_test.go b/cmd/mattermost/commands/command_test.go index 878a1af0b9..5ed07cd418 100644 --- a/cmd/mattermost/commands/command_test.go +++ b/cmd/mattermost/commands/command_test.go @@ -131,6 +131,51 @@ func TestCreateCommand(t *testing.T) { } } +func TestShowCommand(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + url := "http://localhost:8000/test-command" + team := th.BasicTeam + user := th.BasicUser + th.LinkUserToTeam(user, team) + trigger := "trigger_" + model.NewId() + displayName := "dn_" + model.NewId() + + c := &model.Command{ + DisplayName: displayName, + Method: "G", + TeamId: team.Id, + Username: user.Username, + CreatorId: user.Id, + URL: url, + Trigger: trigger, + } + + t.Run("existing command", func(t *testing.T) { + command, err := th.App.CreateCommand(c) + require.Nil(t, err) + commands, err := th.App.ListTeamCommands(team.Id) + require.Nil(t, err) + assert.Equal(t, len(commands), 1) + + output := th.CheckCommand(t, "command", "show", command.Id) + assert.Contains(t, string(output), command.Id) + assert.Contains(t, string(output), command.TeamId) + assert.Contains(t, string(output), trigger) + assert.Contains(t, string(output), displayName) + assert.Contains(t, string(output), user.Username) + }) + + t.Run("not existing command", func(t *testing.T) { + assert.Error(t, th.RunCommand(t, "command", "show", "invalid")) + }) + + t.Run("no commandID", func(t *testing.T) { + assert.Error(t, th.RunCommand(t, "command", "show")) + }) +} + func TestDeleteCommand(t *testing.T) { // Skipped due to v5.6 RC build issues. t.Skip() diff --git a/cmd/mattermost/commands/config.go b/cmd/mattermost/commands/config.go index 7d1afa85d8..bb22447e61 100644 --- a/cmd/mattermost/commands/config.go +++ b/cmd/mattermost/commands/config.go @@ -4,7 +4,6 @@ package commands import ( - "bytes" "encoding/json" "fmt" "os" @@ -15,7 +14,6 @@ import ( "github.com/pkg/errors" "github.com/spf13/cobra" - "github.com/mattermost/mattermost-server/mlog" "github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/utils" "github.com/mattermost/mattermost-server/utils/fileutils" @@ -178,7 +176,7 @@ func configShowCmdF(command *cobra.Command, args []string) error { config := app.Config() // pretty print - fmt.Printf("%s", prettyPrint(configToMap(*config))) + fmt.Printf("%s", prettyPrintStruct(*config)) return nil } @@ -205,37 +203,6 @@ func printConfigValues(configMap map[string]interface{}, configSetting []string, } } -// prettyPrint the map -func prettyPrint(configMap map[string]interface{}) string { - value := reflect.ValueOf(configMap) - return printMap(value, 0) -} - -// printMap takes a reflect.Value and print it out, recursively if its a map with the given tab settings. -func printMap(value reflect.Value, tabVal int) string { - - out := &bytes.Buffer{} - - for _, key := range value.MapKeys() { - val := value.MapIndex(key) - if newVal, ok := val.Interface().(map[string]interface{}); !ok { - fmt.Fprintf(out, "%s", strings.Repeat("\t", tabVal)) - fmt.Fprintf(out, "%v: \"%v\"\n", key.Interface(), val.Interface()) - } else { - fmt.Fprintf(out, "%s", strings.Repeat("\t", tabVal)) - fmt.Fprintf(out, "%v:\n", key.Interface()) - // going one level in, increase the tab - tabVal++ - fmt.Fprintf(out, "%s", printMap(reflect.ValueOf(newVal), tabVal)) - // coming back one level, decrease the tab - tabVal-- - } - } - - return out.String() - -} - func configSetCmdF(command *cobra.Command, args []string) error { app, err := InitDBCommandContextCobra(command) if err != nil { @@ -388,44 +355,3 @@ func UpdateMap(configMap map[string]interface{}, configSettings []string, newVal func configToMap(s interface{}) map[string]interface{} { return structToMap(s) } - -// structToMap converts a struct into a map -func structToMap(t interface{}) map[string]interface{} { - defer func() { - if r := recover(); r != nil { - mlog.Error(fmt.Sprintf("Panicked in structToMap. This should never happen. %v", r)) - } - }() - - val := reflect.ValueOf(t) - - if val.Kind() != reflect.Struct { - return nil - } - - out := map[string]interface{}{} - - for i := 0; i < val.NumField(); i++ { - field := val.Field(i) - - var value interface{} - - switch field.Kind() { - case reflect.Struct: - value = structToMap(field.Interface()) - case reflect.Ptr: - indirectType := field.Elem() - - if indirectType.Kind() == reflect.Struct { - value = structToMap(indirectType.Interface()) - } else { - value = indirectType.Interface() - } - default: - value = field.Interface() - } - - out[val.Type().Field(i).Name] = value - } - return out -} diff --git a/cmd/mattermost/commands/config_test.go b/cmd/mattermost/commands/config_test.go index e974d8fe8b..8480219d9f 100644 --- a/cmd/mattermost/commands/config_test.go +++ b/cmd/mattermost/commands/config_test.go @@ -151,93 +151,8 @@ func TestConfigSet(t *testing.T) { }) } -func TestStructToMap(t *testing.T) { - cases := []struct { - Name string - Input interface{} - Expected map[string]interface{} - }{ - { - Name: "Struct with one string field", - Input: struct { - Test string - }{ - Test: "test", - }, - Expected: map[string]interface{}{ - "Test": "test", - }, - }, - { - Name: "String with multiple fields of different ", - Input: struct { - Test1 string - Test2 int - Test3 string - Test4 bool - }{ - Test1: "test1", - Test2: 21, - Test3: "test2", - Test4: false, - }, - Expected: map[string]interface{}{ - "Test1": "test1", - "Test2": 21, - "Test3": "test2", - "Test4": false, - }, - }, - { - Name: "Nested fields", - Input: TestConfig{ - TestServiceSettings{"abc", "def", "ghi"}, - TestTeamSettings{"abc", 1}, - TestClientRequirements{"abc", "def", "ghi"}, - TestMessageExportSettings{true, "abc", TestGlobalRelaySettings{"abc", "def", "ghi"}}, - }, - Expected: map[string]interface{}{ - "TestServiceSettings": map[string]interface{}{ - "Siteurl": "abc", - "Websocketurl": "def", - "Licensedfieldlocation": "ghi", - }, - "TestTeamSettings": map[string]interface{}{ - "Sitename": "abc", - "Maxuserperteam": 1, - }, - "TestClientRequirements": map[string]interface{}{ - "Androidlatestversion": "abc", - "Androidminversion": "def", - "Desktoplatestversion": "ghi", - }, - "TestMessageExportSettings": map[string]interface{}{ - "Enableexport": true, - "Exportformat": "abc", - "TestGlobalRelaySettings": map[string]interface{}{ - "Customertype": "abc", - "Smtpusername": "def", - "Smtppassword": "ghi", - }, - }, - }, - }, - } - - for _, test := range cases { - t.Run(test.Name, func(t *testing.T) { - res := structToMap(test.Input) - - if !reflect.DeepEqual(res, test.Expected) { - t.Errorf("got %v want %v ", res, test.Expected) - } - }) - } - -} - func TestConfigToMap(t *testing.T) { - // This test is almost the same as TestMapToStruct, but I have it here for the sake of completions + // This test is almost the same as TestStructToMap, but I have it here for the sake of completions cases := []struct { Name string Input interface{} @@ -321,68 +236,6 @@ func TestConfigToMap(t *testing.T) { } } -func TestPrintMap(t *testing.T) { - inputCases := []interface{}{ - map[string]interface{}{ - "CustomerType": "A9", - "SmtpUsername": "", - "SmtpPassword": "", - "EmailAddress": "", - }, - map[string]interface{}{ - "EnableExport": false, - "ExportFormat": "actiance", - "DailyRunTime": "01:00", - "GlobalRelaySettings": map[string]interface{}{ - "CustomerType": "A9", - "SmtpUsername": "", - "SmtpPassword": "", - "EmailAddress": "", - }, - }, - } - - outputCases := []string{ - "CustomerType: \"A9\"\nSmtpUsername: \"\"\nSmtpPassword: \"\"\nEmailAddress: \"\"\n", - "EnableExport: \"false\"\nExportFormat: \"actiance\"\nDailyRunTime: \"01:00\"\nGlobalRelaySettings:\n\t CustomerType: \"A9\"\n\tSmtpUsername: \"\"\n\tSmtpPassword: \"\"\n\tEmailAddress: \"\"\n", - } - - cases := []struct { - Name string - Input reflect.Value - Expected string - }{ - { - Name: "Basic print", - Input: reflect.ValueOf(inputCases[0]), - Expected: outputCases[0], - }, - { - Name: "Complex print", - Input: reflect.ValueOf(inputCases[1]), - Expected: outputCases[1], - }, - } - - for _, test := range cases { - t.Run(test.Name, func(t *testing.T) { - res := printMap(test.Input, 0) - - // create two slice of string formed by splitting our strings on \n - slice1 := strings.Split(res, "\n") - slice2 := strings.Split(res, "\n") - - sort.Strings(slice1) - sort.Strings(slice2) - - if !reflect.DeepEqual(slice1, slice2) { - t.Errorf("got '%#v' want '%#v", slice1, slice2) - } - - }) - } -} - func TestPrintConfigValues(t *testing.T) { outputs := []string{ "Siteurl: \"abc\"\nWebsocketurl: \"def\"\nLicensedfieldlocation: \"ghi\"\n", diff --git a/cmd/mattermost/commands/utils.go b/cmd/mattermost/commands/utils.go new file mode 100644 index 0000000000..44569ea2b2 --- /dev/null +++ b/cmd/mattermost/commands/utils.go @@ -0,0 +1,89 @@ +// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package commands + +import ( + "bytes" + "fmt" + "reflect" + "strings" + + "github.com/mattermost/mattermost-server/mlog" +) + +// prettyPrintStruct will return a prettyPrint version of a given struct +func prettyPrintStruct(t interface{}) string { + return prettyPrintMap(structToMap(t)) +} + +// structToMap converts a struct into a map +func structToMap(t interface{}) map[string]interface{} { + defer func() { + if r := recover(); r != nil { + mlog.Error(fmt.Sprintf("Panicked in structToMap. This should never happen. %v", r)) + } + }() + + val := reflect.ValueOf(t) + + if val.Kind() != reflect.Struct { + return nil + } + + out := map[string]interface{}{} + + for i := 0; i < val.NumField(); i++ { + field := val.Field(i) + + var value interface{} + + switch field.Kind() { + case reflect.Struct: + value = structToMap(field.Interface()) + case reflect.Ptr: + indirectType := field.Elem() + + if indirectType.Kind() == reflect.Struct { + value = structToMap(indirectType.Interface()) + } else { + value = indirectType.Interface() + } + default: + value = field.Interface() + } + + out[val.Type().Field(i).Name] = value + } + + return out +} + +// prettyPrintMap will return a prettyPrint version of a given map +func prettyPrintMap(configMap map[string]interface{}) string { + value := reflect.ValueOf(configMap) + return printMap(value, 0) +} + +// printMap takes a reflect.Value and prints it out, recursively if it's a map with the given tab settings +func printMap(value reflect.Value, tabVal int) string { + out := &bytes.Buffer{} + + for _, key := range value.MapKeys() { + val := value.MapIndex(key) + if newVal, ok := val.Interface().(map[string]interface{}); !ok { + fmt.Fprintf(out, "%s", strings.Repeat("\t", tabVal)) + fmt.Fprintf(out, "%v: \"%v\"\n", key.Interface(), val.Interface()) + } else { + fmt.Fprintf(out, "%s", strings.Repeat("\t", tabVal)) + fmt.Fprintf(out, "%v:\n", key.Interface()) + // going one level in, increase the tab + tabVal++ + fmt.Fprintf(out, "%s", printMap(reflect.ValueOf(newVal), tabVal)) + // coming back one level, decrease the tab + tabVal-- + } + } + + return out.String() +} diff --git a/cmd/mattermost/commands/utils_test.go b/cmd/mattermost/commands/utils_test.go new file mode 100644 index 0000000000..c5669a4901 --- /dev/null +++ b/cmd/mattermost/commands/utils_test.go @@ -0,0 +1,157 @@ +// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +package commands + +import ( + "reflect" + "sort" + "strings" + "testing" +) + +func TestStructToMap(t *testing.T) { + cases := []struct { + Name string + Input interface{} + Expected map[string]interface{} + }{ + { + Name: "Struct with one string field", + Input: struct { + Test string + }{ + Test: "test", + }, + Expected: map[string]interface{}{ + "Test": "test", + }, + }, + { + Name: "String with multiple fields of different ", + Input: struct { + Test1 string + Test2 int + Test3 string + Test4 bool + }{ + Test1: "test1", + Test2: 21, + Test3: "test2", + Test4: false, + }, + Expected: map[string]interface{}{ + "Test1": "test1", + "Test2": 21, + "Test3": "test2", + "Test4": false, + }, + }, + { + Name: "Nested fields", + Input: TestConfig{ + TestServiceSettings{"abc", "def", "ghi"}, + TestTeamSettings{"abc", 1}, + TestClientRequirements{"abc", "def", "ghi"}, + TestMessageExportSettings{true, "abc", TestGlobalRelaySettings{"abc", "def", "ghi"}}, + }, + Expected: map[string]interface{}{ + "TestServiceSettings": map[string]interface{}{ + "Siteurl": "abc", + "Websocketurl": "def", + "Licensedfieldlocation": "ghi", + }, + "TestTeamSettings": map[string]interface{}{ + "Sitename": "abc", + "Maxuserperteam": 1, + }, + "TestClientRequirements": map[string]interface{}{ + "Androidlatestversion": "abc", + "Androidminversion": "def", + "Desktoplatestversion": "ghi", + }, + "TestMessageExportSettings": map[string]interface{}{ + "Enableexport": true, + "Exportformat": "abc", + "TestGlobalRelaySettings": map[string]interface{}{ + "Customertype": "abc", + "Smtpusername": "def", + "Smtppassword": "ghi", + }, + }, + }, + }, + } + + for _, test := range cases { + t.Run(test.Name, func(t *testing.T) { + res := structToMap(test.Input) + + if !reflect.DeepEqual(res, test.Expected) { + t.Errorf("got %v want %v ", res, test.Expected) + } + }) + } +} + +func TestPrintMap(t *testing.T) { + inputCases := []interface{}{ + map[string]interface{}{ + "CustomerType": "A9", + "SmtpUsername": "", + "SmtpPassword": "", + "EmailAddress": "", + }, + map[string]interface{}{ + "EnableExport": false, + "ExportFormat": "actiance", + "DailyRunTime": "01:00", + "GlobalRelaySettings": map[string]interface{}{ + "CustomerType": "A9", + "SmtpUsername": "", + "SmtpPassword": "", + "EmailAddress": "", + }, + }, + } + + outputCases := []string{ + "CustomerType: \"A9\"\nSmtpUsername: \"\"\nSmtpPassword: \"\"\nEmailAddress: \"\"\n", + "EnableExport: \"false\"\nExportFormat: \"actiance\"\nDailyRunTime: \"01:00\"\nGlobalRelaySettings:\n\t CustomerType: \"A9\"\n\tSmtpUsername: \"\"\n\tSmtpPassword: \"\"\n\tEmailAddress: \"\"\n", + } + + cases := []struct { + Name string + Input reflect.Value + Expected string + }{ + { + Name: "Basic print", + Input: reflect.ValueOf(inputCases[0]), + Expected: outputCases[0], + }, + { + Name: "Complex print", + Input: reflect.ValueOf(inputCases[1]), + Expected: outputCases[1], + }, + } + + for _, test := range cases { + t.Run(test.Name, func(t *testing.T) { + res := printMap(test.Input, 0) + + // create two slice of string formed by splitting our strings on \n + slice1 := strings.Split(res, "\n") + slice2 := strings.Split(res, "\n") + + sort.Strings(slice1) + sort.Strings(slice2) + + if !reflect.DeepEqual(slice1, slice2) { + t.Errorf("got '%#v' want '%#v", slice1, slice2) + } + + }) + } +}