github.com/mattermost/mattermost-server/server/v8@v8.0.0-20230610055354-a6d1d38b273d/config/database_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 "bytes" 8 "encoding/json" 9 "errors" 10 "fmt" 11 "os" 12 "strings" 13 "testing" 14 "time" 15 16 "github.com/stretchr/testify/assert" 17 "github.com/stretchr/testify/require" 18 19 "github.com/mattermost/mattermost-server/server/public/model" 20 ) 21 22 func getDsn(driver string, source string) string { 23 if driver == model.DatabaseDriverMysql { 24 return driver + "://" + source 25 } 26 return source 27 } 28 29 func setupConfigDatabase(t *testing.T, cfg *model.Config, files map[string][]byte) (string, func()) { 30 if testing.Short() { 31 t.SkipNow() 32 } 33 t.Helper() 34 os.Clearenv() 35 truncateTables(t) 36 37 cfgData, err := marshalConfig(cfg) 38 require.NoError(t, err) 39 40 ds := &DatabaseStore{ 41 driverName: *mainHelper.GetSQLSettings().DriverName, 42 db: mainHelper.GetSQLStore().GetMasterX().DB, 43 dataSourceName: *mainHelper.Settings.DataSource, 44 } 45 46 err = ds.initializeConfigurationsTable() 47 require.NoError(t, err) 48 49 id := model.NewId() 50 _, err = ds.db.NamedExec("INSERT INTO Configurations (Id, Value, CreateAt, Active) VALUES(:Id, :Value, :CreateAt, TRUE)", map[string]any{ 51 "Id": id, 52 "Value": cfgData, 53 "CreateAt": model.GetMillis(), 54 }) 55 require.NoError(t, err) 56 57 for name, data := range files { 58 params := map[string]any{ 59 "name": name, 60 "data": data, 61 "create_at": model.GetMillis(), 62 "update_at": model.GetMillis(), 63 } 64 65 _, err = ds.db.NamedExec("INSERT INTO ConfigurationFiles (Name, Data, CreateAt, UpdateAt) VALUES (:name, :data, :create_at, :update_at)", params) 66 require.NoError(t, err) 67 } 68 69 return id, func() { 70 truncateTables(t) 71 } 72 } 73 74 // getActualDatabaseConfig returns the active configuration in the database without relying on a config store. 75 func getActualDatabaseConfig(t *testing.T) (string, *model.Config) { 76 t.Helper() 77 78 if *mainHelper.GetSQLSettings().DriverName == "postgres" { 79 var actual struct { 80 ID string `db:"id"` 81 Value []byte `db:"value"` 82 } 83 err := mainHelper.GetSQLStore().GetMasterX().Get(&actual, "SELECT Id, Value FROM Configurations WHERE Active") 84 require.NoError(t, err) 85 86 var actualCfg *model.Config 87 err = json.Unmarshal(actual.Value, &actualCfg) 88 require.NoError(t, err) 89 return actual.ID, actualCfg 90 } 91 var actual struct { 92 ID string `db:"Id"` 93 Value []byte `db:"Value"` 94 } 95 err := mainHelper.GetSQLStore().GetMasterX().Get(&actual, "SELECT Id, Value FROM Configurations WHERE Active") 96 require.NoError(t, err) 97 98 var actualCfg *model.Config 99 err = json.Unmarshal(actual.Value, &actualCfg) 100 require.NoError(t, err) 101 return actual.ID, actualCfg 102 } 103 104 // assertDatabaseEqualsConfig verifies the active in-database configuration equals the given config. 105 func assertDatabaseEqualsConfig(t *testing.T, expectedCfg *model.Config) { 106 t.Helper() 107 108 _, actualCfg := getActualDatabaseConfig(t) 109 assert.Equal(t, expectedCfg, actualCfg) 110 } 111 112 // assertDatabaseNotEqualsConfig verifies the in-database configuration does not equal the given config. 113 func assertDatabaseNotEqualsConfig(t *testing.T, expectedCfg *model.Config) { 114 t.Helper() 115 116 _, actualCfg := getActualDatabaseConfig(t) 117 assert.NotEqual(t, expectedCfg, actualCfg) 118 } 119 120 func newTestDatabaseStore(customDefaults *model.Config) (*Store, error) { 121 sqlSettings := mainHelper.GetSQLSettings() 122 dss, err := NewDatabaseStore(getDsn(*sqlSettings.DriverName, *sqlSettings.DataSource)) 123 if err != nil { 124 return nil, err 125 } 126 127 cStore, err := NewStoreFromBacking(dss, customDefaults, false) 128 if err != nil { 129 return nil, err 130 } 131 132 return cStore, nil 133 } 134 135 func TestDatabaseStoreNew(t *testing.T) { 136 if testing.Short() { 137 t.SkipNow() 138 } 139 sqlSettings := mainHelper.GetSQLSettings() 140 141 t.Run("no existing configuration - initialization required", func(t *testing.T) { 142 ds, err := newTestDatabaseStore(nil) 143 require.NoError(t, err) 144 defer ds.Close() 145 146 assert.Equal(t, "", *ds.Get().ServiceSettings.SiteURL) 147 }) 148 149 t.Run("no existing configuration with custom defaults", func(t *testing.T) { 150 truncateTables(t) 151 ds, err := newTestDatabaseStore(customConfigDefaults) 152 require.NoError(t, err) 153 defer ds.Close() 154 155 assert.Equal(t, *customConfigDefaults.ServiceSettings.SiteURL, *ds.Get().ServiceSettings.SiteURL) 156 assert.Equal(t, *customConfigDefaults.DisplaySettings.ExperimentalTimezone, *ds.Get().DisplaySettings.ExperimentalTimezone) 157 }) 158 159 t.Run("existing config, initialization required", func(t *testing.T) { 160 _, tearDown := setupConfigDatabase(t, testConfig, nil) 161 defer tearDown() 162 163 ds, err := newTestDatabaseStore(nil) 164 require.NoError(t, err) 165 defer ds.Close() 166 167 assert.Equal(t, "http://TestStoreNew", *ds.Get().ServiceSettings.SiteURL) 168 assertDatabaseNotEqualsConfig(t, testConfig) 169 }) 170 171 t.Run("existing config with custom defaults, initialization required", func(t *testing.T) { 172 _, tearDown := setupConfigDatabase(t, testConfig, nil) 173 defer tearDown() 174 175 ds, err := newTestDatabaseStore(customConfigDefaults) 176 require.NoError(t, err) 177 defer ds.Close() 178 179 // already existing value should not be overwritten by the 180 // custom default value 181 assert.Equal(t, "http://TestStoreNew", *ds.Get().ServiceSettings.SiteURL) 182 // not existing value should be overwritten by the custom 183 // default value 184 assert.Equal(t, *customConfigDefaults.DisplaySettings.ExperimentalTimezone, *ds.Get().DisplaySettings.ExperimentalTimezone) 185 assertDatabaseNotEqualsConfig(t, testConfig) 186 }) 187 188 t.Run("already minimally configured", func(t *testing.T) { 189 _, tearDown := setupConfigDatabase(t, minimalConfigNoFF, nil) 190 defer tearDown() 191 192 ds, err := newTestDatabaseStore(nil) 193 require.NoError(t, err) 194 defer ds.Close() 195 196 assert.Equal(t, "http://minimal", *ds.Get().ServiceSettings.SiteURL) 197 assertDatabaseEqualsConfig(t, minimalConfigNoFF) 198 }) 199 200 t.Run("already minimally configured with custom defaults", func(t *testing.T) { 201 _, tearDown := setupConfigDatabase(t, minimalConfigNoFF, nil) 202 defer tearDown() 203 204 ds, err := newTestDatabaseStore(customConfigDefaults) 205 require.NoError(t, err) 206 defer ds.Close() 207 208 // as the whole config has default values already, custom 209 // defaults should have no effect 210 assert.Equal(t, "http://minimal", *ds.Get().ServiceSettings.SiteURL) 211 assert.NotEqual(t, *customConfigDefaults.DisplaySettings.ExperimentalTimezone, *ds.Get().DisplaySettings.ExperimentalTimezone) 212 assertDatabaseEqualsConfig(t, minimalConfigNoFF) 213 }) 214 215 t.Run("invalid url", func(t *testing.T) { 216 _, err := NewDatabaseStore("") 217 require.Error(t, err) 218 219 _, err = NewDatabaseStore("mysql") 220 require.Error(t, err) 221 }) 222 223 t.Run("unsupported scheme", func(t *testing.T) { 224 _, err := NewDatabaseStore("invalid") 225 require.Error(t, err) 226 }) 227 228 t.Run("unsupported scheme with valid data source", func(t *testing.T) { 229 _, err := NewDatabaseStore(fmt.Sprintf("invalid://%s", *sqlSettings.DataSource)) 230 require.Error(t, err) 231 }) 232 } 233 234 func TestDatabaseStoreGet(t *testing.T) { 235 _, tearDown := setupConfigDatabase(t, testConfig, nil) 236 defer tearDown() 237 238 ds, err := newTestDatabaseStore(nil) 239 require.NoError(t, err) 240 defer ds.Close() 241 242 cfg := ds.Get() 243 assert.Equal(t, "http://TestStoreNew", *cfg.ServiceSettings.SiteURL) 244 245 cfg2 := ds.Get() 246 assert.Equal(t, "http://TestStoreNew", *cfg.ServiceSettings.SiteURL) 247 248 assert.True(t, cfg == cfg2, "Get() returned different configuration instances") 249 } 250 251 func TestDatabaseStoreGetEnvironmentOverrides(t *testing.T) { 252 t.Run("get override for a string variable", func(t *testing.T) { 253 _, tearDown := setupConfigDatabase(t, testConfig, nil) 254 defer tearDown() 255 256 ds, err := newTestDatabaseStore(nil) 257 require.NoError(t, err) 258 defer ds.Close() 259 260 assert.Equal(t, "http://TestStoreNew", *ds.Get().ServiceSettings.SiteURL) 261 assert.Empty(t, ds.GetEnvironmentOverrides()) 262 263 os.Setenv("MM_SERVICESETTINGS_SITEURL", "http://override") 264 defer os.Unsetenv("MM_SERVICESETTINGS_SITEURL") 265 266 ds, err = newTestDatabaseStore(nil) 267 require.NoError(t, err) 268 defer ds.Close() 269 270 assert.Equal(t, "http://override", *ds.Get().ServiceSettings.SiteURL) 271 assert.Equal(t, map[string]any{"ServiceSettings": map[string]any{"SiteURL": true}}, ds.GetEnvironmentOverrides()) 272 }) 273 274 t.Run("get override for a string variable with a custom default value", func(t *testing.T) { 275 _, tearDown := setupConfigDatabase(t, testConfig, nil) 276 defer tearDown() 277 278 ds, err := newTestDatabaseStore(customConfigDefaults) 279 require.NoError(t, err) 280 defer ds.Close() 281 282 assert.Equal(t, "http://TestStoreNew", *ds.Get().ServiceSettings.SiteURL) 283 assert.Empty(t, ds.GetEnvironmentOverrides()) 284 285 os.Setenv("MM_SERVICESETTINGS_SITEURL", "http://override") 286 defer os.Unsetenv("MM_SERVICESETTINGS_SITEURL") 287 288 ds, err = newTestDatabaseStore(customConfigDefaults) 289 require.NoError(t, err) 290 defer ds.Close() 291 292 // environment override should take priority over the custom default value 293 assert.Equal(t, "http://override", *ds.Get().ServiceSettings.SiteURL) 294 assert.Equal(t, map[string]any{"ServiceSettings": map[string]any{"SiteURL": true}}, ds.GetEnvironmentOverrides()) 295 }) 296 297 t.Run("get override for a bool variable", func(t *testing.T) { 298 _, tearDown := setupConfigDatabase(t, testConfig, nil) 299 defer tearDown() 300 301 ds, err := newTestDatabaseStore(nil) 302 require.NoError(t, err) 303 defer ds.Close() 304 305 assert.Equal(t, false, *ds.Get().PluginSettings.EnableUploads) 306 assert.Empty(t, ds.GetEnvironmentOverrides()) 307 308 os.Setenv("MM_PLUGINSETTINGS_ENABLEUPLOADS", "true") 309 defer os.Unsetenv("MM_PLUGINSETTINGS_ENABLEUPLOADS") 310 311 ds, err = newTestDatabaseStore(nil) 312 require.NoError(t, err) 313 defer ds.Close() 314 315 assert.Equal(t, true, *ds.Get().PluginSettings.EnableUploads) 316 assert.Equal(t, map[string]any{"PluginSettings": map[string]any{"EnableUploads": true}}, ds.GetEnvironmentOverrides()) 317 }) 318 319 t.Run("get override for an int variable", func(t *testing.T) { 320 _, tearDown := setupConfigDatabase(t, testConfig, nil) 321 defer tearDown() 322 323 ds, err := newTestDatabaseStore(nil) 324 require.NoError(t, err) 325 defer ds.Close() 326 327 assert.Equal(t, model.TeamSettingsDefaultMaxUsersPerTeam, *ds.Get().TeamSettings.MaxUsersPerTeam) 328 assert.Empty(t, ds.GetEnvironmentOverrides()) 329 330 os.Setenv("MM_TEAMSETTINGS_MAXUSERSPERTEAM", "3000") 331 defer os.Unsetenv("MM_TEAMSETTINGS_MAXUSERSPERTEAM") 332 333 ds, err = newTestDatabaseStore(nil) 334 require.NoError(t, err) 335 defer ds.Close() 336 337 assert.Equal(t, 3000, *ds.Get().TeamSettings.MaxUsersPerTeam) 338 assert.Equal(t, map[string]any{"TeamSettings": map[string]any{"MaxUsersPerTeam": true}}, ds.GetEnvironmentOverrides()) 339 }) 340 341 t.Run("get override for an int64 variable", func(t *testing.T) { 342 _, tearDown := setupConfigDatabase(t, testConfig, nil) 343 defer tearDown() 344 345 ds, err := newTestDatabaseStore(nil) 346 require.NoError(t, err) 347 defer ds.Close() 348 349 assert.Equal(t, int64(63072000), *ds.Get().ServiceSettings.TLSStrictTransportMaxAge) 350 assert.Empty(t, ds.GetEnvironmentOverrides()) 351 352 os.Setenv("MM_SERVICESETTINGS_TLSSTRICTTRANSPORTMAXAGE", "123456") 353 defer os.Unsetenv("MM_SERVICESETTINGS_TLSSTRICTTRANSPORTMAXAGE") 354 355 ds, err = newTestDatabaseStore(nil) 356 require.NoError(t, err) 357 defer ds.Close() 358 359 assert.Equal(t, int64(123456), *ds.Get().ServiceSettings.TLSStrictTransportMaxAge) 360 assert.Equal(t, map[string]any{"ServiceSettings": map[string]any{"TLSStrictTransportMaxAge": true}}, ds.GetEnvironmentOverrides()) 361 }) 362 363 t.Run("get override for a slice variable - one value", func(t *testing.T) { 364 _, tearDown := setupConfigDatabase(t, testConfig, nil) 365 defer tearDown() 366 367 ds, err := newTestDatabaseStore(nil) 368 require.NoError(t, err) 369 defer ds.Close() 370 371 assert.Equal(t, []string{}, ds.Get().SqlSettings.DataSourceReplicas) 372 assert.Empty(t, ds.GetEnvironmentOverrides()) 373 374 os.Setenv("MM_SQLSETTINGS_DATASOURCEREPLICAS", "user:pwd@db:5432/test-db") 375 defer os.Unsetenv("MM_SQLSETTINGS_DATASOURCEREPLICAS") 376 377 ds, err = newTestDatabaseStore(nil) 378 require.NoError(t, err) 379 defer ds.Close() 380 381 assert.Equal(t, []string{"user:pwd@db:5432/test-db"}, ds.Get().SqlSettings.DataSourceReplicas) 382 assert.Equal(t, map[string]any{"SqlSettings": map[string]any{"DataSourceReplicas": true}}, ds.GetEnvironmentOverrides()) 383 }) 384 385 t.Run("get override for a slice variable - three values", func(t *testing.T) { 386 // This should work, but Viper (or we) don't parse environment variables to turn strings with spaces into slices. 387 t.Skip("not implemented yet") 388 389 _, tearDown := setupConfigDatabase(t, testConfig, nil) 390 defer tearDown() 391 392 ds, err := newTestDatabaseStore(nil) 393 require.NoError(t, err) 394 defer ds.Close() 395 396 assert.Equal(t, []string{}, ds.Get().SqlSettings.DataSourceReplicas) 397 assert.Empty(t, ds.GetEnvironmentOverrides()) 398 399 os.Setenv("MM_SQLSETTINGS_DATASOURCEREPLICAS", "user:pwd@db:5432/test-db user:pwd@db2:5433/test-db2 user:pwd@db3:5434/test-db3") 400 defer os.Unsetenv("MM_SQLSETTINGS_DATASOURCEREPLICAS") 401 402 ds, err = newTestDatabaseStore(nil) 403 require.NoError(t, err) 404 defer ds.Close() 405 406 assert.Equal(t, []string{"user:pwd@db:5432/test-db", "user:pwd@db2:5433/test-db2", "user:pwd@db3:5434/test-db3"}, ds.Get().SqlSettings.DataSourceReplicas) 407 assert.Equal(t, map[string]any{"SqlSettings": map[string]any{"DataSourceReplicas": true}}, ds.GetEnvironmentOverrides()) 408 }) 409 } 410 411 func TestDatabaseStoreSet(t *testing.T) { 412 if testing.Short() { 413 t.SkipNow() 414 } 415 416 t.Run("set same pointer value", func(t *testing.T) { 417 t.Skip("not yet implemented") 418 419 _, tearDown := setupConfigDatabase(t, emptyConfig, nil) 420 defer tearDown() 421 422 ds, err := newTestDatabaseStore(nil) 423 require.NoError(t, err) 424 defer ds.Close() 425 426 _, _, err = ds.Set(ds.Get()) 427 if assert.Error(t, err) { 428 assert.EqualError(t, err, "old configuration modified instead of cloning") 429 } 430 }) 431 432 t.Run("defaults required", func(t *testing.T) { 433 _, tearDown := setupConfigDatabase(t, minimalConfig, nil) 434 defer tearDown() 435 436 ds, err := newTestDatabaseStore(nil) 437 require.NoError(t, err) 438 defer ds.Close() 439 440 newCfg := &model.Config{} 441 442 _, _, err = ds.Set(newCfg) 443 require.NoError(t, err) 444 445 assert.Equal(t, "", *ds.Get().ServiceSettings.SiteURL) 446 }) 447 448 t.Run("desanitization required", func(t *testing.T) { 449 _, tearDown := setupConfigDatabase(t, ldapConfig, nil) 450 defer tearDown() 451 452 ds, err := newTestDatabaseStore(nil) 453 require.NoError(t, err) 454 defer ds.Close() 455 456 newCfg := &model.Config{} 457 newCfg.LdapSettings.BindPassword = model.NewString(model.FakeSetting) 458 459 _, _, err = ds.Set(newCfg) 460 require.NoError(t, err) 461 462 assert.Equal(t, "password", *ds.Get().LdapSettings.BindPassword) 463 }) 464 465 t.Run("invalid", func(t *testing.T) { 466 _, tearDown := setupConfigDatabase(t, emptyConfig, nil) 467 defer tearDown() 468 469 ds, err := newTestDatabaseStore(nil) 470 require.NoError(t, err) 471 defer ds.Close() 472 473 newCfg := &model.Config{} 474 newCfg.ServiceSettings.SiteURL = model.NewString("invalid") 475 476 _, _, err = ds.Set(newCfg) 477 if assert.Error(t, err) { 478 assert.EqualError(t, err, "new configuration is invalid: Config.IsValid: model.config.is_valid.site_url.app_error, parse \"invalid\": invalid URI for request") 479 } 480 481 assert.Equal(t, "", *ds.Get().ServiceSettings.SiteURL) 482 }) 483 484 t.Run("duplicate ignored", func(t *testing.T) { 485 _, tearDown := setupConfigDatabase(t, minimalConfig, nil) 486 defer tearDown() 487 488 ds, err := newTestDatabaseStore(nil) 489 require.NoError(t, err) 490 defer ds.Close() 491 492 _, _, err = ds.Set(ds.Get()) 493 require.NoError(t, err) 494 495 beforeID, _ := getActualDatabaseConfig(t) 496 _, _, err = ds.Set(ds.Get()) 497 require.NoError(t, err) 498 499 afterID, _ := getActualDatabaseConfig(t) 500 assert.Equal(t, beforeID, afterID, "new record should not have been written") 501 }) 502 503 t.Run("read-only ignored", func(t *testing.T) { 504 _, tearDown := setupConfigDatabase(t, readOnlyConfig, nil) 505 defer tearDown() 506 507 ds, err := newTestDatabaseStore(nil) 508 require.NoError(t, err) 509 defer ds.Close() 510 511 newCfg := &model.Config{ 512 ServiceSettings: model.ServiceSettings{ 513 SiteURL: model.NewString("http://new"), 514 }, 515 } 516 517 _, _, err = ds.Set(newCfg) 518 require.NoError(t, err) 519 520 assert.Equal(t, "http://new", *ds.Get().ServiceSettings.SiteURL) 521 }) 522 523 t.Run("set with automatic save", func(t *testing.T) { 524 _, tearDown := setupConfigDatabase(t, minimalConfig, nil) 525 defer tearDown() 526 527 ds, err := newTestDatabaseStore(nil) 528 require.NoError(t, err) 529 defer ds.Close() 530 531 newCfg := &model.Config{ 532 ServiceSettings: model.ServiceSettings{ 533 SiteURL: model.NewString("http://new"), 534 }, 535 } 536 537 _, _, err = ds.Set(newCfg) 538 require.NoError(t, err) 539 540 err = ds.Load() 541 require.NoError(t, err) 542 543 assert.Equal(t, "http://new", *ds.Get().ServiceSettings.SiteURL) 544 }) 545 546 t.Run("persist failed", func(t *testing.T) { 547 _, tearDown := setupConfigDatabase(t, emptyConfig, nil) 548 defer tearDown() 549 550 ds, err := newTestDatabaseStore(nil) 551 require.NoError(t, err) 552 defer ds.Close() 553 554 _, err = mainHelper.GetSQLStore().GetMasterX().Exec("DROP TABLE Configurations") 555 require.NoError(t, err) 556 557 newCfg := minimalConfig 558 559 _, _, err = ds.Set(newCfg) 560 require.Error(t, err) 561 assert.True(t, strings.HasPrefix(err.Error(), "failed to persist: failed to query active configuration"), "unexpected error: "+err.Error()) 562 563 assert.Equal(t, "", *ds.Get().ServiceSettings.SiteURL) 564 }) 565 566 t.Run("persist failed: too long", func(t *testing.T) { 567 if *mainHelper.Settings.DriverName == "postgres" { 568 t.Skip("No limit for postgres") 569 } 570 _, tearDown := setupConfigDatabase(t, emptyConfig, nil) 571 defer tearDown() 572 573 ds, err := newTestDatabaseStore(nil) 574 require.NoError(t, err) 575 defer ds.Close() 576 577 longSiteURL := fmt.Sprintf("http://%s", strings.Repeat("a", MaxWriteLength)) 578 newCfg := emptyConfig.Clone() 579 newCfg.ServiceSettings.SiteURL = model.NewString(longSiteURL) 580 581 _, _, err = ds.Set(newCfg) 582 require.Error(t, err) 583 assert.True(t, strings.HasPrefix(err.Error(), "failed to persist: marshalled configuration failed length check: value is too long"), "unexpected error: "+err.Error()) 584 }) 585 586 t.Run("listeners notified", func(t *testing.T) { 587 activeID, tearDown := setupConfigDatabase(t, emptyConfig, nil) 588 defer tearDown() 589 590 ds, err := newTestDatabaseStore(nil) 591 require.NoError(t, err) 592 defer ds.Close() 593 594 called := make(chan bool, 1) 595 callback := func(oldfg, newCfg *model.Config) { 596 called <- true 597 } 598 ds.AddListener(callback) 599 600 newCfg := minimalConfig 601 602 _, _, err = ds.Set(newCfg) 603 require.NoError(t, err) 604 605 id, _ := getActualDatabaseConfig(t) 606 assert.NotEqual(t, activeID, id, "new record should have been written") 607 608 require.True(t, wasCalled(called, 5*time.Second), "callback should have been called when config written") 609 }) 610 611 t.Run("setting config without persistent feature flag", func(t *testing.T) { 612 _, tearDown := setupConfigDatabase(t, minimalConfig, nil) 613 defer tearDown() 614 615 ds, err := newTestDatabaseStore(nil) 616 require.NoError(t, err) 617 defer ds.Close() 618 619 ds.SetReadOnlyFF(true) 620 _, _, err = ds.Set(minimalConfig) 621 require.NoError(t, err) 622 623 assert.Equal(t, "http://minimal", *ds.Get().ServiceSettings.SiteURL) 624 assertDatabaseEqualsConfig(t, minimalConfigNoFF) 625 }) 626 627 t.Run("setting config with persistent feature flags", func(t *testing.T) { 628 _, tearDown := setupConfigDatabase(t, minimalConfig, nil) 629 defer tearDown() 630 631 ds, err := newTestDatabaseStore(nil) 632 require.NoError(t, err) 633 defer ds.Close() 634 635 ds.SetReadOnlyFF(false) 636 637 _, _, err = ds.Set(minimalConfig) 638 require.NoError(t, err) 639 640 assert.Equal(t, "http://minimal", *ds.Get().ServiceSettings.SiteURL) 641 assertDatabaseEqualsConfig(t, minimalConfig) 642 }) 643 644 } 645 646 func TestDatabaseStoreLoad(t *testing.T) { 647 if testing.Short() { 648 t.SkipNow() 649 } 650 651 t.Run("active configuration no longer exists", func(t *testing.T) { 652 _, tearDown := setupConfigDatabase(t, emptyConfig, nil) 653 defer tearDown() 654 655 ds, err := newTestDatabaseStore(nil) 656 require.NoError(t, err) 657 defer ds.Close() 658 659 truncateTables(t) 660 661 err = ds.Load() 662 require.NoError(t, err) 663 assertDatabaseNotEqualsConfig(t, emptyConfig) 664 }) 665 666 t.Run("honour environment", func(t *testing.T) { 667 _, tearDown := setupConfigDatabase(t, minimalConfig, nil) 668 defer tearDown() 669 670 ds, err := newTestDatabaseStore(nil) 671 require.NoError(t, err) 672 defer ds.Close() 673 674 assert.Equal(t, "http://minimal", *ds.Get().ServiceSettings.SiteURL) 675 676 os.Setenv("MM_SERVICESETTINGS_SITEURL", "http://override") 677 defer os.Unsetenv("MM_SERVICESETTINGS_SITEURL") 678 679 err = ds.Load() 680 require.NoError(t, err) 681 assert.Equal(t, "http://override", *ds.Get().ServiceSettings.SiteURL) 682 assert.Equal(t, map[string]any{"ServiceSettings": map[string]any{"SiteURL": true}}, ds.GetEnvironmentOverrides()) 683 }) 684 685 t.Run("do not persist environment variables - string", func(t *testing.T) { 686 _, tearDown := setupConfigDatabase(t, minimalConfig, nil) 687 defer tearDown() 688 689 os.Setenv("MM_SERVICESETTINGS_SITEURL", "http://overridePersistEnvVariables") 690 defer os.Unsetenv("MM_SERVICESETTINGS_SITEURL") 691 692 ds, err := newTestDatabaseStore(nil) 693 require.NoError(t, err) 694 defer ds.Close() 695 696 _, _, err = ds.Set(ds.Get()) 697 require.NoError(t, err) 698 699 assert.Equal(t, "http://overridePersistEnvVariables", *ds.Get().ServiceSettings.SiteURL) 700 assert.Equal(t, map[string]any{"ServiceSettings": map[string]any{"SiteURL": true}}, ds.GetEnvironmentOverrides()) 701 // check that in DB config does not include overwritten variable 702 _, actualConfig := getActualDatabaseConfig(t) 703 assert.Equal(t, "http://minimal", *actualConfig.ServiceSettings.SiteURL) 704 }) 705 706 t.Run("do not persist environment variables - boolean", func(t *testing.T) { 707 _, tearDown := setupConfigDatabase(t, minimalConfig, nil) 708 defer tearDown() 709 710 os.Setenv("MM_PLUGINSETTINGS_ENABLEUPLOADS", "true") 711 defer os.Unsetenv("MM_PLUGINSETTINGS_ENABLEUPLOADS") 712 713 ds, err := newTestDatabaseStore(nil) 714 require.NoError(t, err) 715 defer ds.Close() 716 717 assert.Equal(t, true, *ds.Get().PluginSettings.EnableUploads) 718 719 _, _, err = ds.Set(ds.Get()) 720 require.NoError(t, err) 721 722 assert.Equal(t, true, *ds.Get().PluginSettings.EnableUploads) 723 assert.Equal(t, map[string]any{"PluginSettings": map[string]any{"EnableUploads": true}}, ds.GetEnvironmentOverrides()) 724 // check that in DB config does not include overwritten variable 725 _, actualConfig := getActualDatabaseConfig(t) 726 assert.Equal(t, false, *actualConfig.PluginSettings.EnableUploads) 727 }) 728 729 t.Run("do not persist environment variables - int", func(t *testing.T) { 730 _, tearDown := setupConfigDatabase(t, minimalConfig, nil) 731 defer tearDown() 732 733 os.Setenv("MM_TEAMSETTINGS_MAXUSERSPERTEAM", "3000") 734 defer os.Unsetenv("MM_TEAMSETTINGS_MAXUSERSPERTEAM") 735 736 ds, err := newTestDatabaseStore(nil) 737 require.NoError(t, err) 738 defer ds.Close() 739 740 assert.Equal(t, 3000, *ds.Get().TeamSettings.MaxUsersPerTeam) 741 742 _, _, err = ds.Set(ds.Get()) 743 require.NoError(t, err) 744 745 assert.Equal(t, 3000, *ds.Get().TeamSettings.MaxUsersPerTeam) 746 assert.Equal(t, map[string]any{"TeamSettings": map[string]any{"MaxUsersPerTeam": true}}, ds.GetEnvironmentOverrides()) 747 // check that in DB config does not include overwritten variable 748 _, actualConfig := getActualDatabaseConfig(t) 749 assert.Equal(t, model.TeamSettingsDefaultMaxUsersPerTeam, *actualConfig.TeamSettings.MaxUsersPerTeam) 750 }) 751 752 t.Run("do not persist environment variables - int64", func(t *testing.T) { 753 _, tearDown := setupConfigDatabase(t, minimalConfig, nil) 754 defer tearDown() 755 756 os.Setenv("MM_SERVICESETTINGS_TLSSTRICTTRANSPORTMAXAGE", "123456") 757 defer os.Unsetenv("MM_SERVICESETTINGS_TLSSTRICTTRANSPORTMAXAGE") 758 759 ds, err := newTestDatabaseStore(nil) 760 require.NoError(t, err) 761 defer ds.Close() 762 763 assert.Equal(t, int64(123456), *ds.Get().ServiceSettings.TLSStrictTransportMaxAge) 764 765 _, _, err = ds.Set(ds.Get()) 766 require.NoError(t, err) 767 768 assert.Equal(t, int64(123456), *ds.Get().ServiceSettings.TLSStrictTransportMaxAge) 769 assert.Equal(t, map[string]any{"ServiceSettings": map[string]any{"TLSStrictTransportMaxAge": true}}, ds.GetEnvironmentOverrides()) 770 // check that in DB config does not include overwritten variable 771 _, actualConfig := getActualDatabaseConfig(t) 772 assert.Equal(t, int64(63072000), *actualConfig.ServiceSettings.TLSStrictTransportMaxAge) 773 }) 774 775 t.Run("do not persist environment variables - string slice beginning with default", func(t *testing.T) { 776 _, tearDown := setupConfigDatabase(t, minimalConfig, nil) 777 defer tearDown() 778 779 os.Setenv("MM_SQLSETTINGS_DATASOURCEREPLICAS", "user:pwd@db:5432/test-db") 780 defer os.Unsetenv("MM_SQLSETTINGS_DATASOURCEREPLICAS") 781 782 ds, err := newTestDatabaseStore(nil) 783 require.NoError(t, err) 784 defer ds.Close() 785 786 assert.Equal(t, []string{"user:pwd@db:5432/test-db"}, ds.Get().SqlSettings.DataSourceReplicas) 787 788 _, _, err = ds.Set(ds.Get()) 789 require.NoError(t, err) 790 791 assert.Equal(t, []string{"user:pwd@db:5432/test-db"}, ds.Get().SqlSettings.DataSourceReplicas) 792 assert.Equal(t, map[string]any{"SqlSettings": map[string]any{"DataSourceReplicas": true}}, ds.GetEnvironmentOverrides()) 793 // check that in DB config does not include overwritten variable 794 _, actualConfig := getActualDatabaseConfig(t) 795 assert.Equal(t, []string{}, actualConfig.SqlSettings.DataSourceReplicas) 796 }) 797 798 t.Run("do not persist environment variables - string slice beginning with slice of three", func(t *testing.T) { 799 modifiedMinimalConfig := minimalConfig.Clone() 800 modifiedMinimalConfig.SqlSettings.DataSourceReplicas = []string{"user:pwd@db:5432/test-db", "user:pwd@db2:5433/test-db2", "user:pwd@db3:5434/test-db3"} 801 _, tearDown := setupConfigDatabase(t, modifiedMinimalConfig, nil) 802 defer tearDown() 803 804 os.Setenv("MM_SQLSETTINGS_DATASOURCEREPLICAS", "user:pwd@db:5432/test-db") 805 defer os.Unsetenv("MM_SQLSETTINGS_DATASOURCEREPLICAS") 806 807 ds, err := newTestDatabaseStore(nil) 808 require.NoError(t, err) 809 defer ds.Close() 810 811 assert.Equal(t, []string{"user:pwd@db:5432/test-db"}, ds.Get().SqlSettings.DataSourceReplicas) 812 813 _, _, err = ds.Set(ds.Get()) 814 require.NoError(t, err) 815 816 assert.Equal(t, []string{"user:pwd@db:5432/test-db"}, ds.Get().SqlSettings.DataSourceReplicas) 817 assert.Equal(t, map[string]any{"SqlSettings": map[string]any{"DataSourceReplicas": true}}, ds.GetEnvironmentOverrides()) 818 // check that in DB config does not include overwritten variable 819 _, actualConfig := getActualDatabaseConfig(t) 820 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) 821 }) 822 823 t.Run("invalid", func(t *testing.T) { 824 _, tearDown := setupConfigDatabase(t, emptyConfig, nil) 825 defer tearDown() 826 827 ds, err := newTestDatabaseStore(nil) 828 require.NoError(t, err) 829 defer ds.Close() 830 831 cfgData, err := marshalConfig(invalidConfig) 832 require.NoError(t, err) 833 834 truncateTables(t) 835 id := model.NewId() 836 _, err = mainHelper.GetSQLStore().GetMasterX().NamedExec("INSERT INTO Configurations (Id, Value, CreateAt, Active) VALUES(:id, :value, :createat, TRUE)", map[string]any{ 837 "id": id, 838 "value": cfgData, 839 "createat": model.GetMillis(), 840 }) 841 t.Logf("%v\n", err) 842 require.NoErrorf(t, err, "what is - %v", err) 843 844 err = ds.Load() 845 if assert.Error(t, err) { 846 var appErr *model.AppError 847 require.True(t, errors.As(err, &appErr)) 848 assert.Equal(t, appErr.Id, "model.config.is_valid.site_url.app_error") 849 } 850 }) 851 852 t.Run("fixes required", func(t *testing.T) { 853 _, tearDown := setupConfigDatabase(t, fixesRequiredConfig, nil) 854 defer tearDown() 855 856 ds, err := newTestDatabaseStore(nil) 857 require.NoError(t, err) 858 defer ds.Close() 859 860 err = ds.Load() 861 require.NoError(t, err) 862 assertDatabaseNotEqualsConfig(t, fixesRequiredConfig) 863 assert.Equal(t, "http://trailingslash", *ds.Get().ServiceSettings.SiteURL) 864 }) 865 866 t.Run("listeners notified on change", func(t *testing.T) { 867 _, tearDown := setupConfigDatabase(t, emptyConfig, nil) 868 defer tearDown() 869 870 ds, err := newTestDatabaseStore(nil) 871 require.NoError(t, err) 872 defer ds.Close() 873 874 called := make(chan bool, 1) 875 callback := func(oldfg, newCfg *model.Config) { 876 called <- true 877 } 878 ds.AddListener(callback) 879 880 newCfg := minimalConfig.Clone() 881 dbStore, ok := ds.backingStore.(*DatabaseStore) 882 require.True(t, ok) 883 err = dbStore.persist(newCfg) 884 require.NoError(t, err) 885 886 err = ds.Load() 887 require.NoError(t, err) 888 889 require.True(t, wasCalled(called, 5*time.Second), "callback should have been called when config changed on load") 890 }) 891 } 892 893 func TestDatabaseGetFile(t *testing.T) { 894 _, tearDown := setupConfigDatabase(t, minimalConfig, map[string][]byte{ 895 "empty-file": {}, 896 "test-file": []byte("test"), 897 }) 898 defer tearDown() 899 900 ds, err := newTestDatabaseStore(nil) 901 require.NoError(t, err) 902 defer ds.Close() 903 904 t.Run("get empty filename", func(t *testing.T) { 905 _, err := ds.GetFile("") 906 require.Error(t, err) 907 }) 908 909 t.Run("get non-existent file", func(t *testing.T) { 910 _, err := ds.GetFile("unknown") 911 require.Error(t, err) 912 }) 913 914 t.Run("get empty file", func(t *testing.T) { 915 data, err := ds.GetFile("empty-file") 916 require.NoError(t, err) 917 require.Empty(t, data) 918 }) 919 920 t.Run("get non-empty file", func(t *testing.T) { 921 data, err := ds.GetFile("test-file") 922 require.NoError(t, err) 923 require.Equal(t, []byte("test"), data) 924 }) 925 } 926 927 func TestDatabaseSetFile(t *testing.T) { 928 _, tearDown := setupConfigDatabase(t, minimalConfig, nil) 929 defer tearDown() 930 931 ds, err := newTestDatabaseStore(nil) 932 require.NoError(t, err) 933 defer ds.Close() 934 935 t.Run("set new file", func(t *testing.T) { 936 err := ds.SetFile("new", []byte("new file")) 937 require.NoError(t, err) 938 939 data, err := ds.GetFile("new") 940 require.NoError(t, err) 941 require.Equal(t, []byte("new file"), data) 942 }) 943 944 t.Run("overwrite existing file", func(t *testing.T) { 945 err := ds.SetFile("existing", []byte("existing file")) 946 require.NoError(t, err) 947 948 err = ds.SetFile("existing", []byte("overwritten file")) 949 require.NoError(t, err) 950 951 data, err := ds.GetFile("existing") 952 require.NoError(t, err) 953 require.Equal(t, []byte("overwritten file"), data) 954 }) 955 956 t.Run("max length", func(t *testing.T) { 957 if *mainHelper.Settings.DriverName == "postgres" { 958 t.Skip("No limit for postgres") 959 } 960 longFile := bytes.Repeat([]byte("a"), MaxWriteLength) 961 962 err := ds.SetFile("toolong", longFile) 963 require.NoError(t, err) 964 }) 965 966 t.Run("too long", func(t *testing.T) { 967 if *mainHelper.Settings.DriverName == "postgres" { 968 t.Skip("No limit for postgres") 969 } 970 longFile := bytes.Repeat([]byte("a"), MaxWriteLength+1) 971 972 err := ds.SetFile("toolong", longFile) 973 if assert.Error(t, err) { 974 assert.True(t, strings.HasPrefix(err.Error(), "file data failed length check: value is too long")) 975 } 976 }) 977 } 978 979 func TestDatabaseHasFile(t *testing.T) { 980 t.Run("has non-existent", func(t *testing.T) { 981 _, tearDown := setupConfigDatabase(t, minimalConfig, nil) 982 defer tearDown() 983 984 ds, err := newTestDatabaseStore(nil) 985 require.NoError(t, err) 986 defer ds.Close() 987 988 has, err := ds.HasFile("non-existent") 989 require.NoError(t, err) 990 require.False(t, has) 991 }) 992 993 t.Run("has existing", func(t *testing.T) { 994 _, tearDown := setupConfigDatabase(t, minimalConfig, nil) 995 defer tearDown() 996 997 ds, err := newTestDatabaseStore(nil) 998 require.NoError(t, err) 999 defer ds.Close() 1000 1001 err = ds.SetFile("existing", []byte("existing file")) 1002 require.NoError(t, err) 1003 1004 has, err := ds.HasFile("existing") 1005 require.NoError(t, err) 1006 require.True(t, has) 1007 }) 1008 1009 t.Run("has manually created file", func(t *testing.T) { 1010 _, tearDown := setupConfigDatabase(t, minimalConfig, map[string][]byte{ 1011 "manual": []byte("manual file"), 1012 }) 1013 defer tearDown() 1014 1015 ds, err := newTestDatabaseStore(nil) 1016 require.NoError(t, err) 1017 defer ds.Close() 1018 1019 has, err := ds.HasFile("manual") 1020 require.NoError(t, err) 1021 require.True(t, has) 1022 }) 1023 1024 t.Run("has non-existent empty string", func(t *testing.T) { 1025 _, tearDown := setupConfigDatabase(t, minimalConfig, nil) 1026 defer tearDown() 1027 1028 ds, err := newTestDatabaseStore(nil) 1029 require.NoError(t, err) 1030 defer ds.Close() 1031 1032 has, err := ds.HasFile("") 1033 require.NoError(t, err) 1034 require.False(t, has) 1035 }) 1036 } 1037 1038 func TestDatabaseRemoveFile(t *testing.T) { 1039 t.Run("remove non-existent", func(t *testing.T) { 1040 _, tearDown := setupConfigDatabase(t, minimalConfig, nil) 1041 defer tearDown() 1042 1043 ds, err := newTestDatabaseStore(nil) 1044 require.NoError(t, err) 1045 defer ds.Close() 1046 1047 err = ds.RemoveFile("non-existent") 1048 require.NoError(t, err) 1049 }) 1050 1051 t.Run("remove existing", func(t *testing.T) { 1052 _, tearDown := setupConfigDatabase(t, minimalConfig, nil) 1053 defer tearDown() 1054 1055 ds, err := newTestDatabaseStore(nil) 1056 require.NoError(t, err) 1057 defer ds.Close() 1058 1059 err = ds.SetFile("existing", []byte("existing file")) 1060 require.NoError(t, err) 1061 1062 err = ds.RemoveFile("existing") 1063 require.NoError(t, err) 1064 1065 has, err := ds.HasFile("existing") 1066 require.NoError(t, err) 1067 require.False(t, has) 1068 1069 _, err = ds.GetFile("existing") 1070 require.Error(t, err) 1071 }) 1072 1073 t.Run("remove manually created file", func(t *testing.T) { 1074 _, tearDown := setupConfigDatabase(t, minimalConfig, map[string][]byte{ 1075 "manual": []byte("manual file"), 1076 }) 1077 defer tearDown() 1078 1079 ds, err := newTestDatabaseStore(nil) 1080 require.NoError(t, err) 1081 defer ds.Close() 1082 1083 err = ds.RemoveFile("manual") 1084 require.NoError(t, err) 1085 1086 has, err := ds.HasFile("manual") 1087 require.NoError(t, err) 1088 require.False(t, has) 1089 1090 _, err = ds.GetFile("manual") 1091 require.Error(t, err) 1092 }) 1093 } 1094 1095 func TestDatabaseStoreString(t *testing.T) { 1096 if testing.Short() { 1097 t.SkipNow() 1098 } 1099 _, tearDown := setupConfigDatabase(t, emptyConfig, nil) 1100 defer tearDown() 1101 1102 ds, err := newTestDatabaseStore(nil) 1103 require.NoError(t, err) 1104 require.NotNil(t, ds) 1105 defer ds.Close() 1106 1107 if *mainHelper.GetSQLSettings().DriverName == "postgres" { 1108 maskedDSN := ds.String() 1109 assert.True(t, strings.HasPrefix(maskedDSN, "postgres://")) 1110 assert.False(t, strings.Contains(maskedDSN, "mmuser")) 1111 assert.False(t, strings.Contains(maskedDSN, "mostest")) 1112 } else { 1113 maskedDSN := ds.String() 1114 assert.False(t, strings.HasPrefix(maskedDSN, "mysql://")) 1115 assert.False(t, strings.Contains(maskedDSN, "mmuser")) 1116 assert.False(t, strings.Contains(maskedDSN, "mostest")) 1117 } 1118 } 1119 1120 func TestCleanUp(t *testing.T) { 1121 _, tearDown := setupConfigDatabase(t, emptyConfig, nil) 1122 defer tearDown() 1123 1124 ds, err := newTestDatabaseStore(nil) 1125 require.NoError(t, err) 1126 require.NotNil(t, ds) 1127 defer ds.Close() 1128 1129 dbs, ok := ds.backingStore.(*DatabaseStore) 1130 require.True(t, ok, "should be a DatabaseStore instance") 1131 1132 b, err := marshalConfig(ds.config) 1133 require.NoError(t, err) 1134 1135 ds.config.JobSettings.CleanupConfigThresholdDays = model.NewInt(30) // we set 30 days as threshold 1136 1137 now := time.Now() 1138 for i := 0; i < 5; i++ { 1139 // 20 days, we expect to remove at least 3 configuration values from the store 1140 // first 2 (0 and 1) will be within a month constraint, others will be older than 1141 // a month hence we expect 3 configurations to be removed from the database. 1142 m := -1 * i * 24 * 20 1143 params := map[string]any{ 1144 "id": model.NewId(), 1145 "value": string(b), 1146 "create_at": model.GetMillisForTime(now.Add(time.Duration(m) * time.Hour)), 1147 } 1148 1149 _, err = dbs.db.NamedExec("INSERT INTO Configurations (Id, Value, CreateAt) VALUES (:id, :value, :create_at)", params) 1150 require.NoError(t, err) 1151 } 1152 var initialCount int 1153 row := dbs.db.QueryRow("SELECT COUNT(*) FROM Configurations") 1154 err = row.Scan(&initialCount) 1155 require.NoError(t, err) 1156 1157 err = ds.CleanUp() 1158 require.NoError(t, err) 1159 1160 var count int 1161 row = dbs.db.QueryRow("SELECT COUNT(*) FROM Configurations") 1162 err = row.Scan(&count) 1163 require.NoError(t, err) 1164 require.True(t, count+3 == initialCount) 1165 }