// File: lixenwraith/config/builder_test.go package config_test import ( "errors" "os" "testing" "github.com/lixenwraith/config" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestBuilder(t *testing.T) { t.Run("Basic Builder", func(t *testing.T) { type AppConfig struct { Name string `toml:"name"` Version string `toml:"version"` Debug bool `toml:"debug"` } defaults := AppConfig{ Name: "testapp", Version: "1.0.0", Debug: false, } cfg, err := config.NewBuilder(). WithDefaults(defaults). WithPrefix("app."). Build() require.NoError(t, err) // Check registered paths paths := cfg.GetRegisteredPaths("app.") assert.Len(t, paths, 3) // Check values name, err := cfg.String("app.name") require.NoError(t, err) assert.Equal(t, "testapp", name) }) t.Run("Builder with All Options", func(t *testing.T) { os.Setenv("BUILDER_SERVER_PORT", "5555") defer os.Unsetenv("BUILDER_SERVER_PORT") type Config struct { Server struct { Host string `toml:"host"` Port int `toml:"port"` } `toml:"server"` API struct { Key string `toml:"key"` Timeout int `toml:"timeout"` } `toml:"api"` } defaults := Config{} defaults.Server.Host = "localhost" defaults.Server.Port = 8080 defaults.API.Timeout = 30 cfg, err := config.NewBuilder(). WithDefaults(defaults). WithEnvPrefix("BUILDER_"). WithArgs([]string{"--api.key=test-key"}). WithSources( config.SourceCLI, config.SourceEnv, config.SourceDefault, ). WithEnvWhitelist("server.port", "api.key"). Build() require.NoError(t, err) // CLI should provide api.key apiKey, err := cfg.String("api.key") require.NoError(t, err) assert.Equal(t, "test-key", apiKey) // Env should provide server.port (whitelisted) port, err := cfg.Int64("server.port") require.NoError(t, err) assert.Equal(t, int64(5555), port) // Non-whitelisted env should not load os.Setenv("BUILDER_API_TIMEOUT", "99") defer os.Unsetenv("BUILDER_API_TIMEOUT") cfg2, err := config.NewBuilder(). WithDefaults(defaults). WithEnvPrefix("BUILDER_"). WithEnvWhitelist("server.port"). // api.timeout NOT whitelisted Build() require.NoError(t, err) timeout, err := cfg2.Int64("api.timeout") require.NoError(t, err) assert.Equal(t, int64(30), timeout, "non-whitelisted env should not load") }) t.Run("Builder Custom Transform", func(t *testing.T) { os.Setenv("PORT", "3333") os.Setenv("DB_URL", "postgres://custom") defer func() { os.Unsetenv("PORT") os.Unsetenv("DB_URL") }() type Config struct { Server struct { Port int `toml:"port"` } `toml:"server"` Database struct { URL string `toml:"url"` } `toml:"database"` } cfg, err := config.NewBuilder(). WithDefaults(Config{}). WithEnvTransform(func(path string) string { switch path { case "server.port": return "PORT" case "database.url": return "DB_URL" default: return "" } }). Build() require.NoError(t, err) port, err := cfg.Int64("server.port") require.NoError(t, err) assert.Equal(t, int64(3333), port) dbURL, err := cfg.String("database.url") require.NoError(t, err) assert.Equal(t, "postgres://custom", dbURL) }) t.Run("MustBuild Panic", func(t *testing.T) { assert.Panics(t, func() { config.NewBuilder(). WithDefaults("not a struct"). MustBuild() }) }) t.Run("Builder with Validator", func(t *testing.T) { type Config struct { Server struct { Host string `toml:"host"` Port int `toml:"port"` } `toml:"server"` MaxConns int `toml:"max_conns"` } defaults := Config{} defaults.Server.Host = "localhost" defaults.Server.Port = 8080 defaults.MaxConns = 100 // Validator that fails failingValidator := func(c *config.Config) error { port, err := c.Int64("server.port") if err != nil { return err } if port == 8080 { return errors.New("port 8080 is not allowed") } return nil } // Validator that succeeds passingValidator := func(c *config.Config) error { host, err := c.String("server.host") if err != nil { return err } if host == "" { return errors.New("host cannot be empty") } return nil } // Test case 1: Validator fails _, err := config.NewBuilder(). WithDefaults(defaults). WithValidator(failingValidator). Build() require.Error(t, err) assert.Contains(t, err.Error(), "port 8080 is not allowed") // Test case 2: Validator passes cfg, err := config.NewBuilder(). WithDefaults(defaults). WithArgs([]string{"--server.port=9000"}). // Change the port so it passes WithValidator(failingValidator). WithValidator(passingValidator). Build() require.NoError(t, err) assert.NotNil(t, cfg) port, _ := cfg.Int64("server.port") assert.Equal(t, int64(9000), port) // Test case 3: MustBuild panics on validation failure assert.PanicsWithError(t, "configuration validation failed: port 8080 is not allowed", func() { config.NewBuilder(). WithDefaults(defaults). WithValidator(failingValidator). MustBuild() }) }) } func TestQuickFunctions(t *testing.T) { t.Run("Quick Success", func(t *testing.T) { type Config struct { App struct { Name string `toml:"name"` } `toml:"app"` } defaults := Config{} defaults.App.Name = "quicktest" cfg, err := config.Quick(defaults, "QUICK_", "") require.NoError(t, err) name, err := cfg.String("app.name") require.NoError(t, err) assert.Equal(t, "quicktest", name) }) t.Run("QuickCustom", func(t *testing.T) { opts := config.LoadOptions{ Sources: []config.Source{ config.SourceDefault, config.SourceEnv, }, EnvPrefix: "CUSTOM_", } cfg, err := config.QuickCustom(nil, opts, "") require.NoError(t, err) assert.NotNil(t, cfg) }) t.Run("MustQuick Panic", func(t *testing.T) { assert.Panics(t, func() { config.MustQuick("invalid", "TEST_", "") }) }) } func TestConvenienceFunctions(t *testing.T) { t.Run("Validate", func(t *testing.T) { cfg := config.New() cfg.Register("required1", "") cfg.Register("required2", 0) cfg.Register("optional", "has-default") // Initial validation should fail err := cfg.Validate("required1", "required2") assert.Error(t, err, "expected validation to fail for empty values") // Set required values cfg.Set("required1", "value1") cfg.Set("required2", 42) // Now should pass err = cfg.Validate("required1", "required2") assert.NoError(t, err) // Validate unregistered path err = cfg.Validate("unregistered") assert.Error(t, err, "expected error for unregistered path") }) t.Run("Debug Output", func(t *testing.T) { cfg := config.New() cfg.Register("test.value", "default") cfg.SetSource("test.value", config.SourceFile, "from-file") cfg.SetSource("test.value", config.SourceEnv, "from-env") debug := cfg.Debug() // Should contain key information assert.NotEmpty(t, debug) // Should show sources - checking for actual source string values assert.Contains(t, debug, "file") assert.Contains(t, debug, "from-file") assert.Contains(t, debug, "env") assert.Contains(t, debug, "from-env") }) t.Run("Clone", func(t *testing.T) { cfg := config.New() cfg.Register("original", "value") cfg.Set("original", "modified") // Clone configuration clone := cfg.Clone() // Clone should have same values val, err := clone.String("original") require.NoError(t, err) assert.Equal(t, "modified", val) // Modifying clone should not affect original clone.Set("original", "clone-modified") origVal, err := cfg.String("original") require.NoError(t, err) assert.Equal(t, "modified", origVal, "original should not be affected by clone modification") }) t.Run("GetRegisteredPathsWithDefaults", func(t *testing.T) { cfg := config.New() cfg.Register("app.name", "myapp") cfg.Register("app.version", "1.0.0") cfg.Register("server.port", 8080) // Get paths with defaults paths := cfg.GetRegisteredPathsWithDefaults("app.") assert.Len(t, paths, 2) assert.Equal(t, "myapp", paths["app.name"]) assert.Equal(t, "1.0.0", paths["app.version"]) }) }