MM-12359 Add 'command show' CLI command (#10073)

* MM-12359 Add 'command show' CLI command

This new CLI command prints out the detailed information of a given
slash command.

* Fix typo in error returned when moving slash commands

* Move prettyPrint CLI functions to util

* Move prettyPrint CLI tests to util
Этот коммит содержится в:
Gabriel Jackson
2019-01-29 02:01:24 -05:00
коммит произвёл Jesús Espino
родитель 179e98c245
Коммит 58ef6853f1
6 изменённых файлов: 322 добавлений и 224 удалений

Просмотреть файл

@@ -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])

Просмотреть файл

@@ -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()

Просмотреть файл

@@ -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
}

Просмотреть файл

@@ -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",

89
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()
}

157
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)
}
})
}
}