MM-13893: refactor config (#10230)
* refactor utils/config* to config/ * pull validateLdapFilter into app * clean up Config/GetConfig/GetSanitizedConfig usage Eliminate app.GetConfig() in favour of just using app.Config() directly, but expose app.GetSanitizedConfig() for when the old behaviour was required. * web: isolate config setup * TestInvitePeopleProvider: make config explicit * regenerateClientConfig: avoid racey map access * integrate watch flag into app.ConfigFile option * make app.Option return an error * release.mk: only cp static files from config/ * release.mk: fix cp static files from config/ * api4: TestPlugin cleanup * s/c/cfg/ for clarity * fix merge conflict * testlib: allow customization of testlib driver name
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
aca8914e35
Коммит
3a71709103
723
config/config.go
Обычный файл
723
config/config.go
Обычный файл
@@ -0,0 +1,723 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/mattermost/viper"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"net/http"
|
||||
|
||||
"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"
|
||||
"github.com/mattermost/mattermost-server/utils/jsonutils"
|
||||
)
|
||||
|
||||
var (
|
||||
termsOfServiceEnabledAndEmpty = model.NewAppError(
|
||||
"Config.IsValid",
|
||||
"model.config.is_valid.support.custom_terms_of_service_text.app_error",
|
||||
nil,
|
||||
"",
|
||||
http.StatusBadRequest,
|
||||
)
|
||||
)
|
||||
|
||||
func SaveConfig(fileName string, config *model.Config) *model.AppError {
|
||||
b, err := json.MarshalIndent(config, "", " ")
|
||||
if err != nil {
|
||||
return model.NewAppError("SaveConfig", "utils.config.save_config.saving.app_error",
|
||||
map[string]interface{}{"Filename": fileName}, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(fileName, b, 0644)
|
||||
if err != nil {
|
||||
return model.NewAppError("SaveConfig", "utils.config.save_config.saving.app_error",
|
||||
map[string]interface{}{"Filename": fileName}, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type ConfigWatcher struct {
|
||||
watcher *fsnotify.Watcher
|
||||
close chan struct{}
|
||||
closed chan struct{}
|
||||
}
|
||||
|
||||
func NewConfigWatcher(cfgFileName string, f func()) (*ConfigWatcher, error) {
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to create config watcher for file: "+cfgFileName)
|
||||
}
|
||||
|
||||
configFile := filepath.Clean(cfgFileName)
|
||||
configDir, _ := filepath.Split(configFile)
|
||||
watcher.Add(configDir)
|
||||
|
||||
ret := &ConfigWatcher{
|
||||
watcher: watcher,
|
||||
close: make(chan struct{}),
|
||||
closed: make(chan struct{}),
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer close(ret.closed)
|
||||
defer watcher.Close()
|
||||
|
||||
for {
|
||||
select {
|
||||
case event := <-watcher.Events:
|
||||
// we only care about the config file
|
||||
if filepath.Clean(event.Name) == configFile {
|
||||
if event.Op&fsnotify.Write == fsnotify.Write || event.Op&fsnotify.Create == fsnotify.Create {
|
||||
mlog.Info(fmt.Sprintf("Config file watcher detected a change reloading %v", cfgFileName))
|
||||
|
||||
if _, _, configReadErr := ReadConfigFile(cfgFileName, true); configReadErr == nil {
|
||||
f()
|
||||
} else {
|
||||
mlog.Error(fmt.Sprintf("Failed to read while watching config file at %v with err=%v", cfgFileName, configReadErr.Error()))
|
||||
}
|
||||
}
|
||||
}
|
||||
case err := <-watcher.Errors:
|
||||
mlog.Error(fmt.Sprintf("Failed while watching config file at %v with err=%v", cfgFileName, err.Error()))
|
||||
case <-ret.close:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (w *ConfigWatcher) Close() {
|
||||
close(w.close)
|
||||
<-w.closed
|
||||
}
|
||||
|
||||
// ReadConfig reads and parses the given configuration.
|
||||
func ReadConfig(r io.Reader, allowEnvironmentOverrides bool) (*model.Config, map[string]interface{}, error) {
|
||||
// Pre-flight check the syntax of the configuration file to improve error messaging.
|
||||
configData, err := ioutil.ReadAll(r)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
} else {
|
||||
var rawConfig interface{}
|
||||
if err := json.Unmarshal(configData, &rawConfig); err != nil {
|
||||
return nil, nil, jsonutils.HumanizeJsonError(err, configData)
|
||||
}
|
||||
}
|
||||
|
||||
v := newViper(allowEnvironmentOverrides)
|
||||
if err := v.ReadConfig(bytes.NewReader(configData)); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
var config model.Config
|
||||
unmarshalErr := v.Unmarshal(&config)
|
||||
// https://github.com/spf13/viper/issues/324
|
||||
// https://github.com/spf13/viper/issues/348
|
||||
if unmarshalErr == nil {
|
||||
config.PluginSettings.Plugins = make(map[string]map[string]interface{})
|
||||
unmarshalErr = v.UnmarshalKey("pluginsettings.plugins", &config.PluginSettings.Plugins)
|
||||
}
|
||||
if unmarshalErr == nil {
|
||||
config.PluginSettings.PluginStates = make(map[string]*model.PluginState)
|
||||
unmarshalErr = v.UnmarshalKey("pluginsettings.pluginstates", &config.PluginSettings.PluginStates)
|
||||
}
|
||||
|
||||
envConfig := v.EnvSettings()
|
||||
|
||||
var envErr error
|
||||
if envConfig, envErr = fixEnvSettingsCase(envConfig); envErr != nil {
|
||||
return nil, nil, envErr
|
||||
}
|
||||
|
||||
return &config, envConfig, unmarshalErr
|
||||
}
|
||||
|
||||
func newViper(allowEnvironmentOverrides bool) *viper.Viper {
|
||||
v := viper.New()
|
||||
|
||||
v.SetConfigType("json")
|
||||
|
||||
if allowEnvironmentOverrides {
|
||||
v.SetEnvPrefix("mm")
|
||||
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||
v.AutomaticEnv()
|
||||
}
|
||||
|
||||
// Set zeroed defaults for all the config settings so that Viper knows what environment variables
|
||||
// it needs to be looking for. The correct defaults will later be applied using Config.SetDefaults.
|
||||
defaults := getDefaultsFromStruct(model.Config{})
|
||||
|
||||
for key, value := range defaults {
|
||||
if key == "PluginSettings.Plugins" || key == "PluginSettings.PluginStates" {
|
||||
continue
|
||||
}
|
||||
|
||||
v.SetDefault(key, value)
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
func getDefaultsFromStruct(s interface{}) map[string]interface{} {
|
||||
return flattenStructToMap(structToMap(reflect.TypeOf(s)))
|
||||
}
|
||||
|
||||
// Converts a struct type into a nested map with keys matching the struct's fields and values
|
||||
// matching the zeroed value of the corresponding field.
|
||||
func structToMap(t reflect.Type) (out map[string]interface{}) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
mlog.Error(fmt.Sprintf("Panicked in structToMap. This should never happen. %v", r))
|
||||
}
|
||||
}()
|
||||
|
||||
if t.Kind() != reflect.Struct {
|
||||
// Should never hit this, but this will prevent a panic if that does happen somehow
|
||||
return nil
|
||||
}
|
||||
|
||||
out = map[string]interface{}{}
|
||||
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
|
||||
var value interface{}
|
||||
|
||||
switch field.Type.Kind() {
|
||||
case reflect.Struct:
|
||||
value = structToMap(field.Type)
|
||||
case reflect.Ptr:
|
||||
indirectType := field.Type.Elem()
|
||||
|
||||
if indirectType.Kind() == reflect.Struct {
|
||||
// Follow pointers to structs since we need to define defaults for their fields
|
||||
value = structToMap(indirectType)
|
||||
} else {
|
||||
value = nil
|
||||
}
|
||||
default:
|
||||
value = reflect.Zero(field.Type).Interface()
|
||||
}
|
||||
|
||||
out[field.Name] = value
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Flattens a nested map so that the result is a single map with keys corresponding to the
|
||||
// path through the original map. For example,
|
||||
// {
|
||||
// "a": {
|
||||
// "b": 1
|
||||
// },
|
||||
// "c": "sea"
|
||||
// }
|
||||
// would flatten to
|
||||
// {
|
||||
// "a.b": 1,
|
||||
// "c": "sea"
|
||||
// }
|
||||
func flattenStructToMap(in map[string]interface{}) map[string]interface{} {
|
||||
out := make(map[string]interface{})
|
||||
|
||||
for key, value := range in {
|
||||
if valueAsMap, ok := value.(map[string]interface{}); ok {
|
||||
sub := flattenStructToMap(valueAsMap)
|
||||
|
||||
for subKey, subValue := range sub {
|
||||
out[key+"."+subKey] = subValue
|
||||
}
|
||||
} else {
|
||||
out[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// Fixes the case of the environment variables sent back from Viper since Viper stores
|
||||
// everything as lower case.
|
||||
func fixEnvSettingsCase(in map[string]interface{}) (out map[string]interface{}, err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
mlog.Error(fmt.Sprintf("Panicked in fixEnvSettingsCase. This should never happen. %v", r))
|
||||
out = in
|
||||
}
|
||||
}()
|
||||
|
||||
var fixCase func(map[string]interface{}, reflect.Type) map[string]interface{}
|
||||
fixCase = func(in map[string]interface{}, t reflect.Type) map[string]interface{} {
|
||||
if t.Kind() != reflect.Struct {
|
||||
// Should never hit this, but this will prevent a panic if that does happen somehow
|
||||
return nil
|
||||
}
|
||||
|
||||
fixCaseOut := make(map[string]interface{}, len(in))
|
||||
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
|
||||
key := field.Name
|
||||
if value, ok := in[strings.ToLower(key)]; ok {
|
||||
if valueAsMap, ok := value.(map[string]interface{}); ok {
|
||||
fixCaseOut[key] = fixCase(valueAsMap, field.Type)
|
||||
} else {
|
||||
fixCaseOut[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fixCaseOut
|
||||
}
|
||||
|
||||
out = fixCase(in, reflect.TypeOf(model.Config{}))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ReadConfigFile reads and parses the configuration at the given file path.
|
||||
func ReadConfigFile(path string, allowEnvironmentOverrides bool) (*model.Config, map[string]interface{}, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
return ReadConfig(f, allowEnvironmentOverrides)
|
||||
}
|
||||
|
||||
// EnsureConfigFile will attempt to locate a config file with the given name. If it does not exist,
|
||||
// it will attempt to locate a default config file, and copy it to a file named fileName in the same
|
||||
// directory. In either case, the config file path is returned.
|
||||
func EnsureConfigFile(fileName string) (string, error) {
|
||||
if configFile := fileutils.FindConfigFile(fileName); configFile != "" {
|
||||
return configFile, nil
|
||||
}
|
||||
if defaultPath := fileutils.FindConfigFile("default.json"); defaultPath != "" {
|
||||
destPath := filepath.Join(filepath.Dir(defaultPath), fileName)
|
||||
src, err := os.Open(defaultPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer src.Close()
|
||||
dest, err := os.OpenFile(destPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer dest.Close()
|
||||
if _, err := io.Copy(dest, src); err == nil {
|
||||
return destPath, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("no config file found")
|
||||
}
|
||||
|
||||
// LoadConfig will try to search around for the corresponding config file. It will search
|
||||
// /tmp/fileName then attempt ./config/fileName, then ../config/fileName and last it will look at
|
||||
// fileName.
|
||||
func LoadConfig(fileName string) (*model.Config, string, map[string]interface{}, *model.AppError) {
|
||||
var configPath string
|
||||
|
||||
if fileName != filepath.Base(fileName) {
|
||||
configPath = fileName
|
||||
} else {
|
||||
if path, err := EnsureConfigFile(fileName); err != nil {
|
||||
appErr := model.NewAppError("LoadConfig", "utils.config.load_config.opening.panic", map[string]interface{}{"Filename": fileName, "Error": err.Error()}, "", 0)
|
||||
return nil, "", nil, appErr
|
||||
} else {
|
||||
configPath = path
|
||||
}
|
||||
}
|
||||
|
||||
config, envConfig, err := ReadConfigFile(configPath, true)
|
||||
if err != nil {
|
||||
appErr := model.NewAppError("LoadConfig", "utils.config.load_config.decoding.panic", map[string]interface{}{"Filename": fileName, "Error": err.Error()}, "", 0)
|
||||
return nil, "", nil, appErr
|
||||
}
|
||||
|
||||
needSave := config.SqlSettings.AtRestEncryptKey == nil || len(*config.SqlSettings.AtRestEncryptKey) == 0 ||
|
||||
config.FileSettings.PublicLinkSalt == nil || len(*config.FileSettings.PublicLinkSalt) == 0 ||
|
||||
config.EmailSettings.InviteSalt == nil || len(*config.EmailSettings.InviteSalt) == 0
|
||||
|
||||
config.SetDefaults()
|
||||
|
||||
// Don't treat it as an error right now if custom terms of service are enabled but text is empty.
|
||||
// This is because terms of service text will be fetched from database at a later state, but
|
||||
// the flag indicating it is enabled is fetched from config file right away.
|
||||
if err := config.IsValid(); err != nil && err.Id != termsOfServiceEnabledAndEmpty.Id {
|
||||
return nil, "", nil, err
|
||||
}
|
||||
|
||||
if needSave {
|
||||
if err := SaveConfig(configPath, config); err != nil {
|
||||
mlog.Warn(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if err := ValidateLocales(config); err != nil {
|
||||
if err := SaveConfig(configPath, config); err != nil {
|
||||
mlog.Warn(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if *config.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL {
|
||||
dir := config.FileSettings.Directory
|
||||
dirString := *dir
|
||||
if len(*dir) > 0 && dirString[len(dirString)-1:] != "/" {
|
||||
*config.FileSettings.Directory += "/"
|
||||
}
|
||||
}
|
||||
|
||||
return config, configPath, envConfig, nil
|
||||
}
|
||||
|
||||
func GenerateClientConfig(c *model.Config, diagnosticId string, license *model.License) map[string]string {
|
||||
props := GenerateLimitedClientConfig(c, diagnosticId, license)
|
||||
|
||||
props["SiteURL"] = strings.TrimRight(*c.ServiceSettings.SiteURL, "/")
|
||||
props["EnableUserDeactivation"] = strconv.FormatBool(*c.TeamSettings.EnableUserDeactivation)
|
||||
props["RestrictDirectMessage"] = *c.TeamSettings.RestrictDirectMessage
|
||||
props["EnableXToLeaveChannelsFromLHS"] = strconv.FormatBool(*c.TeamSettings.EnableXToLeaveChannelsFromLHS)
|
||||
props["TeammateNameDisplay"] = *c.TeamSettings.TeammateNameDisplay
|
||||
props["ExperimentalPrimaryTeam"] = *c.TeamSettings.ExperimentalPrimaryTeam
|
||||
props["ExperimentalViewArchivedChannels"] = strconv.FormatBool(*c.TeamSettings.ExperimentalViewArchivedChannels)
|
||||
|
||||
props["EnableOAuthServiceProvider"] = strconv.FormatBool(*c.ServiceSettings.EnableOAuthServiceProvider)
|
||||
props["GoogleDeveloperKey"] = *c.ServiceSettings.GoogleDeveloperKey
|
||||
props["EnableIncomingWebhooks"] = strconv.FormatBool(*c.ServiceSettings.EnableIncomingWebhooks)
|
||||
props["EnableOutgoingWebhooks"] = strconv.FormatBool(*c.ServiceSettings.EnableOutgoingWebhooks)
|
||||
props["EnableCommands"] = strconv.FormatBool(*c.ServiceSettings.EnableCommands)
|
||||
props["EnablePostUsernameOverride"] = strconv.FormatBool(*c.ServiceSettings.EnablePostUsernameOverride)
|
||||
props["EnablePostIconOverride"] = strconv.FormatBool(*c.ServiceSettings.EnablePostIconOverride)
|
||||
props["EnableUserAccessTokens"] = strconv.FormatBool(*c.ServiceSettings.EnableUserAccessTokens)
|
||||
props["EnableLinkPreviews"] = strconv.FormatBool(*c.ServiceSettings.EnableLinkPreviews)
|
||||
props["EnableTesting"] = strconv.FormatBool(*c.ServiceSettings.EnableTesting)
|
||||
props["EnableDeveloper"] = strconv.FormatBool(*c.ServiceSettings.EnableDeveloper)
|
||||
props["PostEditTimeLimit"] = fmt.Sprintf("%v", *c.ServiceSettings.PostEditTimeLimit)
|
||||
props["CloseUnusedDirectMessages"] = strconv.FormatBool(*c.ServiceSettings.CloseUnusedDirectMessages)
|
||||
props["EnablePreviewFeatures"] = strconv.FormatBool(*c.ServiceSettings.EnablePreviewFeatures)
|
||||
props["EnableTutorial"] = strconv.FormatBool(*c.ServiceSettings.EnableTutorial)
|
||||
props["ExperimentalEnableDefaultChannelLeaveJoinMessages"] = strconv.FormatBool(*c.ServiceSettings.ExperimentalEnableDefaultChannelLeaveJoinMessages)
|
||||
props["ExperimentalGroupUnreadChannels"] = *c.ServiceSettings.ExperimentalGroupUnreadChannels
|
||||
|
||||
// This setting is only temporary, so keep using the old setting name for the mobile and web apps
|
||||
props["ExperimentalEnablePostMetadata"] = strconv.FormatBool(!*c.ExperimentalSettings.DisablePostMetadata)
|
||||
|
||||
if *c.ServiceSettings.ExperimentalChannelOrganization || *c.ServiceSettings.ExperimentalGroupUnreadChannels != model.GROUP_UNREAD_CHANNELS_DISABLED {
|
||||
props["ExperimentalChannelOrganization"] = strconv.FormatBool(true)
|
||||
} else {
|
||||
props["ExperimentalChannelOrganization"] = strconv.FormatBool(false)
|
||||
}
|
||||
|
||||
props["ExperimentalEnableAutomaticReplies"] = strconv.FormatBool(*c.TeamSettings.ExperimentalEnableAutomaticReplies)
|
||||
props["ExperimentalTimezone"] = strconv.FormatBool(*c.DisplaySettings.ExperimentalTimezone)
|
||||
|
||||
props["SendEmailNotifications"] = strconv.FormatBool(*c.EmailSettings.SendEmailNotifications)
|
||||
props["SendPushNotifications"] = strconv.FormatBool(*c.EmailSettings.SendPushNotifications)
|
||||
props["RequireEmailVerification"] = strconv.FormatBool(*c.EmailSettings.RequireEmailVerification)
|
||||
props["EnableEmailBatching"] = strconv.FormatBool(*c.EmailSettings.EnableEmailBatching)
|
||||
props["EnablePreviewModeBanner"] = strconv.FormatBool(*c.EmailSettings.EnablePreviewModeBanner)
|
||||
props["EmailNotificationContentsType"] = *c.EmailSettings.EmailNotificationContentsType
|
||||
|
||||
props["ShowEmailAddress"] = strconv.FormatBool(*c.PrivacySettings.ShowEmailAddress)
|
||||
|
||||
props["EnableFileAttachments"] = strconv.FormatBool(*c.FileSettings.EnableFileAttachments)
|
||||
props["EnablePublicLink"] = strconv.FormatBool(*c.FileSettings.EnablePublicLink)
|
||||
|
||||
props["AvailableLocales"] = *c.LocalizationSettings.AvailableLocales
|
||||
props["SQLDriverName"] = *c.SqlSettings.DriverName
|
||||
|
||||
props["EnableEmojiPicker"] = strconv.FormatBool(*c.ServiceSettings.EnableEmojiPicker)
|
||||
props["EnableGifPicker"] = strconv.FormatBool(*c.ServiceSettings.EnableGifPicker)
|
||||
props["GfycatApiKey"] = *c.ServiceSettings.GfycatApiKey
|
||||
props["GfycatApiSecret"] = *c.ServiceSettings.GfycatApiSecret
|
||||
props["MaxFileSize"] = strconv.FormatInt(*c.FileSettings.MaxFileSize, 10)
|
||||
|
||||
props["MaxNotificationsPerChannel"] = strconv.FormatInt(*c.TeamSettings.MaxNotificationsPerChannel, 10)
|
||||
props["EnableConfirmNotificationsToChannel"] = strconv.FormatBool(*c.TeamSettings.EnableConfirmNotificationsToChannel)
|
||||
props["TimeBetweenUserTypingUpdatesMilliseconds"] = strconv.FormatInt(*c.ServiceSettings.TimeBetweenUserTypingUpdatesMilliseconds, 10)
|
||||
props["EnableUserTypingMessages"] = strconv.FormatBool(*c.ServiceSettings.EnableUserTypingMessages)
|
||||
props["EnableChannelViewedMessages"] = strconv.FormatBool(*c.ServiceSettings.EnableChannelViewedMessages)
|
||||
|
||||
props["RunJobs"] = strconv.FormatBool(*c.JobSettings.RunJobs)
|
||||
|
||||
props["EnableEmailInvitations"] = strconv.FormatBool(*c.ServiceSettings.EnableEmailInvitations)
|
||||
|
||||
// Set default values for all options that require a license.
|
||||
props["ExperimentalHideTownSquareinLHS"] = "false"
|
||||
props["ExperimentalTownSquareIsReadOnly"] = "false"
|
||||
props["ExperimentalEnableAuthenticationTransfer"] = "true"
|
||||
props["LdapNicknameAttributeSet"] = "false"
|
||||
props["LdapFirstNameAttributeSet"] = "false"
|
||||
props["LdapLastNameAttributeSet"] = "false"
|
||||
props["EnableCompliance"] = "false"
|
||||
props["EnableMobileFileDownload"] = "true"
|
||||
props["EnableMobileFileUpload"] = "true"
|
||||
props["SamlFirstNameAttributeSet"] = "false"
|
||||
props["SamlLastNameAttributeSet"] = "false"
|
||||
props["SamlNicknameAttributeSet"] = "false"
|
||||
props["EnableCluster"] = "false"
|
||||
props["EnableMetrics"] = "false"
|
||||
props["PasswordMinimumLength"] = "0"
|
||||
props["PasswordRequireLowercase"] = "false"
|
||||
props["PasswordRequireUppercase"] = "false"
|
||||
props["PasswordRequireNumber"] = "false"
|
||||
props["PasswordRequireSymbol"] = "false"
|
||||
props["EnableBanner"] = "false"
|
||||
props["BannerText"] = ""
|
||||
props["BannerColor"] = ""
|
||||
props["BannerTextColor"] = ""
|
||||
props["AllowBannerDismissal"] = "false"
|
||||
props["EnableThemeSelection"] = "true"
|
||||
props["DefaultTheme"] = ""
|
||||
props["AllowCustomThemes"] = "true"
|
||||
props["AllowedThemes"] = ""
|
||||
props["DataRetentionEnableMessageDeletion"] = "false"
|
||||
props["DataRetentionMessageRetentionDays"] = "0"
|
||||
props["DataRetentionEnableFileDeletion"] = "false"
|
||||
props["DataRetentionFileRetentionDays"] = "0"
|
||||
props["PasswordMinimumLength"] = fmt.Sprintf("%v", *c.PasswordSettings.MinimumLength)
|
||||
props["PasswordRequireLowercase"] = strconv.FormatBool(*c.PasswordSettings.Lowercase)
|
||||
props["PasswordRequireUppercase"] = strconv.FormatBool(*c.PasswordSettings.Uppercase)
|
||||
props["PasswordRequireNumber"] = strconv.FormatBool(*c.PasswordSettings.Number)
|
||||
props["PasswordRequireSymbol"] = strconv.FormatBool(*c.PasswordSettings.Symbol)
|
||||
props["CustomUrlSchemes"] = strings.Join(c.DisplaySettings.CustomUrlSchemes, ",")
|
||||
|
||||
if license != nil {
|
||||
props["ExperimentalHideTownSquareinLHS"] = strconv.FormatBool(*c.TeamSettings.ExperimentalHideTownSquareinLHS)
|
||||
props["ExperimentalTownSquareIsReadOnly"] = strconv.FormatBool(*c.TeamSettings.ExperimentalTownSquareIsReadOnly)
|
||||
props["ExperimentalEnableAuthenticationTransfer"] = strconv.FormatBool(*c.ServiceSettings.ExperimentalEnableAuthenticationTransfer)
|
||||
|
||||
if *license.Features.LDAP {
|
||||
props["LdapNicknameAttributeSet"] = strconv.FormatBool(*c.LdapSettings.NicknameAttribute != "")
|
||||
props["LdapFirstNameAttributeSet"] = strconv.FormatBool(*c.LdapSettings.FirstNameAttribute != "")
|
||||
props["LdapLastNameAttributeSet"] = strconv.FormatBool(*c.LdapSettings.LastNameAttribute != "")
|
||||
}
|
||||
|
||||
if *license.Features.Compliance {
|
||||
props["EnableCompliance"] = strconv.FormatBool(*c.ComplianceSettings.Enable)
|
||||
props["EnableMobileFileDownload"] = strconv.FormatBool(*c.FileSettings.EnableMobileDownload)
|
||||
props["EnableMobileFileUpload"] = strconv.FormatBool(*c.FileSettings.EnableMobileUpload)
|
||||
}
|
||||
|
||||
if *license.Features.SAML {
|
||||
props["SamlFirstNameAttributeSet"] = strconv.FormatBool(*c.SamlSettings.FirstNameAttribute != "")
|
||||
props["SamlLastNameAttributeSet"] = strconv.FormatBool(*c.SamlSettings.LastNameAttribute != "")
|
||||
props["SamlNicknameAttributeSet"] = strconv.FormatBool(*c.SamlSettings.NicknameAttribute != "")
|
||||
|
||||
// do this under the correct licensed feature
|
||||
props["ExperimentalClientSideCertEnable"] = strconv.FormatBool(*c.ExperimentalSettings.ClientSideCertEnable)
|
||||
props["ExperimentalClientSideCertCheck"] = *c.ExperimentalSettings.ClientSideCertCheck
|
||||
}
|
||||
|
||||
if *license.Features.Cluster {
|
||||
props["EnableCluster"] = strconv.FormatBool(*c.ClusterSettings.Enable)
|
||||
}
|
||||
|
||||
if *license.Features.Cluster {
|
||||
props["EnableMetrics"] = strconv.FormatBool(*c.MetricsSettings.Enable)
|
||||
}
|
||||
|
||||
if *license.Features.Announcement {
|
||||
props["EnableBanner"] = strconv.FormatBool(*c.AnnouncementSettings.EnableBanner)
|
||||
props["BannerText"] = *c.AnnouncementSettings.BannerText
|
||||
props["BannerColor"] = *c.AnnouncementSettings.BannerColor
|
||||
props["BannerTextColor"] = *c.AnnouncementSettings.BannerTextColor
|
||||
props["AllowBannerDismissal"] = strconv.FormatBool(*c.AnnouncementSettings.AllowBannerDismissal)
|
||||
}
|
||||
|
||||
if *license.Features.ThemeManagement {
|
||||
props["EnableThemeSelection"] = strconv.FormatBool(*c.ThemeSettings.EnableThemeSelection)
|
||||
props["DefaultTheme"] = *c.ThemeSettings.DefaultTheme
|
||||
props["AllowCustomThemes"] = strconv.FormatBool(*c.ThemeSettings.AllowCustomThemes)
|
||||
props["AllowedThemes"] = strings.Join(c.ThemeSettings.AllowedThemes, ",")
|
||||
}
|
||||
|
||||
if *license.Features.DataRetention {
|
||||
props["DataRetentionEnableMessageDeletion"] = strconv.FormatBool(*c.DataRetentionSettings.EnableMessageDeletion)
|
||||
props["DataRetentionMessageRetentionDays"] = strconv.FormatInt(int64(*c.DataRetentionSettings.MessageRetentionDays), 10)
|
||||
props["DataRetentionEnableFileDeletion"] = strconv.FormatBool(*c.DataRetentionSettings.EnableFileDeletion)
|
||||
props["DataRetentionFileRetentionDays"] = strconv.FormatInt(int64(*c.DataRetentionSettings.FileRetentionDays), 10)
|
||||
}
|
||||
}
|
||||
|
||||
return props
|
||||
}
|
||||
|
||||
func GenerateLimitedClientConfig(c *model.Config, diagnosticId string, license *model.License) map[string]string {
|
||||
props := make(map[string]string)
|
||||
|
||||
props["Version"] = model.CurrentVersion
|
||||
props["BuildNumber"] = model.BuildNumber
|
||||
props["BuildDate"] = model.BuildDate
|
||||
props["BuildHash"] = model.BuildHash
|
||||
props["BuildHashEnterprise"] = model.BuildHashEnterprise
|
||||
props["BuildEnterpriseReady"] = model.BuildEnterpriseReady
|
||||
|
||||
props["SiteName"] = *c.TeamSettings.SiteName
|
||||
props["WebsocketURL"] = strings.TrimRight(*c.ServiceSettings.WebsocketURL, "/")
|
||||
props["WebsocketPort"] = fmt.Sprintf("%v", *c.ServiceSettings.WebsocketPort)
|
||||
props["WebsocketSecurePort"] = fmt.Sprintf("%v", *c.ServiceSettings.WebsocketSecurePort)
|
||||
props["EnableUserCreation"] = strconv.FormatBool(*c.TeamSettings.EnableUserCreation)
|
||||
props["EnableOpenServer"] = strconv.FormatBool(*c.TeamSettings.EnableOpenServer)
|
||||
|
||||
props["AndroidLatestVersion"] = c.ClientRequirements.AndroidLatestVersion
|
||||
props["AndroidMinVersion"] = c.ClientRequirements.AndroidMinVersion
|
||||
props["DesktopLatestVersion"] = c.ClientRequirements.DesktopLatestVersion
|
||||
props["DesktopMinVersion"] = c.ClientRequirements.DesktopMinVersion
|
||||
props["IosLatestVersion"] = c.ClientRequirements.IosLatestVersion
|
||||
props["IosMinVersion"] = c.ClientRequirements.IosMinVersion
|
||||
|
||||
props["EnableDiagnostics"] = strconv.FormatBool(*c.LogSettings.EnableDiagnostics)
|
||||
|
||||
props["EnableSignUpWithEmail"] = strconv.FormatBool(*c.EmailSettings.EnableSignUpWithEmail)
|
||||
props["EnableSignInWithEmail"] = strconv.FormatBool(*c.EmailSettings.EnableSignInWithEmail)
|
||||
props["EnableSignInWithUsername"] = strconv.FormatBool(*c.EmailSettings.EnableSignInWithUsername)
|
||||
|
||||
props["EmailLoginButtonColor"] = *c.EmailSettings.LoginButtonColor
|
||||
props["EmailLoginButtonBorderColor"] = *c.EmailSettings.LoginButtonBorderColor
|
||||
props["EmailLoginButtonTextColor"] = *c.EmailSettings.LoginButtonTextColor
|
||||
|
||||
props["EnableSignUpWithGitLab"] = strconv.FormatBool(*c.GitLabSettings.Enable)
|
||||
|
||||
props["TermsOfServiceLink"] = *c.SupportSettings.TermsOfServiceLink
|
||||
props["PrivacyPolicyLink"] = *c.SupportSettings.PrivacyPolicyLink
|
||||
props["AboutLink"] = *c.SupportSettings.AboutLink
|
||||
props["HelpLink"] = *c.SupportSettings.HelpLink
|
||||
props["ReportAProblemLink"] = *c.SupportSettings.ReportAProblemLink
|
||||
props["SupportEmail"] = *c.SupportSettings.SupportEmail
|
||||
|
||||
props["DefaultClientLocale"] = *c.LocalizationSettings.DefaultClientLocale
|
||||
|
||||
props["EnableCustomEmoji"] = strconv.FormatBool(*c.ServiceSettings.EnableCustomEmoji)
|
||||
props["AppDownloadLink"] = *c.NativeAppSettings.AppDownloadLink
|
||||
props["AndroidAppDownloadLink"] = *c.NativeAppSettings.AndroidAppDownloadLink
|
||||
props["IosAppDownloadLink"] = *c.NativeAppSettings.IosAppDownloadLink
|
||||
|
||||
props["DiagnosticId"] = diagnosticId
|
||||
props["DiagnosticsEnabled"] = strconv.FormatBool(*c.LogSettings.EnableDiagnostics)
|
||||
|
||||
props["HasImageProxy"] = strconv.FormatBool(*c.ImageProxySettings.Enable)
|
||||
|
||||
props["PluginsEnabled"] = strconv.FormatBool(*c.PluginSettings.Enable)
|
||||
|
||||
// Set default values for all options that require a license.
|
||||
props["EnableCustomBrand"] = "false"
|
||||
props["CustomBrandText"] = ""
|
||||
props["CustomDescriptionText"] = ""
|
||||
props["EnableLdap"] = "false"
|
||||
props["LdapLoginFieldName"] = ""
|
||||
props["LdapLoginButtonColor"] = ""
|
||||
props["LdapLoginButtonBorderColor"] = ""
|
||||
props["LdapLoginButtonTextColor"] = ""
|
||||
props["EnableSaml"] = "false"
|
||||
props["SamlLoginButtonText"] = ""
|
||||
props["SamlLoginButtonColor"] = ""
|
||||
props["SamlLoginButtonBorderColor"] = ""
|
||||
props["SamlLoginButtonTextColor"] = ""
|
||||
props["EnableSignUpWithGoogle"] = "false"
|
||||
props["EnableSignUpWithOffice365"] = "false"
|
||||
props["EnableCustomBrand"] = strconv.FormatBool(*c.TeamSettings.EnableCustomBrand)
|
||||
props["CustomBrandText"] = *c.TeamSettings.CustomBrandText
|
||||
props["CustomDescriptionText"] = *c.TeamSettings.CustomDescriptionText
|
||||
props["EnableMultifactorAuthentication"] = strconv.FormatBool(*c.ServiceSettings.EnableMultifactorAuthentication)
|
||||
props["EnforceMultifactorAuthentication"] = "false"
|
||||
|
||||
if license != nil {
|
||||
if *license.Features.LDAP {
|
||||
props["EnableLdap"] = strconv.FormatBool(*c.LdapSettings.Enable)
|
||||
props["LdapLoginFieldName"] = *c.LdapSettings.LoginFieldName
|
||||
props["LdapLoginButtonColor"] = *c.LdapSettings.LoginButtonColor
|
||||
props["LdapLoginButtonBorderColor"] = *c.LdapSettings.LoginButtonBorderColor
|
||||
props["LdapLoginButtonTextColor"] = *c.LdapSettings.LoginButtonTextColor
|
||||
}
|
||||
|
||||
if *license.Features.SAML {
|
||||
props["EnableSaml"] = strconv.FormatBool(*c.SamlSettings.Enable)
|
||||
props["SamlLoginButtonText"] = *c.SamlSettings.LoginButtonText
|
||||
props["SamlLoginButtonColor"] = *c.SamlSettings.LoginButtonColor
|
||||
props["SamlLoginButtonBorderColor"] = *c.SamlSettings.LoginButtonBorderColor
|
||||
props["SamlLoginButtonTextColor"] = *c.SamlSettings.LoginButtonTextColor
|
||||
}
|
||||
|
||||
if *license.Features.GoogleOAuth {
|
||||
props["EnableSignUpWithGoogle"] = strconv.FormatBool(*c.GoogleSettings.Enable)
|
||||
}
|
||||
|
||||
if *license.Features.Office365OAuth {
|
||||
props["EnableSignUpWithOffice365"] = strconv.FormatBool(*c.Office365Settings.Enable)
|
||||
}
|
||||
|
||||
if *license.Features.CustomTermsOfService {
|
||||
props["EnableCustomTermsOfService"] = strconv.FormatBool(*c.SupportSettings.CustomTermsOfServiceEnabled)
|
||||
props["CustomTermsOfServiceReAcceptancePeriod"] = strconv.FormatInt(int64(*c.SupportSettings.CustomTermsOfServiceReAcceptancePeriod), 10)
|
||||
}
|
||||
|
||||
if *license.Features.MFA {
|
||||
props["EnforceMultifactorAuthentication"] = strconv.FormatBool(*c.ServiceSettings.EnforceMultifactorAuthentication)
|
||||
}
|
||||
}
|
||||
|
||||
return props
|
||||
}
|
||||
|
||||
func ValidateLocales(cfg *model.Config) *model.AppError {
|
||||
var err *model.AppError
|
||||
locales := utils.GetSupportedLocales()
|
||||
if _, ok := locales[*cfg.LocalizationSettings.DefaultServerLocale]; !ok {
|
||||
*cfg.LocalizationSettings.DefaultServerLocale = model.DEFAULT_LOCALE
|
||||
err = model.NewAppError("ValidateLocales", "utils.config.supported_server_locale.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if _, ok := locales[*cfg.LocalizationSettings.DefaultClientLocale]; !ok {
|
||||
*cfg.LocalizationSettings.DefaultClientLocale = model.DEFAULT_LOCALE
|
||||
err = model.NewAppError("ValidateLocales", "utils.config.supported_client_locale.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if len(*cfg.LocalizationSettings.AvailableLocales) > 0 {
|
||||
isDefaultClientLocaleInAvailableLocales := false
|
||||
for _, word := range strings.Split(*cfg.LocalizationSettings.AvailableLocales, ",") {
|
||||
if _, ok := locales[word]; !ok {
|
||||
*cfg.LocalizationSettings.AvailableLocales = ""
|
||||
isDefaultClientLocaleInAvailableLocales = true
|
||||
err = model.NewAppError("ValidateLocales", "utils.config.supported_available_locales.app_error", nil, "", http.StatusBadRequest)
|
||||
break
|
||||
}
|
||||
|
||||
if word == *cfg.LocalizationSettings.DefaultClientLocale {
|
||||
isDefaultClientLocaleInAvailableLocales = true
|
||||
}
|
||||
}
|
||||
|
||||
availableLocales := *cfg.LocalizationSettings.AvailableLocales
|
||||
|
||||
if !isDefaultClientLocaleInAvailableLocales {
|
||||
availableLocales += "," + *cfg.LocalizationSettings.DefaultClientLocale
|
||||
err = model.NewAppError("ValidateLocales", "utils.config.add_client_locale.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
*cfg.LocalizationSettings.AvailableLocales = strings.Join(utils.RemoveDuplicatesFromStringArray(strings.Split(availableLocales, ",")), ",")
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
700
config/config_test.go
Обычный файл
700
config/config_test.go
Обычный файл
@@ -0,0 +1,700 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/mattermost/mattermost-server/utils"
|
||||
)
|
||||
|
||||
func TestConfig(t *testing.T) {
|
||||
utils.TranslationsPreInit()
|
||||
_, _, _, err := LoadConfig("config.json")
|
||||
require.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestReadConfig(t *testing.T) {
|
||||
utils.TranslationsPreInit()
|
||||
|
||||
_, _, err := ReadConfig(bytes.NewReader([]byte(``)), false)
|
||||
require.EqualError(t, err, "parsing error at line 1, character 1: unexpected end of JSON input")
|
||||
|
||||
_, _, err = ReadConfig(bytes.NewReader([]byte(`
|
||||
{
|
||||
malformed
|
||||
`)), false)
|
||||
require.EqualError(t, err, "parsing error at line 3, character 5: invalid character 'm' looking for beginning of object key string")
|
||||
}
|
||||
|
||||
func TestReadConfig_PluginSettings(t *testing.T) {
|
||||
utils.TranslationsPreInit()
|
||||
|
||||
config, _, err := ReadConfig(bytes.NewReader([]byte(`{
|
||||
"PluginSettings": {
|
||||
"Directory": "/temp/mattermost-plugins",
|
||||
"Plugins": {
|
||||
"com.example.plugin": {
|
||||
"number": 1,
|
||||
"string": "abc",
|
||||
"boolean": false,
|
||||
"abc.def.ghi": {
|
||||
"abc": 123,
|
||||
"def": "456"
|
||||
}
|
||||
},
|
||||
"jira": {
|
||||
"number": 2,
|
||||
"string": "123",
|
||||
"boolean": true,
|
||||
"abc.def.ghi": {
|
||||
"abc": 456,
|
||||
"def": "123"
|
||||
}
|
||||
}
|
||||
},
|
||||
"PluginStates": {
|
||||
"com.example.plugin": {
|
||||
"enable": true
|
||||
},
|
||||
"jira": {
|
||||
"enable": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}`)), false)
|
||||
require.Nil(t, err)
|
||||
|
||||
assert.Equal(t, "/temp/mattermost-plugins", *config.PluginSettings.Directory)
|
||||
|
||||
if assert.Contains(t, config.PluginSettings.Plugins, "com.example.plugin") {
|
||||
assert.Equal(t, map[string]interface{}{
|
||||
"number": float64(1),
|
||||
"string": "abc",
|
||||
"boolean": false,
|
||||
"abc.def.ghi": map[string]interface{}{
|
||||
"abc": float64(123),
|
||||
"def": "456",
|
||||
},
|
||||
}, config.PluginSettings.Plugins["com.example.plugin"])
|
||||
}
|
||||
if assert.Contains(t, config.PluginSettings.PluginStates, "com.example.plugin") {
|
||||
assert.Equal(t, model.PluginState{
|
||||
Enable: true,
|
||||
}, *config.PluginSettings.PluginStates["com.example.plugin"])
|
||||
}
|
||||
|
||||
if assert.Contains(t, config.PluginSettings.Plugins, "jira") {
|
||||
assert.Equal(t, map[string]interface{}{
|
||||
"number": float64(2),
|
||||
"string": "123",
|
||||
"boolean": true,
|
||||
"abc.def.ghi": map[string]interface{}{
|
||||
"abc": float64(456),
|
||||
"def": "123",
|
||||
},
|
||||
}, config.PluginSettings.Plugins["jira"])
|
||||
}
|
||||
if assert.Contains(t, config.PluginSettings.PluginStates, "jira") {
|
||||
assert.Equal(t, model.PluginState{
|
||||
Enable: false,
|
||||
}, *config.PluginSettings.PluginStates["jira"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadConfig_ImageProxySettings(t *testing.T) {
|
||||
utils.TranslationsPreInit()
|
||||
|
||||
t.Run("deprecated settings should still be read properly", func(t *testing.T) {
|
||||
config, _, err := ReadConfig(bytes.NewReader([]byte(`{
|
||||
"ServiceSettings": {
|
||||
"ImageProxyType": "OldImageProxyType",
|
||||
"ImageProxyURL": "OldImageProxyURL",
|
||||
"ImageProxyOptions": "OldImageProxyOptions"
|
||||
}
|
||||
}`)), false)
|
||||
|
||||
require.Nil(t, err)
|
||||
|
||||
assert.Equal(t, model.NewString("OldImageProxyType"), config.ServiceSettings.DEPRECATED_DO_NOT_USE_ImageProxyType)
|
||||
assert.Equal(t, model.NewString("OldImageProxyURL"), config.ServiceSettings.DEPRECATED_DO_NOT_USE_ImageProxyURL)
|
||||
assert.Equal(t, model.NewString("OldImageProxyOptions"), config.ServiceSettings.DEPRECATED_DO_NOT_USE_ImageProxyOptions)
|
||||
})
|
||||
}
|
||||
|
||||
func TestConfigFromEnviroVars(t *testing.T) {
|
||||
utils.TranslationsPreInit()
|
||||
|
||||
config := `{
|
||||
"ServiceSettings": {
|
||||
"EnableCommands": true,
|
||||
"ReadTimeout": 100
|
||||
},
|
||||
"TeamSettings": {
|
||||
"SiteName": "Mattermost",
|
||||
"CustomBrandText": ""
|
||||
},
|
||||
"SupportSettings": {
|
||||
"TermsOfServiceLink": "https://about.mattermost.com/default-terms/"
|
||||
},
|
||||
"PluginSettings": {
|
||||
"Enable": true,
|
||||
"Plugins": {
|
||||
"jira": {
|
||||
"enabled": "true",
|
||||
"secret": "config-secret"
|
||||
}
|
||||
},
|
||||
"PluginStates": {
|
||||
"jira": {
|
||||
"Enable": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
t.Run("string settings", func(t *testing.T) {
|
||||
os.Setenv("MM_TEAMSETTINGS_SITENAME", "From Environment")
|
||||
os.Setenv("MM_TEAMSETTINGS_CUSTOMBRANDTEXT", "Custom Brand")
|
||||
|
||||
cfg, envCfg, err := ReadConfig(strings.NewReader(config), true)
|
||||
require.Nil(t, err)
|
||||
|
||||
if *cfg.TeamSettings.SiteName != "From Environment" {
|
||||
t.Fatal("Couldn't read config from environment var")
|
||||
}
|
||||
|
||||
if *cfg.TeamSettings.CustomBrandText != "Custom Brand" {
|
||||
t.Fatal("Couldn't read config from environment var")
|
||||
}
|
||||
|
||||
if teamSettings, ok := envCfg["TeamSettings"]; !ok {
|
||||
t.Fatal("TeamSettings is missing from envConfig")
|
||||
} else if teamSettingsAsMap, ok := teamSettings.(map[string]interface{}); !ok {
|
||||
t.Fatal("TeamSettings is not a map in envConfig")
|
||||
} else {
|
||||
if siteNameInEnv, ok := teamSettingsAsMap["SiteName"].(bool); !ok || !siteNameInEnv {
|
||||
t.Fatal("SiteName should be in envConfig")
|
||||
}
|
||||
|
||||
if customBrandTextInEnv, ok := teamSettingsAsMap["CustomBrandText"].(bool); !ok || !customBrandTextInEnv {
|
||||
t.Fatal("SiteName should be in envConfig")
|
||||
}
|
||||
}
|
||||
|
||||
os.Unsetenv("MM_TEAMSETTINGS_SITENAME")
|
||||
os.Unsetenv("MM_TEAMSETTINGS_CUSTOMBRANDTEXT")
|
||||
|
||||
cfg, envCfg, err = ReadConfig(strings.NewReader(config), true)
|
||||
require.Nil(t, err)
|
||||
|
||||
if *cfg.TeamSettings.SiteName != "Mattermost" {
|
||||
t.Fatal("should have been reset")
|
||||
}
|
||||
|
||||
if _, ok := envCfg["TeamSettings"]; ok {
|
||||
t.Fatal("TeamSettings should be missing from envConfig")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("boolean setting", func(t *testing.T) {
|
||||
os.Setenv("MM_SERVICESETTINGS_ENABLECOMMANDS", "false")
|
||||
defer os.Unsetenv("MM_SERVICESETTINGS_ENABLECOMMANDS")
|
||||
|
||||
cfg, envCfg, err := ReadConfig(strings.NewReader(config), true)
|
||||
require.Nil(t, err)
|
||||
|
||||
if *cfg.ServiceSettings.EnableCommands {
|
||||
t.Fatal("Couldn't read config from environment var")
|
||||
}
|
||||
|
||||
if serviceSettings, ok := envCfg["ServiceSettings"]; !ok {
|
||||
t.Fatal("ServiceSettings is missing from envConfig")
|
||||
} else if serviceSettingsAsMap, ok := serviceSettings.(map[string]interface{}); !ok {
|
||||
t.Fatal("ServiceSettings is not a map in envConfig")
|
||||
} else {
|
||||
if enableCommandsInEnv, ok := serviceSettingsAsMap["EnableCommands"].(bool); !ok || !enableCommandsInEnv {
|
||||
t.Fatal("EnableCommands should be in envConfig")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("integer setting", func(t *testing.T) {
|
||||
os.Setenv("MM_SERVICESETTINGS_READTIMEOUT", "400")
|
||||
defer os.Unsetenv("MM_SERVICESETTINGS_READTIMEOUT")
|
||||
|
||||
cfg, envCfg, err := ReadConfig(strings.NewReader(config), true)
|
||||
require.Nil(t, err)
|
||||
|
||||
if *cfg.ServiceSettings.ReadTimeout != 400 {
|
||||
t.Fatal("Couldn't read config from environment var")
|
||||
}
|
||||
|
||||
if serviceSettings, ok := envCfg["ServiceSettings"]; !ok {
|
||||
t.Fatal("ServiceSettings is missing from envConfig")
|
||||
} else if serviceSettingsAsMap, ok := serviceSettings.(map[string]interface{}); !ok {
|
||||
t.Fatal("ServiceSettings is not a map in envConfig")
|
||||
} else {
|
||||
if readTimeoutInEnv, ok := serviceSettingsAsMap["ReadTimeout"].(bool); !ok || !readTimeoutInEnv {
|
||||
t.Fatal("ReadTimeout should be in envConfig")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("setting missing from config.json", func(t *testing.T) {
|
||||
os.Setenv("MM_SERVICESETTINGS_SITEURL", "https://example.com")
|
||||
defer os.Unsetenv("MM_SERVICESETTINGS_SITEURL")
|
||||
|
||||
cfg, envCfg, err := ReadConfig(strings.NewReader(config), true)
|
||||
require.Nil(t, err)
|
||||
|
||||
if *cfg.ServiceSettings.SiteURL != "https://example.com" {
|
||||
t.Fatal("Couldn't read config from environment var")
|
||||
}
|
||||
|
||||
if serviceSettings, ok := envCfg["ServiceSettings"]; !ok {
|
||||
t.Fatal("ServiceSettings is missing from envConfig")
|
||||
} else if serviceSettingsAsMap, ok := serviceSettings.(map[string]interface{}); !ok {
|
||||
t.Fatal("ServiceSettings is not a map in envConfig")
|
||||
} else {
|
||||
if siteURLInEnv, ok := serviceSettingsAsMap["SiteURL"].(bool); !ok || !siteURLInEnv {
|
||||
t.Fatal("SiteURL should be in envConfig")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty string setting", func(t *testing.T) {
|
||||
os.Setenv("MM_SUPPORTSETTINGS_TERMSOFSERVICELINK", "")
|
||||
defer os.Unsetenv("MM_SUPPORTSETTINGS_TERMSOFSERVICELINK")
|
||||
|
||||
cfg, envCfg, err := ReadConfig(strings.NewReader(config), true)
|
||||
require.Nil(t, err)
|
||||
|
||||
if *cfg.SupportSettings.TermsOfServiceLink != "" {
|
||||
t.Fatal("Couldn't read empty TermsOfServiceLink from environment var")
|
||||
}
|
||||
|
||||
if supportSettings, ok := envCfg["SupportSettings"]; !ok {
|
||||
t.Fatal("SupportSettings is missing from envConfig")
|
||||
} else if supportSettingsAsMap, ok := supportSettings.(map[string]interface{}); !ok {
|
||||
t.Fatal("SupportSettings is not a map in envConfig")
|
||||
} else {
|
||||
if termsOfServiceLinkInEnv, ok := supportSettingsAsMap["TermsOfServiceLink"].(bool); !ok || !termsOfServiceLinkInEnv {
|
||||
t.Fatal("TermsOfServiceLink should be in envConfig")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("plugin directory settings", func(t *testing.T) {
|
||||
os.Setenv("MM_PLUGINSETTINGS_ENABLE", "false")
|
||||
os.Setenv("MM_PLUGINSETTINGS_DIRECTORY", "/temp/plugins")
|
||||
os.Setenv("MM_PLUGINSETTINGS_CLIENTDIRECTORY", "/temp/clientplugins")
|
||||
defer os.Unsetenv("MM_PLUGINSETTINGS_ENABLE")
|
||||
defer os.Unsetenv("MM_PLUGINSETTINGS_DIRECTORY")
|
||||
defer os.Unsetenv("MM_PLUGINSETTINGS_CLIENTDIRECTORY")
|
||||
|
||||
cfg, envCfg, err := ReadConfig(strings.NewReader(config), true)
|
||||
require.Nil(t, err)
|
||||
|
||||
assert.Equal(t, false, *cfg.PluginSettings.Enable)
|
||||
assert.Equal(t, "/temp/plugins", *cfg.PluginSettings.Directory)
|
||||
assert.Equal(t, "/temp/clientplugins", *cfg.PluginSettings.ClientDirectory)
|
||||
|
||||
if pluginSettings, ok := envCfg["PluginSettings"]; !ok {
|
||||
t.Fatal("PluginSettings is missing from envConfig")
|
||||
} else if pluginSettingsAsMap, ok := pluginSettings.(map[string]interface{}); !ok {
|
||||
t.Fatal("PluginSettings is not a map in envConfig")
|
||||
} else {
|
||||
if directory, ok := pluginSettingsAsMap["Directory"].(bool); !ok || !directory {
|
||||
t.Fatal("Directory should be in envConfig")
|
||||
}
|
||||
if clientDirectory, ok := pluginSettingsAsMap["ClientDirectory"].(bool); !ok || !clientDirectory {
|
||||
t.Fatal("ClientDirectory should be in envConfig")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("plugin specific settings cannot be overridden via environment", func(t *testing.T) {
|
||||
os.Setenv("MM_PLUGINSETTINGS_PLUGINS_JIRA_ENABLED", "false")
|
||||
os.Setenv("MM_PLUGINSETTINGS_PLUGINS_JIRA_SECRET", "env-secret")
|
||||
os.Setenv("MM_PLUGINSETTINGS_PLUGINSTATES_JIRA_ENABLE", "false")
|
||||
defer os.Unsetenv("MM_PLUGINSETTINGS_PLUGINS_JIRA_ENABLED")
|
||||
defer os.Unsetenv("MM_PLUGINSETTINGS_PLUGINS_JIRA_SECRET")
|
||||
defer os.Unsetenv("MM_PLUGINSETTINGS_PLUGINSTATES_JIRA_ENABLE")
|
||||
|
||||
cfg, envCfg, err := ReadConfig(strings.NewReader(config), true)
|
||||
require.Nil(t, err)
|
||||
|
||||
if pluginsJira, ok := cfg.PluginSettings.Plugins["jira"]; !ok {
|
||||
t.Fatal("PluginSettings.Plugins.jira is missing from config")
|
||||
} else {
|
||||
if enabled, ok := pluginsJira["enabled"]; !ok {
|
||||
t.Fatal("PluginSettings.Plugins.jira.enabled is missing from config")
|
||||
} else {
|
||||
assert.Equal(t, "true", enabled)
|
||||
}
|
||||
|
||||
if secret, ok := pluginsJira["secret"]; !ok {
|
||||
t.Fatal("PluginSettings.Plugins.jira.secret is missing from config")
|
||||
} else {
|
||||
assert.Equal(t, "config-secret", secret)
|
||||
}
|
||||
}
|
||||
|
||||
if pluginStatesJira, ok := cfg.PluginSettings.PluginStates["jira"]; !ok {
|
||||
t.Fatal("PluginSettings.PluginStates.jira is missing from config")
|
||||
} else {
|
||||
require.Equal(t, true, pluginStatesJira.Enable)
|
||||
}
|
||||
|
||||
if pluginSettings, ok := envCfg["PluginSettings"]; !ok {
|
||||
t.Fatal("PluginSettings is missing from envConfig")
|
||||
} else if pluginSettingsAsMap, ok := pluginSettings.(map[string]interface{}); !ok {
|
||||
t.Fatal("PluginSettings is not a map in envConfig")
|
||||
} else {
|
||||
if plugins, ok := pluginSettingsAsMap["Plugins"].(map[string]interface{}); !ok {
|
||||
t.Fatal("PluginSettings.Plugins is not a map in envConfig")
|
||||
} else if _, ok := plugins["jira"].(map[string]interface{}); ok {
|
||||
t.Fatal("PluginSettings.Plugins.jira should not be a map in envConfig")
|
||||
}
|
||||
|
||||
if pluginStates, ok := pluginSettingsAsMap["PluginStates"].(map[string]interface{}); !ok {
|
||||
t.Fatal("PluginSettings.PluginStates is missing from envConfig")
|
||||
} else if _, ok := pluginStates["jira"].(map[string]interface{}); ok {
|
||||
t.Fatal("PluginSettings.PluginStates.jira should not be a map in envConfig")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestValidateLocales(t *testing.T) {
|
||||
utils.TranslationsPreInit()
|
||||
cfg, _, _, err := LoadConfig("config.json")
|
||||
require.Nil(t, err)
|
||||
|
||||
*cfg.LocalizationSettings.DefaultServerLocale = "en"
|
||||
*cfg.LocalizationSettings.DefaultClientLocale = "en"
|
||||
*cfg.LocalizationSettings.AvailableLocales = ""
|
||||
|
||||
// t.Logf("*cfg.LocalizationSettings.DefaultClientLocale: %+v", *cfg.LocalizationSettings.DefaultClientLocale)
|
||||
if err := ValidateLocales(cfg); err != nil {
|
||||
t.Fatal("Should have not returned an error")
|
||||
}
|
||||
|
||||
// validate DefaultServerLocale
|
||||
*cfg.LocalizationSettings.DefaultServerLocale = "junk"
|
||||
if err := ValidateLocales(cfg); err != nil {
|
||||
if *cfg.LocalizationSettings.DefaultServerLocale != "en" {
|
||||
t.Fatal("DefaultServerLocale should have assigned to en as a default value")
|
||||
}
|
||||
} else {
|
||||
t.Fatal("Should have returned an error validating DefaultServerLocale")
|
||||
}
|
||||
|
||||
*cfg.LocalizationSettings.DefaultServerLocale = ""
|
||||
if err := ValidateLocales(cfg); err != nil {
|
||||
if *cfg.LocalizationSettings.DefaultServerLocale != "en" {
|
||||
t.Fatal("DefaultServerLocale should have assigned to en as a default value")
|
||||
}
|
||||
} else {
|
||||
t.Fatal("Should have returned an error validating DefaultServerLocale")
|
||||
}
|
||||
|
||||
*cfg.LocalizationSettings.AvailableLocales = "en"
|
||||
*cfg.LocalizationSettings.DefaultServerLocale = "de"
|
||||
if err := ValidateLocales(cfg); err != nil {
|
||||
if strings.Contains(*cfg.LocalizationSettings.AvailableLocales, *cfg.LocalizationSettings.DefaultServerLocale) {
|
||||
t.Fatal("DefaultServerLocale should not be added to AvailableLocales")
|
||||
}
|
||||
t.Fatal("Should have not returned an error validating DefaultServerLocale")
|
||||
}
|
||||
|
||||
// validate DefaultClientLocale
|
||||
*cfg.LocalizationSettings.AvailableLocales = ""
|
||||
*cfg.LocalizationSettings.DefaultClientLocale = "junk"
|
||||
if err := ValidateLocales(cfg); err != nil {
|
||||
if *cfg.LocalizationSettings.DefaultClientLocale != "en" {
|
||||
t.Fatal("DefaultClientLocale should have assigned to en as a default value")
|
||||
}
|
||||
} else {
|
||||
|
||||
t.Fatal("Should have returned an error validating DefaultClientLocale")
|
||||
}
|
||||
|
||||
*cfg.LocalizationSettings.DefaultClientLocale = ""
|
||||
if err := ValidateLocales(cfg); err != nil {
|
||||
if *cfg.LocalizationSettings.DefaultClientLocale != "en" {
|
||||
t.Fatal("DefaultClientLocale should have assigned to en as a default value")
|
||||
}
|
||||
} else {
|
||||
t.Fatal("Should have returned an error validating DefaultClientLocale")
|
||||
}
|
||||
|
||||
*cfg.LocalizationSettings.AvailableLocales = "en"
|
||||
*cfg.LocalizationSettings.DefaultClientLocale = "de"
|
||||
if err := ValidateLocales(cfg); err != nil {
|
||||
if !strings.Contains(*cfg.LocalizationSettings.AvailableLocales, *cfg.LocalizationSettings.DefaultClientLocale) {
|
||||
t.Fatal("DefaultClientLocale should have added to AvailableLocales")
|
||||
}
|
||||
} else {
|
||||
t.Fatal("Should have returned an error validating DefaultClientLocale")
|
||||
}
|
||||
|
||||
// validate AvailableLocales
|
||||
*cfg.LocalizationSettings.DefaultServerLocale = "en"
|
||||
*cfg.LocalizationSettings.DefaultClientLocale = "en"
|
||||
*cfg.LocalizationSettings.AvailableLocales = "junk"
|
||||
if err := ValidateLocales(cfg); err != nil {
|
||||
if *cfg.LocalizationSettings.AvailableLocales != "" {
|
||||
t.Fatal("AvailableLocales should have assigned to empty string as a default value")
|
||||
}
|
||||
} else {
|
||||
t.Fatal("Should have returned an error validating AvailableLocales")
|
||||
}
|
||||
|
||||
*cfg.LocalizationSettings.AvailableLocales = "en,de,junk"
|
||||
if err := ValidateLocales(cfg); err != nil {
|
||||
if *cfg.LocalizationSettings.AvailableLocales != "" {
|
||||
t.Fatal("AvailableLocales should have assigned to empty string as a default value")
|
||||
}
|
||||
} else {
|
||||
t.Fatal("Should have returned an error validating AvailableLocales")
|
||||
}
|
||||
|
||||
*cfg.LocalizationSettings.DefaultServerLocale = "fr"
|
||||
*cfg.LocalizationSettings.DefaultClientLocale = "de"
|
||||
*cfg.LocalizationSettings.AvailableLocales = "en"
|
||||
if err := ValidateLocales(cfg); err != nil {
|
||||
if strings.Contains(*cfg.LocalizationSettings.AvailableLocales, *cfg.LocalizationSettings.DefaultServerLocale) {
|
||||
t.Fatal("DefaultServerLocale should not be added to AvailableLocales")
|
||||
}
|
||||
if !strings.Contains(*cfg.LocalizationSettings.AvailableLocales, *cfg.LocalizationSettings.DefaultClientLocale) {
|
||||
t.Fatal("DefaultClientLocale should have added to AvailableLocales")
|
||||
}
|
||||
} else {
|
||||
t.Fatal("Should have returned an error validating AvailableLocales")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetClientConfig(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCases := []struct {
|
||||
description string
|
||||
config *model.Config
|
||||
diagnosticId string
|
||||
license *model.License
|
||||
expectedFields map[string]string
|
||||
}{
|
||||
{
|
||||
"unlicensed",
|
||||
&model.Config{
|
||||
EmailSettings: model.EmailSettings{
|
||||
EmailNotificationContentsType: sToP(model.EMAIL_NOTIFICATION_CONTENTS_FULL),
|
||||
},
|
||||
ThemeSettings: model.ThemeSettings{
|
||||
// Ignored, since not licensed.
|
||||
AllowCustomThemes: bToP(false),
|
||||
},
|
||||
ServiceSettings: model.ServiceSettings{
|
||||
WebsocketURL: sToP("ws://mattermost.example.com:8065"),
|
||||
WebsocketPort: iToP(80),
|
||||
WebsocketSecurePort: iToP(443),
|
||||
},
|
||||
},
|
||||
"",
|
||||
nil,
|
||||
map[string]string{
|
||||
"DiagnosticId": "",
|
||||
"EmailNotificationContentsType": "full",
|
||||
"AllowCustomThemes": "true",
|
||||
"EnforceMultifactorAuthentication": "false",
|
||||
"WebsocketURL": "ws://mattermost.example.com:8065",
|
||||
"WebsocketPort": "80",
|
||||
"WebsocketSecurePort": "443",
|
||||
},
|
||||
},
|
||||
{
|
||||
"licensed, but not for theme management",
|
||||
&model.Config{
|
||||
EmailSettings: model.EmailSettings{
|
||||
EmailNotificationContentsType: sToP(model.EMAIL_NOTIFICATION_CONTENTS_FULL),
|
||||
},
|
||||
ThemeSettings: model.ThemeSettings{
|
||||
// Ignored, since not licensed.
|
||||
AllowCustomThemes: bToP(false),
|
||||
},
|
||||
},
|
||||
"tag1",
|
||||
&model.License{
|
||||
Features: &model.Features{
|
||||
ThemeManagement: bToP(false),
|
||||
},
|
||||
},
|
||||
map[string]string{
|
||||
"DiagnosticId": "tag1",
|
||||
"EmailNotificationContentsType": "full",
|
||||
"AllowCustomThemes": "true",
|
||||
},
|
||||
},
|
||||
{
|
||||
"licensed for theme management",
|
||||
&model.Config{
|
||||
EmailSettings: model.EmailSettings{
|
||||
EmailNotificationContentsType: sToP(model.EMAIL_NOTIFICATION_CONTENTS_FULL),
|
||||
},
|
||||
ThemeSettings: model.ThemeSettings{
|
||||
AllowCustomThemes: bToP(false),
|
||||
},
|
||||
},
|
||||
"tag2",
|
||||
&model.License{
|
||||
Features: &model.Features{
|
||||
ThemeManagement: bToP(true),
|
||||
},
|
||||
},
|
||||
map[string]string{
|
||||
"DiagnosticId": "tag2",
|
||||
"EmailNotificationContentsType": "full",
|
||||
"AllowCustomThemes": "false",
|
||||
},
|
||||
},
|
||||
{
|
||||
"licensed for enforcement",
|
||||
&model.Config{
|
||||
ServiceSettings: model.ServiceSettings{
|
||||
EnforceMultifactorAuthentication: bToP(true),
|
||||
},
|
||||
},
|
||||
"tag1",
|
||||
&model.License{
|
||||
Features: &model.Features{
|
||||
MFA: bToP(true),
|
||||
},
|
||||
},
|
||||
map[string]string{
|
||||
"EnforceMultifactorAuthentication": "true",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(testCase.description, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCase.config.SetDefaults()
|
||||
if testCase.license != nil {
|
||||
testCase.license.Features.SetDefaults()
|
||||
}
|
||||
|
||||
configMap := GenerateClientConfig(testCase.config, testCase.diagnosticId, testCase.license)
|
||||
for expectedField, expectedValue := range testCase.expectedFields {
|
||||
actualValue, ok := configMap[expectedField]
|
||||
if assert.True(t, ok, fmt.Sprintf("config does not contain %v", expectedField)) {
|
||||
assert.Equal(t, expectedValue, actualValue)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetLimitedClientConfig(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCases := []struct {
|
||||
description string
|
||||
config *model.Config
|
||||
diagnosticId string
|
||||
license *model.License
|
||||
expectedFields map[string]string
|
||||
}{
|
||||
{
|
||||
"unlicensed",
|
||||
&model.Config{
|
||||
EmailSettings: model.EmailSettings{
|
||||
EmailNotificationContentsType: sToP(model.EMAIL_NOTIFICATION_CONTENTS_FULL),
|
||||
},
|
||||
ThemeSettings: model.ThemeSettings{
|
||||
// Ignored, since not licensed.
|
||||
AllowCustomThemes: bToP(false),
|
||||
},
|
||||
ServiceSettings: model.ServiceSettings{
|
||||
WebsocketURL: sToP("ws://mattermost.example.com:8065"),
|
||||
WebsocketPort: iToP(80),
|
||||
WebsocketSecurePort: iToP(443),
|
||||
},
|
||||
},
|
||||
"",
|
||||
nil,
|
||||
map[string]string{
|
||||
"DiagnosticId": "",
|
||||
"EnforceMultifactorAuthentication": "false",
|
||||
"WebsocketURL": "ws://mattermost.example.com:8065",
|
||||
"WebsocketPort": "80",
|
||||
"WebsocketSecurePort": "443",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(testCase.description, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCase.config.SetDefaults()
|
||||
if testCase.license != nil {
|
||||
testCase.license.Features.SetDefaults()
|
||||
}
|
||||
|
||||
configMap := GenerateLimitedClientConfig(testCase.config, testCase.diagnosticId, testCase.license)
|
||||
for expectedField, expectedValue := range testCase.expectedFields {
|
||||
actualValue, ok := configMap[expectedField]
|
||||
if assert.True(t, ok, fmt.Sprintf("config does not contain %v", expectedField)) {
|
||||
assert.Equal(t, expectedValue, actualValue)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func sToP(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
||||
func bToP(b bool) *bool {
|
||||
return &b
|
||||
}
|
||||
|
||||
func iToP(i int) *int {
|
||||
return &i
|
||||
}
|
||||
|
||||
func TestGetDefaultsFromStruct(t *testing.T) {
|
||||
s := struct {
|
||||
TestSettings struct {
|
||||
IntValue int
|
||||
BoolValue bool
|
||||
StringValue string
|
||||
}
|
||||
PointerToTestSettings *struct {
|
||||
Value int
|
||||
}
|
||||
}{}
|
||||
|
||||
defaults := getDefaultsFromStruct(s)
|
||||
|
||||
assert.Equal(t, defaults["TestSettings.IntValue"], 0)
|
||||
assert.Equal(t, defaults["TestSettings.BoolValue"], false)
|
||||
assert.Equal(t, defaults["TestSettings.StringValue"], "")
|
||||
assert.Equal(t, defaults["PointerToTestSettings.Value"], 0)
|
||||
assert.NotContains(t, defaults, "PointerToTestSettings")
|
||||
assert.Len(t, defaults, 4)
|
||||
}
|
||||
Ссылка в новой задаче
Block a user