github.com/jxskiss/gopkg/v2@v2.14.9-0.20240514120614-899f3e7952b4/confr/loader_test.go (about) 1 package confr 2 3 import ( 4 "flag" 5 "os" 6 "reflect" 7 "sync" 8 "testing" 9 "time" 10 11 "github.com/stretchr/testify/assert" 12 ) 13 14 type DBConfig struct { 15 MySQL string `json:"mysql" toml:"mysql" yaml:"mysql" flag:"mysql"` 16 Redis string `json:"redis" toml:"redis" yaml:"redis"` 17 } 18 19 type MQConfig struct { 20 Cluster string `json:"cluster" toml:"cluster" yaml:"cluster" default:"some_default_cluster"` 21 Topic string `json:"topic" toml:"topic" yaml:"topic"` 22 Group string `json:"group" toml:"group" yaml:"group"` 23 } 24 25 type TestConfig struct { 26 Some1 string `json:"some_1" toml:"some_1" yaml:"some_1" flag:"some1"` 27 Some1Ptr *string `json:"some_1_ptr" toml:"some_1_ptr" yaml:"some_1_ptr" flag:"some1_ptr"` 28 Some2 int `json:"some_2" toml:"some_2" yaml:"some_2" flag:"some2"` 29 Some2Ptr *int `json:"some_2_ptr" toml:"some_2_ptr" yaml:"some_2_ptr"` 30 Some3 int64 `json:"some_3" toml:"some_3" yaml:"some_3"` 31 Some3Ptr *int64 `json:"some_3_ptr" toml:"some_3_ptr" yaml:"some_3_ptr"` 32 Some4 int32 `json:"some_4" toml:"some_4" yaml:"some_4"` 33 Some4Ptr *int32 `json:"some_4_ptr" toml:"some_4_ptr" yaml:"some_4_ptr"` 34 35 Some5 []int `json:"some_5" toml:"some_5" yaml:"some_5"` 36 Some6 []int64 `json:"some_6" toml:"some_6" yaml:"some_6"` 37 Some7 []int32 `json:"some_7" toml:"some_7" yaml:"some_7"` 38 Some8 []string `json:"some_8" toml:"some_8" yaml:"some_8"` 39 Some9 []string `json:"some_9" toml:"some_9" yaml:"some_9"` 40 41 SomeBool bool `json:"some_bool" toml:"some_bool" yaml:"some_bool" flag:"some-bool"` 42 SomeBoolPtr1 *bool `json:"some_bool_ptr1" toml:"some_bool_ptr1" yaml:"some_bool_ptr1" flag:"some-bool-ptr1"` 43 SomeBoolPtr2 *bool `json:"some_bool_ptr2" toml:"some_bool_ptr2" yaml:"some_bool_ptr2" flag:"some-bool-ptr2"` 44 45 OverrideEnvVar int64 `json:"override_env_var" toml:"override_env_var" yaml:"override_env_var" env:"OVERRIDE_ENV_VAR"` 46 ExplicitEnvVar1 string `env:"EXPLICIT_ENV_VAR1"` 47 ExplicitEnvVar2 *float64 `env:"EXPLICIT_ENV_VAR2"` 48 49 ImplicitEnvVar1 *string // test with Implicit_Env_Var1 50 ImplicitEnvVar2 string // test with IMPLICIT_ENV_VAR2 51 ImplicitEnvVarOverride string `json:"implicit_env_var_override" toml:"implicit_env_var_override" yaml:"implicit_env_var_override"` 52 53 SomeRemoteValue1 string `custom:"some_remote_value_1"` 54 SomeRemoteValue1Ptr *string `custom:"some_remote_value_1_ptr"` 55 SomeRemoteValue2 []string `custom:"some_remote_value_2"` 56 SomeRemoteStruct *MQConfig `custom:"some_remote_struct"` 57 58 DB DBConfig `json:"db" toml:"db" yaml:"db"` 59 DBPtr *DBConfig `json:"db_ptr" toml:"db_ptr" yaml:"db_ptr"` 60 61 DefaultMQ MQConfig `json:"default_mq" toml:"default_mq" yaml:"default_mq"` 62 DefaultMQPtr *MQConfig `json:"default_mq_ptr" toml:"default_mq_ptr" yaml:"default_mq_ptr"` 63 64 MQList []*MQConfig `json:"mq_list" toml:"mq_list" yaml:"mq_list"` 65 66 SomeDuration time.Duration `json:"some_duration" yaml:"some_duration" default:"2s"` 67 } 68 69 func TestLoad_SingleFile_JSON(t *testing.T) { 70 configFiles := []string{ 71 "./testdata/config.test.json", 72 } 73 testLoad_SingleFile(t, configFiles...) 74 } 75 76 func TestLoad_SingleFile_TOML(t *testing.T) { 77 configFiles := []string{ 78 "./testdata/config.test.toml", 79 } 80 testLoad_SingleFile(t, configFiles...) 81 } 82 83 func TestLoad_SingleFile_YAML(t *testing.T) { 84 configFiles := []string{ 85 "./testdata/config.test.yml", 86 } 87 testLoad_SingleFile(t, configFiles...) 88 } 89 90 func testLoad_SingleFile(t *testing.T, files ...string) { 91 cfg := &TestConfig{} 92 err := New(&Config{Verbose: true}).loadFiles(cfg, files...) 93 assert.Nil(t, err) 94 assertSingleFileConfig(t, cfg) 95 96 assert.Empty(t, cfg.Some4) // not set in test config 97 assert.Equal(t, []int{5, 6, 7}, cfg.Some5) // from test config 98 assert.Equal(t, []int64{6, 7, 8}, cfg.Some6) // from test config 99 assert.Empty(t, cfg.Some7) // not set in test config 100 } 101 102 func assertSingleFileConfig(t *testing.T, cfg *TestConfig, exclude ...string) { 103 testFields := []struct { 104 Key string 105 Expected any 106 Actual any 107 }{ 108 {"some_1", "some_1", cfg.Some1}, 109 {"some_1_ptr", "some_1_ptr", *cfg.Some1Ptr}, 110 {"some_2", 2345, cfg.Some2}, 111 {"some_2_ptr", 23456, *cfg.Some2Ptr}, 112 {"some_8", []string{"a", "b", "c"}, cfg.Some8}, 113 {"mysql_dsn", "mysql_dsn", cfg.DBPtr.MySQL}, 114 {"redis_dsn", "redis_dsn", cfg.DBPtr.Redis}, 115 {"override_env_var", int64(12345), cfg.OverrideEnvVar}, 116 } 117 for _, tf := range testFields { 118 if !inStrings(exclude, tf.Key) { 119 assert.Equal(t, tf.Expected, tf.Actual) 120 } 121 } 122 assert.NotEmpty(t, cfg.MQList) 123 } 124 125 func inStrings(slice []string, elem string) bool { 126 for _, x := range slice { 127 if x == elem { 128 return true 129 } 130 } 131 return false 132 } 133 134 func TestLoad_MultipleFiles_JSON(t *testing.T) { 135 configFiles := []string{ 136 "./testdata/config.test.json", 137 "./testdata/config.common.json", 138 } 139 testLoad_MultipleFiles(t, configFiles...) 140 } 141 142 func TestLoad_MultipleFiles_TOML(t *testing.T) { 143 configFiles := []string{ 144 "./testdata/config.test.toml", 145 "./testdata/config.common.toml", 146 } 147 testLoad_MultipleFiles(t, configFiles...) 148 } 149 150 func TestLoad_MultipleFiles_YAML(t *testing.T) { 151 configFiles := []string{ 152 "./testdata/config.test.yml", 153 "./testdata/config.common.yml", 154 } 155 testLoad_MultipleFiles(t, configFiles...) 156 } 157 158 func testLoad_MultipleFiles(t *testing.T, files ...string) { 159 cfg := &TestConfig{} 160 loader := New(&Config{Verbose: true}) 161 err := loader.loadFiles(cfg, files...) 162 assert.Nil(t, err) 163 assertSingleFileConfig(t, cfg) 164 assertCommonConfig(t, cfg) 165 166 assert.Equal(t, 2345, cfg.Some2) // config.test override config.common 167 } 168 169 func assertCommonConfig(t *testing.T, cfg *TestConfig) { 170 assert.Equal(t, int64(-3456), cfg.Some3) 171 assert.Equal(t, int64(-34567), *cfg.Some3Ptr) 172 assert.Equal(t, "common_mysql_dsn", cfg.DB.MySQL) 173 assert.Equal(t, "common_redis_dsn", cfg.DB.Redis) 174 assert.Equal(t, "common_default_mq_topic", cfg.DefaultMQ.Topic) 175 assert.Empty(t, cfg.DefaultMQ.Cluster) // processDefaults not called 176 assert.Empty(t, cfg.DefaultMQ.Group) // neither set in config.test.yml or config.common.yml 177 178 assert.Empty(t, cfg.Some4) // not set in either test config or common config 179 assert.Equal(t, []int{5, 6, 7}, cfg.Some5) // from test config 180 assert.Equal(t, []int64{6, 7, 8}, cfg.Some6) // from test config 181 assert.Equal(t, []int32{-7, -8, -9}, cfg.Some7) // from common config 182 assert.Equal(t, "env_var_override", cfg.ImplicitEnvVarOverride) 183 } 184 185 func TestLoad_DefaultValues_JSON(t *testing.T) { 186 configFiles := []string{ 187 "./testdata/config.test.json", 188 "./testdata/config.common.json", 189 } 190 testLoad_DefaultValues(t, configFiles...) 191 } 192 193 func TestLoad_DefaultValues_TOML(t *testing.T) { 194 configFiles := []string{ 195 "./testdata/config.test.toml", 196 "./testdata/config.common.toml", 197 } 198 testLoad_DefaultValues(t, configFiles...) 199 } 200 201 func TestLoad_DefaultValues_YAML(t *testing.T) { 202 configFiles := []string{ 203 "./testdata/config.test.yml", 204 "./testdata/config.common.yml", 205 } 206 testLoad_DefaultValues(t, configFiles...) 207 } 208 209 func testLoad_DefaultValues(t *testing.T, files ...string) { 210 cfg := &TestConfig{} 211 loader := New(&Config{Verbose: true}) 212 err := loader.loadFiles(cfg, files...) 213 assert.Nil(t, err) 214 err = loader.processDefaults(cfg) 215 assert.Nil(t, err) 216 assertSingleFileConfig(t, cfg) 217 218 assert.Empty(t, cfg.DefaultMQPtr) // pointer value not set 219 assert.Equal(t, "some_default_cluster", cfg.DefaultMQ.Cluster) 220 assert.Equal(t, "some_default_cluster", cfg.MQList[0].Cluster) 221 assert.Equal(t, "topic_0", cfg.MQList[0].Topic) 222 assert.Equal(t, "group_0", cfg.MQList[0].Group) 223 assert.Equal(t, "some_default_cluster", cfg.MQList[1].Cluster) 224 assert.Equal(t, "topic_1", cfg.MQList[1].Topic) 225 assert.Equal(t, "group_1", cfg.MQList[1].Group) 226 assert.Equal(t, "cluster_2", cfg.MQList[2].Cluster) 227 assert.Equal(t, "topic_2", cfg.MQList[2].Topic) 228 assert.Equal(t, "group_2", cfg.MQList[2].Group) 229 assert.Equal(t, 2*time.Second, cfg.SomeDuration) 230 } 231 232 func TestLoad_CommandLineFlag_JSON(t *testing.T) { 233 configFiles := []string{ 234 "./testdata/config.test.json", 235 "./testdata/config.common.json", 236 } 237 testLoad_CommandLineFlag(t, configFiles...) 238 } 239 240 func TestLoad_CommandLineFlag_TOML(t *testing.T) { 241 configFiles := []string{ 242 "./testdata/config.test.toml", 243 "./testdata/config.common.toml", 244 } 245 testLoad_CommandLineFlag(t, configFiles...) 246 } 247 248 func TestLoad_CommandLineFlag_YAML(t *testing.T) { 249 configFiles := []string{ 250 "./testdata/config.test.yml", 251 "./testdata/config.common.yml", 252 } 253 testLoad_CommandLineFlag(t, configFiles...) 254 } 255 256 var defineFlagsOnce sync.Once 257 258 func testLoad_CommandLineFlag(t *testing.T, files ...string) { 259 defineFlagsOnce.Do(func() { 260 flag.String("some1", "", "some string value 1") 261 flag.String("some1_ptr", "flag value some1_ptr", "some string ptr value 1") 262 flag.Int("some2", 0, "some int value 2") 263 flag.Bool("some-bool", false, "some bool value") 264 flag.Bool("some-bool-ptr1", false, "some bool ptr value 1") 265 flag.Bool("some-bool-ptr2", true, "some bool ptr value 2") 266 }) 267 268 var err error 269 err = flag.Set("some1", "some1 flag.Set called") 270 assert.Nil(t, err) 271 err = flag.Set("some2", "5432") 272 assert.Nil(t, err) 273 err = flag.Set("some-bool", "true") 274 assert.Nil(t, err) 275 err = flag.Set("some-bool-ptr1", "false") 276 assert.Nil(t, err) 277 278 cfg := &TestConfig{} 279 loader := New(&Config{ 280 Verbose: true, 281 FlagSet: flag.CommandLine, 282 }) 283 err = loader.loadFiles(cfg, files...) 284 assert.Nil(t, err) 285 err = loader.processFlags(cfg) 286 assert.Nil(t, err) 287 assertSingleFileConfig(t, cfg, "some_1", "some_2") 288 assertCommonConfig(t, cfg) 289 290 // flags 291 292 assert.Equal(t, "some1 flag.Set called", cfg.Some1) // flag override config.test 293 assert.Equal(t, 5432, cfg.Some2) // flag override config.test and config.common 294 295 assert.True(t, cfg.SomeBool) // set to true 296 assert.False(t, *cfg.SomeBoolPtr1) // set to false 297 assert.True(t, *cfg.SomeBoolPtr2) // flag default value true 298 } 299 300 func TestLoad_CustomLoader_JSON(t *testing.T) { 301 configFiles := []string{ 302 "./testdata/config.test.json", 303 "./testdata/config.common.json", 304 } 305 testLoad_CustomLoader(t, configFiles...) 306 } 307 308 func TestLoad_CustomLoader_TOML(t *testing.T) { 309 configFiles := []string{ 310 "./testdata/config.test.toml", 311 "./testdata/config.common.toml", 312 } 313 testLoad_CustomLoader(t, configFiles...) 314 } 315 316 func TestLoad_CustomLoader_YAML(t *testing.T) { 317 configFiles := []string{ 318 "./testdata/config.test.yml", 319 "./testdata/config.common.yml", 320 } 321 testLoad_CustomLoader(t, configFiles...) 322 } 323 324 func testLoad_CustomLoader(t *testing.T, files ...string) { 325 cfg := &TestConfig{} 326 loader := New(&Config{ 327 Verbose: true, 328 CustomLoader: testCustomLoader, 329 }) 330 err := loader.loadFiles(cfg, files...) 331 assert.Nil(t, err) 332 assertSingleFileConfig(t, cfg) 333 err = loader.processCustom(cfg) 334 assert.Nil(t, err) 335 336 assert.Equal(t, "remote_value_1", cfg.SomeRemoteValue1) 337 assert.Equal(t, "remote_value_1_ptr", *cfg.SomeRemoteValue1Ptr) 338 assert.Equal(t, []string{"a", "b", "c"}, cfg.SomeRemoteValue2) 339 assert.Equal(t, MQConfig{ 340 Cluster: "remote_struct_cluster", 341 Topic: "remote_struct_topic", 342 Group: "remote_struct_group", 343 }, *cfg.SomeRemoteStruct) 344 } 345 346 func testCustomLoader(typ reflect.Type, tag string) (any, error) { 347 return testCustomLoaderData[tag], nil 348 } 349 350 var testCustomLoaderData = map[string]any{ 351 "some_remote_value_1": "remote_value_1", 352 "some_remote_value_1_ptr": "remote_value_1_ptr", 353 "some_remote_value_2": []string{"a", "b", "c"}, 354 "some_remote_struct": &MQConfig{ 355 Cluster: "remote_struct_cluster", 356 Topic: "remote_struct_topic", 357 Group: "remote_struct_group", 358 }, 359 } 360 361 func TestUnmarshalFunc(t *testing.T) { 362 type testStruct struct { 363 A, B string 364 } 365 loader := New(&Config{ 366 UnmarshalFunc: func(data []byte, v any, disallowUnknownFields bool) error { 367 x := v.(*testStruct) 368 x.A = "aaa" 369 x.B = "bbb" 370 return nil 371 }, 372 }) 373 var dst testStruct 374 err := loader.Load(&dst, "./testdata/config.test.yml") 375 assert.Nil(t, err) 376 assert.Equal(t, "aaa", dst.A) 377 assert.Equal(t, "bbb", dst.B) 378 } 379 380 func Test_getEnvName(t *testing.T) { 381 testcases := [][]string{ 382 {"ManualOverride1", "Manual_Override1"}, 383 {"DefaultVar", "Default_Var"}, 384 {"AutoSplitVar", "Auto_Split_Var"}, 385 {"SomeID", "Some_ID"}, 386 {"SomeHTMLWord", "Some_HTML_Word"}, 387 {"Parent2_SomeID", "Parent2_Some_ID"}, 388 {"Parent2_SomeHTMLWord", "Parent2_Some_HTML_Word"}, 389 } 390 loader := New(&Config{}) 391 for _, tc := range testcases { 392 name := tc[0] 393 want1 := "Confr_" + tc[1] 394 got1 := loader.getEnvName("", name) 395 assert.Equal(t, want1, got1) 396 397 want2 := "MyApp_" + tc[1] 398 got2 := loader.getEnvName("MyApp", name) 399 assert.Equal(t, want2, got2) 400 } 401 } 402 403 func TestLoad_ExplicitEnv(t *testing.T) { 404 testEnv := [][]string{ 405 {"OVERRIDE_ENV_VAR", "54321"}, 406 {"EXPLICIT_ENV_VAR1", "ExplicitEnvVar1"}, 407 } 408 for _, te := range testEnv { 409 _ = os.Setenv(te[0], te[1]) 410 } 411 defer func() { 412 for _, te := range testEnv { 413 _ = os.Unsetenv(te[0]) 414 } 415 }() 416 417 configFiles := []string{ 418 "./testdata/config.test.yml", 419 "./testdata/config.common.yml", 420 } 421 cfg := &TestConfig{} 422 loader := New(&Config{ 423 Verbose: true, 424 CustomLoader: testCustomLoader, 425 }) 426 err := loader.loadFiles(cfg, configFiles...) 427 assert.Nil(t, err) 428 assertSingleFileConfig(t, cfg) 429 err = loader.processCustom(cfg) 430 assert.Nil(t, err) 431 err = loader.processEnv(cfg, "") 432 assert.Nil(t, err) 433 434 assert.Equal(t, int64(54321), cfg.OverrideEnvVar) 435 assert.Equal(t, "ExplicitEnvVar1", cfg.ExplicitEnvVar1) 436 assert.Nil(t, cfg.ExplicitEnvVar2) 437 438 assert.Empty(t, cfg.ImplicitEnvVar1) 439 assert.Empty(t, cfg.ImplicitEnvVar2) 440 assert.Equal(t, "env_var_override", cfg.ImplicitEnvVarOverride) // from config.common 441 } 442 443 func TestLoad_ImplicitEnv(t *testing.T) { 444 testEnv := [][]string{ 445 {"Confr_Implicit_Env_Var1", "implicit env var1"}, 446 {"CONFR_IMPLICIT_ENV_VAR2", "implicit env var2"}, 447 {"Confr_Implicit_Env_Var_Override", "implicit env var override"}, 448 } 449 for _, te := range testEnv { 450 _ = os.Setenv(te[0], te[1]) 451 } 452 defer func() { 453 for _, te := range testEnv { 454 _ = os.Unsetenv(te[0]) 455 } 456 }() 457 458 configFiles := []string{ 459 "./testdata/config.test.yml", 460 "./testdata/config.common.yml", 461 } 462 cfg := &TestConfig{} 463 loader := New(&Config{ 464 Verbose: true, 465 EnableImplicitEnv: true, 466 CustomLoader: testCustomLoader, 467 }) 468 err := loader.loadFiles(cfg, configFiles...) 469 assert.Nil(t, err) 470 assertSingleFileConfig(t, cfg) 471 err = loader.processCustom(cfg) 472 assert.Nil(t, err) 473 err = loader.processEnv(cfg, "") 474 assert.Nil(t, err) 475 476 assert.Equal(t, "implicit env var1", *cfg.ImplicitEnvVar1) 477 assert.Equal(t, "implicit env var2", cfg.ImplicitEnvVar2) 478 assert.Equal(t, "implicit env var override", cfg.ImplicitEnvVarOverride) 479 } 480 481 func TestLoad_AllowUnknownFields_JSON(t *testing.T) { 482 configFiles := []string{"./testdata/config.unknown_fields.json"} 483 testLoad_AllowUnknownFields(t, configFiles...) 484 } 485 486 func TestLoad_AllowUnknownFields_TOML(t *testing.T) { 487 configFiles := []string{"./testdata/config.unknown_fields.toml"} 488 testLoad_AllowUnknownFields(t, configFiles...) 489 } 490 491 func TestLoad_AllowUnknownFields_YAML(t *testing.T) { 492 configFiles := []string{"./testdata/config.unknown_fields.yml"} 493 testLoad_AllowUnknownFields(t, configFiles...) 494 } 495 496 func testLoad_AllowUnknownFields(t *testing.T, files ...string) { 497 cfg := &TestConfig{} 498 loader := New(&Config{}) 499 err := loader.Load(cfg, files...) 500 assert.Nil(t, err) 501 } 502 503 func TestLoad_DisallowUnknownFields_JSON(t *testing.T) { 504 configFiles := []string{"./testdata/config.unknown_fields.json"} 505 testLoad_DisallowUnknownFields(t, configFiles...) 506 } 507 508 func TestLoad_DisallowUnknownFields_TOML(t *testing.T) { 509 configFiles := []string{"./testdata/config.unknown_fields.toml"} 510 testLoad_DisallowUnknownFields(t, configFiles...) 511 } 512 513 func TestLoad_DisallowUnknownFields_YAML(t *testing.T) { 514 configFiles := []string{"./testdata/config.unknown_fields.yml"} 515 testLoad_DisallowUnknownFields(t, configFiles...) 516 } 517 518 func testLoad_DisallowUnknownFields(t *testing.T, files ...string) { 519 cfg := &TestConfig{} 520 loader := New(&Config{DisallowUnknownFields: true}) 521 err := loader.Load(cfg, files...) 522 assert.NotNil(t, err) 523 assert.Contains(t, err.Error(), "unknown_field") 524 }