// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. package config import ( "testing" "github.com/mattermost/mattermost/server/public/model" "github.com/stretchr/testify/require" ) func defaultConfigGen() *model.Config { cfg := &model.Config{} cfg.SetDefaults() return cfg } func BenchmarkDiff(b *testing.B) { b.Run("equal empty", func(b *testing.B) { baseCfg := &model.Config{} actualCfg := &model.Config{} for b.Loop() { _, _ = Diff(baseCfg, actualCfg) } }) b.Run("equal with defaults", func(b *testing.B) { baseCfg := defaultConfigGen() actualCfg := defaultConfigGen() for b.Loop() { _, _ = Diff(baseCfg, actualCfg) } }) b.Run("actual empty", func(b *testing.B) { baseCfg := defaultConfigGen() actualCfg := &model.Config{} for b.Loop() { _, _ = Diff(baseCfg, actualCfg) } }) b.Run("base empty", func(b *testing.B) { baseCfg := &model.Config{} actualCfg := defaultConfigGen() for b.Loop() { _, _ = Diff(baseCfg, actualCfg) } }) b.Run("some diffs", func(b *testing.B) { baseCfg := defaultConfigGen() actualCfg := defaultConfigGen() baseCfg.ServiceSettings.SiteURL = model.NewPointer("http://localhost") baseCfg.ServiceSettings.ReadTimeout = model.NewPointer(300) baseCfg.SqlSettings.QueryTimeout = model.NewPointer(0) actualCfg.PluginSettings.EnableUploads = nil actualCfg.TeamSettings.MaxChannelsPerTeam = model.NewPointer(int64(100000)) actualCfg.FeatureFlags = nil actualCfg.SqlSettings.DataSourceReplicas = []string{ "ds0", "ds1", "ds2", } for b.Loop() { _, _ = Diff(baseCfg, actualCfg) } }) } func TestDiffSanitized(t *testing.T) { tcs := []struct { name string base *model.Config actual *model.Config diffs ConfigDiffs err string }{ { "nil", nil, nil, nil, "input configs should not be nil", }, { "empty", &model.Config{}, &model.Config{}, nil, "", }, { "defaults", defaultConfigGen(), defaultConfigGen(), nil, "", }, { "default base, actual empty", defaultConfigGen(), &model.Config{}, ConfigDiffs{ { Path: "", BaseVal: func() model.Config { cfg := defaultConfigGen() cfg.Sanitize(nil, nil) return *cfg }(), ActualVal: model.Config{}, }, }, "", }, { "empty base, actual default", &model.Config{}, defaultConfigGen(), ConfigDiffs{ { Path: "", BaseVal: model.Config{}, ActualVal: func() model.Config { cfg := defaultConfigGen() cfg.Sanitize(nil, nil) return *cfg }(), }, }, "", }, { "sensitive LdapSettings.BindPassword", func() *model.Config { cfg := defaultConfigGen() cfg.LdapSettings.BindPassword = model.NewPointer("base") return cfg }(), func() *model.Config { cfg := defaultConfigGen() cfg.LdapSettings.BindPassword = model.NewPointer("actual") return cfg }(), ConfigDiffs{ { Path: "LdapSettings.BindPassword", BaseVal: model.FakeSetting, ActualVal: model.FakeSetting, }, }, "", }, { "sensitive FileSettings.PublicLinkSalt", func() *model.Config { cfg := defaultConfigGen() cfg.FileSettings.PublicLinkSalt = model.NewPointer("base") return cfg }(), func() *model.Config { cfg := defaultConfigGen() cfg.FileSettings.PublicLinkSalt = model.NewPointer("actual") return cfg }(), ConfigDiffs{ { Path: "FileSettings.PublicLinkSalt", BaseVal: model.FakeSetting, ActualVal: model.FakeSetting, }, }, "", }, { "sensitive FileSettings.AmazonS3SecretAccessKey", func() *model.Config { cfg := defaultConfigGen() cfg.FileSettings.AmazonS3SecretAccessKey = model.NewPointer("base") return cfg }(), func() *model.Config { cfg := defaultConfigGen() cfg.FileSettings.AmazonS3SecretAccessKey = model.NewPointer("actual") return cfg }(), ConfigDiffs{ { Path: "FileSettings.AmazonS3SecretAccessKey", BaseVal: model.FakeSetting, ActualVal: model.FakeSetting, }, }, "", }, { "sensitive SqlSettings.DataSource", func() *model.Config { cfg := defaultConfigGen() cfg.SqlSettings.DataSource = model.NewPointer("base") return cfg }(), func() *model.Config { cfg := defaultConfigGen() cfg.SqlSettings.DataSource = model.NewPointer("actual") return cfg }(), ConfigDiffs{ { Path: "SqlSettings.DataSource", BaseVal: model.FakeSetting, ActualVal: model.FakeSetting, }, }, "", }, { "sensitive SqlSettings.AtRestEncryptKey", func() *model.Config { cfg := defaultConfigGen() cfg.SqlSettings.AtRestEncryptKey = model.NewPointer("base") return cfg }(), func() *model.Config { cfg := defaultConfigGen() cfg.SqlSettings.AtRestEncryptKey = model.NewPointer("actual") return cfg }(), ConfigDiffs{ { Path: "SqlSettings.AtRestEncryptKey", BaseVal: model.FakeSetting, ActualVal: model.FakeSetting, }, }, "", }, { "sensitive SqlSettings.DataSourceReplicas", func() *model.Config { cfg := defaultConfigGen() cfg.SqlSettings.DataSourceReplicas = []string{ "ds0", "ds1", } return cfg }(), func() *model.Config { cfg := defaultConfigGen() cfg.SqlSettings.DataSourceReplicas = []string{ "ds0", "ds1", "ds2", } return cfg }(), ConfigDiffs{ { Path: "SqlSettings.DataSourceReplicas", BaseVal: model.FakeSetting, ActualVal: model.FakeSetting, }, }, "", }, { "sensitive SqlSettings.DataSourceSearchReplicas", func() *model.Config { cfg := defaultConfigGen() cfg.SqlSettings.DataSourceSearchReplicas = []string{ "ds0", "ds1", } return cfg }(), func() *model.Config { cfg := defaultConfigGen() cfg.SqlSettings.DataSourceSearchReplicas = []string{ "ds0", "ds1", "ds2", } return cfg }(), ConfigDiffs{ { Path: "SqlSettings.DataSourceSearchReplicas", BaseVal: model.FakeSetting, ActualVal: model.FakeSetting, }, }, "", }, { "sensitive EmailSettings.SMTPPassword", func() *model.Config { cfg := defaultConfigGen() cfg.EmailSettings.SMTPPassword = model.NewPointer("base") return cfg }(), func() *model.Config { cfg := defaultConfigGen() cfg.EmailSettings.SMTPPassword = model.NewPointer("actual") return cfg }(), ConfigDiffs{ { Path: "EmailSettings.SMTPPassword", BaseVal: model.FakeSetting, ActualVal: model.FakeSetting, }, }, "", }, { "sensitive GitLabSettings.Secret", func() *model.Config { cfg := defaultConfigGen() cfg.GitLabSettings.Secret = model.NewPointer("base") return cfg }(), func() *model.Config { cfg := defaultConfigGen() cfg.GitLabSettings.Secret = model.NewPointer("actual") return cfg }(), ConfigDiffs{ { Path: "GitLabSettings.Secret", BaseVal: model.FakeSetting, ActualVal: model.FakeSetting, }, }, "", }, { "sensitive GoogleSettings.Secret", func() *model.Config { cfg := defaultConfigGen() cfg.GoogleSettings.Secret = model.NewPointer("base") return cfg }(), func() *model.Config { cfg := defaultConfigGen() cfg.GoogleSettings.Secret = model.NewPointer("actual") return cfg }(), ConfigDiffs{ { Path: "GoogleSettings.Secret", BaseVal: model.FakeSetting, ActualVal: model.FakeSetting, }, }, "", }, { "sensitive Office365Settings.Secret", func() *model.Config { cfg := defaultConfigGen() cfg.Office365Settings.Secret = model.NewPointer("base") return cfg }(), func() *model.Config { cfg := defaultConfigGen() cfg.Office365Settings.Secret = model.NewPointer("actual") return cfg }(), ConfigDiffs{ { Path: "Office365Settings.Secret", BaseVal: model.FakeSetting, ActualVal: model.FakeSetting, }, }, "", }, { "sensitive OpenIdSettings.Secret", func() *model.Config { cfg := defaultConfigGen() cfg.OpenIdSettings.Secret = model.NewPointer("base") return cfg }(), func() *model.Config { cfg := defaultConfigGen() cfg.OpenIdSettings.Secret = model.NewPointer("actual") return cfg }(), ConfigDiffs{ { Path: "OpenIdSettings.Secret", BaseVal: model.FakeSetting, ActualVal: model.FakeSetting, }, }, "", }, { "sensitive ElasticsearchSettings.Password", func() *model.Config { cfg := defaultConfigGen() cfg.ElasticsearchSettings.Password = model.NewPointer("base") return cfg }(), func() *model.Config { cfg := defaultConfigGen() cfg.ElasticsearchSettings.Password = model.NewPointer("actual") return cfg }(), ConfigDiffs{ { Path: "ElasticsearchSettings.Password", BaseVal: model.FakeSetting, ActualVal: model.FakeSetting, }, }, "", }, { "sensitive MessageExportSettings.GlobalRelaySettings", func() *model.Config { cfg := defaultConfigGen() cfg.MessageExportSettings.GlobalRelaySettings = &model.GlobalRelayMessageExportSettings{ SMTPUsername: model.NewPointer("base"), SMTPPassword: model.NewPointer("base"), EmailAddress: model.NewPointer("base"), } return cfg }(), func() *model.Config { cfg := defaultConfigGen() cfg.MessageExportSettings.GlobalRelaySettings = &model.GlobalRelayMessageExportSettings{ SMTPUsername: model.NewPointer("actual"), SMTPPassword: model.NewPointer("actual"), EmailAddress: model.NewPointer("actual"), } return cfg }(), ConfigDiffs{ { Path: "MessageExportSettings.GlobalRelaySettings.SMTPUsername", BaseVal: model.FakeSetting, ActualVal: model.FakeSetting, }, { Path: "MessageExportSettings.GlobalRelaySettings.SMTPPassword", BaseVal: model.FakeSetting, ActualVal: model.FakeSetting, }, { Path: "MessageExportSettings.GlobalRelaySettings.EmailAddress", BaseVal: model.FakeSetting, ActualVal: model.FakeSetting, }, }, "", }, { "sensitive ServiceSettings.SplitKey", func() *model.Config { cfg := defaultConfigGen() cfg.ServiceSettings.SplitKey = model.NewPointer("base") return cfg }(), func() *model.Config { cfg := defaultConfigGen() cfg.ServiceSettings.SplitKey = model.NewPointer("actual") return cfg }(), ConfigDiffs{ { Path: "ServiceSettings.SplitKey", BaseVal: model.FakeSetting, ActualVal: model.FakeSetting, }, }, "", }, { "plugin config", defaultConfigGen(), func() *model.Config { cfg := defaultConfigGen() cfg.PluginSettings.Plugins = map[string]map[string]any{ "com.mattermost.newplugin": { "key": true, }, } return cfg }(), ConfigDiffs{ { Path: "PluginSettings.Plugins", BaseVal: model.FakeSetting, ActualVal: model.FakeSetting, }, }, "", }, } for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { diffs, err := Diff(tc.base, tc.actual) if tc.err != "" { require.EqualError(t, err, tc.err) require.Nil(t, diffs) } else { require.NoError(t, err) } require.Equal(t, tc.diffs, diffs.Sanitize()) }) } } func TestDiff(t *testing.T) { tcs := []struct { name string base *model.Config actual *model.Config diffs ConfigDiffs err string }{ { "nil", nil, nil, nil, "input configs should not be nil", }, { "empty", &model.Config{}, &model.Config{}, nil, "", }, { "defaults", defaultConfigGen(), defaultConfigGen(), nil, "", }, { "default base, actual empty", defaultConfigGen(), &model.Config{}, ConfigDiffs{ { Path: "", BaseVal: *defaultConfigGen(), ActualVal: model.Config{}, }, }, "", }, { "empty base, actual default", &model.Config{}, defaultConfigGen(), ConfigDiffs{ { Path: "", BaseVal: model.Config{}, ActualVal: *defaultConfigGen(), }, }, "", }, { "string change", defaultConfigGen(), func() *model.Config { cfg := defaultConfigGen() cfg.ServiceSettings.SiteURL = model.NewPointer("http://changed") return cfg }(), ConfigDiffs{ { Path: "ServiceSettings.SiteURL", BaseVal: *defaultConfigGen().ServiceSettings.SiteURL, ActualVal: "http://changed", }, }, "", }, { "string nil", defaultConfigGen(), func() *model.Config { cfg := defaultConfigGen() cfg.ServiceSettings.SiteURL = nil return cfg }(), ConfigDiffs{ { Path: "ServiceSettings.SiteURL", BaseVal: defaultConfigGen().ServiceSettings.SiteURL, ActualVal: func() *string { return nil }(), }, }, "", }, { "bool change", defaultConfigGen(), func() *model.Config { cfg := defaultConfigGen() cfg.PluginSettings.Enable = model.NewPointer(!*cfg.PluginSettings.Enable) return cfg }(), ConfigDiffs{ { Path: "PluginSettings.Enable", BaseVal: true, ActualVal: false, }, }, "", }, { "bool nil", defaultConfigGen(), func() *model.Config { cfg := defaultConfigGen() cfg.PluginSettings.Enable = nil return cfg }(), ConfigDiffs{ { Path: "PluginSettings.Enable", BaseVal: defaultConfigGen().PluginSettings.Enable, ActualVal: func() *bool { return nil }(), }, }, "", }, { "int change", defaultConfigGen(), func() *model.Config { cfg := defaultConfigGen() cfg.ServiceSettings.ReadTimeout = model.NewPointer(0) return cfg }(), ConfigDiffs{ { Path: "ServiceSettings.ReadTimeout", BaseVal: *defaultConfigGen().ServiceSettings.ReadTimeout, ActualVal: 0, }, }, "", }, { "int nil", defaultConfigGen(), func() *model.Config { cfg := defaultConfigGen() cfg.ServiceSettings.ReadTimeout = nil return cfg }(), ConfigDiffs{ { Path: "ServiceSettings.ReadTimeout", BaseVal: defaultConfigGen().ServiceSettings.ReadTimeout, ActualVal: func() *int { return nil }(), }, }, "", }, { "slice addition", defaultConfigGen(), func() *model.Config { cfg := defaultConfigGen() cfg.SqlSettings.DataSourceReplicas = []string{ "ds0", "ds1", } return cfg }(), ConfigDiffs{ { Path: "SqlSettings.DataSourceReplicas", BaseVal: defaultConfigGen().SqlSettings.DataSourceReplicas, ActualVal: []string{ "ds0", "ds1", }, }, }, "", }, { "slice deletion", func() *model.Config { cfg := defaultConfigGen() cfg.SqlSettings.DataSourceReplicas = []string{ "ds0", "ds1", } return cfg }(), func() *model.Config { cfg := defaultConfigGen() cfg.SqlSettings.DataSourceReplicas = []string{ "ds0", } return cfg }(), ConfigDiffs{ { Path: "SqlSettings.DataSourceReplicas", BaseVal: []string{ "ds0", "ds1", }, ActualVal: []string{ "ds0", }, }, }, "", }, { "slice nil", func() *model.Config { cfg := defaultConfigGen() cfg.SqlSettings.DataSourceReplicas = []string{ "ds0", "ds1", } return cfg }(), func() *model.Config { cfg := defaultConfigGen() cfg.SqlSettings.DataSourceReplicas = nil return cfg }(), ConfigDiffs{ { Path: "SqlSettings.DataSourceReplicas", BaseVal: []string{ "ds0", "ds1", }, ActualVal: func() []string { return nil }(), }, }, "", }, { "map change", defaultConfigGen(), func() *model.Config { cfg := defaultConfigGen() cfg.PluginSettings.PluginStates["com.mattermost.nps"] = &model.PluginState{ Enable: !cfg.PluginSettings.PluginStates["com.mattermost.nps"].Enable, } return cfg }(), ConfigDiffs{ { Path: "PluginSettings.PluginStates", BaseVal: defaultConfigGen().PluginSettings.PluginStates, ActualVal: map[string]*model.PluginState{ "com.mattermost.nps": { Enable: !defaultConfigGen().PluginSettings.PluginStates["com.mattermost.nps"].Enable, }, "com.mattermost.calls": { Enable: true, }, "mattermost-ai": { Enable: true, }, "playbooks": { Enable: true, }, }, }, }, "", }, { "map addition", defaultConfigGen(), func() *model.Config { cfg := defaultConfigGen() cfg.PluginSettings.PluginStates["com.mattermost.newplugin"] = &model.PluginState{ Enable: true, } return cfg }(), ConfigDiffs{ { Path: "PluginSettings.PluginStates", BaseVal: defaultConfigGen().PluginSettings.PluginStates, ActualVal: map[string]*model.PluginState{ "com.mattermost.nps": { Enable: defaultConfigGen().PluginSettings.PluginStates["com.mattermost.nps"].Enable, }, "com.mattermost.newplugin": { Enable: true, }, "com.mattermost.calls": { Enable: true, }, "mattermost-ai": { Enable: true, }, "playbooks": { Enable: true, }, }, }, }, "", }, { "map deletion", defaultConfigGen(), func() *model.Config { cfg := defaultConfigGen() delete(cfg.PluginSettings.PluginStates, "com.mattermost.nps") return cfg }(), ConfigDiffs{ { Path: "PluginSettings.PluginStates", BaseVal: defaultConfigGen().PluginSettings.PluginStates, ActualVal: map[string]*model.PluginState{ "com.mattermost.calls": { Enable: true, }, "mattermost-ai": { Enable: true, }, "playbooks": { Enable: true, }, }, }, }, "", }, { "map nil", defaultConfigGen(), func() *model.Config { cfg := defaultConfigGen() cfg.PluginSettings.PluginStates = nil return cfg }(), ConfigDiffs{ { Path: "PluginSettings.PluginStates", BaseVal: defaultConfigGen().PluginSettings.PluginStates, ActualVal: func() map[string]*model.PluginState { return nil }(), }, }, "", }, { "map type change", func() *model.Config { cfg := defaultConfigGen() cfg.PluginSettings.Plugins = map[string]map[string]any{ "com.mattermost.newplugin": { "key": true, }, } return cfg }(), func() *model.Config { cfg := defaultConfigGen() cfg.PluginSettings.Plugins = map[string]map[string]any{ "com.mattermost.newplugin": { "key": "string", }, } return cfg }(), ConfigDiffs{ { Path: "PluginSettings.Plugins", BaseVal: func() any { return map[string]map[string]any{ "com.mattermost.newplugin": { "key": true, }, } }(), ActualVal: func() any { return map[string]map[string]any{ "com.mattermost.newplugin": { "key": "string", }, } }(), }, }, "", }, } for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { diffs, err := Diff(tc.base, tc.actual) if tc.err != "" { require.EqualError(t, err, tc.err) require.Nil(t, diffs) } else { require.NoError(t, err) } require.Equal(t, tc.diffs, diffs) }) } }