github.com/masterhung0112/hk_server/v5@v5.0.0-20220302090640-ec71aef15e1c/config/file_test.go (about)

     1  // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
     2  // See LICENSE.txt for license information.
     3  
     4  package config
     5  
     6  import (
     7  	"encoding/json"
     8  	"io/ioutil"
     9  	"os"
    10  	"path/filepath"
    11  	"strings"
    12  	"testing"
    13  	"time"
    14  
    15  	"github.com/pkg/errors"
    16  	"github.com/stretchr/testify/assert"
    17  	"github.com/stretchr/testify/require"
    18  
    19  	"github.com/masterhung0112/hk_server/v5/model"
    20  	"github.com/masterhung0112/hk_server/v5/utils"
    21  )
    22  
    23  func setupConfigFile(t *testing.T, cfg *model.Config) (string, func()) {
    24  	os.Clearenv()
    25  	t.Helper()
    26  
    27  	tempDir, err := ioutil.TempDir("", "setupConfigFile")
    28  	require.NoError(t, err)
    29  
    30  	err = os.Chdir(tempDir)
    31  	require.NoError(t, err)
    32  
    33  	var name string
    34  	if cfg != nil {
    35  		f, err := ioutil.TempFile(tempDir, "setupConfigFile")
    36  		require.NoError(t, err)
    37  
    38  		cfgData, err := marshalConfig(cfg)
    39  		require.NoError(t, err)
    40  
    41  		ioutil.WriteFile(f.Name(), cfgData, 0644)
    42  
    43  		name = f.Name()
    44  	}
    45  
    46  	return name, func() {
    47  		os.RemoveAll(tempDir)
    48  	}
    49  }
    50  
    51  func setupConfigFileStore(t *testing.T, cfg *model.Config) (*Store, func()) {
    52  	t.Helper()
    53  	path, tearDown := setupConfigFile(t, cfg)
    54  	fs, err := NewFileStore(path, false)
    55  	require.NoError(t, err)
    56  	configStore, err := NewStoreFromBacking(fs, nil, false)
    57  	require.NoError(t, err)
    58  	return configStore, func() {
    59  		tearDown()
    60  		configStore.Close()
    61  	}
    62  }
    63  
    64  // getActualFileConfig returns the configuration present in the given file without relying on a config store.
    65  func getActualFileConfig(t *testing.T, path string) *model.Config {
    66  	t.Helper()
    67  
    68  	f, err := os.Open(path)
    69  	require.NoError(t, err)
    70  	defer f.Close()
    71  
    72  	var actualCfg *model.Config
    73  	err = json.NewDecoder(f).Decode(&actualCfg)
    74  	require.NoError(t, err)
    75  
    76  	return actualCfg
    77  }
    78  
    79  // assertFileEqualsConfig verifies the on disk contents of the given path equal the given
    80  func assertFileEqualsConfig(t *testing.T, expectedCfg *model.Config, path string) {
    81  	t.Helper()
    82  
    83  	actualCfg := getActualFileConfig(t, path)
    84  
    85  	assert.Equal(t, expectedCfg, actualCfg)
    86  }
    87  
    88  // assertFileNotEqualsConfig verifies the on disk contents of the given path does not equal the given
    89  func assertFileNotEqualsConfig(t *testing.T, expectedCfg *model.Config, path string) {
    90  	t.Helper()
    91  
    92  	actualCfg := getActualFileConfig(t, path)
    93  
    94  	assert.NotEqual(t, expectedCfg, actualCfg)
    95  }
    96  
    97  func TestFileStoreNew(t *testing.T) {
    98  	utils.TranslationsPreInit()
    99  
   100  	t.Run("absolute path, initialization required", func(t *testing.T) {
   101  		path, tearDown := setupConfigFile(t, testConfig)
   102  		defer tearDown()
   103  
   104  		fs, err := NewFileStore(path, false)
   105  		require.NoError(t, err)
   106  		configStore, err := NewStoreFromBacking(fs, nil, false)
   107  		require.NoError(t, err)
   108  		defer configStore.Close()
   109  
   110  		assert.Equal(t, "http://TestStoreNew", *configStore.Get().ServiceSettings.SiteURL)
   111  		assertFileNotEqualsConfig(t, testConfig, path)
   112  	})
   113  
   114  	t.Run("absolute path, initialization required, with custom defaults", func(t *testing.T) {
   115  		path, tearDown := setupConfigFile(t, testConfig)
   116  		defer tearDown()
   117  
   118  		fs, err := NewFileStore(path, false)
   119  		require.NoError(t, err)
   120  		configStore, err := NewStoreFromBacking(fs, customConfigDefaults, false)
   121  		require.NoError(t, err)
   122  		defer configStore.Close()
   123  
   124  		// already existing value should not be affected by the custom
   125  		// defaults
   126  		assert.Equal(t, "http://TestStoreNew", *configStore.Get().ServiceSettings.SiteURL)
   127  		// nonexisting value should be overwritten by the custom
   128  		// defaults
   129  		assert.Equal(t, *customConfigDefaults.DisplaySettings.ExperimentalTimezone, *configStore.Get().DisplaySettings.ExperimentalTimezone)
   130  		assertFileNotEqualsConfig(t, testConfig, path)
   131  	})
   132  
   133  	t.Run("absolute path, already minimally configured", func(t *testing.T) {
   134  		path, tearDown := setupConfigFile(t, minimalConfigNoFF)
   135  		defer tearDown()
   136  
   137  		fs, err := NewFileStore(path, false)
   138  		require.NoError(t, err)
   139  		configStore, err := NewStoreFromBacking(fs, nil, false)
   140  		require.NoError(t, err)
   141  		defer configStore.Close()
   142  
   143  		assert.Equal(t, "http://minimal", *configStore.Get().ServiceSettings.SiteURL)
   144  		assertFileEqualsConfig(t, minimalConfigNoFF, path)
   145  	})
   146  
   147  	t.Run("absolute path, already minimally configured, with custom defaults", func(t *testing.T) {
   148  		path, tearDown := setupConfigFile(t, minimalConfigNoFF)
   149  		defer tearDown()
   150  
   151  		fs, err := NewFileStore(path, false)
   152  		require.NoError(t, err)
   153  		configStore, err := NewStoreFromBacking(fs, customConfigDefaults, false)
   154  		require.NoError(t, err)
   155  		defer configStore.Close()
   156  
   157  		// as the whole config has default values already, custom
   158  		// defaults should have no effect
   159  		assert.Equal(t, "http://minimal", *configStore.Get().ServiceSettings.SiteURL)
   160  		assert.NotEqual(t, *customConfigDefaults.DisplaySettings.ExperimentalTimezone, *configStore.Get().DisplaySettings.ExperimentalTimezone)
   161  		assertFileEqualsConfig(t, minimalConfigNoFF, path)
   162  	})
   163  
   164  	t.Run("absolute path, file does not exist", func(t *testing.T) {
   165  		_, tearDown := setupConfigFile(t, nil)
   166  		defer tearDown()
   167  
   168  		tempDir, err := ioutil.TempDir("", "TestFileStoreNew")
   169  		require.NoError(t, err)
   170  		defer os.RemoveAll(tempDir)
   171  
   172  		path := filepath.Join(tempDir, "does_not_exist")
   173  		fs, err := NewFileStore(path, false)
   174  		require.NoError(t, err)
   175  		configStore, err := NewStoreFromBacking(fs, nil, false)
   176  		require.NoError(t, err)
   177  		defer configStore.Close()
   178  
   179  		assert.Equal(t, "", *configStore.Get().ServiceSettings.SiteURL)
   180  		assertFileNotEqualsConfig(t, testConfig, path)
   181  	})
   182  
   183  	t.Run("absolute path, file does not exist, with custom defaults", func(t *testing.T) {
   184  		_, tearDown := setupConfigFile(t, nil)
   185  		defer tearDown()
   186  
   187  		tempDir, err := ioutil.TempDir("", "TestFileStoreNew")
   188  		require.NoError(t, err)
   189  		defer os.RemoveAll(tempDir)
   190  
   191  		path := filepath.Join(tempDir, "does_not_exist")
   192  		fs, err := NewFileStore(path, false)
   193  		require.NoError(t, err)
   194  		configStore, err := NewStoreFromBacking(fs, customConfigDefaults, false)
   195  		require.NoError(t, err)
   196  		defer configStore.Close()
   197  
   198  		assert.Equal(t, *customConfigDefaults.ServiceSettings.SiteURL, *configStore.Get().ServiceSettings.SiteURL)
   199  		assert.Equal(t, *customConfigDefaults.DisplaySettings.ExperimentalTimezone, *configStore.Get().DisplaySettings.ExperimentalTimezone)
   200  	})
   201  
   202  	t.Run("absolute path, path to file does not exist", func(t *testing.T) {
   203  		_, tearDown := setupConfigFile(t, nil)
   204  		defer tearDown()
   205  
   206  		tempDir, err := ioutil.TempDir("", "TestFileStoreNew")
   207  		require.NoError(t, err)
   208  		defer os.RemoveAll(tempDir)
   209  
   210  		path := filepath.Join(tempDir, "does/not/exist")
   211  		fs, err := NewFileStore(path, false)
   212  		require.NoError(t, err)
   213  		configStore, err := NewStoreFromBacking(fs, nil, false)
   214  		require.Nil(t, configStore)
   215  		require.Error(t, err)
   216  	})
   217  
   218  	t.Run("relative path, file exists", func(t *testing.T) {
   219  		_, tearDown := setupConfigFile(t, nil)
   220  		defer tearDown()
   221  
   222  		err := os.MkdirAll("TestFileStoreNew/a/b/c", 0700)
   223  		require.NoError(t, err)
   224  		defer os.RemoveAll("TestFileStoreNew")
   225  
   226  		path := "TestFileStoreNew/a/b/c/config.json"
   227  
   228  		cfgData, err := marshalConfig(testConfig)
   229  		require.NoError(t, err)
   230  
   231  		ioutil.WriteFile(path, cfgData, 0644)
   232  
   233  		fs, err := NewFileStore(path, false)
   234  		require.NoError(t, err)
   235  		configStore, err := NewStoreFromBacking(fs, nil, false)
   236  		require.NoError(t, err)
   237  		defer configStore.Close()
   238  
   239  		assert.Equal(t, "http://TestStoreNew", *configStore.Get().ServiceSettings.SiteURL)
   240  		assertFileNotEqualsConfig(t, testConfig, path)
   241  	})
   242  
   243  	t.Run("relative path, file does not exist", func(t *testing.T) {
   244  		_, tearDown := setupConfigFile(t, nil)
   245  		defer tearDown()
   246  
   247  		err := os.MkdirAll("config/TestFileStoreNew/a/b/c", 0700)
   248  		require.NoError(t, err)
   249  		defer os.RemoveAll("config/TestFileStoreNew")
   250  
   251  		path := "TestFileStoreNew/a/b/c/config.json"
   252  		fs, err := NewFileStore(path, false)
   253  		require.NoError(t, err)
   254  		configStore, err := NewStoreFromBacking(fs, nil, false)
   255  		require.NoError(t, err)
   256  		defer configStore.Close()
   257  
   258  		assert.Equal(t, "", *configStore.Get().ServiceSettings.SiteURL)
   259  		assertFileNotEqualsConfig(t, testConfig, filepath.Join("config", path))
   260  	})
   261  }
   262  
   263  func TestFileStoreGet(t *testing.T) {
   264  	configStore, tearDown := setupConfigFileStore(t, testConfig)
   265  	defer tearDown()
   266  
   267  	cfg := configStore.Get()
   268  	assert.Equal(t, "http://TestStoreNew", *cfg.ServiceSettings.SiteURL)
   269  
   270  	cfg2 := configStore.Get()
   271  	assert.Equal(t, "http://TestStoreNew", *cfg.ServiceSettings.SiteURL)
   272  
   273  	assert.True(t, cfg == cfg2, "Get() returned different configuration instances")
   274  
   275  	newCfg := &model.Config{}
   276  	_, _, err := configStore.Set(newCfg)
   277  	require.NoError(t, err)
   278  
   279  	assert.False(t, newCfg == cfg, "returned config should have been different from original")
   280  }
   281  
   282  func TestFileStoreGetEnivironmentOverrides(t *testing.T) {
   283  	t.Run("get override for a string variable", func(t *testing.T) {
   284  		path, tearDown := setupConfigFile(t, testConfig)
   285  		defer tearDown()
   286  
   287  		fsInner, err := NewFileStore(path, false)
   288  		require.NoError(t, err)
   289  		fs, err := NewStoreFromBacking(fsInner, nil, false)
   290  		require.NoError(t, err)
   291  		defer fs.Close()
   292  
   293  		assert.Equal(t, "http://TestStoreNew", *fs.Get().ServiceSettings.SiteURL)
   294  		assert.Empty(t, fs.GetEnvironmentOverrides())
   295  
   296  		os.Setenv("MM_SERVICESETTINGS_SITEURL", "http://override")
   297  		defer os.Unsetenv("MM_SERVICESETTINGS_SITEURL")
   298  
   299  		fsInner, err = NewFileStore(path, false)
   300  		require.NoError(t, err)
   301  		fs, err = NewStoreFromBacking(fsInner, nil, false)
   302  		require.NoError(t, err)
   303  		defer fs.Close()
   304  
   305  		assert.Equal(t, "http://override", *fs.Get().ServiceSettings.SiteURL)
   306  		assert.Equal(t, map[string]interface{}{"ServiceSettings": map[string]interface{}{"SiteURL": true}}, fs.GetEnvironmentOverrides())
   307  	})
   308  
   309  	t.Run("get override for a string variable, with custom defaults", func(t *testing.T) {
   310  		path, tearDown := setupConfigFile(t, testConfig)
   311  		defer tearDown()
   312  
   313  		fsInner, err := NewFileStore(path, false)
   314  		require.NoError(t, err)
   315  		fs, err := NewStoreFromBacking(fsInner, customConfigDefaults, false)
   316  		require.NoError(t, err)
   317  		defer fs.Close()
   318  
   319  		assert.Equal(t, "http://TestStoreNew", *fs.Get().ServiceSettings.SiteURL)
   320  		assert.Empty(t, fs.GetEnvironmentOverrides())
   321  
   322  		os.Setenv("MM_SERVICESETTINGS_SITEURL", "http://override")
   323  		defer os.Unsetenv("MM_SERVICESETTINGS_SITEURL")
   324  
   325  		fsInner, err = NewFileStore(path, false)
   326  		require.NoError(t, err)
   327  		fs, err = NewStoreFromBacking(fsInner, customConfigDefaults, false)
   328  		require.NoError(t, err)
   329  		defer fs.Close()
   330  
   331  		// environment override should take priority over the custom default value
   332  		assert.Equal(t, "http://override", *fs.Get().ServiceSettings.SiteURL)
   333  		assert.Equal(t, map[string]interface{}{"ServiceSettings": map[string]interface{}{"SiteURL": true}}, fs.GetEnvironmentOverrides())
   334  	})
   335  
   336  	t.Run("get override for a bool variable", func(t *testing.T) {
   337  		path, tearDown := setupConfigFile(t, testConfig)
   338  		defer tearDown()
   339  
   340  		fsInner, err := NewFileStore(path, false)
   341  		require.NoError(t, err)
   342  		fs, err := NewStoreFromBacking(fsInner, nil, false)
   343  		require.NoError(t, err)
   344  		defer fs.Close()
   345  
   346  		assert.Equal(t, false, *fs.Get().PluginSettings.EnableUploads)
   347  		assert.Empty(t, fs.GetEnvironmentOverrides())
   348  
   349  		os.Setenv("MM_PLUGINSETTINGS_ENABLEUPLOADS", "true")
   350  		defer os.Unsetenv("MM_PLUGINSETTINGS_ENABLEUPLOADS")
   351  
   352  		fsInner, err = NewFileStore(path, false)
   353  		require.NoError(t, err)
   354  		fs, err = NewStoreFromBacking(fsInner, nil, false)
   355  		require.NoError(t, err)
   356  		defer fs.Close()
   357  
   358  		assert.Equal(t, true, *fs.Get().PluginSettings.EnableUploads)
   359  		assert.Equal(t, map[string]interface{}{"PluginSettings": map[string]interface{}{"EnableUploads": true}}, fs.GetEnvironmentOverrides())
   360  	})
   361  
   362  	t.Run("get override for an int variable", func(t *testing.T) {
   363  		path, tearDown := setupConfigFile(t, testConfig)
   364  		defer tearDown()
   365  
   366  		fsInner, err := NewFileStore(path, false)
   367  		require.NoError(t, err)
   368  		fs, err := NewStoreFromBacking(fsInner, nil, false)
   369  		require.NoError(t, err)
   370  		defer fs.Close()
   371  
   372  		assert.Equal(t, model.TEAM_SETTINGS_DEFAULT_MAX_USERS_PER_TEAM, *fs.Get().TeamSettings.MaxUsersPerTeam)
   373  		assert.Empty(t, fs.GetEnvironmentOverrides())
   374  
   375  		os.Setenv("MM_TEAMSETTINGS_MAXUSERSPERTEAM", "3000")
   376  		defer os.Unsetenv("MM_TEAMSETTINGS_MAXUSERSPERTEAM")
   377  
   378  		fsInner, err = NewFileStore(path, false)
   379  		require.NoError(t, err)
   380  		fs, err = NewStoreFromBacking(fsInner, nil, false)
   381  		require.NoError(t, err)
   382  		defer fs.Close()
   383  
   384  		assert.Equal(t, 3000, *fs.Get().TeamSettings.MaxUsersPerTeam)
   385  		assert.Equal(t, map[string]interface{}{"TeamSettings": map[string]interface{}{"MaxUsersPerTeam": true}}, fs.GetEnvironmentOverrides())
   386  	})
   387  
   388  	t.Run("get override for an int64 variable", func(t *testing.T) {
   389  		path, tearDown := setupConfigFile(t, testConfig)
   390  		defer tearDown()
   391  
   392  		fsInner, err := NewFileStore(path, false)
   393  		require.NoError(t, err)
   394  		fs, err := NewStoreFromBacking(fsInner, nil, false)
   395  		require.NoError(t, err)
   396  		defer fs.Close()
   397  
   398  		assert.Equal(t, int64(63072000), *fs.Get().ServiceSettings.TLSStrictTransportMaxAge)
   399  		assert.Empty(t, fs.GetEnvironmentOverrides())
   400  
   401  		os.Setenv("MM_SERVICESETTINGS_TLSSTRICTTRANSPORTMAXAGE", "123456")
   402  		defer os.Unsetenv("MM_SERVICESETTINGS_TLSSTRICTTRANSPORTMAXAGE")
   403  
   404  		fsInner, err = NewFileStore(path, false)
   405  		require.NoError(t, err)
   406  		fs, err = NewStoreFromBacking(fsInner, nil, false)
   407  		require.NoError(t, err)
   408  		defer fs.Close()
   409  
   410  		assert.Equal(t, int64(123456), *fs.Get().ServiceSettings.TLSStrictTransportMaxAge)
   411  		assert.Equal(t, map[string]interface{}{"ServiceSettings": map[string]interface{}{"TLSStrictTransportMaxAge": true}}, fs.GetEnvironmentOverrides())
   412  	})
   413  
   414  	t.Run("get override for a slice variable - one value", func(t *testing.T) {
   415  		path, tearDown := setupConfigFile(t, testConfig)
   416  		defer tearDown()
   417  
   418  		fsInner, err := NewFileStore(path, false)
   419  		require.NoError(t, err)
   420  		fs, err := NewStoreFromBacking(fsInner, nil, false)
   421  		require.NoError(t, err)
   422  		defer fs.Close()
   423  
   424  		assert.Equal(t, []string{}, fs.Get().SqlSettings.DataSourceReplicas)
   425  		assert.Empty(t, fs.GetEnvironmentOverrides())
   426  
   427  		os.Setenv("MM_SQLSETTINGS_DATASOURCEREPLICAS", "user:pwd@db:5432/test-db")
   428  		defer os.Unsetenv("MM_SQLSETTINGS_DATASOURCEREPLICAS")
   429  
   430  		fsInner, err = NewFileStore(path, false)
   431  		require.NoError(t, err)
   432  		fs, err = NewStoreFromBacking(fsInner, nil, false)
   433  		require.NoError(t, err)
   434  		defer fs.Close()
   435  
   436  		assert.Equal(t, []string{"user:pwd@db:5432/test-db"}, fs.Get().SqlSettings.DataSourceReplicas)
   437  		assert.Equal(t, map[string]interface{}{"SqlSettings": map[string]interface{}{"DataSourceReplicas": true}}, fs.GetEnvironmentOverrides())
   438  	})
   439  
   440  	t.Run("get override for a slice variable - three values", func(t *testing.T) {
   441  		path, tearDown := setupConfigFile(t, testConfig)
   442  		defer tearDown()
   443  
   444  		fsInner, err := NewFileStore(path, false)
   445  		require.NoError(t, err)
   446  		fs, err := NewStoreFromBacking(fsInner, nil, false)
   447  		require.NoError(t, err)
   448  		defer fs.Close()
   449  
   450  		assert.Equal(t, []string{}, fs.Get().SqlSettings.DataSourceReplicas)
   451  		assert.Empty(t, fs.GetEnvironmentOverrides())
   452  
   453  		os.Setenv("MM_SQLSETTINGS_DATASOURCEREPLICAS", "user:pwd@db:5432/test-db user:pwd@db2:5433/test-db2 user:pwd@db3:5434/test-db3")
   454  		defer os.Unsetenv("MM_SQLSETTINGS_DATASOURCEREPLICAS")
   455  
   456  		fsInner, err = NewFileStore(path, false)
   457  		require.NoError(t, err)
   458  		fs, err = NewStoreFromBacking(fsInner, nil, false)
   459  		require.NoError(t, err)
   460  		defer fs.Close()
   461  
   462  		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)
   463  		assert.Equal(t, map[string]interface{}{"SqlSettings": map[string]interface{}{"DataSourceReplicas": true}}, fs.GetEnvironmentOverrides())
   464  	})
   465  }
   466  
   467  func TestFileStoreSet(t *testing.T) {
   468  	t.Run("defaults required", func(t *testing.T) {
   469  		configStore, tearDown := setupConfigFileStore(t, minimalConfig)
   470  		defer tearDown()
   471  
   472  		oldCfg := configStore.Get().Clone()
   473  		newCfg := &model.Config{}
   474  
   475  		retCfg, newConfig, err := configStore.Set(newCfg)
   476  		require.NoError(t, err)
   477  		require.Equal(t, oldCfg, retCfg)
   478  		require.NotEqual(t, newCfg, newConfig)
   479  
   480  		assert.Equal(t, "", *configStore.Get().ServiceSettings.SiteURL)
   481  	})
   482  
   483  	t.Run("desanitization required", func(t *testing.T) {
   484  		configStore, tearDown := setupConfigFileStore(t, ldapConfig)
   485  		defer tearDown()
   486  
   487  		newCfg := &model.Config{}
   488  		newCfg.LdapSettings.BindPassword = model.NewString(model.FAKE_SETTING)
   489  
   490  		_, newConfig, err := configStore.Set(newCfg)
   491  		require.NoError(t, err)
   492  		require.NotEqual(t, newCfg, newConfig)
   493  
   494  		assert.Equal(t, "password", *configStore.Get().LdapSettings.BindPassword)
   495  	})
   496  
   497  	t.Run("invalid", func(t *testing.T) {
   498  		configStore, tearDown := setupConfigFileStore(t, emptyConfig)
   499  		defer tearDown()
   500  
   501  		newCfg := &model.Config{}
   502  		newCfg.ServiceSettings.SiteURL = model.NewString("invalid")
   503  
   504  		_, _, err := configStore.Set(newCfg)
   505  		if assert.Error(t, err) {
   506  			assert.EqualError(t, err, "new configuration is invalid: Config.IsValid: model.config.is_valid.site_url.app_error, ")
   507  		}
   508  
   509  		assert.Equal(t, "", *configStore.Get().ServiceSettings.SiteURL)
   510  	})
   511  
   512  	t.Run("read-only", func(t *testing.T) {
   513  		configStore, tearDown := setupConfigFileStore(t, readOnlyConfig)
   514  		defer tearDown()
   515  
   516  		newReadOnlyConfig := readOnlyConfig.Clone()
   517  		newReadOnlyConfig.ServiceSettings = model.ServiceSettings{
   518  			SiteURL: model.NewString("http://test"),
   519  		}
   520  		_, _, err := configStore.Set(newReadOnlyConfig)
   521  		if assert.Error(t, err) {
   522  			assert.Equal(t, ErrReadOnlyConfiguration, errors.Cause(err))
   523  		}
   524  
   525  		assert.Equal(t, "", *configStore.Get().ServiceSettings.SiteURL)
   526  	})
   527  
   528  	t.Run("persist failed", func(t *testing.T) {
   529  		path, tearDown := setupConfigFile(t, emptyConfig)
   530  		defer tearDown()
   531  
   532  		fsInner, err := NewFileStore(path, false)
   533  		require.NoError(t, err)
   534  		fs, err := NewStoreFromBacking(fsInner, nil, false)
   535  		require.NoError(t, err)
   536  		defer fs.Close()
   537  
   538  		fsInner.path = ""
   539  
   540  		newCfg := &model.Config{}
   541  
   542  		_, _, err = fs.Set(newCfg)
   543  		if assert.Error(t, err) {
   544  			assert.True(t, strings.HasPrefix(err.Error(), "failed to persist: failed to write file"))
   545  		}
   546  
   547  		assert.Equal(t, "", *fs.Get().ServiceSettings.SiteURL)
   548  	})
   549  
   550  	t.Run("listeners notified", func(t *testing.T) {
   551  		configStore, tearDown := setupConfigFileStore(t, emptyConfig)
   552  		defer tearDown()
   553  
   554  		called := make(chan bool, 1)
   555  		callback := func(oldCfg, newCfg *model.Config) {
   556  			require.NotEqual(t, oldCfg, newCfg)
   557  			called <- true
   558  		}
   559  		configStore.AddListener(callback)
   560  
   561  		newCfg := minimalConfig
   562  
   563  		_, _, err := configStore.Set(newCfg)
   564  		require.NoError(t, err)
   565  
   566  		require.True(t, wasCalled(called, 5*time.Second), "callback should have been called when config written")
   567  	})
   568  
   569  	t.Run("listeners notified, only env change", func(t *testing.T) {
   570  		configStore, tearDown := setupConfigFileStore(t, minimalConfig)
   571  		defer tearDown()
   572  
   573  		called := make(chan bool, 1)
   574  		callback := func(oldCfg, newCfg *model.Config) {
   575  			require.NotEqual(t, oldCfg, newCfg)
   576  			expectedConfig := minimalConfig.Clone()
   577  			expectedConfig.ServiceSettings.SiteURL = model.NewString("http://override")
   578  			require.Equal(t, minimalConfig, oldCfg)
   579  			require.Equal(t, expectedConfig, newCfg)
   580  			called <- true
   581  		}
   582  		configStore.AddListener(callback)
   583  
   584  		newCfg := minimalConfig
   585  		os.Setenv("MM_SERVICESETTINGS_SITEURL", "http://override")
   586  		defer os.Unsetenv("MM_SERVICESETTINGS_SITEURL")
   587  
   588  		_, _, err := configStore.Set(newCfg)
   589  		require.NoError(t, err)
   590  
   591  		require.True(t, wasCalled(called, 5*time.Second), "callback should have been called when config changed")
   592  	})
   593  
   594  	t.Run("listeners notified, feature flags change only", func(t *testing.T) {
   595  		configStore, tearDown := setupConfigFileStore(t, minimalConfig)
   596  		defer tearDown()
   597  
   598  		expectedOldConfig := minimalConfig.Clone()
   599  		var expectedNewConfig *model.Config
   600  		called := make(chan bool, 1)
   601  		callback := func(oldCfg, newCfg *model.Config) {
   602  			require.NotEqual(t, oldCfg, newCfg)
   603  			require.Equal(t, expectedOldConfig, oldCfg)
   604  			require.Equal(t, expectedNewConfig, newCfg)
   605  			called <- true
   606  		}
   607  		configStore.AddListener(callback)
   608  
   609  		configStore.SetReadOnlyFF(true)
   610  
   611  		expectedNewConfig = minimalConfig.Clone()
   612  		expectedNewConfig.FeatureFlags.TestFeature = "test"
   613  		_, _, err := configStore.Set(expectedNewConfig)
   614  		require.NoError(t, err)
   615  
   616  		require.False(t, wasCalled(called, 5*time.Second))
   617  
   618  		configStore.SetReadOnlyFF(false)
   619  
   620  		expectedNewConfig.FeatureFlags.TestFeature = "test2"
   621  		_, _, err = configStore.Set(expectedNewConfig)
   622  		require.NoError(t, err)
   623  
   624  		require.True(t, wasCalled(called, 5*time.Second))
   625  	})
   626  
   627  	t.Run("watcher restarted", func(t *testing.T) {
   628  		if testing.Short() {
   629  			t.Skip("skipping watcher test in short mode")
   630  		}
   631  
   632  		path, tearDown := setupConfigFile(t, emptyConfig)
   633  		defer tearDown()
   634  
   635  		fsInner, err := NewFileStore(path, true)
   636  		require.NoError(t, err)
   637  		fs, err := NewStoreFromBacking(fsInner, nil, false)
   638  		require.NoError(t, err)
   639  		defer fs.Close()
   640  
   641  		_, _, err = fs.Set(minimalConfig)
   642  		require.NoError(t, err)
   643  
   644  		// Let the initial call to invokeConfigListeners finish.
   645  		time.Sleep(1 * time.Second)
   646  
   647  		called := make(chan bool, 1)
   648  		callback := func(oldCfg, newCfg *model.Config) {
   649  			called <- true
   650  		}
   651  		fs.AddListener(callback)
   652  
   653  		// Rewrite the config to the file on disk
   654  		cfgData, err := marshalConfig(emptyConfig)
   655  		require.NoError(t, err)
   656  
   657  		ioutil.WriteFile(path, cfgData, 0644)
   658  		require.True(t, wasCalled(called, 5*time.Second), "callback should have been called when config written")
   659  	})
   660  }
   661  
   662  func TestFileStoreLoad(t *testing.T) {
   663  	t.Run("file no longer exists", func(t *testing.T) {
   664  		path, tearDown := setupConfigFile(t, emptyConfig)
   665  		defer tearDown()
   666  
   667  		fsInner, err := NewFileStore(path, false)
   668  		require.NoError(t, err)
   669  		fs, err := NewStoreFromBacking(fsInner, nil, false)
   670  		require.NoError(t, err)
   671  		defer fs.Close()
   672  
   673  		os.Remove(path)
   674  
   675  		err = fs.Load()
   676  		require.NoError(t, err)
   677  		assertFileNotEqualsConfig(t, emptyConfig, path)
   678  	})
   679  
   680  	t.Run("honour environment", func(t *testing.T) {
   681  		configStore, tearDown := setupConfigFileStore(t, minimalConfig)
   682  		defer tearDown()
   683  
   684  		assert.Equal(t, "http://minimal", *configStore.Get().ServiceSettings.SiteURL)
   685  
   686  		os.Setenv("MM_SERVICESETTINGS_SITEURL", "http://override")
   687  		defer os.Unsetenv("MM_SERVICESETTINGS_SITEURL")
   688  
   689  		err := configStore.Load()
   690  		require.NoError(t, err)
   691  		assert.Equal(t, "http://override", *configStore.Get().ServiceSettings.SiteURL)
   692  		assert.Equal(t, map[string]interface{}{"ServiceSettings": map[string]interface{}{"SiteURL": true}}, configStore.GetEnvironmentOverrides())
   693  	})
   694  
   695  	t.Run("do not persist environment variables - string", func(t *testing.T) {
   696  		path, tearDown := setupConfigFile(t, minimalConfig)
   697  		defer tearDown()
   698  
   699  		os.Setenv("MM_SERVICESETTINGS_SITEURL", "http://overridePersistEnvVariables")
   700  		defer os.Unsetenv("MM_SERVICESETTINGS_SITEURL")
   701  
   702  		fsInner, err := NewFileStore(path, false)
   703  		require.NoError(t, err)
   704  		fs, err := NewStoreFromBacking(fsInner, nil, false)
   705  		require.NoError(t, err)
   706  		defer fs.Close()
   707  
   708  		assert.Equal(t, "http://overridePersistEnvVariables", *fs.Get().ServiceSettings.SiteURL)
   709  
   710  		_, _, err = fs.Set(fs.Get())
   711  		require.NoError(t, err)
   712  
   713  		assert.Equal(t, "http://overridePersistEnvVariables", *fs.Get().ServiceSettings.SiteURL)
   714  		assert.Equal(t, map[string]interface{}{"ServiceSettings": map[string]interface{}{"SiteURL": true}}, fs.GetEnvironmentOverrides())
   715  		// check that on disk config does not include overwritten variable
   716  		actualConfig := getActualFileConfig(t, path)
   717  		assert.Equal(t, "http://minimal", *actualConfig.ServiceSettings.SiteURL)
   718  	})
   719  
   720  	t.Run("do not persist environment variables - boolean", func(t *testing.T) {
   721  		path, tearDown := setupConfigFile(t, minimalConfig)
   722  		defer tearDown()
   723  
   724  		os.Setenv("MM_PLUGINSETTINGS_ENABLEUPLOADS", "true")
   725  		defer os.Unsetenv("MM_PLUGINSETTINGS_ENABLEUPLOADS")
   726  
   727  		fsInner, err := NewFileStore(path, false)
   728  		require.NoError(t, err)
   729  		fs, err := NewStoreFromBacking(fsInner, nil, false)
   730  		require.NoError(t, err)
   731  		defer fs.Close()
   732  
   733  		assert.Equal(t, true, *fs.Get().PluginSettings.EnableUploads)
   734  
   735  		_, _, err = fs.Set(fs.Get())
   736  		require.NoError(t, err)
   737  
   738  		assert.Equal(t, true, *fs.Get().PluginSettings.EnableUploads)
   739  		assert.Equal(t, map[string]interface{}{"PluginSettings": map[string]interface{}{"EnableUploads": true}}, fs.GetEnvironmentOverrides())
   740  		// check that on disk config does not include overwritten variable
   741  		actualConfig := getActualFileConfig(t, path)
   742  		assert.Equal(t, false, *actualConfig.PluginSettings.EnableUploads)
   743  	})
   744  
   745  	t.Run("do not persist environment variables - int", func(t *testing.T) {
   746  		path, tearDown := setupConfigFile(t, minimalConfig)
   747  		defer tearDown()
   748  
   749  		os.Setenv("MM_TEAMSETTINGS_MAXUSERSPERTEAM", "3000")
   750  		defer os.Unsetenv("MM_TEAMSETTINGS_MAXUSERSPERTEAM")
   751  
   752  		fsInner, err := NewFileStore(path, false)
   753  		require.NoError(t, err)
   754  		fs, err := NewStoreFromBacking(fsInner, nil, false)
   755  		require.NoError(t, err)
   756  		defer fs.Close()
   757  
   758  		assert.Equal(t, 3000, *fs.Get().TeamSettings.MaxUsersPerTeam)
   759  
   760  		_, _, err = fs.Set(fs.Get())
   761  		require.NoError(t, err)
   762  
   763  		assert.Equal(t, 3000, *fs.Get().TeamSettings.MaxUsersPerTeam)
   764  		assert.Equal(t, map[string]interface{}{"TeamSettings": map[string]interface{}{"MaxUsersPerTeam": true}}, fs.GetEnvironmentOverrides())
   765  		// check that on disk config does not include overwritten variable
   766  		actualConfig := getActualFileConfig(t, path)
   767  		assert.Equal(t, model.TEAM_SETTINGS_DEFAULT_MAX_USERS_PER_TEAM, *actualConfig.TeamSettings.MaxUsersPerTeam)
   768  	})
   769  
   770  	t.Run("do not persist environment variables - int64", func(t *testing.T) {
   771  		path, tearDown := setupConfigFile(t, minimalConfig)
   772  		defer tearDown()
   773  
   774  		os.Setenv("MM_SERVICESETTINGS_TLSSTRICTTRANSPORTMAXAGE", "123456")
   775  		defer os.Unsetenv("MM_SERVICESETTINGS_TLSSTRICTTRANSPORTMAXAGE")
   776  
   777  		fsInner, err := NewFileStore(path, false)
   778  		require.NoError(t, err)
   779  		fs, err := NewStoreFromBacking(fsInner, nil, false)
   780  		require.NoError(t, err)
   781  		defer fs.Close()
   782  
   783  		assert.Equal(t, int64(123456), *fs.Get().ServiceSettings.TLSStrictTransportMaxAge)
   784  
   785  		_, _, err = fs.Set(fs.Get())
   786  		require.NoError(t, err)
   787  
   788  		assert.Equal(t, int64(123456), *fs.Get().ServiceSettings.TLSStrictTransportMaxAge)
   789  		assert.Equal(t, map[string]interface{}{"ServiceSettings": map[string]interface{}{"TLSStrictTransportMaxAge": true}}, fs.GetEnvironmentOverrides())
   790  		// check that on disk config does not include overwritten variable
   791  		actualConfig := getActualFileConfig(t, path)
   792  		assert.Equal(t, int64(63072000), *actualConfig.ServiceSettings.TLSStrictTransportMaxAge)
   793  	})
   794  
   795  	t.Run("do not persist environment variables - string slice beginning with default", func(t *testing.T) {
   796  		path, tearDown := setupConfigFile(t, minimalConfig)
   797  		defer tearDown()
   798  
   799  		os.Setenv("MM_SQLSETTINGS_DATASOURCEREPLICAS", "user:pwd@db:5432/test-db")
   800  		defer os.Unsetenv("MM_SQLSETTINGS_DATASOURCEREPLICAS")
   801  
   802  		fsInner, err := NewFileStore(path, false)
   803  		require.NoError(t, err)
   804  		fs, err := NewStoreFromBacking(fsInner, nil, false)
   805  		require.NoError(t, err)
   806  		defer fs.Close()
   807  
   808  		assert.Equal(t, []string{"user:pwd@db:5432/test-db"}, fs.Get().SqlSettings.DataSourceReplicas)
   809  
   810  		_, _, err = fs.Set(fs.Get())
   811  		require.NoError(t, err)
   812  
   813  		assert.Equal(t, []string{"user:pwd@db:5432/test-db"}, fs.Get().SqlSettings.DataSourceReplicas)
   814  		assert.Equal(t, map[string]interface{}{"SqlSettings": map[string]interface{}{"DataSourceReplicas": true}}, fs.GetEnvironmentOverrides())
   815  		// check that on disk config does not include overwritten variable
   816  		actualConfig := getActualFileConfig(t, path)
   817  		assert.Equal(t, []string{}, actualConfig.SqlSettings.DataSourceReplicas)
   818  	})
   819  
   820  	t.Run("do not persist environment variables - string slice beginning with slice of three", func(t *testing.T) {
   821  		modifiedMinimalConfig := minimalConfig.Clone()
   822  		modifiedMinimalConfig.SqlSettings.DataSourceReplicas = []string{"user:pwd@db:5432/test-db", "user:pwd@db2:5433/test-db2", "user:pwd@db3:5434/test-db3"}
   823  		path, tearDown := setupConfigFile(t, modifiedMinimalConfig)
   824  		defer tearDown()
   825  
   826  		os.Setenv("MM_SQLSETTINGS_DATASOURCEREPLICAS", "user:pwd@db:5432/test-db")
   827  		defer os.Unsetenv("MM_SQLSETTINGS_DATASOURCEREPLICAS")
   828  
   829  		fsInner, err := NewFileStore(path, false)
   830  		require.NoError(t, err)
   831  		fs, err := NewStoreFromBacking(fsInner, nil, false)
   832  		require.NoError(t, err)
   833  		defer fs.Close()
   834  
   835  		assert.Equal(t, []string{"user:pwd@db:5432/test-db"}, fs.Get().SqlSettings.DataSourceReplicas)
   836  
   837  		_, _, err = fs.Set(fs.Get())
   838  		require.NoError(t, err)
   839  
   840  		assert.Equal(t, []string{"user:pwd@db:5432/test-db"}, fs.Get().SqlSettings.DataSourceReplicas)
   841  		assert.Equal(t, map[string]interface{}{"SqlSettings": map[string]interface{}{"DataSourceReplicas": true}}, fs.GetEnvironmentOverrides())
   842  		// check that on disk config does not include overwritten variable
   843  		actualConfig := getActualFileConfig(t, path)
   844  		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)
   845  	})
   846  
   847  	t.Run("invalid", func(t *testing.T) {
   848  		path, tearDown := setupConfigFile(t, emptyConfig)
   849  		defer tearDown()
   850  
   851  		fsInner, err := NewFileStore(path, false)
   852  		require.NoError(t, err)
   853  		fs, err := NewStoreFromBacking(fsInner, nil, false)
   854  		require.NoError(t, err)
   855  		defer fs.Close()
   856  
   857  		cfgData, err := marshalConfig(invalidConfig)
   858  		require.NoError(t, err)
   859  
   860  		ioutil.WriteFile(path, cfgData, 0644)
   861  
   862  		err = fs.Load()
   863  		if assert.Error(t, err) {
   864  			assert.EqualError(t, err, "invalid config: Config.IsValid: model.config.is_valid.site_url.app_error, ")
   865  		}
   866  	})
   867  
   868  	t.Run("invalid environment value", func(t *testing.T) {
   869  		configStore, tearDown := setupConfigFileStore(t, emptyConfig)
   870  		defer tearDown()
   871  
   872  		os.Setenv("MM_SERVICESETTINGS_SITEURL", "invalid_url")
   873  		defer os.Unsetenv("MM_SERVICESETTINGS_SITEURL")
   874  
   875  		newCfg := minimalConfig
   876  		_, _, err := configStore.Set(newCfg)
   877  		require.Error(t, err)
   878  		require.EqualError(t, err, "new configuration is invalid: Config.IsValid: model.config.is_valid.site_url.app_error, ")
   879  	})
   880  
   881  	t.Run("fixes required", func(t *testing.T) {
   882  		path, tearDown := setupConfigFile(t, fixesRequiredConfig)
   883  		defer tearDown()
   884  
   885  		fsInner, err := NewFileStore(path, false)
   886  		require.NoError(t, err)
   887  		fs, err := NewStoreFromBacking(fsInner, nil, false)
   888  		require.NoError(t, err)
   889  		defer fs.Close()
   890  
   891  		err = fs.Load()
   892  		require.NoError(t, err)
   893  		assertFileNotEqualsConfig(t, fixesRequiredConfig, path)
   894  		assert.Equal(t, "http://trailingslash", *fs.Get().ServiceSettings.SiteURL)
   895  		assert.Equal(t, "/path/to/directory/", *fs.Get().FileSettings.Directory)
   896  		assert.Equal(t, "en", *fs.Get().LocalizationSettings.DefaultServerLocale)
   897  		assert.Equal(t, "en", *fs.Get().LocalizationSettings.DefaultClientLocale)
   898  	})
   899  
   900  	t.Run("listeners notifed", func(t *testing.T) {
   901  		path, tearDown := setupConfigFile(t, emptyConfig)
   902  		defer tearDown()
   903  
   904  		fsInner, err := NewFileStore(path, false)
   905  		require.NoError(t, err)
   906  		fs, err := NewStoreFromBacking(fsInner, nil, false)
   907  		require.NoError(t, err)
   908  		defer fs.Close()
   909  
   910  		called := make(chan bool, 1)
   911  		callback := func(oldCfg, newCfg *model.Config) {
   912  			called <- true
   913  		}
   914  		fs.AddListener(callback)
   915  
   916  		cfgData, err := marshalConfig(minimalConfig)
   917  		require.NoError(t, err)
   918  
   919  		err = ioutil.WriteFile(path, cfgData, 0644)
   920  		require.NoError(t, err)
   921  
   922  		err = fs.Load()
   923  		require.NoError(t, err)
   924  
   925  		require.True(t, wasCalled(called, 5*time.Second), "callback should have been called when config changed on load")
   926  	})
   927  
   928  	t.Run("no change", func(t *testing.T) {
   929  		configStore, tearDown := setupConfigFileStore(t, testConfig)
   930  		defer tearDown()
   931  
   932  		called := make(chan bool, 1)
   933  		callback := func(oldCfg, newCfg *model.Config) {
   934  			called <- true
   935  		}
   936  		configStore.AddListener(callback)
   937  
   938  		err := configStore.Load()
   939  		require.NoError(t, err)
   940  
   941  		require.False(t, wasCalled(called, 5*time.Second), "callback should not have been called if nothing changed")
   942  	})
   943  
   944  	t.Run("listeners notified, only env change", func(t *testing.T) {
   945  		configStore, tearDown := setupConfigFileStore(t, minimalConfig)
   946  		defer tearDown()
   947  
   948  		time.Sleep(1 * time.Second)
   949  
   950  		called := make(chan bool, 1)
   951  		callback := func(oldCfg, newCfg *model.Config) {
   952  			require.NotEqual(t, oldCfg, newCfg)
   953  			expectedConfig := minimalConfig.Clone()
   954  			expectedConfig.ServiceSettings.SiteURL = model.NewString("http://override")
   955  			require.Equal(t, minimalConfig, oldCfg)
   956  			require.Equal(t, expectedConfig, newCfg)
   957  			called <- true
   958  		}
   959  		configStore.AddListener(callback)
   960  
   961  		os.Setenv("MM_SERVICESETTINGS_SITEURL", "http://override")
   962  		defer os.Unsetenv("MM_SERVICESETTINGS_SITEURL")
   963  
   964  		err := configStore.Load()
   965  		require.NoError(t, err)
   966  
   967  		require.True(t, wasCalled(called, 5*time.Second), "callback should have been called when config changed")
   968  	})
   969  }
   970  
   971  func TestFileStoreWatcherEmitter(t *testing.T) {
   972  	if testing.Short() {
   973  		t.Skip("skipping watcher test in short mode")
   974  	}
   975  
   976  	t.Run("disabled", func(t *testing.T) {
   977  		path, tearDown := setupConfigFile(t, emptyConfig)
   978  		defer tearDown()
   979  		fsInner, err := NewFileStore(path, false)
   980  		require.NoError(t, err)
   981  		fs, err := NewStoreFromBacking(fsInner, nil, false)
   982  		require.NoError(t, err)
   983  		defer fs.Close()
   984  
   985  		// Let the initial call to invokeConfigListeners finish.
   986  		time.Sleep(1 * time.Second)
   987  
   988  		called := make(chan bool, 1)
   989  		callback := func(oldCfg, newCfg *model.Config) {
   990  			called <- true
   991  		}
   992  		fs.AddListener(callback)
   993  
   994  		// Rewrite the config to the file on disk
   995  		cfgData, err := marshalConfig(emptyConfig)
   996  		require.NoError(t, err)
   997  
   998  		ioutil.WriteFile(path, cfgData, 0644)
   999  		require.False(t, wasCalled(called, 1*time.Second), "callback should not have been called since watching disabled")
  1000  	})
  1001  
  1002  	t.Run("enabled", func(t *testing.T) {
  1003  		path, tearDown := setupConfigFile(t, emptyConfig)
  1004  		defer tearDown()
  1005  		fsInner, err := NewFileStore(path, true)
  1006  		require.NoError(t, err)
  1007  		fs, err := NewStoreFromBacking(fsInner, nil, false)
  1008  		require.NoError(t, err)
  1009  		defer fs.Close()
  1010  
  1011  		called := make(chan bool, 1)
  1012  		callback := func(oldCCfg, newCfg *model.Config) {
  1013  			called <- true
  1014  		}
  1015  		fs.AddListener(callback)
  1016  
  1017  		// Rewrite the config to the file on disk
  1018  		cfgData, err := marshalConfig(minimalConfig)
  1019  		require.NoError(t, err)
  1020  
  1021  		f, err := os.OpenFile(path, os.O_WRONLY, 0644)
  1022  		require.NoError(t, err)
  1023  		defer f.Close()
  1024  		_, err = f.Write(cfgData)
  1025  		require.NoError(t, err)
  1026  
  1027  		require.True(t, wasCalled(called, 5*time.Second), "callback should have been called when config written")
  1028  	})
  1029  
  1030  	t.Run("no change", func(t *testing.T) {
  1031  		path, tearDown := setupConfigFile(t, minimalConfig)
  1032  		defer tearDown()
  1033  		fsInner, err := NewFileStore(path, true)
  1034  		require.NoError(t, err)
  1035  		fs, err := NewStoreFromBacking(fsInner, nil, false)
  1036  		require.NoError(t, err)
  1037  		defer fs.Close()
  1038  
  1039  		// Let the initial call to invokeConfigListeners finish.
  1040  		time.Sleep(1 * time.Second)
  1041  
  1042  		called := make(chan bool, 1)
  1043  		callback := func(oldCfg, newCfg *model.Config) {
  1044  			called <- true
  1045  		}
  1046  		fs.AddListener(callback)
  1047  
  1048  		_, _, err = fs.Set(minimalConfig)
  1049  		require.NoError(t, err)
  1050  
  1051  		require.False(t, wasCalled(called, 1*time.Second), "callback should not have been called since no change has happened")
  1052  	})
  1053  
  1054  	t.Run("env only change", func(t *testing.T) {
  1055  		path, tearDown := setupConfigFile(t, minimalConfig)
  1056  		defer tearDown()
  1057  		fsInner, err := NewFileStore(path, true)
  1058  		require.NoError(t, err)
  1059  		fs, err := NewStoreFromBacking(fsInner, nil, false)
  1060  		require.NoError(t, err)
  1061  		defer fs.Close()
  1062  
  1063  		// Let the initial call to invokeConfigListeners finish.
  1064  		time.Sleep(1 * time.Second)
  1065  
  1066  		called := make(chan bool, 1)
  1067  		callback := func(oldCfg, newCfg *model.Config) {
  1068  			require.NotEqual(t, oldCfg, newCfg)
  1069  			expectedConfig := minimalConfig.Clone()
  1070  			expectedConfig.ServiceSettings.SiteURL = model.NewString("http://override")
  1071  			require.Equal(t, minimalConfig, oldCfg)
  1072  			require.Equal(t, expectedConfig, newCfg)
  1073  			called <- true
  1074  		}
  1075  		fs.AddListener(callback)
  1076  
  1077  		os.Setenv("MM_SERVICESETTINGS_SITEURL", "http://override")
  1078  		defer os.Unsetenv("MM_SERVICESETTINGS_SITEURL")
  1079  		_, _, err = fs.Set(minimalConfig)
  1080  		require.NoError(t, err)
  1081  
  1082  		require.True(t, wasCalled(called, 5*time.Second), "callback should have been called since no change has happened")
  1083  	})
  1084  }
  1085  
  1086  func TestFileStoreSave(t *testing.T) {
  1087  	store, tearDown := setupConfigFileStore(t, minimalConfig)
  1088  	defer tearDown()
  1089  
  1090  	newCfg := &model.Config{
  1091  		ServiceSettings: model.ServiceSettings{
  1092  			SiteURL: model.NewString("http://new"),
  1093  		},
  1094  	}
  1095  
  1096  	t.Run("set with automatic save", func(t *testing.T) {
  1097  		_, _, err := store.Set(newCfg)
  1098  		require.NoError(t, err)
  1099  
  1100  		err = store.Load()
  1101  		require.NoError(t, err)
  1102  
  1103  		assert.Equal(t, "http://new", *store.Get().ServiceSettings.SiteURL)
  1104  	})
  1105  }
  1106  
  1107  func TestFileGetFile(t *testing.T) {
  1108  	path, tearDown := setupConfigFile(t, minimalConfig)
  1109  	defer tearDown()
  1110  
  1111  	fs, err := NewFileStore(path, true)
  1112  	require.NoError(t, err)
  1113  	defer fs.Close()
  1114  
  1115  	t.Run("get empty filename", func(t *testing.T) {
  1116  		_, err := fs.GetFile("")
  1117  		require.Error(t, err)
  1118  	})
  1119  
  1120  	t.Run("get non-existent file", func(t *testing.T) {
  1121  		_, err := fs.GetFile("unknown")
  1122  		require.Error(t, err)
  1123  	})
  1124  
  1125  	t.Run("get empty file", func(t *testing.T) {
  1126  		err := os.MkdirAll("config", 0700)
  1127  		require.NoError(t, err)
  1128  
  1129  		f, err := ioutil.TempFile("config", "empty-file")
  1130  		require.NoError(t, err)
  1131  		defer os.Remove(f.Name())
  1132  
  1133  		err = ioutil.WriteFile(f.Name(), nil, 0777)
  1134  		require.NoError(t, err)
  1135  
  1136  		data, err := fs.GetFile(f.Name())
  1137  		require.NoError(t, err)
  1138  		require.Empty(t, data)
  1139  	})
  1140  
  1141  	t.Run("get non-empty file", func(t *testing.T) {
  1142  		err := os.MkdirAll("config", 0700)
  1143  		require.NoError(t, err)
  1144  
  1145  		f, err := ioutil.TempFile("config", "test-file")
  1146  		require.NoError(t, err)
  1147  		defer os.Remove(f.Name())
  1148  
  1149  		err = ioutil.WriteFile(f.Name(), []byte("test"), 0777)
  1150  		require.NoError(t, err)
  1151  
  1152  		data, err := fs.GetFile(f.Name())
  1153  		require.NoError(t, err)
  1154  		require.Equal(t, []byte("test"), data)
  1155  	})
  1156  
  1157  	t.Run("get via absolute path", func(t *testing.T) {
  1158  		err := fs.SetFile("new", []byte("new file"))
  1159  		require.NoError(t, err)
  1160  
  1161  		data, err := fs.GetFile(filepath.Join(filepath.Dir(path), "new"))
  1162  
  1163  		require.NoError(t, err)
  1164  		require.Equal(t, []byte("new file"), data)
  1165  	})
  1166  
  1167  }
  1168  
  1169  func TestFileSetFile(t *testing.T) {
  1170  	path, tearDown := setupConfigFile(t, minimalConfig)
  1171  	defer tearDown()
  1172  
  1173  	fs, err := NewFileStore(path, true)
  1174  	require.NoError(t, err)
  1175  	defer fs.Close()
  1176  
  1177  	t.Run("set new file", func(t *testing.T) {
  1178  		err := fs.SetFile("new", []byte("new file"))
  1179  		require.NoError(t, err)
  1180  
  1181  		data, err := fs.GetFile("new")
  1182  		require.NoError(t, err)
  1183  		require.Equal(t, []byte("new file"), data)
  1184  	})
  1185  
  1186  	t.Run("overwrite existing file", func(t *testing.T) {
  1187  		err := fs.SetFile("existing", []byte("existing file"))
  1188  		require.NoError(t, err)
  1189  
  1190  		err = fs.SetFile("existing", []byte("overwritten file"))
  1191  		require.NoError(t, err)
  1192  
  1193  		data, err := fs.GetFile("existing")
  1194  		require.NoError(t, err)
  1195  		require.Equal(t, []byte("overwritten file"), data)
  1196  	})
  1197  
  1198  	t.Run("set via absolute path", func(t *testing.T) {
  1199  		absolutePath := filepath.Join(filepath.Dir(path), "new")
  1200  		err := fs.SetFile(absolutePath, []byte("new file"))
  1201  		require.NoError(t, err)
  1202  
  1203  		data, err := fs.GetFile("new")
  1204  
  1205  		require.NoError(t, err)
  1206  		require.Equal(t, []byte("new file"), data)
  1207  	})
  1208  
  1209  	t.Run("should set right permissions", func(t *testing.T) {
  1210  		absolutePath := filepath.Join(filepath.Dir(path), "new")
  1211  		err := fs.SetFile(absolutePath, []byte("data"))
  1212  		require.NoError(t, err)
  1213  		fi, err := os.Stat(absolutePath)
  1214  		require.NoError(t, err)
  1215  		require.Equal(t, os.FileMode(0600), fi.Mode().Perm())
  1216  	})
  1217  }
  1218  
  1219  func TestFileHasFile(t *testing.T) {
  1220  	t.Run("has non-existent", func(t *testing.T) {
  1221  		path, tearDown := setupConfigFile(t, minimalConfig)
  1222  		defer tearDown()
  1223  
  1224  		fs, err := NewFileStore(path, true)
  1225  		require.NoError(t, err)
  1226  		defer fs.Close()
  1227  
  1228  		has, err := fs.HasFile("non-existent")
  1229  		require.NoError(t, err)
  1230  		require.False(t, has)
  1231  	})
  1232  
  1233  	t.Run("has existing", func(t *testing.T) {
  1234  		path, tearDown := setupConfigFile(t, minimalConfig)
  1235  		defer tearDown()
  1236  
  1237  		fs, err := NewFileStore(path, true)
  1238  		require.NoError(t, err)
  1239  		defer fs.Close()
  1240  
  1241  		err = fs.SetFile("existing", []byte("existing file"))
  1242  		require.NoError(t, err)
  1243  
  1244  		has, err := fs.HasFile("existing")
  1245  		require.NoError(t, err)
  1246  		require.True(t, has)
  1247  	})
  1248  
  1249  	t.Run("has manually created file", func(t *testing.T) {
  1250  		path, tearDown := setupConfigFile(t, minimalConfig)
  1251  		defer tearDown()
  1252  
  1253  		fs, err := NewFileStore(path, true)
  1254  		require.NoError(t, err)
  1255  		defer fs.Close()
  1256  
  1257  		err = os.MkdirAll("config", 0700)
  1258  		require.NoError(t, err)
  1259  
  1260  		f, err := ioutil.TempFile("config", "test-file")
  1261  		require.NoError(t, err)
  1262  		defer os.Remove(f.Name())
  1263  
  1264  		err = ioutil.WriteFile(f.Name(), []byte("test"), 0777)
  1265  		require.NoError(t, err)
  1266  
  1267  		has, err := fs.HasFile(f.Name())
  1268  		require.NoError(t, err)
  1269  		require.True(t, has)
  1270  	})
  1271  
  1272  	t.Run("has empty string", func(t *testing.T) {
  1273  		path, tearDown := setupConfigFile(t, minimalConfig)
  1274  		defer tearDown()
  1275  
  1276  		fs, err := NewFileStore(path, true)
  1277  		require.NoError(t, err)
  1278  		defer fs.Close()
  1279  
  1280  		has, err := fs.HasFile("")
  1281  		require.NoError(t, err)
  1282  		require.False(t, has)
  1283  	})
  1284  
  1285  	t.Run("has via absolute path", func(t *testing.T) {
  1286  		path, tearDown := setupConfigFile(t, minimalConfig)
  1287  		defer tearDown()
  1288  
  1289  		fs, err := NewFileStore(path, true)
  1290  		require.NoError(t, err)
  1291  		defer fs.Close()
  1292  
  1293  		err = fs.SetFile("existing", []byte("existing file"))
  1294  		require.NoError(t, err)
  1295  
  1296  		has, err := fs.HasFile(filepath.Join(filepath.Dir(path), "existing"))
  1297  		require.NoError(t, err)
  1298  		require.True(t, has)
  1299  	})
  1300  
  1301  }
  1302  
  1303  func TestFileRemoveFile(t *testing.T) {
  1304  	t.Run("remove non-existent", func(t *testing.T) {
  1305  		path, tearDown := setupConfigFile(t, minimalConfig)
  1306  		defer tearDown()
  1307  
  1308  		fs, err := NewFileStore(path, true)
  1309  		require.NoError(t, err)
  1310  		defer fs.Close()
  1311  
  1312  		err = fs.RemoveFile("non-existent")
  1313  		require.NoError(t, err)
  1314  	})
  1315  
  1316  	t.Run("remove existing", func(t *testing.T) {
  1317  		path, tearDown := setupConfigFile(t, minimalConfig)
  1318  		defer tearDown()
  1319  
  1320  		fs, err := NewFileStore(path, true)
  1321  		require.NoError(t, err)
  1322  		defer fs.Close()
  1323  
  1324  		err = fs.SetFile("existing", []byte("existing file"))
  1325  		require.NoError(t, err)
  1326  
  1327  		err = fs.RemoveFile("existing")
  1328  		require.NoError(t, err)
  1329  
  1330  		has, err := fs.HasFile("existing")
  1331  		require.NoError(t, err)
  1332  		require.False(t, has)
  1333  
  1334  		_, err = fs.GetFile("existing")
  1335  		require.Error(t, err)
  1336  	})
  1337  
  1338  	t.Run("remove manually created file", func(t *testing.T) {
  1339  		path, tearDown := setupConfigFile(t, minimalConfig)
  1340  		defer tearDown()
  1341  
  1342  		fs, err := NewFileStore(path, true)
  1343  		require.NoError(t, err)
  1344  		defer fs.Close()
  1345  
  1346  		err = os.MkdirAll("config", 0700)
  1347  		require.NoError(t, err)
  1348  
  1349  		f, err := ioutil.TempFile("config", "test-file")
  1350  		require.NoError(t, err)
  1351  		defer os.Remove(f.Name())
  1352  
  1353  		err = ioutil.WriteFile(f.Name(), []byte("test"), 0777)
  1354  		require.NoError(t, err)
  1355  
  1356  		err = fs.RemoveFile(f.Name())
  1357  		require.NoError(t, err)
  1358  
  1359  		has, err := fs.HasFile("existing")
  1360  		require.NoError(t, err)
  1361  		require.False(t, has)
  1362  
  1363  		_, err = fs.GetFile("existing")
  1364  		require.Error(t, err)
  1365  	})
  1366  
  1367  	t.Run("don't remove via absolute path", func(t *testing.T) {
  1368  		path, tearDown := setupConfigFile(t, minimalConfig)
  1369  		defer tearDown()
  1370  
  1371  		fs, err := NewFileStore(path, true)
  1372  		require.NoError(t, err)
  1373  		defer fs.Close()
  1374  
  1375  		err = fs.SetFile("existing", []byte("existing file"))
  1376  		require.NoError(t, err)
  1377  
  1378  		filename := filepath.Join(filepath.Dir(path), "existing")
  1379  		err = fs.RemoveFile(filename)
  1380  		require.NoError(t, err)
  1381  
  1382  		has, err := fs.HasFile(filename)
  1383  		require.NoError(t, err)
  1384  		require.True(t, has)
  1385  
  1386  	})
  1387  }
  1388  
  1389  func TestFileStoreString(t *testing.T) {
  1390  	path, tearDown := setupConfigFile(t, emptyConfig)
  1391  	defer tearDown()
  1392  
  1393  	fs, err := NewFileStore(path, false)
  1394  	require.NoError(t, err)
  1395  	defer fs.Close()
  1396  
  1397  	assert.Equal(t, "file://"+path, fs.String())
  1398  }
  1399  
  1400  // wasCalled reports whether a given callback channel was called
  1401  // within the specified time duration or not.
  1402  func wasCalled(c chan bool, duration time.Duration) bool {
  1403  	select {
  1404  	case <-c:
  1405  		return true
  1406  	case <-time.After(duration):
  1407  	}
  1408  	return false
  1409  }
  1410  
  1411  func TestFileStoreReadOnly(t *testing.T) {
  1412  	path, tearDown := setupConfigFile(t, emptyConfig)
  1413  	defer tearDown()
  1414  	fsInner, err := NewFileStore(path, true)
  1415  	require.NoError(t, err)
  1416  	fs, err := NewStoreFromBacking(fsInner, nil, true)
  1417  	require.NoError(t, err)
  1418  	defer fs.Close()
  1419  
  1420  	called := make(chan bool, 1)
  1421  	callback := func(oldCfg, newCfg *model.Config) {
  1422  		called <- true
  1423  	}
  1424  	fs.AddListener(callback)
  1425  
  1426  	cfg, _, err := fs.Set(minimalConfig)
  1427  	require.Nil(t, cfg)
  1428  	require.Equal(t, ErrReadOnlyStore, err)
  1429  
  1430  	require.False(t, wasCalled(called, 1*time.Second), "callback should not have been called since config is read-only")
  1431  }
  1432  
  1433  func TestFileStoreSetReadOnlyFF(t *testing.T) {
  1434  	t.Run("read only true", func(t *testing.T) {
  1435  		store, tearDown := setupConfigFileStore(t, minimalConfig)
  1436  		defer tearDown()
  1437  		config := store.Get()
  1438  		require.Equal(t, minimalConfig.FeatureFlags, config.FeatureFlags)
  1439  
  1440  		newCfg := config.Clone()
  1441  		newCfg.FeatureFlags.TestFeature = "test"
  1442  
  1443  		// store has read-only FF by default.
  1444  		_, _, err := store.Set(newCfg)
  1445  		require.NoError(t, err)
  1446  
  1447  		config = store.Get()
  1448  		require.Equal(t, minimalConfig.FeatureFlags, config.FeatureFlags)
  1449  	})
  1450  
  1451  	t.Run("read only false", func(t *testing.T) {
  1452  		store, tearDown := setupConfigFileStore(t, minimalConfig)
  1453  		defer tearDown()
  1454  		config := store.Get()
  1455  		require.Equal(t, minimalConfig.FeatureFlags, config.FeatureFlags)
  1456  
  1457  		newCfg := config.Clone()
  1458  		newCfg.FeatureFlags.TestFeature = "test"
  1459  
  1460  		store.SetReadOnlyFF(false)
  1461  
  1462  		_, _, err := store.Set(newCfg)
  1463  		require.NoError(t, err)
  1464  
  1465  		config = store.Get()
  1466  		require.Equal(t, newCfg.FeatureFlags, config.FeatureFlags)
  1467  	})
  1468  }