goyave.dev/goyave/v5@v5.0.0-rc9.0.20240517145003-d3f977d0b9f3/config/config_test.go (about) 1 package config 2 3 import ( 4 "fmt" 5 "reflect" 6 "testing" 7 8 "github.com/stretchr/testify/assert" 9 "github.com/stretchr/testify/require" 10 ) 11 12 func TestConfigError(t *testing.T) { 13 e := &Error{ 14 err: fmt.Errorf("test error"), 15 } 16 17 assert.Equal(t, "Config error: test error", e.Error()) 18 assert.Equal(t, e.err, e.Unwrap()) 19 } 20 21 func TestGetConfigFilePath(t *testing.T) { 22 t.Setenv("GOYAVE_ENV", "localhost") 23 assert.Equal(t, "config.json", getConfigFilePath()) 24 25 t.Setenv("GOYAVE_ENV", "test") 26 assert.Equal(t, "config.test.json", getConfigFilePath()) 27 28 t.Setenv("GOYAVE_ENV", "production") 29 assert.Equal(t, "config.production.json", getConfigFilePath()) 30 } 31 32 func TestRegister(t *testing.T) { 33 entry := Entry{ 34 Value: "", 35 AuthorizedValues: []any{}, 36 Type: reflect.String, 37 IsSlice: false, 38 } 39 Register("testEntry", entry) 40 assert.Equal(t, &entry, defaultLoader.defaults["testEntry"]) 41 } 42 43 func TestLoad(t *testing.T) { 44 t.Run("Load", func(t *testing.T) { 45 // Should use automatically generated path (based on GOYAVE_ENV) 46 t.Setenv("GOYAVE_ENV", "test") 47 cfg, err := Load() 48 require.NoError(t, err) 49 50 assert.NotNil(t, cfg) 51 52 expected := &Entry{ 53 Value: "root level content", 54 AuthorizedValues: []any{}, 55 Type: reflect.String, 56 IsSlice: false, 57 } 58 assert.Equal(t, expected, cfg.config["rootLevel"]) 59 60 // Default config also loaded 61 expected = &Entry{ 62 Value: "goyave", 63 AuthorizedValues: []any{}, 64 Type: reflect.String, 65 IsSlice: false, 66 } 67 assert.Equal(t, expected, cfg.config["app"].(object)["name"]) 68 }) 69 70 t.Run("Load Invalid", func(t *testing.T) { 71 t.Setenv("GOYAVE_ENV", "test_invalid") 72 cfg, err := Load() 73 assert.Nil(t, cfg) 74 require.Error(t, err) 75 }) 76 77 t.Run("Load Default", func(t *testing.T) { 78 cfg := LoadDefault() 79 assert.Equal(t, defaultLoader.defaults, cfg.config) 80 }) 81 82 t.Run("Load Non Existing", func(t *testing.T) { 83 t.Setenv("GOYAVE_ENV", "nonexisting") 84 cfg, err := Load() 85 assert.Nil(t, cfg) 86 require.Error(t, err) 87 }) 88 89 t.Run("LoadFrom", func(t *testing.T) { 90 cfg, err := LoadFrom("../resources/custom_config.json") 91 require.NoError(t, err) 92 93 assert.NotNil(t, cfg) 94 95 expected := &Entry{ 96 Value: "value", 97 AuthorizedValues: []any{}, 98 Type: reflect.String, 99 IsSlice: false, 100 } 101 assert.Equal(t, expected, cfg.config["custom-entry"]) 102 103 // Default config also loaded 104 expected = &Entry{ 105 Value: "goyave", 106 AuthorizedValues: []any{}, 107 Type: reflect.String, 108 IsSlice: false, 109 } 110 assert.Equal(t, expected, cfg.config["app"].(object)["name"]) 111 }) 112 113 t.Run("LoadJSON", func(t *testing.T) { 114 cfg, err := LoadJSON(`{"custom-entry": "value"}`) 115 require.NoError(t, err) 116 117 assert.NotNil(t, cfg) 118 119 expected := &Entry{ 120 Value: "value", 121 AuthorizedValues: []any{}, 122 Type: reflect.String, 123 IsSlice: false, 124 } 125 assert.Equal(t, expected, cfg.config["custom-entry"]) 126 127 // Default config also loaded 128 expected = &Entry{ 129 Value: "goyave", 130 AuthorizedValues: []any{}, 131 Type: reflect.String, 132 IsSlice: false, 133 } 134 assert.Equal(t, expected, cfg.config["app"].(object)["name"]) 135 }) 136 137 t.Run("LoadJSON Invalid", func(t *testing.T) { 138 cfg, err := LoadJSON(`{"unclosed":`) 139 assert.Nil(t, cfg) 140 require.Error(t, err) 141 }) 142 143 t.Run("Load Override Entry With Category", func(t *testing.T) { 144 cfg, err := LoadJSON(`{"app": {"name": {}}}`) 145 assert.Nil(t, cfg) 146 require.Error(t, err) 147 assert.Equal(t, "Config error: \n\t- cannot override entry \"name\" with a category", err.Error()) 148 }) 149 150 t.Run("Load Override Category With Entry", func(t *testing.T) { 151 cfg, err := LoadJSON(`{"app": "value"}`) 152 assert.Nil(t, cfg) 153 require.Error(t, err) 154 assert.Equal(t, "Config error: \n\t- cannot override category \"app\" with an entry", err.Error()) 155 }) 156 157 t.Run("Validation", func(t *testing.T) { 158 cfg, err := LoadJSON(`{"app": {"name": 123}}`) 159 assert.Nil(t, cfg) 160 require.Error(t, err) 161 assert.Equal(t, "Config error: \n\t- \"app.name\" type must be string", err.Error()) 162 }) 163 164 t.Run("Load Env Variables", func(t *testing.T) { 165 defaultLoader.mu.Lock() 166 loader := loader{ 167 defaults: make(object, len(defaultLoader.defaults)), 168 } 169 loadDefaults(defaultLoader.defaults, loader.defaults) 170 defaultLoader.mu.Unlock() 171 172 loader.register("envString", Entry{ 173 Value: "", 174 AuthorizedValues: []any{}, 175 Type: reflect.String, 176 IsSlice: false, 177 }) 178 loader.register("envInt", Entry{ 179 Value: 0, 180 AuthorizedValues: []any{}, 181 Type: reflect.Int, 182 IsSlice: false, 183 }) 184 loader.register("envFloat", Entry{ 185 Value: 0.0, 186 AuthorizedValues: []any{}, 187 Type: reflect.Float64, 188 IsSlice: false, 189 }) 190 loader.register("envBool", Entry{ 191 Value: false, 192 AuthorizedValues: []any{}, 193 Type: reflect.Bool, 194 IsSlice: false, 195 }) 196 197 t.Setenv("TEST_ENV_STRING", "hello") 198 t.Setenv("TEST_ENV_INT", "123") 199 t.Setenv("TEST_ENV_FLOAT", "123.456") 200 t.Setenv("TEST_ENV_BOOL", "TRUE") 201 202 json := `{ 203 "envString": "${TEST_ENV_STRING}", 204 "envInt": "${TEST_ENV_INT}", 205 "envFloat": "${TEST_ENV_FLOAT}", 206 "envBool": "${TEST_ENV_BOOL}" 207 }` 208 209 cfg, err := loader.loadJSON(json) 210 require.NoError(t, err) 211 212 assert.Equal(t, "hello", cfg.Get("envString")) 213 assert.Equal(t, 123, cfg.Get("envInt")) 214 assert.InEpsilon(t, 123.456, cfg.Get("envFloat"), 0) 215 assert.Equal(t, true, cfg.Get("envBool")) 216 217 // Invalid int 218 t.Setenv("TEST_ENV_INT", "hello") 219 cfg, err = loader.loadJSON(json) 220 require.Error(t, err) 221 assert.Nil(t, cfg) 222 t.Setenv("TEST_ENV_INT", "123") 223 224 // Invalid float 225 t.Setenv("TEST_ENV_FLOAT", "hello") 226 cfg, err = loader.loadJSON(json) 227 require.Error(t, err) 228 assert.Nil(t, cfg) 229 t.Setenv("TEST_ENV_FLOAT", "123.456") 230 231 // Invalid bool 232 t.Setenv("TEST_ENV_BOOL", "hello") 233 cfg, err = loader.loadJSON(json) 234 require.Error(t, err) 235 assert.Nil(t, cfg) 236 t.Setenv("TEST_ENV_BOOL", "TRUE") 237 }) 238 239 t.Run("Load Env Variables Unsupported", func(t *testing.T) { 240 loader := loader{ 241 defaults: make(object, len(defaultLoader.defaults)), 242 } 243 244 loader.register("envUnsupported", Entry{ 245 Value: []string{}, 246 AuthorizedValues: []any{}, 247 Type: reflect.String, 248 IsSlice: true, 249 }) 250 251 t.Setenv("TEST_ENV_UNSUPPORTED", "[hello]") 252 253 json := `{ 254 "envUnsupported": "${TEST_ENV_UNSUPPORTED}" 255 }` 256 257 cfg, err := loader.loadJSON(json) 258 require.Error(t, err) 259 assert.Nil(t, cfg) 260 }) 261 262 t.Run("Load Env Variables Missing", func(t *testing.T) { 263 defaultLoader.mu.Lock() 264 loader := loader{ 265 defaults: make(object, len(defaultLoader.defaults)), 266 } 267 loadDefaults(defaultLoader.defaults, loader.defaults) 268 defaultLoader.mu.Unlock() 269 270 loader.register("envUnset", Entry{ 271 Value: "", 272 AuthorizedValues: []any{}, 273 Type: reflect.String, 274 IsSlice: false, 275 }) 276 277 json := `{ 278 "envUnsupported": "${TEST_ENV_UNSET}" 279 }` 280 281 cfg, err := loader.loadJSON(json) 282 require.Error(t, err) 283 assert.Nil(t, cfg) 284 }) 285 286 t.Run("Create Missing Categories", func(t *testing.T) { 287 json := `{ 288 "category": { 289 "entry": 123, 290 "subcategory": { 291 "subentry": 456, 292 "array": ["a", "b"], 293 "deep": { 294 "deepEntry": "deepValue" 295 } 296 } 297 } 298 }` 299 cfg, err := LoadJSON(json) 300 require.NoError(t, err) 301 302 assert.NotNil(t, cfg) 303 304 cat, ok := cfg.config["category"].(object) 305 if !assert.True(t, ok) { 306 return 307 } 308 expected := &Entry{ 309 Value: 123.0, 310 AuthorizedValues: []any{}, 311 // It is float here because we haven't registered the config entry, so no conversion 312 Type: reflect.Float64, 313 IsSlice: false, 314 } 315 assert.Equal(t, expected, cat["entry"]) 316 317 subcat, ok := cat["subcategory"].(object) 318 if !assert.True(t, ok) { 319 return 320 } 321 expected = &Entry{ 322 Value: 456.0, 323 AuthorizedValues: []any{}, 324 Type: reflect.Float64, 325 IsSlice: false, 326 } 327 assert.Equal(t, expected, subcat["subentry"]) 328 expected = &Entry{ 329 Value: []any{"a", "b"}, 330 AuthorizedValues: []any{}, 331 Type: reflect.Interface, 332 IsSlice: true, 333 } 334 assert.Equal(t, expected, subcat["array"]) 335 336 deep, ok := subcat["deep"].(object) 337 if !assert.True(t, ok) { 338 return 339 } 340 expected = &Entry{ 341 Value: "deepValue", 342 AuthorizedValues: []any{}, 343 Type: reflect.String, 344 IsSlice: false, 345 } 346 assert.Equal(t, expected, deep["deepEntry"]) 347 348 // With partial existence 349 cfg, err = LoadJSON(`{"app": {"subcategory": {"subentry": 456}}}`) 350 require.NoError(t, err) 351 352 assert.NotNil(t, cfg) 353 354 cat, ok = cfg.config["app"].(object) 355 if !assert.True(t, ok) { 356 return 357 } 358 359 subcat, ok = cat["subcategory"].(object) 360 if !assert.True(t, ok) { 361 return 362 } 363 expected = &Entry{ 364 Value: 456.0, 365 AuthorizedValues: []any{}, 366 Type: reflect.Float64, 367 IsSlice: false, 368 } 369 assert.Equal(t, expected, subcat["subentry"]) 370 }) 371 } 372 373 func TestConfig(t *testing.T) { 374 defaultLoader.mu.Lock() 375 loader := loader{ 376 defaults: make(object, len(defaultLoader.defaults)), 377 } 378 loadDefaults(defaultLoader.defaults, loader.defaults) 379 defaultLoader.mu.Unlock() 380 381 loader.register("testCategory.string", Entry{ 382 Value: "", 383 AuthorizedValues: []any{}, 384 Type: reflect.String, 385 IsSlice: false, 386 }) 387 loader.register("testCategory.int", Entry{ 388 Value: 0, 389 AuthorizedValues: []any{}, 390 Type: reflect.Int, 391 IsSlice: false, 392 }) 393 loader.register("testCategory.float", Entry{ 394 Value: 0.0, 395 AuthorizedValues: []any{}, 396 Type: reflect.Float64, 397 IsSlice: false, 398 }) 399 loader.register("testCategory.bool", Entry{ 400 Value: false, 401 AuthorizedValues: []any{}, 402 Type: reflect.Bool, 403 IsSlice: false, 404 }) 405 loader.register("testCategory.stringSlice", Entry{ 406 Value: []string{}, 407 AuthorizedValues: []any{}, 408 Type: reflect.String, 409 IsSlice: true, 410 }) 411 loader.register("testCategory.intSlice", Entry{ 412 Value: []int{}, 413 AuthorizedValues: []any{}, 414 Type: reflect.Int, 415 IsSlice: true, 416 }) 417 loader.register("testCategory.defaultIntSlice", Entry{ 418 Value: []int{0, 1}, 419 AuthorizedValues: []any{}, 420 Type: reflect.Int, 421 IsSlice: true, 422 }) 423 loader.register("testCategory.floatSlice", Entry{ 424 Value: []float64{}, 425 AuthorizedValues: []any{}, 426 Type: reflect.Float64, 427 IsSlice: true, 428 }) 429 loader.register("testCategory.boolSlice", Entry{ 430 Value: []bool{}, 431 AuthorizedValues: []any{}, 432 Type: reflect.Bool, 433 IsSlice: true, 434 }) 435 436 loader.register("testCategory.set", Entry{ 437 Value: 0, 438 AuthorizedValues: []any{456, 789}, 439 Type: reflect.Int, 440 IsSlice: false, 441 }) 442 443 loader.register("testCategory.setSlice", Entry{ 444 Value: 0, 445 AuthorizedValues: []any{456, 789}, 446 Type: reflect.Int, 447 IsSlice: true, 448 }) 449 450 cfgJSON := `{ 451 "rootLevel": "root", 452 "testCategory": { 453 "string": "hello", 454 "int": 123, 455 "float": 123.456, 456 "bool": true, 457 "stringSlice": ["a", "b"], 458 "intSlice": [1, 2], 459 "floatSlice": [1.2, 3.4], 460 "boolSlice": [true, false], 461 "set": 456, 462 "setSlice": [] 463 } 464 }` 465 466 cfg, err := loader.loadJSON(cfgJSON) 467 require.NoError(t, err) 468 469 t.Run("Get", func(t *testing.T) { 470 v := cfg.Get("testCategory.int") 471 assert.Equal(t, 123, v) 472 473 v = cfg.Get("rootLevel") 474 assert.Equal(t, "root", v) 475 476 assert.Panics(t, func() { 477 cfg.Get("testCategory.nonexistent") 478 }) 479 }) 480 481 t.Run("Get Deep", func(t *testing.T) { 482 cfg.Set("testCategory.subcategory.deep.entry", "hello") 483 v := cfg.Get("testCategory.subcategory.deep.entry") 484 assert.Equal(t, "hello", v) 485 }) 486 487 t.Run("GetString", func(t *testing.T) { 488 v := cfg.GetString("testCategory.string") 489 assert.Equal(t, "hello", v) 490 491 assert.Panics(t, func() { 492 cfg.GetString("testCategory.int") 493 }) 494 }) 495 496 t.Run("GetInt", func(t *testing.T) { 497 v := cfg.GetInt("testCategory.int") 498 assert.Equal(t, 123, v) 499 500 assert.Panics(t, func() { 501 cfg.GetInt("testCategory.string") 502 }) 503 }) 504 505 t.Run("GetBool", func(t *testing.T) { 506 v := cfg.GetBool("testCategory.bool") 507 assert.True(t, v) 508 509 assert.Panics(t, func() { 510 cfg.GetBool("testCategory.string") 511 }) 512 }) 513 514 t.Run("GetFloat", func(t *testing.T) { 515 v := cfg.GetFloat("testCategory.float") 516 assert.InEpsilon(t, 123.456, v, 0) 517 518 assert.Panics(t, func() { 519 cfg.GetFloat("testCategory.string") 520 }) 521 }) 522 523 t.Run("GetStringSlice", func(t *testing.T) { 524 v := cfg.GetStringSlice("testCategory.stringSlice") 525 assert.Equal(t, []string{"a", "b"}, v) 526 527 assert.Panics(t, func() { 528 cfg.GetStringSlice("testCategory.string") 529 }) 530 }) 531 532 t.Run("GetBoolSlice", func(t *testing.T) { 533 v := cfg.GetBoolSlice("testCategory.boolSlice") 534 assert.Equal(t, []bool{true, false}, v) 535 536 assert.Panics(t, func() { 537 cfg.GetBoolSlice("testCategory.string") 538 }) 539 }) 540 541 t.Run("GetIntSlice", func(t *testing.T) { 542 v := cfg.GetIntSlice("testCategory.intSlice") 543 assert.Equal(t, []int{1, 2}, v) 544 545 assert.Panics(t, func() { 546 cfg.GetIntSlice("testCategory.string") 547 }) 548 549 v = cfg.GetIntSlice("testCategory.defaultIntSlice") 550 assert.Equal(t, []int{0, 1}, v) 551 }) 552 553 t.Run("GetFloatSlice", func(t *testing.T) { 554 v := cfg.GetFloatSlice("testCategory.floatSlice") 555 assert.Equal(t, []float64{1.2, 3.4}, v) 556 557 assert.Panics(t, func() { 558 cfg.GetFloatSlice("testCategory.string") 559 }) 560 }) 561 562 t.Run("Has", func(t *testing.T) { 563 assert.True(t, cfg.Has("testCategory.string")) 564 assert.False(t, cfg.Has("testCategory.nonexistent")) 565 }) 566 567 t.Run("Set", func(t *testing.T) { 568 cfg.Set("testCategory.set", 789) 569 expected := &Entry{ 570 Value: 789, 571 AuthorizedValues: []any{456, 789}, 572 Type: reflect.Int, 573 IsSlice: false, 574 } 575 assert.Equal(t, expected, cfg.config["testCategory"].(object)["set"]) 576 577 cfg.Set("testCategory.set", 456.0) // Conversion float->int 578 expected = &Entry{ 579 Value: 456, 580 AuthorizedValues: []any{456, 789}, 581 Type: reflect.Int, 582 IsSlice: false, 583 } 584 assert.Equal(t, expected, cfg.config["testCategory"].(object)["set"]) 585 586 cfg.Set("testCategory.setSlice", []int{789, 456}) 587 expected = &Entry{ 588 Value: []int{789, 456}, 589 AuthorizedValues: []any{456, 789}, 590 Type: reflect.Int, 591 IsSlice: true, 592 } 593 assert.Equal(t, expected, cfg.config["testCategory"].(object)["setSlice"]) 594 595 // No need to validate the other conversions, they have been tested indirectly 596 // through the loading at the start of this test 597 598 assert.Panics(t, func() { 599 cfg.Set("testCategory.intSlice", []any{1, "2"}) 600 }) 601 assert.Panics(t, func() { 602 cfg.Set("testCategory.stringSlice", []any{1, "2"}) 603 }) 604 assert.Panics(t, func() { 605 cfg.Set("testCategory.setSlice", "abc123") 606 }) 607 assert.Panics(t, func() { 608 cfg.Set("testCategory.set", "abc123") 609 }) 610 assert.Panics(t, func() { 611 // Unauthorized value 612 cfg.Set("testCategory.set", 123) 613 }) 614 615 assert.Panics(t, func() { 616 // Unauthorized value 617 cfg.Set("testCategory.setSlice", []int{456, 789, 123}) 618 }) 619 }) 620 621 t.Run("Set New Entry", func(t *testing.T) { 622 cfg.Set("testCategory.subcategory.deep.entry", "hello") 623 624 subcategory, ok := cfg.config["testCategory"].(object)["subcategory"].(object)["deep"].(object) 625 if !assert.True(t, ok) { 626 return 627 } 628 629 expected := &Entry{ 630 Value: "hello", 631 AuthorizedValues: []any{}, 632 Type: reflect.String, 633 IsSlice: false, 634 } 635 assert.Equal(t, expected, subcategory["entry"]) 636 }) 637 638 t.Run("Unset", func(t *testing.T) { 639 cfg.Set("testCategory.set", nil) 640 assert.Panics(t, func() { 641 cfg.Get("testCategory.set") 642 }) 643 }) 644 645 t.Run("Set Errors", func(t *testing.T) { 646 assert.Panics(t, func() { 647 cfg.Set("", "hello") 648 }) 649 assert.Panics(t, func() { 650 cfg.Set("testCategory.", "hello") 651 }) 652 }) 653 654 t.Run("Set Override Entry With Category", func(t *testing.T) { 655 assert.Panics(t, func() { 656 cfg.Set("testCategory.string.entry", "hello") 657 }) 658 }) 659 660 t.Run("Set Override Category With Entry", func(t *testing.T) { 661 assert.Panics(t, func() { 662 cfg.Set("testCategory", "hello") 663 }) 664 }) 665 666 t.Run("Register Invalid", func(t *testing.T) { 667 // Entry already exists and matches -> do nothing 668 entry := Entry{ 669 Value: "goyave", 670 AuthorizedValues: []any{}, 671 Type: reflect.String, 672 IsSlice: false, 673 } 674 loader.register("app.name", entry) 675 assert.NotSame(t, &entry, loader.defaults["app"].(object)["name"]) 676 677 // Required values don't match (existing) 678 assert.Panics(t, func() { 679 loader.register("app.name", Entry{ 680 Value: "goyave", 681 AuthorizedValues: []any{"a", "b"}, 682 Type: reflect.String, 683 IsSlice: true, 684 }) 685 }) 686 687 // Type doesn't match (existing) 688 assert.Panics(t, func() { 689 loader.register("app.name", Entry{ 690 Value: "goyave", 691 AuthorizedValues: []any{}, 692 Type: reflect.Int, 693 IsSlice: false, 694 }) 695 }) 696 697 // Value doesn't match 698 assert.Panics(t, func() { 699 loader.register("app.name", Entry{ 700 Value: "not goyave", 701 AuthorizedValues: []any{}, 702 Type: reflect.String, 703 IsSlice: false, 704 }) 705 }) 706 }) 707 }