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  }