Files
mattermost/server/config/file_test.go
ludvigbolin 4547d9ab0e [GH24626] Remove ExperimentalTimezone as this is always true (#24657)
* Remove all occurrences of ExperimentalTimezone in /server

This removes all uses of ExperimentalTimezone and makes all conditions using this setting being either removed (== false) or always used (== true)

* Remove timezone parameter for getChannelMemberByGroup

As timezone is always enabled this function no longer has a second parameter. The endpoint will always be called with includeTimezones set to true

* Remove all uses of ExperimentalTimezone in webapp

All conditions dependent of this are adjusted to always be true if experimentalTimezone should be true and vice versa

* Change all tests checking for correct usage of ExperimentalTimezone

As this parameter is no longer used, the tests which check for correct behaviour of this is no longer needed

* Fix type errors

Tests regarding types does now pass

'npm run check-types' will run successfully

* fix lint issues

Fix all current lint errors and now lint test goes through

* Fix test regarding profile popover

enableTimezone now is gone and all profile_popover.test.tsx can run successfully.

Added state.entities.users.profiles[] as it before threw an error without it.

* Fix tests where "April 1 2019" was changed to "April 01 2019"

Now it will show "April 1 2019" and "timeZone" will only show if its other than empty ("").

* Fix test where date was set to NaN

Now date will show correctly and will not be NaN

* fix minor test case that failed before

* fix linting in server

* Delete tests that are not valid with timeZoneEnabled removed

tests in advanced_create_comment and advanced_create_post had timeZoneEnabled
set to false as default. The tests that use this are now changed to once which are meant to have timeZoneEnabled set to true.

Adjust jest function in advanced_create_post to get correct stacktrace

* Parameter deleted

isTimeZoneEnabled is always true and therefore the parameter can be deleted

* adjust to prettier standard

* Remove all occurrences of ExperimentalTimezone in /server

This removes all uses of ExperimentalTimezone and makes all conditions using this setting being either removed (== false) or always used (== true)

* Remove timezone parameter for getChannelMemberByGroup

As timezone is always enabled this function no longer has a second parameter. The endpoint will always be called with includeTimezones set to true

* Remove all uses of ExperimentalTimezone in webapp

All conditions dependent of this are adjusted to always be true if experimentalTimezone should be true and vice versa

* Change all tests checking for correct usage of ExperimentalTimezone

As this parameter is no longer used, the tests which check for correct behaviour of this is no longer needed

* Fix type errors

Tests regarding types does now pass

'npm run check-types' will run successfully

* fix lint issues

Fix all current lint errors and now lint test goes through

* Fix test regarding profile popover

enableTimezone now is gone and all profile_popover.test.tsx can run successfully.

Added state.entities.users.profiles[] as it before threw an error without it.

* Fix tests where "April 1 2019" was changed to "April 01 2019"

Now it will show "April 1 2019" and "timeZone" will only show if its other than empty ("").

* Fix test where date was set to NaN

Now date will show correctly and will not be NaN

* fix minor test case that failed before

* fix linting in server

* Delete tests that are not valid with timeZoneEnabled removed

tests in advanced_create_comment and advanced_create_post had timeZoneEnabled
set to false as default. The tests that use this are now changed to once which are meant to have timeZoneEnabled set to true.

Adjust jest function in advanced_create_post to get correct stacktrace

* Parameter deleted

isTimeZoneEnabled is always true and therefore the parameter can be deleted

* adjust to prettier standard

---------

Co-authored-by: Harrison Healey <harrisonmhealey@gmail.com>
Co-authored-by: Mattermost Build <build@mattermost.com>
2023-10-30 15:31:05 -04:00

1348 lines
40 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package config
import (
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/v8/channels/utils"
)
func setupConfigFile(t *testing.T, cfg *model.Config) (string, func()) {
os.Clearenv()
t.Helper()
tempDir, err := os.MkdirTemp("", "setupConfigFile")
require.NoError(t, err)
err = os.Chdir(tempDir)
require.NoError(t, err)
var name string
if cfg != nil {
f, err := os.CreateTemp(tempDir, "setupConfigFile")
require.NoError(t, err)
cfgData, err := marshalConfig(cfg)
require.NoError(t, err)
os.WriteFile(f.Name(), cfgData, 0644)
name = f.Name()
}
return name, func() {
os.RemoveAll(tempDir)
}
}
func setupConfigFileStore(t *testing.T, cfg *model.Config) (*Store, func()) {
t.Helper()
path, tearDown := setupConfigFile(t, cfg)
fs, err := NewFileStore(path, false)
require.NoError(t, err)
configStore, err := NewStoreFromBacking(fs, nil, false)
require.NoError(t, err)
return configStore, func() {
tearDown()
configStore.Close()
}
}
// getActualFileConfig returns the configuration present in the given file without relying on a config store.
func getActualFileConfig(t *testing.T, path string) *model.Config {
t.Helper()
f, err := os.Open(path)
require.NoError(t, err)
defer f.Close()
var actualCfg *model.Config
err = json.NewDecoder(f).Decode(&actualCfg)
require.NoError(t, err)
return actualCfg
}
// assertFileEqualsConfig verifies the on disk contents of the given path equal the given
func assertFileEqualsConfig(t *testing.T, expectedCfg *model.Config, path string) {
t.Helper()
actualCfg := getActualFileConfig(t, path)
assert.Equal(t, expectedCfg, actualCfg)
}
// assertFileNotEqualsConfig verifies the on disk contents of the given path does not equal the given
func assertFileNotEqualsConfig(t *testing.T, expectedCfg *model.Config, path string) {
t.Helper()
actualCfg := getActualFileConfig(t, path)
assert.NotEqual(t, expectedCfg, actualCfg)
}
func TestFileStoreNew(t *testing.T) {
utils.TranslationsPreInit()
t.Run("absolute path, initialization required", func(t *testing.T) {
path, tearDown := setupConfigFile(t, testConfig)
defer tearDown()
fs, err := NewFileStore(path, false)
require.NoError(t, err)
configStore, err := NewStoreFromBacking(fs, nil, false)
require.NoError(t, err)
defer configStore.Close()
assert.Equal(t, "http://TestStoreNew", *configStore.Get().ServiceSettings.SiteURL)
assertFileNotEqualsConfig(t, testConfig, path)
})
t.Run("absolute path, initialization required, with custom defaults", func(t *testing.T) {
path, tearDown := setupConfigFile(t, testConfig)
defer tearDown()
fs, err := NewFileStore(path, false)
require.NoError(t, err)
configStore, err := NewStoreFromBacking(fs, customConfigDefaults, false)
require.NoError(t, err)
defer configStore.Close()
// already existing value should not be affected by the custom
// defaults
assert.Equal(t, "http://TestStoreNew", *configStore.Get().ServiceSettings.SiteURL)
// nonexisting value should be overwritten by the custom
// defaults
assertFileNotEqualsConfig(t, testConfig, path)
})
t.Run("absolute path, already minimally configured", func(t *testing.T) {
path, tearDown := setupConfigFile(t, minimalConfigNoFF)
defer tearDown()
fs, err := NewFileStore(path, false)
require.NoError(t, err)
configStore, err := NewStoreFromBacking(fs, nil, false)
require.NoError(t, err)
defer configStore.Close()
assert.Equal(t, "http://minimal", *configStore.Get().ServiceSettings.SiteURL)
assertFileEqualsConfig(t, minimalConfigNoFF, path)
})
t.Run("absolute path, already minimally configured, with custom defaults", func(t *testing.T) {
path, tearDown := setupConfigFile(t, minimalConfigNoFF)
defer tearDown()
fs, err := NewFileStore(path, false)
require.NoError(t, err)
configStore, err := NewStoreFromBacking(fs, customConfigDefaults, false)
require.NoError(t, err)
defer configStore.Close()
// as the whole config has default values already, custom
// defaults should have no effect
assert.Equal(t, "http://minimal", *configStore.Get().ServiceSettings.SiteURL)
assertFileEqualsConfig(t, minimalConfigNoFF, path)
})
t.Run("absolute path, file does not exist", func(t *testing.T) {
_, tearDown := setupConfigFile(t, nil)
defer tearDown()
tempDir, err := os.MkdirTemp("", "TestFileStoreNew")
require.NoError(t, err)
defer os.RemoveAll(tempDir)
path := filepath.Join(tempDir, "does_not_exist")
fs, err := NewFileStore(path, true)
require.NoError(t, err)
configStore, err := NewStoreFromBacking(fs, nil, false)
require.NoError(t, err)
defer configStore.Close()
assert.Equal(t, "", *configStore.Get().ServiceSettings.SiteURL)
assertFileNotEqualsConfig(t, testConfig, path)
})
t.Run("absolute path, file does not exist, with custom defaults", func(t *testing.T) {
_, tearDown := setupConfigFile(t, nil)
defer tearDown()
tempDir, err := os.MkdirTemp("", "TestFileStoreNew")
require.NoError(t, err)
defer os.RemoveAll(tempDir)
path := filepath.Join(tempDir, "does_not_exist")
fs, err := NewFileStore(path, true)
require.NoError(t, err)
configStore, err := NewStoreFromBacking(fs, customConfigDefaults, false)
require.NoError(t, err)
defer configStore.Close()
assert.Equal(t, *customConfigDefaults.ServiceSettings.SiteURL, *configStore.Get().ServiceSettings.SiteURL)
})
t.Run("absolute path, path to file does not exist", func(t *testing.T) {
_, tearDown := setupConfigFile(t, nil)
defer tearDown()
tempDir, err := os.MkdirTemp("", "TestFileStoreNew")
require.NoError(t, err)
defer os.RemoveAll(tempDir)
path := filepath.Join(tempDir, "does/not/exist")
_, err = NewFileStore(path, true)
require.Error(t, err)
})
t.Run("relative path, file exists", func(t *testing.T) {
_, tearDown := setupConfigFile(t, nil)
defer tearDown()
err := os.MkdirAll("TestFileStoreNew/a/b/c", 0700)
require.NoError(t, err)
defer os.RemoveAll("TestFileStoreNew")
path := "TestFileStoreNew/a/b/c/config.json"
cfgData, err := marshalConfig(testConfig)
require.NoError(t, err)
os.WriteFile(path, cfgData, 0644)
fs, err := NewFileStore(path, false)
require.NoError(t, err)
configStore, err := NewStoreFromBacking(fs, nil, false)
require.NoError(t, err)
defer configStore.Close()
assert.Equal(t, "http://TestStoreNew", *configStore.Get().ServiceSettings.SiteURL)
assertFileNotEqualsConfig(t, testConfig, path)
})
t.Run("relative path, file does not exist", func(t *testing.T) {
_, tearDown := setupConfigFile(t, nil)
defer tearDown()
err := os.MkdirAll("config/TestFileStoreNew/a/b/c", 0700)
require.NoError(t, err)
defer os.RemoveAll("config/TestFileStoreNew")
path := "TestFileStoreNew/a/b/c/config.json"
fs, err := NewFileStore(path, false)
require.Error(t, err)
require.Nil(t, fs)
})
}
func TestFileStoreGet(t *testing.T) {
configStore, tearDown := setupConfigFileStore(t, testConfig)
defer tearDown()
cfg := configStore.Get()
assert.Equal(t, "http://TestStoreNew", *cfg.ServiceSettings.SiteURL)
cfg2 := configStore.Get()
assert.Equal(t, "http://TestStoreNew", *cfg.ServiceSettings.SiteURL)
assert.True(t, cfg == cfg2, "Get() returned different configuration instances")
newCfg := &model.Config{}
_, _, err := configStore.Set(newCfg)
require.NoError(t, err)
assert.False(t, newCfg == cfg, "returned config should have been different from original")
}
func TestFileStoreGetEnvironmentOverrides(t *testing.T) {
t.Run("get override for a string variable", func(t *testing.T) {
path, tearDown := setupConfigFile(t, testConfig)
defer tearDown()
fsInner, err := NewFileStore(path, false)
require.NoError(t, err)
fs, err := NewStoreFromBacking(fsInner, nil, false)
require.NoError(t, err)
defer fs.Close()
assert.Equal(t, "http://TestStoreNew", *fs.Get().ServiceSettings.SiteURL)
assert.Empty(t, fs.GetEnvironmentOverrides())
os.Setenv("MM_SERVICESETTINGS_SITEURL", "http://override")
defer os.Unsetenv("MM_SERVICESETTINGS_SITEURL")
fsInner, err = NewFileStore(path, false)
require.NoError(t, err)
fs, err = NewStoreFromBacking(fsInner, nil, false)
require.NoError(t, err)
defer fs.Close()
assert.Equal(t, "http://override", *fs.Get().ServiceSettings.SiteURL)
assert.Equal(t, map[string]any{"ServiceSettings": map[string]any{"SiteURL": true}}, fs.GetEnvironmentOverrides())
})
t.Run("get override for a string variable, with custom defaults", func(t *testing.T) {
path, tearDown := setupConfigFile(t, testConfig)
defer tearDown()
fsInner, err := NewFileStore(path, false)
require.NoError(t, err)
fs, err := NewStoreFromBacking(fsInner, customConfigDefaults, false)
require.NoError(t, err)
defer fs.Close()
assert.Equal(t, "http://TestStoreNew", *fs.Get().ServiceSettings.SiteURL)
assert.Empty(t, fs.GetEnvironmentOverrides())
os.Setenv("MM_SERVICESETTINGS_SITEURL", "http://override")
defer os.Unsetenv("MM_SERVICESETTINGS_SITEURL")
fsInner, err = NewFileStore(path, false)
require.NoError(t, err)
fs, err = NewStoreFromBacking(fsInner, customConfigDefaults, false)
require.NoError(t, err)
defer fs.Close()
// environment override should take priority over the custom default value
assert.Equal(t, "http://override", *fs.Get().ServiceSettings.SiteURL)
assert.Equal(t, map[string]any{"ServiceSettings": map[string]any{"SiteURL": true}}, fs.GetEnvironmentOverrides())
})
t.Run("get override for a bool variable", func(t *testing.T) {
path, tearDown := setupConfigFile(t, testConfig)
defer tearDown()
fsInner, err := NewFileStore(path, false)
require.NoError(t, err)
fs, err := NewStoreFromBacking(fsInner, nil, false)
require.NoError(t, err)
defer fs.Close()
assert.Equal(t, false, *fs.Get().PluginSettings.EnableUploads)
assert.Empty(t, fs.GetEnvironmentOverrides())
os.Setenv("MM_PLUGINSETTINGS_ENABLEUPLOADS", "true")
defer os.Unsetenv("MM_PLUGINSETTINGS_ENABLEUPLOADS")
fsInner, err = NewFileStore(path, false)
require.NoError(t, err)
fs, err = NewStoreFromBacking(fsInner, nil, false)
require.NoError(t, err)
defer fs.Close()
assert.Equal(t, true, *fs.Get().PluginSettings.EnableUploads)
assert.Equal(t, map[string]any{"PluginSettings": map[string]any{"EnableUploads": true}}, fs.GetEnvironmentOverrides())
})
t.Run("get override for an int variable", func(t *testing.T) {
path, tearDown := setupConfigFile(t, testConfig)
defer tearDown()
fsInner, err := NewFileStore(path, false)
require.NoError(t, err)
fs, err := NewStoreFromBacking(fsInner, nil, false)
require.NoError(t, err)
defer fs.Close()
assert.Equal(t, model.TeamSettingsDefaultMaxUsersPerTeam, *fs.Get().TeamSettings.MaxUsersPerTeam)
assert.Empty(t, fs.GetEnvironmentOverrides())
os.Setenv("MM_TEAMSETTINGS_MAXUSERSPERTEAM", "3000")
defer os.Unsetenv("MM_TEAMSETTINGS_MAXUSERSPERTEAM")
fsInner, err = NewFileStore(path, false)
require.NoError(t, err)
fs, err = NewStoreFromBacking(fsInner, nil, false)
require.NoError(t, err)
defer fs.Close()
assert.Equal(t, 3000, *fs.Get().TeamSettings.MaxUsersPerTeam)
assert.Equal(t, map[string]any{"TeamSettings": map[string]any{"MaxUsersPerTeam": true}}, fs.GetEnvironmentOverrides())
})
t.Run("get override for an int64 variable", func(t *testing.T) {
path, tearDown := setupConfigFile(t, testConfig)
defer tearDown()
fsInner, err := NewFileStore(path, false)
require.NoError(t, err)
fs, err := NewStoreFromBacking(fsInner, nil, false)
require.NoError(t, err)
defer fs.Close()
assert.Equal(t, int64(63072000), *fs.Get().ServiceSettings.TLSStrictTransportMaxAge)
assert.Empty(t, fs.GetEnvironmentOverrides())
os.Setenv("MM_SERVICESETTINGS_TLSSTRICTTRANSPORTMAXAGE", "123456")
defer os.Unsetenv("MM_SERVICESETTINGS_TLSSTRICTTRANSPORTMAXAGE")
fsInner, err = NewFileStore(path, false)
require.NoError(t, err)
fs, err = NewStoreFromBacking(fsInner, nil, false)
require.NoError(t, err)
defer fs.Close()
assert.Equal(t, int64(123456), *fs.Get().ServiceSettings.TLSStrictTransportMaxAge)
assert.Equal(t, map[string]any{"ServiceSettings": map[string]any{"TLSStrictTransportMaxAge": true}}, fs.GetEnvironmentOverrides())
})
t.Run("get override for a slice variable - one value", func(t *testing.T) {
path, tearDown := setupConfigFile(t, testConfig)
defer tearDown()
fsInner, err := NewFileStore(path, false)
require.NoError(t, err)
fs, err := NewStoreFromBacking(fsInner, nil, false)
require.NoError(t, err)
defer fs.Close()
assert.Equal(t, []string{}, fs.Get().SqlSettings.DataSourceReplicas)
assert.Empty(t, fs.GetEnvironmentOverrides())
os.Setenv("MM_SQLSETTINGS_DATASOURCEREPLICAS", "user:pwd@db:5432/test-db")
defer os.Unsetenv("MM_SQLSETTINGS_DATASOURCEREPLICAS")
fsInner, err = NewFileStore(path, false)
require.NoError(t, err)
fs, err = NewStoreFromBacking(fsInner, nil, false)
require.NoError(t, err)
defer fs.Close()
assert.Equal(t, []string{"user:pwd@db:5432/test-db"}, fs.Get().SqlSettings.DataSourceReplicas)
assert.Equal(t, map[string]any{"SqlSettings": map[string]any{"DataSourceReplicas": true}}, fs.GetEnvironmentOverrides())
})
t.Run("get override for a slice variable - three values", func(t *testing.T) {
path, tearDown := setupConfigFile(t, testConfig)
defer tearDown()
fsInner, err := NewFileStore(path, false)
require.NoError(t, err)
fs, err := NewStoreFromBacking(fsInner, nil, false)
require.NoError(t, err)
defer fs.Close()
assert.Equal(t, []string{}, fs.Get().SqlSettings.DataSourceReplicas)
assert.Empty(t, fs.GetEnvironmentOverrides())
os.Setenv("MM_SQLSETTINGS_DATASOURCEREPLICAS", "user:pwd@db:5432/test-db user:pwd@db2:5433/test-db2 user:pwd@db3:5434/test-db3")
defer os.Unsetenv("MM_SQLSETTINGS_DATASOURCEREPLICAS")
fsInner, err = NewFileStore(path, false)
require.NoError(t, err)
fs, err = NewStoreFromBacking(fsInner, nil, false)
require.NoError(t, err)
defer fs.Close()
assert.Equal(t, []string{"user:pwd@db:5432/test-db", "user:pwd@db2:5433/test-db2", "user:pwd@db3:5434/test-db3"}, fs.Get().SqlSettings.DataSourceReplicas)
assert.Equal(t, map[string]any{"SqlSettings": map[string]any{"DataSourceReplicas": true}}, fs.GetEnvironmentOverrides())
})
}
func TestFileStoreSet(t *testing.T) {
t.Run("defaults required", func(t *testing.T) {
configStore, tearDown := setupConfigFileStore(t, minimalConfig)
defer tearDown()
oldCfg := configStore.Get().Clone()
newCfg := &model.Config{}
retCfg, newConfig, err := configStore.Set(newCfg)
require.NoError(t, err)
require.Equal(t, oldCfg, retCfg)
require.NotEqual(t, newCfg, newConfig)
assert.Equal(t, "", *configStore.Get().ServiceSettings.SiteURL)
})
t.Run("desanitization required", func(t *testing.T) {
configStore, tearDown := setupConfigFileStore(t, ldapConfig)
defer tearDown()
newCfg := &model.Config{}
newCfg.LdapSettings.BindPassword = model.NewString(model.FakeSetting)
_, newConfig, err := configStore.Set(newCfg)
require.NoError(t, err)
require.NotEqual(t, newCfg, newConfig)
assert.Equal(t, "password", *configStore.Get().LdapSettings.BindPassword)
})
t.Run("invalid", func(t *testing.T) {
configStore, tearDown := setupConfigFileStore(t, emptyConfig)
defer tearDown()
newCfg := &model.Config{}
newCfg.ServiceSettings.SiteURL = model.NewString("invalid")
_, _, err := configStore.Set(newCfg)
if assert.Error(t, err) {
assert.EqualError(t, err, "new configuration is invalid: Config.IsValid: model.config.is_valid.site_url.app_error, parse \"invalid\": invalid URI for request")
}
assert.Equal(t, "", *configStore.Get().ServiceSettings.SiteURL)
})
t.Run("read-only", func(t *testing.T) {
configStore, tearDown := setupConfigFileStore(t, readOnlyConfig)
defer tearDown()
newReadOnlyConfig := readOnlyConfig.Clone()
newReadOnlyConfig.ServiceSettings = model.ServiceSettings{
SiteURL: model.NewString("http://test"),
}
_, _, err := configStore.Set(newReadOnlyConfig)
if assert.Error(t, err) {
assert.Equal(t, ErrReadOnlyConfiguration, errors.Cause(err))
}
assert.Equal(t, "", *configStore.Get().ServiceSettings.SiteURL)
})
t.Run("persist failed", func(t *testing.T) {
path, tearDown := setupConfigFile(t, emptyConfig)
defer tearDown()
fsInner, err := NewFileStore(path, false)
require.NoError(t, err)
fs, err := NewStoreFromBacking(fsInner, nil, false)
require.NoError(t, err)
defer fs.Close()
fsInner.path = ""
newCfg := &model.Config{}
_, _, err = fs.Set(newCfg)
if assert.Error(t, err) {
assert.True(t, strings.HasPrefix(err.Error(), "failed to persist: failed to write file"))
}
assert.Equal(t, "", *fs.Get().ServiceSettings.SiteURL)
})
t.Run("listeners notified", func(t *testing.T) {
configStore, tearDown := setupConfigFileStore(t, emptyConfig)
defer tearDown()
called := make(chan bool, 1)
callback := func(oldCfg, newCfg *model.Config) {
require.NotEqual(t, oldCfg, newCfg)
called <- true
}
configStore.AddListener(callback)
newCfg := minimalConfig
_, _, err := configStore.Set(newCfg)
require.NoError(t, err)
require.True(t, wasCalled(called, 5*time.Second), "callback should have been called when config written")
})
t.Run("listeners notified, only env change", func(t *testing.T) {
configStore, tearDown := setupConfigFileStore(t, minimalConfig)
defer tearDown()
called := make(chan bool, 1)
callback := func(oldCfg, newCfg *model.Config) {
require.NotEqual(t, oldCfg, newCfg)
expectedConfig := minimalConfig.Clone()
expectedConfig.ServiceSettings.SiteURL = model.NewString("http://override")
require.Equal(t, minimalConfig, oldCfg)
require.Equal(t, expectedConfig, newCfg)
called <- true
}
configStore.AddListener(callback)
newCfg := minimalConfig
os.Setenv("MM_SERVICESETTINGS_SITEURL", "http://override")
defer os.Unsetenv("MM_SERVICESETTINGS_SITEURL")
_, _, err := configStore.Set(newCfg)
require.NoError(t, err)
require.True(t, wasCalled(called, 5*time.Second), "callback should have been called when config changed")
})
t.Run("listeners notified, feature flags change only", func(t *testing.T) {
configStore, tearDown := setupConfigFileStore(t, minimalConfig)
defer tearDown()
expectedOldConfig := minimalConfig.Clone()
var expectedNewConfig *model.Config
called := make(chan bool, 1)
callback := func(oldCfg, newCfg *model.Config) {
require.NotEqual(t, oldCfg, newCfg)
require.Equal(t, expectedOldConfig, oldCfg)
require.Equal(t, expectedNewConfig, newCfg)
called <- true
}
configStore.AddListener(callback)
configStore.SetReadOnlyFF(true)
expectedNewConfig = minimalConfig.Clone()
expectedNewConfig.FeatureFlags.TestFeature = "test"
_, _, err := configStore.Set(expectedNewConfig)
require.NoError(t, err)
require.False(t, wasCalled(called, 5*time.Second))
configStore.SetReadOnlyFF(false)
expectedNewConfig.FeatureFlags.TestFeature = "test2"
_, _, err = configStore.Set(expectedNewConfig)
require.NoError(t, err)
require.True(t, wasCalled(called, 5*time.Second))
})
}
func TestFileStoreLoad(t *testing.T) {
t.Run("file no longer exists", func(t *testing.T) {
path, tearDown := setupConfigFile(t, emptyConfig)
defer tearDown()
fsInner, err := NewFileStore(path, false)
require.NoError(t, err)
fs, err := NewStoreFromBacking(fsInner, nil, false)
require.NoError(t, err)
defer fs.Close()
os.Remove(path)
err = fs.Load()
require.NoError(t, err)
assertFileNotEqualsConfig(t, emptyConfig, path)
})
t.Run("honour environment", func(t *testing.T) {
configStore, tearDown := setupConfigFileStore(t, minimalConfig)
defer tearDown()
assert.Equal(t, "http://minimal", *configStore.Get().ServiceSettings.SiteURL)
os.Setenv("MM_SERVICESETTINGS_SITEURL", "http://override")
defer os.Unsetenv("MM_SERVICESETTINGS_SITEURL")
err := configStore.Load()
require.NoError(t, err)
assert.Equal(t, "http://override", *configStore.Get().ServiceSettings.SiteURL)
assert.Equal(t, map[string]any{"ServiceSettings": map[string]any{"SiteURL": true}}, configStore.GetEnvironmentOverrides())
})
t.Run("do not persist environment variables - string", func(t *testing.T) {
path, tearDown := setupConfigFile(t, minimalConfig)
defer tearDown()
os.Setenv("MM_SERVICESETTINGS_SITEURL", "http://overridePersistEnvVariables")
defer os.Unsetenv("MM_SERVICESETTINGS_SITEURL")
fsInner, err := NewFileStore(path, false)
require.NoError(t, err)
fs, err := NewStoreFromBacking(fsInner, nil, false)
require.NoError(t, err)
defer fs.Close()
assert.Equal(t, "http://overridePersistEnvVariables", *fs.Get().ServiceSettings.SiteURL)
_, _, err = fs.Set(fs.Get())
require.NoError(t, err)
assert.Equal(t, "http://overridePersistEnvVariables", *fs.Get().ServiceSettings.SiteURL)
assert.Equal(t, map[string]any{"ServiceSettings": map[string]any{"SiteURL": true}}, fs.GetEnvironmentOverrides())
// check that on disk config does not include overwritten variable
actualConfig := getActualFileConfig(t, path)
assert.Equal(t, "http://minimal", *actualConfig.ServiceSettings.SiteURL)
})
t.Run("do not persist environment variables - boolean", func(t *testing.T) {
path, tearDown := setupConfigFile(t, minimalConfig)
defer tearDown()
os.Setenv("MM_PLUGINSETTINGS_ENABLEUPLOADS", "true")
defer os.Unsetenv("MM_PLUGINSETTINGS_ENABLEUPLOADS")
fsInner, err := NewFileStore(path, false)
require.NoError(t, err)
fs, err := NewStoreFromBacking(fsInner, nil, false)
require.NoError(t, err)
defer fs.Close()
assert.Equal(t, true, *fs.Get().PluginSettings.EnableUploads)
_, _, err = fs.Set(fs.Get())
require.NoError(t, err)
assert.Equal(t, true, *fs.Get().PluginSettings.EnableUploads)
assert.Equal(t, map[string]any{"PluginSettings": map[string]any{"EnableUploads": true}}, fs.GetEnvironmentOverrides())
// check that on disk config does not include overwritten variable
actualConfig := getActualFileConfig(t, path)
assert.Equal(t, false, *actualConfig.PluginSettings.EnableUploads)
})
t.Run("do not persist environment variables - int", func(t *testing.T) {
path, tearDown := setupConfigFile(t, minimalConfig)
defer tearDown()
os.Setenv("MM_TEAMSETTINGS_MAXUSERSPERTEAM", "3000")
defer os.Unsetenv("MM_TEAMSETTINGS_MAXUSERSPERTEAM")
fsInner, err := NewFileStore(path, false)
require.NoError(t, err)
fs, err := NewStoreFromBacking(fsInner, nil, false)
require.NoError(t, err)
defer fs.Close()
assert.Equal(t, 3000, *fs.Get().TeamSettings.MaxUsersPerTeam)
_, _, err = fs.Set(fs.Get())
require.NoError(t, err)
assert.Equal(t, 3000, *fs.Get().TeamSettings.MaxUsersPerTeam)
assert.Equal(t, map[string]any{"TeamSettings": map[string]any{"MaxUsersPerTeam": true}}, fs.GetEnvironmentOverrides())
// check that on disk config does not include overwritten variable
actualConfig := getActualFileConfig(t, path)
assert.Equal(t, model.TeamSettingsDefaultMaxUsersPerTeam, *actualConfig.TeamSettings.MaxUsersPerTeam)
})
t.Run("do not persist environment variables - int64", func(t *testing.T) {
path, tearDown := setupConfigFile(t, minimalConfig)
defer tearDown()
os.Setenv("MM_SERVICESETTINGS_TLSSTRICTTRANSPORTMAXAGE", "123456")
defer os.Unsetenv("MM_SERVICESETTINGS_TLSSTRICTTRANSPORTMAXAGE")
fsInner, err := NewFileStore(path, false)
require.NoError(t, err)
fs, err := NewStoreFromBacking(fsInner, nil, false)
require.NoError(t, err)
defer fs.Close()
assert.Equal(t, int64(123456), *fs.Get().ServiceSettings.TLSStrictTransportMaxAge)
_, _, err = fs.Set(fs.Get())
require.NoError(t, err)
assert.Equal(t, int64(123456), *fs.Get().ServiceSettings.TLSStrictTransportMaxAge)
assert.Equal(t, map[string]any{"ServiceSettings": map[string]any{"TLSStrictTransportMaxAge": true}}, fs.GetEnvironmentOverrides())
// check that on disk config does not include overwritten variable
actualConfig := getActualFileConfig(t, path)
assert.Equal(t, int64(63072000), *actualConfig.ServiceSettings.TLSStrictTransportMaxAge)
})
t.Run("do not persist environment variables - string slice beginning with default", func(t *testing.T) {
path, tearDown := setupConfigFile(t, minimalConfig)
defer tearDown()
os.Setenv("MM_SQLSETTINGS_DATASOURCEREPLICAS", "user:pwd@db:5432/test-db")
defer os.Unsetenv("MM_SQLSETTINGS_DATASOURCEREPLICAS")
fsInner, err := NewFileStore(path, false)
require.NoError(t, err)
fs, err := NewStoreFromBacking(fsInner, nil, false)
require.NoError(t, err)
defer fs.Close()
assert.Equal(t, []string{"user:pwd@db:5432/test-db"}, fs.Get().SqlSettings.DataSourceReplicas)
_, _, err = fs.Set(fs.Get())
require.NoError(t, err)
assert.Equal(t, []string{"user:pwd@db:5432/test-db"}, fs.Get().SqlSettings.DataSourceReplicas)
assert.Equal(t, map[string]any{"SqlSettings": map[string]any{"DataSourceReplicas": true}}, fs.GetEnvironmentOverrides())
// check that on disk config does not include overwritten variable
actualConfig := getActualFileConfig(t, path)
assert.Equal(t, []string{}, actualConfig.SqlSettings.DataSourceReplicas)
})
t.Run("do not persist environment variables - string slice beginning with slice of three", func(t *testing.T) {
modifiedMinimalConfig := minimalConfig.Clone()
modifiedMinimalConfig.SqlSettings.DataSourceReplicas = []string{"user:pwd@db:5432/test-db", "user:pwd@db2:5433/test-db2", "user:pwd@db3:5434/test-db3"}
path, tearDown := setupConfigFile(t, modifiedMinimalConfig)
defer tearDown()
os.Setenv("MM_SQLSETTINGS_DATASOURCEREPLICAS", "user:pwd@db:5432/test-db")
defer os.Unsetenv("MM_SQLSETTINGS_DATASOURCEREPLICAS")
fsInner, err := NewFileStore(path, false)
require.NoError(t, err)
fs, err := NewStoreFromBacking(fsInner, nil, false)
require.NoError(t, err)
defer fs.Close()
assert.Equal(t, []string{"user:pwd@db:5432/test-db"}, fs.Get().SqlSettings.DataSourceReplicas)
_, _, err = fs.Set(fs.Get())
require.NoError(t, err)
assert.Equal(t, []string{"user:pwd@db:5432/test-db"}, fs.Get().SqlSettings.DataSourceReplicas)
assert.Equal(t, map[string]any{"SqlSettings": map[string]any{"DataSourceReplicas": true}}, fs.GetEnvironmentOverrides())
// check that on disk config does not include overwritten variable
actualConfig := getActualFileConfig(t, path)
assert.Equal(t, []string{"user:pwd@db:5432/test-db", "user:pwd@db2:5433/test-db2", "user:pwd@db3:5434/test-db3"}, actualConfig.SqlSettings.DataSourceReplicas)
})
t.Run("invalid", func(t *testing.T) {
path, tearDown := setupConfigFile(t, emptyConfig)
defer tearDown()
fsInner, err := NewFileStore(path, false)
require.NoError(t, err)
fs, err := NewStoreFromBacking(fsInner, nil, false)
require.NoError(t, err)
defer fs.Close()
cfgData, err := marshalConfig(invalidConfig)
require.NoError(t, err)
os.WriteFile(path, cfgData, 0644)
err = fs.Load()
if assert.Error(t, err) {
var appErr *model.AppError
require.True(t, errors.As(err, &appErr))
assert.Equal(t, appErr.Id, "model.config.is_valid.site_url.app_error")
}
})
t.Run("invalid environment value", func(t *testing.T) {
configStore, tearDown := setupConfigFileStore(t, emptyConfig)
defer tearDown()
os.Setenv("MM_SERVICESETTINGS_SITEURL", "invalid_url")
defer os.Unsetenv("MM_SERVICESETTINGS_SITEURL")
newCfg := minimalConfig
_, _, err := configStore.Set(newCfg)
require.Error(t, err)
require.EqualError(t, err, "new configuration is invalid: Config.IsValid: model.config.is_valid.site_url.app_error, parse \"invalid_url\": invalid URI for request")
})
t.Run("fixes required", func(t *testing.T) {
path, tearDown := setupConfigFile(t, fixesRequiredConfig)
defer tearDown()
fsInner, err := NewFileStore(path, false)
require.NoError(t, err)
fs, err := NewStoreFromBacking(fsInner, nil, false)
require.NoError(t, err)
defer fs.Close()
err = fs.Load()
require.NoError(t, err)
assertFileNotEqualsConfig(t, fixesRequiredConfig, path)
assert.Equal(t, "http://trailingslash", *fs.Get().ServiceSettings.SiteURL)
assert.Equal(t, "/path/to/directory/", *fs.Get().FileSettings.Directory)
assert.Equal(t, "en", *fs.Get().LocalizationSettings.DefaultServerLocale)
assert.Equal(t, "en", *fs.Get().LocalizationSettings.DefaultClientLocale)
})
t.Run("listeners notified", func(t *testing.T) {
path, tearDown := setupConfigFile(t, emptyConfig)
defer tearDown()
fsInner, err := NewFileStore(path, false)
require.NoError(t, err)
fs, err := NewStoreFromBacking(fsInner, nil, false)
require.NoError(t, err)
defer fs.Close()
called := make(chan bool, 1)
callback := func(oldCfg, newCfg *model.Config) {
called <- true
}
fs.AddListener(callback)
cfgData, err := marshalConfig(minimalConfig)
require.NoError(t, err)
err = os.WriteFile(path, cfgData, 0644)
require.NoError(t, err)
err = fs.Load()
require.NoError(t, err)
require.True(t, wasCalled(called, 5*time.Second), "callback should have been called when config changed on load")
})
t.Run("no change", func(t *testing.T) {
configStore, tearDown := setupConfigFileStore(t, testConfig)
defer tearDown()
called := make(chan bool, 1)
callback := func(oldCfg, newCfg *model.Config) {
called <- true
}
configStore.AddListener(callback)
err := configStore.Load()
require.NoError(t, err)
require.False(t, wasCalled(called, 5*time.Second), "callback should not have been called if nothing changed")
})
t.Run("listeners notified, only env change", func(t *testing.T) {
configStore, tearDown := setupConfigFileStore(t, minimalConfig)
defer tearDown()
time.Sleep(1 * time.Second)
called := make(chan bool, 1)
callback := func(oldCfg, newCfg *model.Config) {
require.NotEqual(t, oldCfg, newCfg)
expectedConfig := minimalConfig.Clone()
expectedConfig.ServiceSettings.SiteURL = model.NewString("http://override")
require.Equal(t, minimalConfig, oldCfg)
require.Equal(t, expectedConfig, newCfg)
called <- true
}
configStore.AddListener(callback)
os.Setenv("MM_SERVICESETTINGS_SITEURL", "http://override")
defer os.Unsetenv("MM_SERVICESETTINGS_SITEURL")
err := configStore.Load()
require.NoError(t, err)
require.True(t, wasCalled(called, 5*time.Second), "callback should have been called when config changed")
})
}
func TestFileStoreSave(t *testing.T) {
store, tearDown := setupConfigFileStore(t, minimalConfig)
defer tearDown()
newCfg := &model.Config{
ServiceSettings: model.ServiceSettings{
SiteURL: model.NewString("http://new"),
},
}
t.Run("set with automatic save", func(t *testing.T) {
_, _, err := store.Set(newCfg)
require.NoError(t, err)
err = store.Load()
require.NoError(t, err)
assert.Equal(t, "http://new", *store.Get().ServiceSettings.SiteURL)
})
}
func TestFileGetFile(t *testing.T) {
path, tearDown := setupConfigFile(t, minimalConfig)
defer tearDown()
fs, err := NewFileStore(path, false)
require.NoError(t, err)
defer fs.Close()
t.Run("get empty filename", func(t *testing.T) {
_, err := fs.GetFile("")
require.Error(t, err)
})
t.Run("get non-existent file", func(t *testing.T) {
_, err := fs.GetFile("unknown")
require.Error(t, err)
})
t.Run("get empty file", func(t *testing.T) {
err := os.MkdirAll("config", 0700)
require.NoError(t, err)
f, err := os.CreateTemp("config", "empty-file")
require.NoError(t, err)
defer os.Remove(f.Name())
err = os.WriteFile(f.Name(), nil, 0777)
require.NoError(t, err)
data, err := fs.GetFile(f.Name())
require.NoError(t, err)
require.Empty(t, data)
})
t.Run("get non-empty file", func(t *testing.T) {
err := os.MkdirAll("config", 0700)
require.NoError(t, err)
f, err := os.CreateTemp("config", "test-file")
require.NoError(t, err)
defer os.Remove(f.Name())
err = os.WriteFile(f.Name(), []byte("test"), 0777)
require.NoError(t, err)
data, err := fs.GetFile(f.Name())
require.NoError(t, err)
require.Equal(t, []byte("test"), data)
})
t.Run("get via absolute path", func(t *testing.T) {
err := fs.SetFile("new", []byte("new file"))
require.NoError(t, err)
data, err := fs.GetFile(filepath.Join(filepath.Dir(path), "new"))
require.NoError(t, err)
require.Equal(t, []byte("new file"), data)
})
}
func TestFileSetFile(t *testing.T) {
path, tearDown := setupConfigFile(t, minimalConfig)
defer tearDown()
fs, err := NewFileStore(path, false)
require.NoError(t, err)
defer fs.Close()
t.Run("set new file", func(t *testing.T) {
err := fs.SetFile("new", []byte("new file"))
require.NoError(t, err)
data, err := fs.GetFile("new")
require.NoError(t, err)
require.Equal(t, []byte("new file"), data)
})
t.Run("overwrite existing file", func(t *testing.T) {
err := fs.SetFile("existing", []byte("existing file"))
require.NoError(t, err)
err = fs.SetFile("existing", []byte("overwritten file"))
require.NoError(t, err)
data, err := fs.GetFile("existing")
require.NoError(t, err)
require.Equal(t, []byte("overwritten file"), data)
})
t.Run("set via absolute path", func(t *testing.T) {
absolutePath := filepath.Join(filepath.Dir(path), "new")
err := fs.SetFile(absolutePath, []byte("new file"))
require.NoError(t, err)
data, err := fs.GetFile("new")
require.NoError(t, err)
require.Equal(t, []byte("new file"), data)
})
t.Run("should set right permissions", func(t *testing.T) {
absolutePath := filepath.Join(filepath.Dir(path), "new")
err := fs.SetFile(absolutePath, []byte("data"))
require.NoError(t, err)
fi, err := os.Stat(absolutePath)
require.NoError(t, err)
require.Equal(t, os.FileMode(0600), fi.Mode().Perm())
})
}
func TestFileHasFile(t *testing.T) {
t.Run("has non-existent", func(t *testing.T) {
path, tearDown := setupConfigFile(t, minimalConfig)
defer tearDown()
fs, err := NewFileStore(path, false)
require.NoError(t, err)
defer fs.Close()
has, err := fs.HasFile("non-existent")
require.NoError(t, err)
require.False(t, has)
})
t.Run("has existing", func(t *testing.T) {
path, tearDown := setupConfigFile(t, minimalConfig)
defer tearDown()
fs, err := NewFileStore(path, false)
require.NoError(t, err)
defer fs.Close()
err = fs.SetFile("existing", []byte("existing file"))
require.NoError(t, err)
has, err := fs.HasFile("existing")
require.NoError(t, err)
require.True(t, has)
})
t.Run("has manually created file", func(t *testing.T) {
path, tearDown := setupConfigFile(t, minimalConfig)
defer tearDown()
fs, err := NewFileStore(path, false)
require.NoError(t, err)
defer fs.Close()
err = os.MkdirAll("config", 0700)
require.NoError(t, err)
f, err := os.CreateTemp("config", "test-file")
require.NoError(t, err)
defer os.Remove(f.Name())
err = os.WriteFile(f.Name(), []byte("test"), 0777)
require.NoError(t, err)
has, err := fs.HasFile(f.Name())
require.NoError(t, err)
require.True(t, has)
})
t.Run("has empty string", func(t *testing.T) {
path, tearDown := setupConfigFile(t, minimalConfig)
defer tearDown()
fs, err := NewFileStore(path, false)
require.NoError(t, err)
defer fs.Close()
has, err := fs.HasFile("")
require.NoError(t, err)
require.False(t, has)
})
t.Run("has via absolute path", func(t *testing.T) {
path, tearDown := setupConfigFile(t, minimalConfig)
defer tearDown()
fs, err := NewFileStore(path, false)
require.NoError(t, err)
defer fs.Close()
err = fs.SetFile("existing", []byte("existing file"))
require.NoError(t, err)
has, err := fs.HasFile(filepath.Join(filepath.Dir(path), "existing"))
require.NoError(t, err)
require.True(t, has)
})
}
func TestFileRemoveFile(t *testing.T) {
t.Run("remove non-existent", func(t *testing.T) {
path, tearDown := setupConfigFile(t, minimalConfig)
defer tearDown()
fs, err := NewFileStore(path, false)
require.NoError(t, err)
defer fs.Close()
err = fs.RemoveFile("non-existent")
require.NoError(t, err)
})
t.Run("remove existing", func(t *testing.T) {
path, tearDown := setupConfigFile(t, minimalConfig)
defer tearDown()
fs, err := NewFileStore(path, false)
require.NoError(t, err)
defer fs.Close()
err = fs.SetFile("existing", []byte("existing file"))
require.NoError(t, err)
err = fs.RemoveFile("existing")
require.NoError(t, err)
has, err := fs.HasFile("existing")
require.NoError(t, err)
require.False(t, has)
_, err = fs.GetFile("existing")
require.Error(t, err)
})
t.Run("remove manually created file", func(t *testing.T) {
path, tearDown := setupConfigFile(t, minimalConfig)
defer tearDown()
fs, err := NewFileStore(path, false)
require.NoError(t, err)
defer fs.Close()
err = os.MkdirAll("config", 0700)
require.NoError(t, err)
f, err := os.CreateTemp("config", "test-file")
require.NoError(t, err)
defer os.Remove(f.Name())
err = os.WriteFile(f.Name(), []byte("test"), 0777)
require.NoError(t, err)
err = fs.RemoveFile(f.Name())
require.NoError(t, err)
has, err := fs.HasFile("existing")
require.NoError(t, err)
require.False(t, has)
_, err = fs.GetFile("existing")
require.Error(t, err)
})
t.Run("don't remove via absolute path", func(t *testing.T) {
path, tearDown := setupConfigFile(t, minimalConfig)
defer tearDown()
fs, err := NewFileStore(path, false)
require.NoError(t, err)
defer fs.Close()
err = fs.SetFile("existing", []byte("existing file"))
require.NoError(t, err)
filename := filepath.Join(filepath.Dir(path), "existing")
err = fs.RemoveFile(filename)
require.NoError(t, err)
has, err := fs.HasFile(filename)
require.NoError(t, err)
require.True(t, has)
})
}
func TestFileStoreString(t *testing.T) {
path, tearDown := setupConfigFile(t, emptyConfig)
defer tearDown()
fs, err := NewFileStore(path, false)
require.NoError(t, err)
defer fs.Close()
assert.Equal(t, "file://"+path, fs.String())
}
// wasCalled reports whether a given callback channel was called
// within the specified time duration or not.
func wasCalled(c chan bool, duration time.Duration) bool {
select {
case <-c:
return true
case <-time.After(duration):
}
return false
}
func TestFileStoreReadOnly(t *testing.T) {
path, tearDown := setupConfigFile(t, emptyConfig)
defer tearDown()
fsInner, err := NewFileStore(path, false)
require.NoError(t, err)
fs, err := NewStoreFromBacking(fsInner, nil, true)
require.NoError(t, err)
defer fs.Close()
called := make(chan bool, 1)
callback := func(oldCfg, newCfg *model.Config) {
called <- true
}
fs.AddListener(callback)
cfg, _, err := fs.Set(minimalConfig)
require.Nil(t, cfg)
require.Equal(t, ErrReadOnlyStore, err)
require.False(t, wasCalled(called, 1*time.Second), "callback should not have been called since config is read-only")
}
func TestFileStoreSetReadOnlyFF(t *testing.T) {
t.Run("read only true", func(t *testing.T) {
store, tearDown := setupConfigFileStore(t, minimalConfig)
defer tearDown()
config := store.Get()
require.Equal(t, minimalConfig.FeatureFlags, config.FeatureFlags)
newCfg := config.Clone()
newCfg.FeatureFlags.TestFeature = "test"
// store has read-only FF by default.
_, _, err := store.Set(newCfg)
require.NoError(t, err)
config = store.Get()
require.Equal(t, minimalConfig.FeatureFlags, config.FeatureFlags)
})
t.Run("read only false", func(t *testing.T) {
store, tearDown := setupConfigFileStore(t, minimalConfig)
defer tearDown()
config := store.Get()
require.Equal(t, minimalConfig.FeatureFlags, config.FeatureFlags)
newCfg := config.Clone()
newCfg.FeatureFlags.TestFeature = "test"
store.SetReadOnlyFF(false)
_, _, err := store.Set(newCfg)
require.NoError(t, err)
config = store.Get()
require.Equal(t, newCfg.FeatureFlags, config.FeatureFlags)
})
}
func TestResolveConfigPath(t *testing.T) {
t.Run("should be able to resolve an absolute path", func(t *testing.T) {
cf, err := os.CreateTemp("", "config-test.json")
require.NoError(t, err)
info, err := cf.Stat()
require.NoError(t, err)
file := filepath.Join(os.TempDir(), info.Name())
defer os.Remove(file)
resolution, err := resolveConfigFilePath(file)
require.NoError(t, err)
require.Equal(t, file, resolution)
})
t.Run("should be able to resolve relative path", func(t *testing.T) {
tempDir, err := os.MkdirTemp("", "resolveconfig")
require.NoError(t, err)
defer os.RemoveAll(tempDir)
err = os.Chdir(tempDir)
require.NoError(t, err)
file := "config-test-1.json"
_, err = os.Stat(file)
if os.IsNotExist(err) {
defer os.Remove(file)
f, err2 := os.Create(file)
require.NoError(t, err2)
defer f.Close()
}
resolution, err := resolveConfigFilePath(file)
require.NoError(t, err)
require.Contains(t, resolution, filepath.Join(tempDir, file))
})
}