github.com/mad-app/mattermost-server/v5@v5.100.1/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/mad-app/mattermost-server/v5/config" 19 "github.com/mad-app/mattermost-server/v5/model" 20 "github.com/mad-app/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(nil), 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 }