diff options
Diffstat (limited to 'model')
-rw-r--r-- | model/client4.go | 12 | ||||
-rw-r--r-- | model/config.go | 7 | ||||
-rw-r--r-- | model/config_test.go | 46 | ||||
-rw-r--r-- | model/post.go | 2 | ||||
-rw-r--r-- | model/status.go | 1 | ||||
-rw-r--r-- | model/utils.go | 59 | ||||
-rw-r--r-- | model/utils_test.go | 183 | ||||
-rw-r--r-- | model/websocket_client.go | 22 |
8 files changed, 328 insertions, 4 deletions
diff --git a/model/client4.go b/model/client4.go index 3346cc6eb..82d380440 100644 --- a/model/client4.go +++ b/model/client4.go @@ -2167,6 +2167,18 @@ func (c *Client4) GetOldClientConfig(etag string) (map[string]string, *Response) } } +// GetEnvironmentConfig will retrieve a map mirroring the server configuration where fields +// are set to true if the corresponding config setting is set through an environment variable. +// Settings that haven't been set through environment variables will be missing from the map. +func (c *Client4) GetEnvironmentConfig() (map[string]interface{}, *Response) { + if r, err := c.DoApiGet(c.GetConfigRoute()+"/environment", ""); err != nil { + return nil, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + return StringInterfaceFromJson(r.Body), BuildResponse(r) + } +} + // GetOldClientLicense will retrieve the parts of the server license needed by the // client, formatted in the old format. func (c *Client4) GetOldClientLicense(etag string) (map[string]string, *Response) { diff --git a/model/config.go b/model/config.go index 6c6cf90e9..93533b8aa 100644 --- a/model/config.go +++ b/model/config.go @@ -991,6 +991,7 @@ type TeamSettings struct { MaxNotificationsPerChannel *int64 EnableConfirmNotificationsToChannel *bool TeammateNameDisplay *string + ExperimentalEnableAutomaticReplies *bool ExperimentalTownSquareIsReadOnly *bool ExperimentalPrimaryTeam *string } @@ -1085,6 +1086,10 @@ func (s *TeamSettings) SetDefaults() { s.EnableConfirmNotificationsToChannel = NewBool(true) } + if s.ExperimentalEnableAutomaticReplies == nil { + s.ExperimentalEnableAutomaticReplies = NewBool(false) + } + if s.ExperimentalTownSquareIsReadOnly == nil { s.ExperimentalTownSquareIsReadOnly = NewBool(false) } @@ -1710,8 +1715,8 @@ func (s *MessageExportSettings) SetDefaults() { if s.GlobalRelaySettings == nil { s.GlobalRelaySettings = &GlobalRelayMessageExportSettings{} - s.GlobalRelaySettings.SetDefaults() } + s.GlobalRelaySettings.SetDefaults() } type DisplaySettings struct { diff --git a/model/config_test.go b/model/config_test.go index 1f917af27..b7533145b 100644 --- a/model/config_test.go +++ b/model/config_test.go @@ -4,11 +4,57 @@ package model import ( + "fmt" + "reflect" "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +func TestConfigDefaults(t *testing.T) { + t.Parallel() + + t.Run("somewhere nil when uninitialized", func(t *testing.T) { + c := Config{} + require.False(t, checkNowhereNil(t, "config", c)) + }) + + t.Run("nowhere nil when initialized", func(t *testing.T) { + c := Config{} + c.SetDefaults() + require.True(t, checkNowhereNil(t, "config", c)) + }) + + t.Run("nowhere nil when partially initialized", func(t *testing.T) { + var recursivelyUninitialize func(*Config, string, reflect.Value) + recursivelyUninitialize = func(config *Config, name string, v reflect.Value) { + if v.Type().Kind() == reflect.Ptr { + // Set every pointer we find in the tree to nil + v.Set(reflect.Zero(v.Type())) + require.True(t, v.IsNil()) + + // SetDefaults on the root config should make it non-nil, otherwise + // it means that SetDefaults isn't being called recursively in + // all cases. + config.SetDefaults() + if assert.False(t, v.IsNil(), "%s should be non-nil after SetDefaults()", name) { + recursivelyUninitialize(config, fmt.Sprintf("(*%s)", name), v.Elem()) + } + + } else if v.Type().Kind() == reflect.Struct { + for i := 0; i < v.NumField(); i++ { + recursivelyUninitialize(config, fmt.Sprintf("%s.%s", name, v.Type().Field(i).Name), v.Field(i)) + } + } + } + + c := Config{} + c.SetDefaults() + recursivelyUninitialize(&c, "config", reflect.ValueOf(&c).Elem()) + }) +} + func TestConfigDefaultFileSettingsDirectory(t *testing.T) { c1 := Config{} c1.SetDefaults() diff --git a/model/post.go b/model/post.go index 09303c0cd..e74496979 100644 --- a/model/post.go +++ b/model/post.go @@ -25,6 +25,7 @@ const ( POST_LEAVE_CHANNEL = "system_leave_channel" POST_JOIN_TEAM = "system_join_team" POST_LEAVE_TEAM = "system_leave_team" + POST_AUTO_RESPONDER = "system_auto_responder" POST_ADD_REMOVE = "system_add_remove" // Deprecated, use POST_ADD_TO_CHANNEL or POST_REMOVE_FROM_CHANNEL instead POST_ADD_TO_CHANNEL = "system_add_to_channel" POST_REMOVE_FROM_CHANNEL = "system_remove_from_channel" @@ -194,6 +195,7 @@ func (o *Post) IsValid(maxPostSize int) *AppError { case POST_DEFAULT, POST_JOIN_LEAVE, + POST_AUTO_RESPONDER, POST_ADD_REMOVE, POST_JOIN_CHANNEL, POST_LEAVE_CHANNEL, diff --git a/model/status.go b/model/status.go index cd9e32ed3..cf5899446 100644 --- a/model/status.go +++ b/model/status.go @@ -9,6 +9,7 @@ import ( ) const ( + STATUS_OUT_OF_OFFICE = "ooo" STATUS_OFFLINE = "offline" STATUS_AWAY = "away" STATUS_DND = "dnd" diff --git a/model/utils.go b/model/utils.go index 72369852b..2d61b49f6 100644 --- a/model/utils.go +++ b/model/utils.go @@ -15,9 +15,11 @@ import ( "net/http" "net/mail" "net/url" + "reflect" "regexp" "strconv" "strings" + "testing" "time" "unicode" @@ -469,3 +471,60 @@ func IsValidId(value string) bool { return true } + +// checkNowhereNil checks that the given interface value is not nil, and if a struct, that all of +// its public fields are also nowhere nil +func checkNowhereNil(t *testing.T, name string, value interface{}) bool { + if value == nil { + return false + } + + v := reflect.ValueOf(value) + switch v.Type().Kind() { + case reflect.Ptr: + if v.IsNil() { + t.Logf("%s was nil", name) + return false + } + + return checkNowhereNil(t, fmt.Sprintf("(*%s)", name), v.Elem().Interface()) + + case reflect.Map: + if v.IsNil() { + t.Logf("%s was nil", name) + return false + } + + // Don't check map values + return true + + case reflect.Struct: + nowhereNil := true + for i := 0; i < v.NumField(); i++ { + f := v.Field(i) + // Ignore unexported fields + if v.Type().Field(i).PkgPath != "" { + continue + } + + nowhereNil = nowhereNil && checkNowhereNil(t, fmt.Sprintf("%s.%s", name, v.Type().Field(i).Name), f.Interface()) + } + + return nowhereNil + + case reflect.Array: + fallthrough + case reflect.Chan: + fallthrough + case reflect.Func: + fallthrough + case reflect.Interface: + fallthrough + case reflect.UnsafePointer: + t.Logf("unhandled field %s, type: %s", name, v.Type().Kind()) + return false + + default: + return true + } +} diff --git a/model/utils_test.go b/model/utils_test.go index 411d7bf50..92354c0a1 100644 --- a/model/utils_test.go +++ b/model/utils_test.go @@ -7,6 +7,8 @@ import ( "net/http" "strings" "testing" + + "github.com/stretchr/testify/require" ) func TestNewId(t *testing.T) { @@ -367,3 +369,184 @@ func TestIsValidId(t *testing.T) { } } } + +func TestNowhereNil(t *testing.T) { + t.Parallel() + + var nilStringPtr *string + var nonNilStringPtr *string = new(string) + var nilSlice []string + var nilStruct *struct{} + var nilMap map[bool]bool + + var nowhereNilStruct = struct { + X *string + Y *string + }{ + nonNilStringPtr, + nonNilStringPtr, + } + var somewhereNilStruct = struct { + X *string + Y *string + }{ + nonNilStringPtr, + nilStringPtr, + } + + var privateSomewhereNilStruct = struct { + X *string + y *string + }{ + nonNilStringPtr, + nilStringPtr, + } + + testCases := []struct { + Description string + Value interface{} + Expected bool + }{ + { + "nil", + nil, + false, + }, + { + "empty string", + "", + true, + }, + { + "non-empty string", + "not empty!", + true, + }, + { + "nil string pointer", + nilStringPtr, + false, + }, + { + "non-nil string pointer", + nonNilStringPtr, + true, + }, + { + "0", + 0, + true, + }, + { + "1", + 1, + true, + }, + { + "0 (int64)", + int64(0), + true, + }, + { + "1 (int64)", + int64(1), + true, + }, + { + "true", + true, + true, + }, + { + "false", + false, + true, + }, + { + "nil slice", + nilSlice, + // A nil slice is observably the same as an empty slice, so allow it. + true, + }, + { + "empty slice", + []string{}, + true, + }, + { + "slice containing nils", + []*string{nil, nil}, + true, + }, + { + "nil map", + nilMap, + false, + }, + { + "non-nil map", + make(map[bool]bool), + true, + }, + { + "non-nil map containing nil", + map[bool]*string{true: nilStringPtr, false: nonNilStringPtr}, + // Map values are not checked + true, + }, + { + "nil struct", + nilStruct, + false, + }, + { + "empty struct", + struct{}{}, + true, + }, + { + "struct containing no nil", + nowhereNilStruct, + true, + }, + { + "struct containing nil", + somewhereNilStruct, + false, + }, + { + "struct pointer containing no nil", + &nowhereNilStruct, + true, + }, + { + "struct pointer containing nil", + &somewhereNilStruct, + false, + }, + { + "struct containing private nil", + privateSomewhereNilStruct, + true, + }, + { + "struct pointer containing private nil", + &privateSomewhereNilStruct, + true, + }, + } + + for _, testCase := range testCases { + testCase := testCase + t.Run(testCase.Description, func(t *testing.T) { + defer func() { + if r := recover(); r != nil { + t.Errorf("panic: %v", r) + } + }() + + t.Parallel() + require.Equal(t, testCase.Expected, checkNowhereNil(t, "value", testCase.Value)) + }) + } +} diff --git a/model/websocket_client.go b/model/websocket_client.go index cdec75aba..4ff4f617b 100644 --- a/model/websocket_client.go +++ b/model/websocket_client.go @@ -29,7 +29,13 @@ type WebSocketClient struct { // NewWebSocketClient constructs a new WebSocket client with convenience // methods for talking to the server. func NewWebSocketClient(url, authToken string) (*WebSocketClient, *AppError) { - conn, _, err := websocket.DefaultDialer.Dial(url+API_URL_SUFFIX_V3+"/users/websocket", nil) + return NewWebSocketClientWithDialer(websocket.DefaultDialer, url, authToken) +} + +// NewWebSocketClientWithDialer constructs a new WebSocket client with convenience +// methods for talking to the server using a custom dialer. +func NewWebSocketClientWithDialer(dialer *websocket.Dialer, url, authToken string) (*WebSocketClient, *AppError) { + conn, _, err := dialer.Dial(url+API_URL_SUFFIX_V3+"/users/websocket", nil) if err != nil { return nil, NewAppError("NewWebSocketClient", "model.websocket_client.connect_fail.app_error", nil, err.Error(), http.StatusInternalServerError) } @@ -54,7 +60,13 @@ func NewWebSocketClient(url, authToken string) (*WebSocketClient, *AppError) { // NewWebSocketClient4 constructs a new WebSocket client with convenience // methods for talking to the server. Uses the v4 endpoint. func NewWebSocketClient4(url, authToken string) (*WebSocketClient, *AppError) { - conn, _, err := websocket.DefaultDialer.Dial(url+API_URL_SUFFIX+"/websocket", nil) + return NewWebSocketClient4WithDialer(websocket.DefaultDialer, url, authToken) +} + +// NewWebSocketClient4WithDialer constructs a new WebSocket client with convenience +// methods for talking to the server using a custom dialer. Uses the v4 endpoint. +func NewWebSocketClient4WithDialer(dialer *websocket.Dialer, url, authToken string) (*WebSocketClient, *AppError) { + conn, _, err := dialer.Dial(url+API_URL_SUFFIX+"/websocket", nil) if err != nil { return nil, NewAppError("NewWebSocketClient4", "model.websocket_client.connect_fail.app_error", nil, err.Error(), http.StatusInternalServerError) } @@ -77,8 +89,12 @@ func NewWebSocketClient4(url, authToken string) (*WebSocketClient, *AppError) { } func (wsc *WebSocketClient) Connect() *AppError { + return wsc.ConnectWithDialer(websocket.DefaultDialer) +} + +func (wsc *WebSocketClient) ConnectWithDialer(dialer *websocket.Dialer) *AppError { var err error - wsc.Conn, _, err = websocket.DefaultDialer.Dial(wsc.ConnectUrl, nil) + wsc.Conn, _, err = dialer.Dial(wsc.ConnectUrl, nil) if err != nil { return NewAppError("Connect", "model.websocket_client.connect_fail.app_error", nil, err.Error(), http.StatusInternalServerError) } |