mirror of
https://github.com/mattermost/mattermost.git
synced 2026-04-15 05:57:37 -04:00
* ci: enable fullyparallel mode for server tests Replace os.Setenv, os.Chdir, and global state mutations with parallel-safe alternatives (t.Setenv, t.Chdir, test hooks) across 37 files. Refactor GetLogRootPath and MM_INSTALL_TYPE to use package-level test hooks instead of environment variables. This enables gotestsum --fullparallel, allowing all test packages to run with maximum parallelism within each shard. Co-authored-by: Claude <claude@anthropic.com> * ci: split fullyparallel from continue-on-error in workflow template - Add new boolean input 'allow-failure' separate from 'fullyparallel' - Change continue-on-error to use allow-failure instead of fullyparallel - Update server-ci.yml to pass allow-failure: true for test coverage job - Allows independent control of parallel execution and failure tolerance Co-authored-by: Claude <claude@anthropic.com> * fix: protect TestOverrideLogRootPath with sync.Mutex for parallel tests - Replace global var TestOverrideLogRootPath with mutex-protected functions - Add SetTestOverrideLogRootPath() and getTestOverrideLogRootPath() functions - Update GetLogRootPath() to use thread-safe getter - Update all test files to use SetTestOverrideLogRootPath() with t.Cleanup() - Fixes race condition when running tests with t.Parallel() Co-authored-by: Claude <claude@anthropic.com> * fix: configure audit settings before server setup in tests - Move ExperimentalAuditSettings from UpdateConfig() to config defaults - Pass audit config via app.Config() option in SetupWithServerOptions() - Fixes audit test setup ordering to configure BEFORE server initialization - Resolves CodeRabbit's audit config timing issue in api4 tests Co-authored-by: Claude <claude@anthropic.com> * fix: implement SetTestOverrideLogRootPath mutex in logger.go The previous commit updated test callers to use SetTestOverrideLogRootPath() but didn't actually create the function in config/logger.go, causing build failures across all CI shards. This commit: - Replaces the exported var TestOverrideLogRootPath with mutex-protected unexported state (testOverrideLogRootPath + testOverrideLogRootMu) - Adds exported SetTestOverrideLogRootPath() setter - Adds unexported getTestOverrideLogRootPath() getter - Updates GetLogRootPath() to use the thread-safe getter - Fixes log_test.go callers that were missed in the previous commit Co-authored-by: Claude <claude@anthropic.com> * fix(test): use SetupConfig for access_control feature flag registration InitAccessControlPolicy() checks FeatureFlags.AttributeBasedAccessControl at route registration time during server startup. Setting the flag via UpdateConfig after Setup() is too late — routes are never registered and API calls return 404. Use SetupConfig() to pass the feature flag in the initial config before server startup, ensuring routes are properly registered. Co-authored-by: Claude <claude@anthropic.com> * fix(test): restore BurnOnRead flag state in TestRevealPost subtest The 'feature not enabled' subtest disables BurnOnRead without restoring it via t.Cleanup. Subsequent subtests inherit the disabled state, which can cause 501 errors when they expect the feature to be available. Add t.Cleanup to restore FeatureFlags.BurnOnRead = true after the subtest completes. Co-authored-by: Claude <claude@anthropic.com> * fix(test): restore EnableSharedChannelsMemberSync flag via t.Cleanup The test disables EnableSharedChannelsMemberSync without restoring it. If the subtest exits early (e.g., require failure), later sibling subtests inherit a disabled flag and become flaky. Add t.Cleanup to restore the flag after the subtest completes. Co-authored-by: Claude <claude@anthropic.com> * Fix test parallelism: use instance-scoped overrides and init-time audit config Replace package-level test globals (TestOverrideInstallType, SetTestOverrideLogRootPath) with fields on PlatformService so each test gets its own instance without process-wide mutation. Fix three audit tests (TestUserLoginAudit, TestLogoutAuditAuthStatus, TestUpdatePasswordAudit) that configured the audit logger after server init — the audit logger only reads config at startup, so pass audit settings via app.Config() at init time instead. Also revert the Go 1.24.13 downgrade and bump mattermost-govet to v2.0.2 for Go 1.25.8 compatibility. * Fix audit unit tests * Fix MMCLOUDURL unit tests * Fixed unit tests using MM_NOTIFY_ADMIN_COOL_OFF_DAYS * Make app migrations idempotent for parallel test safety Change System().Save() to System().SaveOrUpdate() in all migration completion markers. When two parallel tests share a database pool entry, both may race through the check-then-insert migration pattern. Save() causes a duplicate key fatal crash; SaveOrUpdate() makes the second write a harmless no-op. * test: address review feedback on fullyparallel PR - Use SetLogRootPathOverride() setter instead of direct field access in platform/support_packet_test.go and platform/log_test.go (pvev) - Restore TestGetLogRootPath in config/logger_test.go to keep MM_LOG_PATH env var coverage; test uses t.Setenv so it runs serially which is fine (pvev) - Fix misleading comment in config_test.go: code uses t.Setenv, not os.Setenv (jgheithcock) Co-authored-by: Claude <claude@anthropic.com> * fix: add missing os import in post_test.go The os import was dropped during a merge conflict resolution while burn-on-read shared channel tests from master still use os.Setenv. Co-authored-by: Claude <claude@anthropic.com> --------- Co-authored-by: Claude <claude@anthropic.com> Co-authored-by: wiggin77 <wiggin77@warpmail.net> Co-authored-by: Mattermost Build <build@mattermost.com>
1346 lines
40 KiB
Go
1346 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 := t.TempDir()
|
|
t.Chdir(tempDir)
|
|
|
|
var name string
|
|
if cfg != nil {
|
|
f, err := os.CreateTemp(tempDir, "setupConfigFile")
|
|
require.NoError(t, err)
|
|
|
|
cfgData, err := marshalConfig(cfg)
|
|
require.NoError(t, err)
|
|
|
|
err = os.WriteFile(f.Name(), cfgData, 0644)
|
|
require.NoError(t, err)
|
|
|
|
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) {
|
|
err := utils.TranslationsPreInit()
|
|
require.NoError(t, err)
|
|
|
|
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)
|
|
|
|
err = os.WriteFile(path, cfgData, 0644)
|
|
require.NoError(t, err)
|
|
|
|
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.NewPointer(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.NewPointer("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.NewPointer("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.NewPointer("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)
|
|
|
|
err = os.WriteFile(path, cfgData, 0644)
|
|
require.NoError(t, err)
|
|
|
|
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.NewPointer("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.NewPointer("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 := t.TempDir()
|
|
t.Chdir(tempDir)
|
|
|
|
var err error
|
|
|
|
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))
|
|
})
|
|
}
|