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 }