github.com/mattermosttest/mattermost-server/v5@v5.0.0-20200917143240-9dfa12e121f9/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_test
     5  
     6  import (
     7  	"io/ioutil"
     8  	"os"
     9  	"path/filepath"
    10  	"strings"
    11  	"testing"
    12  	"time"
    13  
    14  	"github.com/pkg/errors"
    15  	"github.com/stretchr/testify/assert"
    16  	"github.com/stretchr/testify/require"
    17  
    18  	"github.com/mattermost/mattermost-server/v5/config"
    19  	"github.com/mattermost/mattermost-server/v5/model"
    20  	"github.com/mattermost/mattermost-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 := config.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  // getActualFileConfig returns the configuration present in the given file without relying on a config store.
    52  func getActualFileConfig(t *testing.T, path string) *model.Config {
    53  	t.Helper()
    54  
    55  	f, err := os.Open(path)
    56  	require.Nil(t, err)
    57  	defer f.Close()
    58  
    59  	actualCfg, _, err := config.UnmarshalConfig(f, false)
    60  	require.Nil(t, err)
    61  
    62  	return actualCfg
    63  }
    64  
    65  // assertFileEqualsConfig verifies the on disk contents of the given path equal the given config.
    66  func assertFileEqualsConfig(t *testing.T, expectedCfg *model.Config, path string) {
    67  	t.Helper()
    68  
    69  	expectedCfg = prepareExpectedConfig(t, expectedCfg)
    70  	actualCfg := getActualFileConfig(t, path)
    71  
    72  	assert.Equal(t, expectedCfg, actualCfg)
    73  }
    74  
    75  // assertFileNotEqualsConfig verifies the on disk contents of the given path does not equal the given config.
    76  func assertFileNotEqualsConfig(t *testing.T, expectedCfg *model.Config, path string) {
    77  	t.Helper()
    78  
    79  	expectedCfg = prepareExpectedConfig(t, expectedCfg)
    80  	actualCfg := getActualFileConfig(t, path)
    81  
    82  	assert.NotEqual(t, expectedCfg, actualCfg)
    83  }
    84  
    85  func TestFileStoreNew(t *testing.T) {
    86  	utils.TranslationsPreInit()
    87  
    88  	t.Run("absolute path, initialization required", func(t *testing.T) {
    89  		path, tearDown := setupConfigFile(t, testConfig)
    90  		defer tearDown()
    91  
    92  		fs, err := config.NewFileStore(path, false)
    93  		require.NoError(t, err)
    94  		defer fs.Close()
    95  
    96  		assert.Equal(t, "http://TestStoreNew", *fs.Get().ServiceSettings.SiteURL)
    97  		assertFileNotEqualsConfig(t, testConfig, path)
    98  	})
    99  
   100  	t.Run("absolute path, already minimally configured", func(t *testing.T) {
   101  		path, tearDown := setupConfigFile(t, minimalConfig)
   102  		defer tearDown()
   103  
   104  		fs, err := config.NewFileStore(path, false)
   105  		require.NoError(t, err)
   106  		defer fs.Close()
   107  
   108  		assert.Equal(t, "http://minimal", *fs.Get().ServiceSettings.SiteURL)
   109  		assertFileEqualsConfig(t, minimalConfig, path)
   110  	})
   111  
   112  	t.Run("absolute path, file does not exist", func(t *testing.T) {
   113  		_, tearDown := setupConfigFile(t, nil)
   114  		defer tearDown()
   115  
   116  		tempDir, err := ioutil.TempDir("", "TestFileStoreNew")
   117  		require.NoError(t, err)
   118  		defer os.RemoveAll(tempDir)
   119  
   120  		path := filepath.Join(tempDir, "does_not_exist")
   121  		fs, err := config.NewFileStore(path, false)
   122  		require.NoError(t, err)
   123  		defer fs.Close()
   124  
   125  		assert.Equal(t, "", *fs.Get().ServiceSettings.SiteURL)
   126  		assertFileNotEqualsConfig(t, testConfig, path)
   127  	})
   128  
   129  	t.Run("absolute path, path to file does not exist", func(t *testing.T) {
   130  		_, tearDown := setupConfigFile(t, nil)
   131  		defer tearDown()
   132  
   133  		tempDir, err := ioutil.TempDir("", "TestFileStoreNew")
   134  		require.NoError(t, err)
   135  		defer os.RemoveAll(tempDir)
   136  
   137  		path := filepath.Join(tempDir, "does/not/exist")
   138  		_, err = config.NewFileStore(path, false)
   139  		require.Error(t, err)
   140  	})
   141  
   142  	t.Run("relative path, file exists", func(t *testing.T) {
   143  		_, tearDown := setupConfigFile(t, nil)
   144  		defer tearDown()
   145  
   146  		err := os.MkdirAll("TestFileStoreNew/a/b/c", 0700)
   147  		require.NoError(t, err)
   148  		defer os.RemoveAll("TestFileStoreNew")
   149  
   150  		path := "TestFileStoreNew/a/b/c/config.json"
   151  
   152  		cfgData, err := config.MarshalConfig(testConfig)
   153  		require.NoError(t, err)
   154  
   155  		ioutil.WriteFile(path, cfgData, 0644)
   156  
   157  		fs, err := config.NewFileStore(path, false)
   158  		require.NoError(t, err)
   159  		defer fs.Close()
   160  
   161  		assert.Equal(t, "http://TestStoreNew", *fs.Get().ServiceSettings.SiteURL)
   162  		assertFileNotEqualsConfig(t, testConfig, path)
   163  	})
   164  
   165  	t.Run("relative path, file does not exist", func(t *testing.T) {
   166  		_, tearDown := setupConfigFile(t, nil)
   167  		defer tearDown()
   168  
   169  		err := os.MkdirAll("config/TestFileStoreNew/a/b/c", 0700)
   170  		require.NoError(t, err)
   171  		defer os.RemoveAll("config/TestFileStoreNew")
   172  
   173  		path := "TestFileStoreNew/a/b/c/config.json"
   174  		fs, err := config.NewFileStore(path, false)
   175  		require.NoError(t, err)
   176  		defer fs.Close()
   177  
   178  		assert.Equal(t, "", *fs.Get().ServiceSettings.SiteURL)
   179  		assertFileNotEqualsConfig(t, testConfig, filepath.Join("config", path))
   180  	})
   181  }
   182  
   183  func TestFileStoreGet(t *testing.T) {
   184  	path, tearDown := setupConfigFile(t, testConfig)
   185  	defer tearDown()
   186  
   187  	fs, err := config.NewFileStore(path, false)
   188  	require.NoError(t, err)
   189  	defer fs.Close()
   190  
   191  	cfg := fs.Get()
   192  	assert.Equal(t, "http://TestStoreNew", *cfg.ServiceSettings.SiteURL)
   193  
   194  	cfg2 := fs.Get()
   195  	assert.Equal(t, "http://TestStoreNew", *cfg.ServiceSettings.SiteURL)
   196  
   197  	assert.True(t, cfg == cfg2, "Get() returned different configuration instances")
   198  
   199  	newCfg := &model.Config{}
   200  	oldCfg, err := fs.Set(newCfg)
   201  	require.NoError(t, err)
   202  
   203  	assert.True(t, oldCfg == cfg, "returned config after set() changed original")
   204  	assert.False(t, newCfg == cfg, "returned config should have been different from original")
   205  }
   206  
   207  func TestFileStoreGetEnivironmentOverrides(t *testing.T) {
   208  	t.Run("get override for a string variable", func(t *testing.T) {
   209  		path, tearDown := setupConfigFile(t, testConfig)
   210  		defer tearDown()
   211  
   212  		fs, err := config.NewFileStore(path, false)
   213  		require.NoError(t, err)
   214  		defer fs.Close()
   215  
   216  		assert.Equal(t, "http://TestStoreNew", *fs.Get().ServiceSettings.SiteURL)
   217  		assert.Empty(t, fs.GetEnvironmentOverrides())
   218  
   219  		os.Setenv("MM_SERVICESETTINGS_SITEURL", "http://override")
   220  		defer os.Unsetenv("MM_SERVICESETTINGS_SITEURL")
   221  
   222  		fs, err = config.NewFileStore(path, false)
   223  		require.NoError(t, err)
   224  		defer fs.Close()
   225  
   226  		assert.Equal(t, "http://override", *fs.Get().ServiceSettings.SiteURL)
   227  		assert.Equal(t, map[string]interface{}{"ServiceSettings": map[string]interface{}{"SiteURL": true}}, fs.GetEnvironmentOverrides())
   228  	})
   229  
   230  	t.Run("get override for a bool variable", func(t *testing.T) {
   231  		path, tearDown := setupConfigFile(t, testConfig)
   232  		defer tearDown()
   233  
   234  		fs, err := config.NewFileStore(path, false)
   235  		require.NoError(t, err)
   236  		defer fs.Close()
   237  
   238  		assert.Equal(t, false, *fs.Get().PluginSettings.EnableUploads)
   239  		assert.Empty(t, fs.GetEnvironmentOverrides())
   240  
   241  		os.Setenv("MM_PLUGINSETTINGS_ENABLEUPLOADS", "true")
   242  		defer os.Unsetenv("MM_PLUGINSETTINGS_ENABLEUPLOADS")
   243  
   244  		fs, err = config.NewFileStore(path, false)
   245  		require.NoError(t, err)
   246  		defer fs.Close()
   247  
   248  		assert.Equal(t, true, *fs.Get().PluginSettings.EnableUploads)
   249  		assert.Equal(t, map[string]interface{}{"PluginSettings": map[string]interface{}{"EnableUploads": true}}, fs.GetEnvironmentOverrides())
   250  	})
   251  
   252  	t.Run("get override for an int variable", func(t *testing.T) {
   253  		path, tearDown := setupConfigFile(t, testConfig)
   254  		defer tearDown()
   255  
   256  		fs, err := config.NewFileStore(path, false)
   257  		require.NoError(t, err)
   258  		defer fs.Close()
   259  
   260  		assert.Equal(t, model.TEAM_SETTINGS_DEFAULT_MAX_USERS_PER_TEAM, *fs.Get().TeamSettings.MaxUsersPerTeam)
   261  		assert.Empty(t, fs.GetEnvironmentOverrides())
   262  
   263  		os.Setenv("MM_TEAMSETTINGS_MAXUSERSPERTEAM", "3000")
   264  		defer os.Unsetenv("MM_TEAMSETTINGS_MAXUSERSPERTEAM")
   265  
   266  		fs, err = config.NewFileStore(path, false)
   267  		require.NoError(t, err)
   268  		defer fs.Close()
   269  
   270  		assert.Equal(t, 3000, *fs.Get().TeamSettings.MaxUsersPerTeam)
   271  		assert.Equal(t, map[string]interface{}{"TeamSettings": map[string]interface{}{"MaxUsersPerTeam": true}}, fs.GetEnvironmentOverrides())
   272  	})
   273  
   274  	t.Run("get override for an int64 variable", func(t *testing.T) {
   275  		path, tearDown := setupConfigFile(t, testConfig)
   276  		defer tearDown()
   277  
   278  		fs, err := config.NewFileStore(path, false)
   279  		require.NoError(t, err)
   280  		defer fs.Close()
   281  
   282  		assert.Equal(t, int64(63072000), *fs.Get().ServiceSettings.TLSStrictTransportMaxAge)
   283  		assert.Empty(t, fs.GetEnvironmentOverrides())
   284  
   285  		os.Setenv("MM_SERVICESETTINGS_TLSSTRICTTRANSPORTMAXAGE", "123456")
   286  		defer os.Unsetenv("MM_SERVICESETTINGS_TLSSTRICTTRANSPORTMAXAGE")
   287  
   288  		fs, err = config.NewFileStore(path, false)
   289  		require.NoError(t, err)
   290  		defer fs.Close()
   291  
   292  		assert.Equal(t, int64(123456), *fs.Get().ServiceSettings.TLSStrictTransportMaxAge)
   293  		assert.Equal(t, map[string]interface{}{"ServiceSettings": map[string]interface{}{"TLSStrictTransportMaxAge": true}}, fs.GetEnvironmentOverrides())
   294  	})
   295  
   296  	t.Run("get override for a slice variable - one value", func(t *testing.T) {
   297  		path, tearDown := setupConfigFile(t, testConfig)
   298  		defer tearDown()
   299  
   300  		fs, err := config.NewFileStore(path, false)
   301  		require.NoError(t, err)
   302  		defer fs.Close()
   303  
   304  		assert.Equal(t, []string{}, fs.Get().SqlSettings.DataSourceReplicas)
   305  		assert.Empty(t, fs.GetEnvironmentOverrides())
   306  
   307  		os.Setenv("MM_SQLSETTINGS_DATASOURCEREPLICAS", "user:pwd@db:5432/test-db")
   308  		defer os.Unsetenv("MM_SQLSETTINGS_DATASOURCEREPLICAS")
   309  
   310  		fs, err = config.NewFileStore(path, false)
   311  		require.NoError(t, err)
   312  		defer fs.Close()
   313  
   314  		assert.Equal(t, []string{"user:pwd@db:5432/test-db"}, fs.Get().SqlSettings.DataSourceReplicas)
   315  		assert.Equal(t, map[string]interface{}{"SqlSettings": map[string]interface{}{"DataSourceReplicas": true}}, fs.GetEnvironmentOverrides())
   316  	})
   317  
   318  	t.Run("get override for a slice variable - three values", func(t *testing.T) {
   319  		// This should work, but Viper (or we) don't parse environment variables to turn strings with spaces into slices.
   320  		t.Skip("not implemented yet")
   321  
   322  		path, tearDown := setupConfigFile(t, testConfig)
   323  		defer tearDown()
   324  
   325  		fs, err := config.NewFileStore(path, false)
   326  		require.NoError(t, err)
   327  		defer fs.Close()
   328  
   329  		assert.Equal(t, []string{}, fs.Get().SqlSettings.DataSourceReplicas)
   330  		assert.Empty(t, fs.GetEnvironmentOverrides())
   331  
   332  		os.Setenv("MM_SQLSETTINGS_DATASOURCEREPLICAS", "user:pwd@db:5432/test-db user:pwd@db2:5433/test-db2 user:pwd@db3:5434/test-db3")
   333  		defer os.Unsetenv("MM_SQLSETTINGS_DATASOURCEREPLICAS")
   334  
   335  		fs, err = config.NewFileStore(path, false)
   336  		require.NoError(t, err)
   337  		defer fs.Close()
   338  
   339  		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)
   340  		assert.Equal(t, map[string]interface{}{"SqlSettings": map[string]interface{}{"DataSourceReplicas": true}}, fs.GetEnvironmentOverrides())
   341  	})
   342  }
   343  
   344  func TestFileStoreSet(t *testing.T) {
   345  	t.Run("set same pointer value", func(t *testing.T) {
   346  		t.Skip("not yet implemented")
   347  
   348  		path, tearDown := setupConfigFile(t, emptyConfig)
   349  		defer tearDown()
   350  
   351  		fs, err := config.NewFileStore(path, false)
   352  		require.NoError(t, err)
   353  		defer fs.Close()
   354  
   355  		_, err = fs.Set(fs.Get())
   356  		if assert.Error(t, err) {
   357  			assert.EqualError(t, err, "old configuration modified instead of cloning")
   358  		}
   359  	})
   360  
   361  	t.Run("defaults required", func(t *testing.T) {
   362  		path, tearDown := setupConfigFile(t, minimalConfig)
   363  		defer tearDown()
   364  
   365  		fs, err := config.NewFileStore(path, false)
   366  		require.NoError(t, err)
   367  		defer fs.Close()
   368  
   369  		oldCfg := fs.Get()
   370  
   371  		newCfg := &model.Config{}
   372  
   373  		retCfg, err := fs.Set(newCfg)
   374  		require.NoError(t, err)
   375  		assert.Equal(t, oldCfg, retCfg)
   376  
   377  		assert.Equal(t, "", *fs.Get().ServiceSettings.SiteURL)
   378  	})
   379  
   380  	t.Run("desanitization required", func(t *testing.T) {
   381  		path, tearDown := setupConfigFile(t, ldapConfig)
   382  		defer tearDown()
   383  
   384  		fs, err := config.NewFileStore(path, false)
   385  		require.NoError(t, err)
   386  		defer fs.Close()
   387  
   388  		oldCfg := fs.Get()
   389  
   390  		newCfg := &model.Config{}
   391  		newCfg.LdapSettings.BindPassword = sToP(model.FAKE_SETTING)
   392  
   393  		retCfg, err := fs.Set(newCfg)
   394  		require.NoError(t, err)
   395  		assert.Equal(t, oldCfg, retCfg)
   396  
   397  		assert.Equal(t, "password", *fs.Get().LdapSettings.BindPassword)
   398  	})
   399  
   400  	t.Run("invalid", func(t *testing.T) {
   401  		path, tearDown := setupConfigFile(t, emptyConfig)
   402  		defer tearDown()
   403  
   404  		fs, err := config.NewFileStore(path, false)
   405  		require.NoError(t, err)
   406  		defer fs.Close()
   407  
   408  		newCfg := &model.Config{}
   409  		newCfg.ServiceSettings.SiteURL = sToP("invalid")
   410  
   411  		_, err = fs.Set(newCfg)
   412  		if assert.Error(t, err) {
   413  			assert.EqualError(t, err, "new configuration is invalid: Config.IsValid: model.config.is_valid.site_url.app_error, ")
   414  		}
   415  
   416  		assert.Equal(t, "", *fs.Get().ServiceSettings.SiteURL)
   417  	})
   418  
   419  	t.Run("read-only", func(t *testing.T) {
   420  		path, tearDown := setupConfigFile(t, readOnlyConfig)
   421  		defer tearDown()
   422  
   423  		fs, err := config.NewFileStore(path, false)
   424  		require.NoError(t, err)
   425  		defer fs.Close()
   426  
   427  		newCfg := &model.Config{}
   428  
   429  		_, err = fs.Set(newCfg)
   430  		if assert.Error(t, err) {
   431  			assert.Equal(t, config.ErrReadOnlyConfiguration, errors.Cause(err))
   432  		}
   433  
   434  		assert.Equal(t, "", *fs.Get().ServiceSettings.SiteURL)
   435  	})
   436  
   437  	t.Run("persist failed", func(t *testing.T) {
   438  		t.Skip("skipping persistence test inside Set")
   439  		path, tearDown := setupConfigFile(t, emptyConfig)
   440  		defer tearDown()
   441  
   442  		fs, err := config.NewFileStore(path, false)
   443  		require.NoError(t, err)
   444  		defer fs.Close()
   445  
   446  		os.Chmod(path, 0500)
   447  
   448  		newCfg := &model.Config{}
   449  
   450  		_, err = fs.Set(newCfg)
   451  		if assert.Error(t, err) {
   452  			assert.True(t, strings.HasPrefix(err.Error(), "failed to persist: failed to write file"))
   453  		}
   454  
   455  		assert.Equal(t, "", *fs.Get().ServiceSettings.SiteURL)
   456  	})
   457  
   458  	t.Run("listeners notified", func(t *testing.T) {
   459  		path, tearDown := setupConfigFile(t, emptyConfig)
   460  		defer tearDown()
   461  
   462  		fs, err := config.NewFileStore(path, false)
   463  		require.NoError(t, err)
   464  		defer fs.Close()
   465  
   466  		oldCfg := fs.Get()
   467  
   468  		called := make(chan bool, 1)
   469  		callback := func(oldfg, newCfg *model.Config) {
   470  			called <- true
   471  		}
   472  		fs.AddListener(callback)
   473  
   474  		newCfg := &model.Config{}
   475  
   476  		retCfg, err := fs.Set(newCfg)
   477  		require.NoError(t, err)
   478  		assert.Equal(t, oldCfg, retCfg)
   479  
   480  		require.True(t, wasCalled(called, 5*time.Second), "callback should have been called when config written")
   481  	})
   482  
   483  	t.Run("watcher restarted", func(t *testing.T) {
   484  		if testing.Short() {
   485  			t.Skip("skipping watcher test in short mode")
   486  		}
   487  
   488  		path, tearDown := setupConfigFile(t, emptyConfig)
   489  		defer tearDown()
   490  
   491  		fs, err := config.NewFileStore(path, true)
   492  		require.NoError(t, err)
   493  		defer fs.Close()
   494  
   495  		_, err = fs.Set(&model.Config{})
   496  		require.NoError(t, err)
   497  
   498  		// Let the initial call to invokeConfigListeners finish.
   499  		time.Sleep(1 * time.Second)
   500  
   501  		called := make(chan bool, 1)
   502  		callback := func(oldfg, newCfg *model.Config) {
   503  			called <- true
   504  		}
   505  		fs.AddListener(callback)
   506  
   507  		// Rewrite the config to the file on disk
   508  		cfgData, err := config.MarshalConfig(emptyConfig)
   509  		require.NoError(t, err)
   510  
   511  		ioutil.WriteFile(path, cfgData, 0644)
   512  		require.True(t, wasCalled(called, 5*time.Second), "callback should have been called when config written")
   513  	})
   514  }
   515  
   516  func TestFileStoreLoad(t *testing.T) {
   517  	t.Run("file no longer exists", func(t *testing.T) {
   518  		path, tearDown := setupConfigFile(t, emptyConfig)
   519  		defer tearDown()
   520  
   521  		fs, err := config.NewFileStore(path, false)
   522  		require.NoError(t, err)
   523  		defer fs.Close()
   524  
   525  		os.Remove(path)
   526  
   527  		err = fs.Load()
   528  		require.NoError(t, err)
   529  		assertFileNotEqualsConfig(t, emptyConfig, path)
   530  	})
   531  
   532  	t.Run("honour environment", func(t *testing.T) {
   533  		path, tearDown := setupConfigFile(t, minimalConfig)
   534  		defer tearDown()
   535  
   536  		fs, err := config.NewFileStore(path, false)
   537  		require.NoError(t, err)
   538  		defer fs.Close()
   539  
   540  		assert.Equal(t, "http://minimal", *fs.Get().ServiceSettings.SiteURL)
   541  
   542  		os.Setenv("MM_SERVICESETTINGS_SITEURL", "http://override")
   543  		defer os.Unsetenv("MM_SERVICESETTINGS_SITEURL")
   544  
   545  		err = fs.Load()
   546  		require.NoError(t, err)
   547  		assert.Equal(t, "http://override", *fs.Get().ServiceSettings.SiteURL)
   548  		assert.Equal(t, map[string]interface{}{"ServiceSettings": map[string]interface{}{"SiteURL": true}}, fs.GetEnvironmentOverrides())
   549  	})
   550  
   551  	t.Run("do not persist environment variables - string", func(t *testing.T) {
   552  		path, tearDown := setupConfigFile(t, minimalConfig)
   553  		defer tearDown()
   554  
   555  		os.Setenv("MM_SERVICESETTINGS_SITEURL", "http://overridePersistEnvVariables")
   556  		defer os.Unsetenv("MM_SERVICESETTINGS_SITEURL")
   557  
   558  		fs, err := config.NewFileStore(path, false)
   559  		require.NoError(t, err)
   560  		defer fs.Close()
   561  
   562  		assert.Equal(t, "http://overridePersistEnvVariables", *fs.Get().ServiceSettings.SiteURL)
   563  
   564  		_, err = fs.Set(fs.Get())
   565  		require.NoError(t, err)
   566  
   567  		assert.Equal(t, "http://overridePersistEnvVariables", *fs.Get().ServiceSettings.SiteURL)
   568  		assert.Equal(t, map[string]interface{}{"ServiceSettings": map[string]interface{}{"SiteURL": true}}, fs.GetEnvironmentOverrides())
   569  		// check that on disk config does not include overwritten variable
   570  		actualConfig := getActualFileConfig(t, path)
   571  		assert.Equal(t, "http://minimal", *actualConfig.ServiceSettings.SiteURL)
   572  	})
   573  
   574  	t.Run("do not persist environment variables - boolean", func(t *testing.T) {
   575  		path, tearDown := setupConfigFile(t, minimalConfig)
   576  		defer tearDown()
   577  
   578  		os.Setenv("MM_PLUGINSETTINGS_ENABLEUPLOADS", "true")
   579  		defer os.Unsetenv("MM_PLUGINSETTINGS_ENABLEUPLOADS")
   580  
   581  		fs, err := config.NewFileStore(path, false)
   582  		require.NoError(t, err)
   583  		defer fs.Close()
   584  
   585  		assert.Equal(t, true, *fs.Get().PluginSettings.EnableUploads)
   586  
   587  		_, err = fs.Set(fs.Get())
   588  		require.NoError(t, err)
   589  
   590  		assert.Equal(t, true, *fs.Get().PluginSettings.EnableUploads)
   591  		assert.Equal(t, map[string]interface{}{"PluginSettings": map[string]interface{}{"EnableUploads": true}}, fs.GetEnvironmentOverrides())
   592  		// check that on disk config does not include overwritten variable
   593  		actualConfig := getActualFileConfig(t, path)
   594  		assert.Equal(t, false, *actualConfig.PluginSettings.EnableUploads)
   595  	})
   596  
   597  	t.Run("do not persist environment variables - int", func(t *testing.T) {
   598  		path, tearDown := setupConfigFile(t, minimalConfig)
   599  		defer tearDown()
   600  
   601  		os.Setenv("MM_TEAMSETTINGS_MAXUSERSPERTEAM", "3000")
   602  		defer os.Unsetenv("MM_TEAMSETTINGS_MAXUSERSPERTEAM")
   603  
   604  		fs, err := config.NewFileStore(path, false)
   605  		require.NoError(t, err)
   606  		defer fs.Close()
   607  
   608  		assert.Equal(t, 3000, *fs.Get().TeamSettings.MaxUsersPerTeam)
   609  
   610  		_, err = fs.Set(fs.Get())
   611  		require.NoError(t, err)
   612  
   613  		assert.Equal(t, 3000, *fs.Get().TeamSettings.MaxUsersPerTeam)
   614  		assert.Equal(t, map[string]interface{}{"TeamSettings": map[string]interface{}{"MaxUsersPerTeam": true}}, fs.GetEnvironmentOverrides())
   615  		// check that on disk config does not include overwritten variable
   616  		actualConfig := getActualFileConfig(t, path)
   617  		assert.Equal(t, model.TEAM_SETTINGS_DEFAULT_MAX_USERS_PER_TEAM, *actualConfig.TeamSettings.MaxUsersPerTeam)
   618  	})
   619  
   620  	t.Run("do not persist environment variables - int64", func(t *testing.T) {
   621  		path, tearDown := setupConfigFile(t, minimalConfig)
   622  		defer tearDown()
   623  
   624  		os.Setenv("MM_SERVICESETTINGS_TLSSTRICTTRANSPORTMAXAGE", "123456")
   625  		defer os.Unsetenv("MM_SERVICESETTINGS_TLSSTRICTTRANSPORTMAXAGE")
   626  
   627  		fs, err := config.NewFileStore(path, false)
   628  		require.NoError(t, err)
   629  		defer fs.Close()
   630  
   631  		assert.Equal(t, int64(123456), *fs.Get().ServiceSettings.TLSStrictTransportMaxAge)
   632  
   633  		_, err = fs.Set(fs.Get())
   634  		require.NoError(t, err)
   635  
   636  		assert.Equal(t, int64(123456), *fs.Get().ServiceSettings.TLSStrictTransportMaxAge)
   637  		assert.Equal(t, map[string]interface{}{"ServiceSettings": map[string]interface{}{"TLSStrictTransportMaxAge": true}}, fs.GetEnvironmentOverrides())
   638  		// check that on disk config does not include overwritten variable
   639  		actualConfig := getActualFileConfig(t, path)
   640  		assert.Equal(t, int64(63072000), *actualConfig.ServiceSettings.TLSStrictTransportMaxAge)
   641  	})
   642  
   643  	t.Run("do not persist environment variables - string slice beginning with default", func(t *testing.T) {
   644  		path, tearDown := setupConfigFile(t, minimalConfig)
   645  		defer tearDown()
   646  
   647  		os.Setenv("MM_SQLSETTINGS_DATASOURCEREPLICAS", "user:pwd@db:5432/test-db")
   648  		defer os.Unsetenv("MM_SQLSETTINGS_DATASOURCEREPLICAS")
   649  
   650  		fs, err := config.NewFileStore(path, false)
   651  		require.NoError(t, err)
   652  		defer fs.Close()
   653  
   654  		assert.Equal(t, []string{"user:pwd@db:5432/test-db"}, fs.Get().SqlSettings.DataSourceReplicas)
   655  
   656  		_, err = fs.Set(fs.Get())
   657  		require.NoError(t, err)
   658  
   659  		assert.Equal(t, []string{"user:pwd@db:5432/test-db"}, fs.Get().SqlSettings.DataSourceReplicas)
   660  		assert.Equal(t, map[string]interface{}{"SqlSettings": map[string]interface{}{"DataSourceReplicas": true}}, fs.GetEnvironmentOverrides())
   661  		// check that on disk config does not include overwritten variable
   662  		actualConfig := getActualFileConfig(t, path)
   663  		assert.Equal(t, []string{}, actualConfig.SqlSettings.DataSourceReplicas)
   664  	})
   665  
   666  	t.Run("do not persist environment variables - string slice beginning with slice of three", func(t *testing.T) {
   667  		modifiedMinimalConfig := minimalConfig.Clone()
   668  		modifiedMinimalConfig.SqlSettings.DataSourceReplicas = []string{"user:pwd@db:5432/test-db", "user:pwd@db2:5433/test-db2", "user:pwd@db3:5434/test-db3"}
   669  		path, tearDown := setupConfigFile(t, modifiedMinimalConfig)
   670  		defer tearDown()
   671  
   672  		os.Setenv("MM_SQLSETTINGS_DATASOURCEREPLICAS", "user:pwd@db:5432/test-db")
   673  		defer os.Unsetenv("MM_SQLSETTINGS_DATASOURCEREPLICAS")
   674  
   675  		fs, err := config.NewFileStore(path, false)
   676  		require.NoError(t, err)
   677  		defer fs.Close()
   678  
   679  		assert.Equal(t, []string{"user:pwd@db:5432/test-db"}, fs.Get().SqlSettings.DataSourceReplicas)
   680  
   681  		_, err = fs.Set(fs.Get())
   682  		require.NoError(t, err)
   683  
   684  		assert.Equal(t, []string{"user:pwd@db:5432/test-db"}, fs.Get().SqlSettings.DataSourceReplicas)
   685  		assert.Equal(t, map[string]interface{}{"SqlSettings": map[string]interface{}{"DataSourceReplicas": true}}, fs.GetEnvironmentOverrides())
   686  		// check that on disk config does not include overwritten variable
   687  		actualConfig := getActualFileConfig(t, path)
   688  		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)
   689  	})
   690  
   691  	t.Run("invalid", func(t *testing.T) {
   692  		path, tearDown := setupConfigFile(t, emptyConfig)
   693  		defer tearDown()
   694  
   695  		fs, err := config.NewFileStore(path, false)
   696  		require.NoError(t, err)
   697  		defer fs.Close()
   698  
   699  		cfgData, err := config.MarshalConfig(invalidConfig)
   700  		require.NoError(t, err)
   701  
   702  		ioutil.WriteFile(path, cfgData, 0644)
   703  
   704  		err = fs.Load()
   705  		if assert.Error(t, err) {
   706  			assert.EqualError(t, err, "invalid config: Config.IsValid: model.config.is_valid.site_url.app_error, ")
   707  		}
   708  	})
   709  
   710  	t.Run("fixes required", func(t *testing.T) {
   711  		path, tearDown := setupConfigFile(t, fixesRequiredConfig)
   712  		defer tearDown()
   713  
   714  		fs, err := config.NewFileStore(path, false)
   715  		require.NoError(t, err)
   716  		defer fs.Close()
   717  
   718  		err = fs.Load()
   719  		require.NoError(t, err)
   720  		assertFileNotEqualsConfig(t, fixesRequiredConfig, path)
   721  		assert.Equal(t, "http://trailingslash", *fs.Get().ServiceSettings.SiteURL)
   722  		assert.Equal(t, "/path/to/directory/", *fs.Get().FileSettings.Directory)
   723  		assert.Equal(t, "en", *fs.Get().LocalizationSettings.DefaultServerLocale)
   724  		assert.Equal(t, "en", *fs.Get().LocalizationSettings.DefaultClientLocale)
   725  	})
   726  
   727  	t.Run("listeners notifed", func(t *testing.T) {
   728  		path, tearDown := setupConfigFile(t, emptyConfig)
   729  		defer tearDown()
   730  
   731  		fs, err := config.NewFileStore(path, false)
   732  		require.NoError(t, err)
   733  		defer fs.Close()
   734  
   735  		called := make(chan bool, 1)
   736  		callback := func(oldfg, newCfg *model.Config) {
   737  			called <- true
   738  		}
   739  		fs.AddListener(callback)
   740  
   741  		err = fs.Load()
   742  		require.NoError(t, err)
   743  
   744  		require.True(t, wasCalled(called, 5*time.Second), "callback should have been called when config loaded")
   745  	})
   746  }
   747  
   748  func TestFileStoreWatcherEmitter(t *testing.T) {
   749  	if testing.Short() {
   750  		t.Skip("skipping watcher test in short mode")
   751  	}
   752  
   753  	t.Parallel()
   754  
   755  	path, tearDown := setupConfigFile(t, emptyConfig)
   756  	defer tearDown()
   757  
   758  	t.Run("disabled", func(t *testing.T) {
   759  		fs, err := config.NewFileStore(path, false)
   760  		require.NoError(t, err)
   761  		defer fs.Close()
   762  
   763  		// Let the initial call to invokeConfigListeners finish.
   764  		time.Sleep(1 * time.Second)
   765  
   766  		called := make(chan bool, 1)
   767  		callback := func(oldfg, newCfg *model.Config) {
   768  			called <- true
   769  		}
   770  		fs.AddListener(callback)
   771  
   772  		// Rewrite the config to the file on disk
   773  		cfgData, err := config.MarshalConfig(emptyConfig)
   774  		require.NoError(t, err)
   775  
   776  		ioutil.WriteFile(path, cfgData, 0644)
   777  		require.False(t, wasCalled(called, 1*time.Second), "callback should not have been called since watching disabled")
   778  	})
   779  
   780  	t.Run("enabled", func(t *testing.T) {
   781  		fs, err := config.NewFileStore(path, true)
   782  		require.NoError(t, err)
   783  		defer fs.Close()
   784  
   785  		called := make(chan bool, 1)
   786  		callback := func(oldfg, newCfg *model.Config) {
   787  			called <- true
   788  		}
   789  		fs.AddListener(callback)
   790  
   791  		// Rewrite the config to the file on disk
   792  		cfgData, err := config.MarshalConfig(emptyConfig)
   793  		require.NoError(t, err)
   794  
   795  		ioutil.WriteFile(path, cfgData, 0644)
   796  		require.True(t, wasCalled(called, 5*time.Second), "callback should have been called when config written")
   797  	})
   798  }
   799  
   800  func TestFileStoreSave(t *testing.T) {
   801  	path, tearDown := setupConfigFile(t, minimalConfig)
   802  	defer tearDown()
   803  
   804  	fs, err := config.NewFileStore(path, true)
   805  	require.NoError(t, err)
   806  	defer fs.Close()
   807  
   808  	newCfg := &model.Config{
   809  		ServiceSettings: model.ServiceSettings{
   810  			SiteURL: sToP("http://new"),
   811  		},
   812  	}
   813  
   814  	t.Run("set with automatic save", func(t *testing.T) {
   815  		_, err = fs.Set(newCfg)
   816  		require.NoError(t, err)
   817  
   818  		err = fs.Load()
   819  		require.NoError(t, err)
   820  
   821  		assert.Equal(t, "http://new", *fs.Get().ServiceSettings.SiteURL)
   822  	})
   823  }
   824  
   825  func TestFileGetFile(t *testing.T) {
   826  	path, tearDown := setupConfigFile(t, minimalConfig)
   827  	defer tearDown()
   828  
   829  	fs, err := config.NewFileStore(path, true)
   830  	require.NoError(t, err)
   831  	defer fs.Close()
   832  
   833  	t.Run("get empty filename", func(t *testing.T) {
   834  		_, err := fs.GetFile("")
   835  		require.Error(t, err)
   836  	})
   837  
   838  	t.Run("get non-existent file", func(t *testing.T) {
   839  		_, err := fs.GetFile("unknown")
   840  		require.Error(t, err)
   841  	})
   842  
   843  	t.Run("get empty file", func(t *testing.T) {
   844  		err := os.MkdirAll("config", 0700)
   845  		require.NoError(t, err)
   846  
   847  		f, err := ioutil.TempFile("config", "empty-file")
   848  		require.NoError(t, err)
   849  		defer os.Remove(f.Name())
   850  
   851  		err = ioutil.WriteFile(f.Name(), nil, 0777)
   852  		require.NoError(t, err)
   853  
   854  		data, err := fs.GetFile(f.Name())
   855  		require.NoError(t, err)
   856  		require.Empty(t, data)
   857  	})
   858  
   859  	t.Run("get non-empty file", func(t *testing.T) {
   860  		err := os.MkdirAll("config", 0700)
   861  		require.NoError(t, err)
   862  
   863  		f, err := ioutil.TempFile("config", "test-file")
   864  		require.NoError(t, err)
   865  		defer os.Remove(f.Name())
   866  
   867  		err = ioutil.WriteFile(f.Name(), []byte("test"), 0777)
   868  		require.NoError(t, err)
   869  
   870  		data, err := fs.GetFile(f.Name())
   871  		require.NoError(t, err)
   872  		require.Equal(t, []byte("test"), data)
   873  	})
   874  
   875  	t.Run("get via absolute path", func(t *testing.T) {
   876  		err := fs.SetFile("new", []byte("new file"))
   877  		require.NoError(t, err)
   878  
   879  		data, err := fs.GetFile(filepath.Join(filepath.Dir(path), "new"))
   880  
   881  		require.NoError(t, err)
   882  		require.Equal(t, []byte("new file"), data)
   883  	})
   884  
   885  }
   886  
   887  func TestFileSetFile(t *testing.T) {
   888  	path, tearDown := setupConfigFile(t, minimalConfig)
   889  	defer tearDown()
   890  
   891  	fs, err := config.NewFileStore(path, true)
   892  	require.NoError(t, err)
   893  	defer fs.Close()
   894  
   895  	t.Run("set new file", func(t *testing.T) {
   896  		err := fs.SetFile("new", []byte("new file"))
   897  		require.NoError(t, err)
   898  
   899  		data, err := fs.GetFile("new")
   900  		require.NoError(t, err)
   901  		require.Equal(t, []byte("new file"), data)
   902  	})
   903  
   904  	t.Run("overwrite existing file", func(t *testing.T) {
   905  		err := fs.SetFile("existing", []byte("existing file"))
   906  		require.NoError(t, err)
   907  
   908  		err = fs.SetFile("existing", []byte("overwritten file"))
   909  		require.NoError(t, err)
   910  
   911  		data, err := fs.GetFile("existing")
   912  		require.NoError(t, err)
   913  		require.Equal(t, []byte("overwritten file"), data)
   914  	})
   915  
   916  	t.Run("set via absolute path", func(t *testing.T) {
   917  		absolutePath := filepath.Join(filepath.Dir(path), "new")
   918  		err := fs.SetFile(absolutePath, []byte("new file"))
   919  		require.NoError(t, err)
   920  
   921  		data, err := fs.GetFile("new")
   922  
   923  		require.NoError(t, err)
   924  		require.Equal(t, []byte("new file"), data)
   925  	})
   926  
   927  	t.Run("should set right permissions", func(t *testing.T) {
   928  		absolutePath := filepath.Join(filepath.Dir(path), "new")
   929  		err := fs.SetFile(absolutePath, []byte("data"))
   930  		require.NoError(t, err)
   931  		fi, err := os.Stat(absolutePath)
   932  		require.NoError(t, err)
   933  		require.Equal(t, os.FileMode(0600), fi.Mode().Perm())
   934  	})
   935  }
   936  
   937  func TestFileHasFile(t *testing.T) {
   938  	t.Run("has non-existent", func(t *testing.T) {
   939  		path, tearDown := setupConfigFile(t, minimalConfig)
   940  		defer tearDown()
   941  
   942  		fs, err := config.NewFileStore(path, true)
   943  		require.NoError(t, err)
   944  		defer fs.Close()
   945  
   946  		has, err := fs.HasFile("non-existent")
   947  		require.NoError(t, err)
   948  		require.False(t, has)
   949  	})
   950  
   951  	t.Run("has existing", func(t *testing.T) {
   952  		path, tearDown := setupConfigFile(t, minimalConfig)
   953  		defer tearDown()
   954  
   955  		fs, err := config.NewFileStore(path, true)
   956  		require.NoError(t, err)
   957  		defer fs.Close()
   958  
   959  		err = fs.SetFile("existing", []byte("existing file"))
   960  		require.NoError(t, err)
   961  
   962  		has, err := fs.HasFile("existing")
   963  		require.NoError(t, err)
   964  		require.True(t, has)
   965  	})
   966  
   967  	t.Run("has manually created file", func(t *testing.T) {
   968  		path, tearDown := setupConfigFile(t, minimalConfig)
   969  		defer tearDown()
   970  
   971  		fs, err := config.NewFileStore(path, true)
   972  		require.NoError(t, err)
   973  		defer fs.Close()
   974  
   975  		err = os.MkdirAll("config", 0700)
   976  		require.NoError(t, err)
   977  
   978  		f, err := ioutil.TempFile("config", "test-file")
   979  		require.NoError(t, err)
   980  		defer os.Remove(f.Name())
   981  
   982  		err = ioutil.WriteFile(f.Name(), []byte("test"), 0777)
   983  		require.NoError(t, err)
   984  
   985  		has, err := fs.HasFile(f.Name())
   986  		require.NoError(t, err)
   987  		require.True(t, has)
   988  	})
   989  
   990  	t.Run("has empty string", func(t *testing.T) {
   991  		path, tearDown := setupConfigFile(t, minimalConfig)
   992  		defer tearDown()
   993  
   994  		fs, err := config.NewFileStore(path, true)
   995  		require.NoError(t, err)
   996  		defer fs.Close()
   997  
   998  		has, err := fs.HasFile("")
   999  		require.NoError(t, err)
  1000  		require.False(t, has)
  1001  	})
  1002  
  1003  	t.Run("has via absolute path", func(t *testing.T) {
  1004  		path, tearDown := setupConfigFile(t, minimalConfig)
  1005  		defer tearDown()
  1006  
  1007  		fs, err := config.NewFileStore(path, true)
  1008  		require.NoError(t, err)
  1009  		defer fs.Close()
  1010  
  1011  		err = fs.SetFile("existing", []byte("existing file"))
  1012  		require.NoError(t, err)
  1013  
  1014  		has, err := fs.HasFile(filepath.Join(filepath.Dir(path), "existing"))
  1015  		require.NoError(t, err)
  1016  		require.True(t, has)
  1017  	})
  1018  
  1019  }
  1020  
  1021  func TestFileRemoveFile(t *testing.T) {
  1022  	t.Run("remove non-existent", func(t *testing.T) {
  1023  		path, tearDown := setupConfigFile(t, minimalConfig)
  1024  		defer tearDown()
  1025  
  1026  		fs, err := config.NewFileStore(path, true)
  1027  		require.NoError(t, err)
  1028  		defer fs.Close()
  1029  
  1030  		err = fs.RemoveFile("non-existent")
  1031  		require.NoError(t, err)
  1032  	})
  1033  
  1034  	t.Run("remove existing", func(t *testing.T) {
  1035  		path, tearDown := setupConfigFile(t, minimalConfig)
  1036  		defer tearDown()
  1037  
  1038  		fs, err := config.NewFileStore(path, true)
  1039  		require.NoError(t, err)
  1040  		defer fs.Close()
  1041  
  1042  		err = fs.SetFile("existing", []byte("existing file"))
  1043  		require.NoError(t, err)
  1044  
  1045  		err = fs.RemoveFile("existing")
  1046  		require.NoError(t, err)
  1047  
  1048  		has, err := fs.HasFile("existing")
  1049  		require.NoError(t, err)
  1050  		require.False(t, has)
  1051  
  1052  		_, err = fs.GetFile("existing")
  1053  		require.Error(t, err)
  1054  	})
  1055  
  1056  	t.Run("remove manually created file", func(t *testing.T) {
  1057  		path, tearDown := setupConfigFile(t, minimalConfig)
  1058  		defer tearDown()
  1059  
  1060  		fs, err := config.NewFileStore(path, true)
  1061  		require.NoError(t, err)
  1062  		defer fs.Close()
  1063  
  1064  		err = os.MkdirAll("config", 0700)
  1065  		require.NoError(t, err)
  1066  
  1067  		f, err := ioutil.TempFile("config", "test-file")
  1068  		require.NoError(t, err)
  1069  		defer os.Remove(f.Name())
  1070  
  1071  		err = ioutil.WriteFile(f.Name(), []byte("test"), 0777)
  1072  		require.NoError(t, err)
  1073  
  1074  		err = fs.RemoveFile(f.Name())
  1075  		require.NoError(t, err)
  1076  
  1077  		has, err := fs.HasFile("existing")
  1078  		require.NoError(t, err)
  1079  		require.False(t, has)
  1080  
  1081  		_, err = fs.GetFile("existing")
  1082  		require.Error(t, err)
  1083  	})
  1084  
  1085  	t.Run("don't remove via absolute path", func(t *testing.T) {
  1086  		path, tearDown := setupConfigFile(t, minimalConfig)
  1087  		defer tearDown()
  1088  
  1089  		fs, err := config.NewFileStore(path, true)
  1090  		require.NoError(t, err)
  1091  		defer fs.Close()
  1092  
  1093  		err = fs.SetFile("existing", []byte("existing file"))
  1094  		require.NoError(t, err)
  1095  
  1096  		filename := filepath.Join(filepath.Dir(path), "existing")
  1097  		err = fs.RemoveFile(filename)
  1098  		require.NoError(t, err)
  1099  
  1100  		has, err := fs.HasFile(filename)
  1101  		require.NoError(t, err)
  1102  		require.True(t, has)
  1103  
  1104  	})
  1105  }
  1106  
  1107  func TestFileStoreString(t *testing.T) {
  1108  	path, tearDown := setupConfigFile(t, emptyConfig)
  1109  	defer tearDown()
  1110  
  1111  	fs, err := config.NewFileStore(path, false)
  1112  	require.NoError(t, err)
  1113  	defer fs.Close()
  1114  
  1115  	assert.Equal(t, "file://"+path, fs.String())
  1116  }
  1117  
  1118  // wasCalled reports whether a given callback channel was called
  1119  // within the specified time duration or not.
  1120  func wasCalled(c chan bool, duration time.Duration) bool {
  1121  	select {
  1122  	case <-c:
  1123  		return true
  1124  	case <-time.After(duration):
  1125  	}
  1126  	return false
  1127  }