e3.0.0 Added env variable support, improved cli arg, added tests, updated documentation.

This commit is contained in:
2025-07-01 13:06:07 -04:00
parent b1e241149e
commit 4053c463d6
17 changed files with 2290 additions and 615 deletions

225
env_test.go Normal file
View File

@ -0,0 +1,225 @@
// File: lixenwraith/config/env_test.go
package config_test
import (
"os"
"testing"
"github.com/lixenwraith/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestEnvironmentVariables(t *testing.T) {
t.Run("Basic Environment Loading", func(t *testing.T) {
// Set up environment
envVars := map[string]string{
"TEST_SERVER_HOST": "env-host",
"TEST_SERVER_PORT": "9999",
"TEST_DEBUG": "true",
}
for k, v := range envVars {
os.Setenv(k, v)
defer os.Unsetenv(k)
}
cfg := config.New()
cfg.Register("server.host", "default-host")
cfg.Register("server.port", 8080)
cfg.Register("debug", false)
// Load environment variables
err := cfg.LoadEnv("TEST_")
require.NoError(t, err)
// Verify values
host, _ := cfg.String("server.host")
assert.Equal(t, "env-host", host)
port, _ := cfg.Int64("server.port")
assert.Equal(t, int64(9999), port)
debug, _ := cfg.Bool("debug")
assert.True(t, debug)
})
t.Run("Custom Environment Transform", func(t *testing.T) {
os.Setenv("PORT", "3000")
os.Setenv("DATABASE_URL", "postgres://localhost/test")
defer func() {
os.Unsetenv("PORT")
os.Unsetenv("DATABASE_URL")
}()
cfg := config.New()
cfg.Register("server.port", 8080)
cfg.Register("database.url", "sqlite://memory")
opts := config.LoadOptions{
Sources: []config.Source{config.SourceEnv, config.SourceDefault},
EnvTransform: func(path string) string {
mapping := map[string]string{
"server.port": "PORT",
"database.url": "DATABASE_URL",
}
return mapping[path]
},
}
err := cfg.LoadWithOptions("", nil, opts)
require.NoError(t, err)
port, _ := cfg.Int64("server.port")
assert.Equal(t, int64(3000), port)
dbURL, _ := cfg.String("database.url")
assert.Equal(t, "postgres://localhost/test", dbURL)
})
t.Run("Environment Discovery", func(t *testing.T) {
// Set up various env vars
envVars := map[string]string{
"APP_SERVER_HOST": "discovered",
"APP_SERVER_PORT": "4444",
"APP_UNREGISTERED": "ignored",
}
for k, v := range envVars {
os.Setenv(k, v)
defer os.Unsetenv(k)
}
cfg := config.New()
cfg.Register("server.host", "default")
cfg.Register("server.port", 8080)
cfg.Register("server.timeout", 30)
// Discover which registered paths have env vars
discovered := cfg.DiscoverEnv("APP_")
// Should find 2 env vars
assert.Len(t, discovered, 2)
assert.Equal(t, "APP_SERVER_HOST", discovered["server.host"])
assert.Equal(t, "APP_SERVER_PORT", discovered["server.port"])
assert.NotContains(t, discovered, "unregistered")
})
t.Run("Environment Whitelist", func(t *testing.T) {
envVars := map[string]string{
"SECRET_API_KEY": "secret-value",
"SECRET_DATABASE_PASSWORD": "db-pass",
"SECRET_SERVER_PORT": "5555",
}
for k, v := range envVars {
os.Setenv(k, v)
defer os.Unsetenv(k)
}
cfg := config.New()
cfg.Register("api.key", "")
cfg.Register("database.password", "")
cfg.Register("server.port", 8080)
opts := config.LoadOptions{
Sources: []config.Source{config.SourceEnv, config.SourceDefault},
EnvPrefix: "SECRET_",
EnvWhitelist: map[string]bool{
"api.key": true,
"database.password": true,
// server.port is NOT whitelisted
},
}
cfg.LoadWithOptions("", nil, opts)
// Whitelisted values should load
apiKey, _ := cfg.String("api.key")
assert.Equal(t, "secret-value", apiKey)
dbPass, _ := cfg.String("database.password")
assert.Equal(t, "db-pass", dbPass)
// Non-whitelisted should use default
port, _ := cfg.Int64("server.port")
assert.Equal(t, int64(8080), port)
})
t.Run("RegisterWithEnv", func(t *testing.T) {
os.Setenv("CUSTOM_PORT", "6666")
defer os.Unsetenv("CUSTOM_PORT")
cfg := config.New()
// Register with explicit env mapping
err := cfg.RegisterWithEnv("server.port", 8080, "CUSTOM_PORT")
require.NoError(t, err)
// Should immediately have env value
port, _ := cfg.Int64("server.port")
assert.Equal(t, int64(6666), port)
})
t.Run("Export Environment", func(t *testing.T) {
cfg := config.New()
cfg.Register("app.name", "myapp")
cfg.Register("app.version", "1.0.0")
cfg.Register("server.port", 8080)
// Set some non-default values
cfg.Set("app.version", "2.0.0")
cfg.Set("server.port", 9090)
// Export as env vars
exports := cfg.ExportEnv("EXPORT_")
// Should export non-default values
assert.Equal(t, "2.0.0", exports["EXPORT_APP_VERSION"])
assert.Equal(t, "9090", exports["EXPORT_SERVER_PORT"])
// Should not export defaults
assert.NotContains(t, exports, "EXPORT_APP_NAME")
})
t.Run("Type Parsing from Environment", func(t *testing.T) {
envVars := map[string]string{
"TYPES_STRING": "hello world",
"TYPES_INT": "42",
"TYPES_FLOAT": "3.14159",
"TYPES_BOOL_TRUE": "true",
"TYPES_BOOL_FALSE": "false",
"TYPES_QUOTED": `"quoted string"`,
}
for k, v := range envVars {
os.Setenv(k, v)
defer os.Unsetenv(k)
}
cfg := config.New()
cfg.Register("string", "")
cfg.Register("int", 0)
cfg.Register("float", 0.0)
cfg.Register("bool.true", false)
cfg.Register("bool.false", true)
cfg.Register("quoted", "")
cfg.LoadEnv("TYPES_")
// Verify type conversions
s, _ := cfg.String("string")
assert.Equal(t, "hello world", s)
i, _ := cfg.Int64("int")
assert.Equal(t, int64(42), i)
f, _ := cfg.Float64("float")
assert.Equal(t, 3.14159, f)
bt, _ := cfg.Bool("bool.true")
assert.True(t, bt)
bf, _ := cfg.Bool("bool.false")
assert.False(t, bf)
q, _ := cfg.String("quoted")
assert.Equal(t, "quoted string", q)
})
}