mirror of
https://github.com/taigrr/jety.git
synced 2026-04-01 19:08:58 -07:00
The documented precedence is Set > env > file > defaults, but collapse() was using maps.Copy(ccm, mapConfig) which let file values (and Set values, stored in the same map) unconditionally overwrite env values. Split mapConfig into fileConfig (from ReadInConfig) and overrideConfig (from Set/SetString/SetBool). collapse() now applies layers in correct order: defaults, then file, then env (for known keys), then overrides. Added TestPrecedenceChain to verify the full layering.
1400 lines
33 KiB
Go
1400 lines
33 KiB
Go
package jety
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// Test file contents
|
|
const (
|
|
tomlConfig = `
|
|
port = 8080
|
|
host = "localhost"
|
|
debug = true
|
|
timeout = "30s"
|
|
rate = 1.5
|
|
tags = ["api", "v1"]
|
|
counts = [1, 2, 3]
|
|
|
|
[database]
|
|
host = "db.example.com"
|
|
port = 5432
|
|
`
|
|
|
|
yamlConfig = `
|
|
port: 9090
|
|
host: "yaml-host"
|
|
debug: false
|
|
timeout: "1m"
|
|
rate: 2.5
|
|
tags:
|
|
- web
|
|
- v2
|
|
counts:
|
|
- 10
|
|
- 20
|
|
database:
|
|
host: "yaml-db.example.com"
|
|
port: 3306
|
|
`
|
|
|
|
jsonConfig = `{
|
|
"port": 7070,
|
|
"host": "json-host",
|
|
"debug": true,
|
|
"timeout": "15s",
|
|
"rate": 3.5,
|
|
"tags": ["json", "v3"],
|
|
"counts": [100, 200],
|
|
"database": {
|
|
"host": "json-db.example.com",
|
|
"port": 27017
|
|
}
|
|
}`
|
|
)
|
|
|
|
func TestNewConfigManager(t *testing.T) {
|
|
cm := NewConfigManager()
|
|
if cm == nil {
|
|
t.Fatal("NewConfigManager returned nil")
|
|
}
|
|
if cm.envConfig == nil {
|
|
t.Error("envConfig not initialized")
|
|
}
|
|
if cm.overrideConfig == nil {
|
|
t.Error("mapConfig not initialized")
|
|
}
|
|
if cm.defaultConfig == nil {
|
|
t.Error("defaultConfig not initialized")
|
|
}
|
|
if cm.combinedConfig == nil {
|
|
t.Error("combinedConfig not initialized")
|
|
}
|
|
}
|
|
|
|
func TestSetAndGetString(t *testing.T) {
|
|
cm := NewConfigManager()
|
|
cm.Set("name", "test-value")
|
|
|
|
got := cm.GetString("name")
|
|
if got != "test-value" {
|
|
t.Errorf("GetString() = %q, want %q", got, "test-value")
|
|
}
|
|
|
|
// Case insensitive
|
|
got = cm.GetString("NAME")
|
|
if got != "test-value" {
|
|
t.Errorf("GetString(NAME) = %q, want %q", got, "test-value")
|
|
}
|
|
}
|
|
|
|
func TestSetAndGetInt(t *testing.T) {
|
|
cm := NewConfigManager()
|
|
|
|
tests := []struct {
|
|
name string
|
|
value any
|
|
want int
|
|
}{
|
|
{"int", 42, 42},
|
|
{"string", "123", 123},
|
|
{"float64", 99.9, 99},
|
|
{"float32", float32(50.5), 50},
|
|
{"invalid string", "not-a-number", 0},
|
|
{"nil", nil, 0},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
cm.Set("key", tt.value)
|
|
got := cm.GetInt("key")
|
|
if got != tt.want {
|
|
t.Errorf("GetInt() = %d, want %d", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSetAndGetBool(t *testing.T) {
|
|
cm := NewConfigManager()
|
|
|
|
tests := []struct {
|
|
name string
|
|
value any
|
|
want bool
|
|
}{
|
|
{"true bool", true, true},
|
|
{"false bool", false, false},
|
|
{"string true", "true", true},
|
|
{"string TRUE", "TRUE", true},
|
|
{"string false", "false", false},
|
|
{"string other", "yes", false},
|
|
{"int zero", 0, false},
|
|
{"int nonzero", 1, true},
|
|
{"float32 zero", float32(0), false},
|
|
{"float32 nonzero", float32(1.5), true},
|
|
{"float64 zero", float64(0), false},
|
|
{"float64 nonzero", float64(1.5), true},
|
|
{"duration zero", time.Duration(0), false},
|
|
{"duration positive", time.Second, true},
|
|
{"nil", nil, false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
cm.Set("key", tt.value)
|
|
got := cm.GetBool("key")
|
|
if got != tt.want {
|
|
t.Errorf("GetBool() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSetAndGetDuration(t *testing.T) {
|
|
cm := NewConfigManager()
|
|
|
|
tests := []struct {
|
|
name string
|
|
value any
|
|
want time.Duration
|
|
}{
|
|
{"duration", 5 * time.Second, 5 * time.Second},
|
|
{"string", "10s", 10 * time.Second},
|
|
{"string minutes", "2m", 2 * time.Minute},
|
|
{"invalid string", "not-duration", 0},
|
|
{"int", 1000, time.Duration(1000)},
|
|
{"int64", int64(2000), time.Duration(2000)},
|
|
{"float64", float64(3000), time.Duration(3000)},
|
|
{"float32", float32(4000), time.Duration(4000)},
|
|
{"nil", nil, 0},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
cm.Set("key", tt.value)
|
|
got := cm.GetDuration("key")
|
|
if got != tt.want {
|
|
t.Errorf("GetDuration() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSetAndGetStringSlice(t *testing.T) {
|
|
cm := NewConfigManager()
|
|
|
|
// Direct string slice
|
|
cm.Set("tags", []string{"a", "b", "c"})
|
|
got := cm.GetStringSlice("tags")
|
|
if len(got) != 3 || got[0] != "a" || got[1] != "b" || got[2] != "c" {
|
|
t.Errorf("GetStringSlice() = %v, want [a b c]", got)
|
|
}
|
|
|
|
// []any slice
|
|
cm.Set("mixed", []any{"x", 123, "z"})
|
|
got = cm.GetStringSlice("mixed")
|
|
if len(got) != 3 || got[0] != "x" || got[1] != "123" || got[2] != "z" {
|
|
t.Errorf("GetStringSlice() = %v, want [x 123 z]", got)
|
|
}
|
|
|
|
// Non-slice returns nil
|
|
cm.Set("notslice", "single")
|
|
got = cm.GetStringSlice("notslice")
|
|
if got != nil {
|
|
t.Errorf("GetStringSlice() = %v, want nil", got)
|
|
}
|
|
}
|
|
|
|
func TestSetAndGetIntSlice(t *testing.T) {
|
|
cm := NewConfigManager()
|
|
|
|
// Direct int slice
|
|
cm.Set("nums", []int{1, 2, 3})
|
|
got := cm.GetIntSlice("nums")
|
|
if len(got) != 3 || got[0] != 1 || got[1] != 2 || got[2] != 3 {
|
|
t.Errorf("GetIntSlice() = %v, want [1 2 3]", got)
|
|
}
|
|
|
|
// []any slice with mixed types
|
|
cm.Set("mixed", []any{10, "20", float64(30), float32(40)})
|
|
got = cm.GetIntSlice("mixed")
|
|
if len(got) != 4 || got[0] != 10 || got[1] != 20 || got[2] != 30 || got[3] != 40 {
|
|
t.Errorf("GetIntSlice() = %v, want [10 20 30 40]", got)
|
|
}
|
|
|
|
// Invalid entries skipped
|
|
cm.Set("invalid", []any{1, "not-a-number", nil, 2})
|
|
got = cm.GetIntSlice("invalid")
|
|
if len(got) != 2 || got[0] != 1 || got[1] != 2 {
|
|
t.Errorf("GetIntSlice() = %v, want [1 2]", got)
|
|
}
|
|
}
|
|
|
|
func TestSetAndGetStringMap(t *testing.T) {
|
|
cm := NewConfigManager()
|
|
|
|
m := map[string]any{"foo": "bar", "num": 123}
|
|
cm.Set("config", m)
|
|
got := cm.GetStringMap("config")
|
|
if got["foo"] != "bar" || got["num"] != 123 {
|
|
t.Errorf("GetStringMap() = %v, want %v", got, m)
|
|
}
|
|
|
|
// Non-map returns nil
|
|
cm.Set("notmap", "string")
|
|
got = cm.GetStringMap("notmap")
|
|
if got != nil {
|
|
t.Errorf("GetStringMap() = %v, want nil", got)
|
|
}
|
|
}
|
|
|
|
func TestGet(t *testing.T) {
|
|
cm := NewConfigManager()
|
|
cm.Set("key", "value")
|
|
|
|
got := cm.Get("key")
|
|
if got != "value" {
|
|
t.Errorf("Get() = %v, want %q", got, "value")
|
|
}
|
|
|
|
// Non-existent key
|
|
got = cm.Get("nonexistent")
|
|
if got != nil {
|
|
t.Errorf("Get(nonexistent) = %v, want nil", got)
|
|
}
|
|
}
|
|
|
|
func TestSetDefault(t *testing.T) {
|
|
cm := NewConfigManager()
|
|
|
|
cm.SetDefault("port", 8080)
|
|
if got := cm.GetInt("port"); got != 8080 {
|
|
t.Errorf("GetInt(port) = %d, want 8080", got)
|
|
}
|
|
|
|
// Set overrides default
|
|
cm.Set("port", 9090)
|
|
if got := cm.GetInt("port"); got != 9090 {
|
|
t.Errorf("GetInt(port) after Set = %d, want 9090", got)
|
|
}
|
|
|
|
// New default doesn't override existing value
|
|
cm.SetDefault("port", 7070)
|
|
if got := cm.GetInt("port"); got != 9090 {
|
|
t.Errorf("GetInt(port) after second SetDefault = %d, want 9090", got)
|
|
}
|
|
}
|
|
|
|
func TestReadTOMLConfig(t *testing.T) {
|
|
dir := t.TempDir()
|
|
configFile := filepath.Join(dir, "config.toml")
|
|
if err := os.WriteFile(configFile, []byte(tomlConfig), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
cm := NewConfigManager()
|
|
cm.SetConfigFile(configFile)
|
|
if err := cm.SetConfigType("toml"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err := cm.ReadInConfig(); err != nil {
|
|
t.Fatalf("ReadInConfig() error = %v", err)
|
|
}
|
|
|
|
if got := cm.GetInt("port"); got != 8080 {
|
|
t.Errorf("GetInt(port) = %d, want 8080", got)
|
|
}
|
|
if got := cm.GetString("host"); got != "localhost" {
|
|
t.Errorf("GetString(host) = %q, want %q", got, "localhost")
|
|
}
|
|
if got := cm.GetBool("debug"); got != true {
|
|
t.Errorf("GetBool(debug) = %v, want true", got)
|
|
}
|
|
if got := cm.ConfigFileUsed(); got != configFile {
|
|
t.Errorf("ConfigFileUsed() = %q, want %q", got, configFile)
|
|
}
|
|
}
|
|
|
|
func TestReadYAMLConfig(t *testing.T) {
|
|
dir := t.TempDir()
|
|
configFile := filepath.Join(dir, "config.yaml")
|
|
if err := os.WriteFile(configFile, []byte(yamlConfig), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
cm := NewConfigManager()
|
|
cm.SetConfigFile(configFile)
|
|
if err := cm.SetConfigType("yaml"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err := cm.ReadInConfig(); err != nil {
|
|
t.Fatalf("ReadInConfig() error = %v", err)
|
|
}
|
|
|
|
if got := cm.GetInt("port"); got != 9090 {
|
|
t.Errorf("GetInt(port) = %d, want 9090", got)
|
|
}
|
|
if got := cm.GetString("host"); got != "yaml-host" {
|
|
t.Errorf("GetString(host) = %q, want %q", got, "yaml-host")
|
|
}
|
|
if got := cm.GetBool("debug"); got != false {
|
|
t.Errorf("GetBool(debug) = %v, want false", got)
|
|
}
|
|
}
|
|
|
|
func TestReadJSONConfig(t *testing.T) {
|
|
dir := t.TempDir()
|
|
configFile := filepath.Join(dir, "config.json")
|
|
if err := os.WriteFile(configFile, []byte(jsonConfig), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
cm := NewConfigManager()
|
|
cm.SetConfigFile(configFile)
|
|
if err := cm.SetConfigType("json"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err := cm.ReadInConfig(); err != nil {
|
|
t.Fatalf("ReadInConfig() error = %v", err)
|
|
}
|
|
|
|
if got := cm.GetInt("port"); got != 7070 {
|
|
t.Errorf("GetInt(port) = %d, want 7070", got)
|
|
}
|
|
if got := cm.GetString("host"); got != "json-host" {
|
|
t.Errorf("GetString(host) = %q, want %q", got, "json-host")
|
|
}
|
|
if got := cm.GetBool("debug"); got != true {
|
|
t.Errorf("GetBool(debug) = %v, want true", got)
|
|
}
|
|
}
|
|
|
|
func TestReadConfigWithDirAndName(t *testing.T) {
|
|
dir := t.TempDir()
|
|
configFile := filepath.Join(dir, "myconfig.yaml")
|
|
if err := os.WriteFile(configFile, []byte(yamlConfig), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
cm := NewConfigManager()
|
|
cm.SetConfigDir(dir)
|
|
cm.SetConfigName("myconfig")
|
|
if err := cm.SetConfigType("yaml"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err := cm.ReadInConfig(); err != nil {
|
|
t.Fatalf("ReadInConfig() error = %v", err)
|
|
}
|
|
|
|
if got := cm.GetInt("port"); got != 9090 {
|
|
t.Errorf("GetInt(port) = %d, want 9090", got)
|
|
}
|
|
if got := cm.ConfigFileUsed(); got != configFile {
|
|
t.Errorf("ConfigFileUsed() = %q, want %q", got, configFile)
|
|
}
|
|
}
|
|
|
|
func TestReadConfigNoFileSpecified(t *testing.T) {
|
|
cm := NewConfigManager()
|
|
if err := cm.SetConfigType("yaml"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err := cm.ReadInConfig()
|
|
if err == nil {
|
|
t.Error("ReadInConfig() expected error, got nil")
|
|
}
|
|
}
|
|
|
|
func TestReadConfigFileNotFound(t *testing.T) {
|
|
cm := NewConfigManager()
|
|
cm.SetConfigFile("/nonexistent/path/config.yaml")
|
|
if err := cm.SetConfigType("yaml"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err := cm.ReadInConfig()
|
|
if err != ErrConfigFileNotFound {
|
|
t.Errorf("ReadInConfig() error = %v, want ErrConfigFileNotFound", err)
|
|
}
|
|
}
|
|
|
|
func TestReadConfigFileEmpty(t *testing.T) {
|
|
dir := t.TempDir()
|
|
configFile := filepath.Join(dir, "empty.yaml")
|
|
if err := os.WriteFile(configFile, []byte{}, 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
cm := NewConfigManager()
|
|
cm.SetConfigFile(configFile)
|
|
if err := cm.SetConfigType("yaml"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err := cm.ReadInConfig()
|
|
if err != ErrConfigFileEmpty {
|
|
t.Errorf("ReadInConfig() error = %v, want ErrConfigFileEmpty", err)
|
|
}
|
|
}
|
|
|
|
func TestWriteConfig(t *testing.T) {
|
|
dir := t.TempDir()
|
|
|
|
tests := []struct {
|
|
name string
|
|
configType string
|
|
ext string
|
|
}{
|
|
{"TOML", "toml", ".toml"},
|
|
{"YAML", "yaml", ".yaml"},
|
|
{"JSON", "json", ".json"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
configFile := filepath.Join(dir, "write_test"+tt.ext)
|
|
|
|
cm := NewConfigManager()
|
|
cm.SetConfigFile(configFile)
|
|
if err := cm.SetConfigType(tt.configType); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
cm.Set("port", 8080)
|
|
cm.Set("host", "example.com")
|
|
|
|
if err := cm.WriteConfig(); err != nil {
|
|
t.Fatalf("WriteConfig() error = %v", err)
|
|
}
|
|
|
|
// Read it back
|
|
cm2 := NewConfigManager()
|
|
cm2.SetConfigFile(configFile)
|
|
if err := cm2.SetConfigType(tt.configType); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := cm2.ReadInConfig(); err != nil {
|
|
t.Fatalf("ReadInConfig() error = %v", err)
|
|
}
|
|
|
|
if got := cm2.GetInt("port"); got != 8080 {
|
|
t.Errorf("GetInt(port) = %d, want 8080", got)
|
|
}
|
|
if got := cm2.GetString("host"); got != "example.com" {
|
|
t.Errorf("GetString(host) = %q, want %q", got, "example.com")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSetConfigTypeInvalid(t *testing.T) {
|
|
cm := NewConfigManager()
|
|
err := cm.SetConfigType("xml")
|
|
if err == nil {
|
|
t.Error("SetConfigType(xml) expected error, got nil")
|
|
}
|
|
}
|
|
|
|
func TestEnvPrefix(t *testing.T) {
|
|
// Set env vars BEFORE creating ConfigManager
|
|
os.Setenv("TESTAPP_PORT", "3000")
|
|
os.Setenv("TESTAPP_HOST", "envhost")
|
|
os.Setenv("OTHER_VAR", "other")
|
|
defer func() {
|
|
os.Unsetenv("TESTAPP_PORT")
|
|
os.Unsetenv("TESTAPP_HOST")
|
|
os.Unsetenv("OTHER_VAR")
|
|
}()
|
|
|
|
// Create new manager AFTER setting env vars, then apply prefix
|
|
cm := NewConfigManager().WithEnvPrefix("TESTAPP_")
|
|
|
|
if got := cm.GetString("port"); got != "3000" {
|
|
t.Errorf("GetString(port) = %q, want %q", got, "3000")
|
|
}
|
|
if got := cm.GetString("host"); got != "envhost" {
|
|
t.Errorf("GetString(host) = %q, want %q", got, "envhost")
|
|
}
|
|
// OTHER_VAR should not be accessible without prefix
|
|
if got := cm.GetString("other_var"); got != "" {
|
|
t.Errorf("GetString(other_var) = %q, want empty", got)
|
|
}
|
|
}
|
|
|
|
func TestEnvVarWithEqualsInValue(t *testing.T) {
|
|
os.Setenv("TEST_CONN", "host=localhost;user=admin")
|
|
defer os.Unsetenv("TEST_CONN")
|
|
|
|
cm := NewConfigManager()
|
|
if got := cm.GetString("test_conn"); got != "host=localhost;user=admin" {
|
|
t.Errorf("GetString(test_conn) = %q, want %q", got, "host=localhost;user=admin")
|
|
}
|
|
}
|
|
|
|
func TestEnvOverridesDefault(t *testing.T) {
|
|
os.Setenv("MYPORT", "5000")
|
|
defer os.Unsetenv("MYPORT")
|
|
|
|
cm := NewConfigManager()
|
|
cm.SetDefault("myport", 8080)
|
|
|
|
if got := cm.GetInt("myport"); got != 5000 {
|
|
t.Errorf("GetInt(myport) = %d, want 5000 (from env)", got)
|
|
}
|
|
}
|
|
|
|
func TestEnvOverridesConfigFile(t *testing.T) {
|
|
os.Setenv("PORT", "5000")
|
|
defer os.Unsetenv("PORT")
|
|
|
|
dir := t.TempDir()
|
|
configFile := filepath.Join(dir, "config.yaml")
|
|
if err := os.WriteFile(configFile, []byte("port: 9000"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
cm := NewConfigManager()
|
|
cm.SetConfigFile(configFile)
|
|
if err := cm.SetConfigType("yaml"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cm.SetDefault("port", 8080)
|
|
|
|
if err := cm.ReadInConfig(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Env should override config file (env > file > defaults)
|
|
if got := cm.GetInt("port"); got != 5000 {
|
|
t.Errorf("GetInt(port) = %d, want 5000 (env overrides file)", got)
|
|
}
|
|
}
|
|
|
|
func TestCaseInsensitiveKeys(t *testing.T) {
|
|
cm := NewConfigManager()
|
|
cm.Set("MyKey", "value")
|
|
|
|
tests := []string{"MyKey", "mykey", "MYKEY", "mYkEy"}
|
|
for _, key := range tests {
|
|
if got := cm.GetString(key); got != "value" {
|
|
t.Errorf("GetString(%q) = %q, want %q", key, got, "value")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGetNonExistentKey(t *testing.T) {
|
|
cm := NewConfigManager()
|
|
|
|
if got := cm.GetString("nonexistent"); got != "" {
|
|
t.Errorf("GetString(nonexistent) = %q, want empty", got)
|
|
}
|
|
if got := cm.GetInt("nonexistent"); got != 0 {
|
|
t.Errorf("GetInt(nonexistent) = %d, want 0", got)
|
|
}
|
|
if got := cm.GetBool("nonexistent"); got != false {
|
|
t.Errorf("GetBool(nonexistent) = %v, want false", got)
|
|
}
|
|
if got := cm.GetDuration("nonexistent"); got != 0 {
|
|
t.Errorf("GetDuration(nonexistent) = %v, want 0", got)
|
|
}
|
|
if got := cm.GetStringSlice("nonexistent"); got != nil {
|
|
t.Errorf("GetStringSlice(nonexistent) = %v, want nil", got)
|
|
}
|
|
if got := cm.GetIntSlice("nonexistent"); got != nil {
|
|
t.Errorf("GetIntSlice(nonexistent) = %v, want nil", got)
|
|
}
|
|
if got := cm.GetStringMap("nonexistent"); got != nil {
|
|
t.Errorf("GetStringMap(nonexistent) = %v, want nil", got)
|
|
}
|
|
}
|
|
|
|
func TestGetBoolUnknownType(t *testing.T) {
|
|
cm := NewConfigManager()
|
|
cm.Set("key", struct{}{})
|
|
|
|
// Should not panic, should return false
|
|
got := cm.GetBool("key")
|
|
if got != false {
|
|
t.Errorf("GetBool(struct) = %v, want false", got)
|
|
}
|
|
}
|
|
|
|
func TestGetDurationUnknownType(t *testing.T) {
|
|
cm := NewConfigManager()
|
|
cm.Set("key", struct{}{})
|
|
|
|
// Should not panic, should return 0
|
|
got := cm.GetDuration("key")
|
|
if got != 0 {
|
|
t.Errorf("GetDuration(struct) = %v, want 0", got)
|
|
}
|
|
}
|
|
|
|
func TestConcurrentAccess(t *testing.T) {
|
|
cm := NewConfigManager()
|
|
var wg sync.WaitGroup
|
|
|
|
// Concurrent writes
|
|
for i := range 100 {
|
|
wg.Add(1)
|
|
go func(n int) {
|
|
defer wg.Done()
|
|
cm.Set("key", n)
|
|
cm.SetDefault("default", n)
|
|
cm.SetString("str", "value")
|
|
cm.SetBool("bool", true)
|
|
}(i)
|
|
}
|
|
|
|
// Concurrent reads
|
|
for range 100 {
|
|
wg.Go(func() {
|
|
_ = cm.GetInt("key")
|
|
_ = cm.GetString("str")
|
|
_ = cm.GetBool("bool")
|
|
_ = cm.Get("key")
|
|
_ = cm.ConfigFileUsed()
|
|
})
|
|
}
|
|
|
|
wg.Wait()
|
|
}
|
|
|
|
func TestConcurrentReadWrite(t *testing.T) {
|
|
dir := t.TempDir()
|
|
configFile := filepath.Join(dir, "concurrent.yaml")
|
|
if err := os.WriteFile(configFile, []byte("port: 8080"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
cm := NewConfigManager()
|
|
cm.SetConfigFile(configFile)
|
|
if err := cm.SetConfigType("yaml"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
// Reader goroutines
|
|
for range 50 {
|
|
wg.Go(func() {
|
|
for range 10 {
|
|
_ = cm.GetInt("port")
|
|
_ = cm.GetString("host")
|
|
}
|
|
})
|
|
}
|
|
|
|
// Writer goroutines
|
|
for i := range 50 {
|
|
wg.Add(1)
|
|
go func(n int) {
|
|
defer wg.Done()
|
|
for range 10 {
|
|
cm.Set("port", n)
|
|
cm.SetDefault("host", "localhost")
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
// Config operations
|
|
for range 10 {
|
|
wg.Go(func() {
|
|
_ = cm.ReadInConfig()
|
|
})
|
|
}
|
|
|
|
wg.Wait()
|
|
}
|
|
|
|
// Package-level function tests (default.go)
|
|
|
|
func TestPackageLevelFunctions(t *testing.T) {
|
|
// Reset default manager for this test
|
|
defaultConfigManager = NewConfigManager()
|
|
|
|
dir := t.TempDir()
|
|
configFile := filepath.Join(dir, "pkg_test.yaml")
|
|
if err := os.WriteFile(configFile, []byte("port: 8888\nhost: pkghost"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
SetConfigFile(configFile)
|
|
if err := SetConfigType("yaml"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
SetDefault("timeout", "30s")
|
|
|
|
if err := ReadInConfig(); err != nil {
|
|
t.Fatalf("ReadInConfig() error = %v", err)
|
|
}
|
|
|
|
// Set() must be called AFTER ReadInConfig to override file values
|
|
Set("debug", true)
|
|
|
|
if got := GetInt("port"); got != 8888 {
|
|
t.Errorf("GetInt(port) = %d, want 8888", got)
|
|
}
|
|
if got := GetString("host"); got != "pkghost" {
|
|
t.Errorf("GetString(host) = %q, want %q", got, "pkghost")
|
|
}
|
|
if got := GetBool("debug"); got != true {
|
|
t.Errorf("GetBool(debug) = %v, want true", got)
|
|
}
|
|
if got := GetDuration("timeout"); got != 30*time.Second {
|
|
t.Errorf("GetDuration(timeout) = %v, want 30s", got)
|
|
}
|
|
if got := ConfigFileUsed(); got != configFile {
|
|
t.Errorf("ConfigFileUsed() = %q, want %q", got, configFile)
|
|
}
|
|
}
|
|
|
|
func TestUseExplicitDefaults(t *testing.T) {
|
|
cm := NewConfigManager()
|
|
cm.UseExplicitDefaults(true)
|
|
|
|
// Just verify it doesn't panic and the field is set
|
|
cm.SetDefault("key", "value")
|
|
if got := cm.GetString("key"); got != "value" {
|
|
t.Errorf("GetString(key) = %q, want %q", got, "value")
|
|
}
|
|
}
|
|
|
|
func TestSetString(t *testing.T) {
|
|
cm := NewConfigManager()
|
|
cm.SetString("name", "test")
|
|
|
|
if got := cm.GetString("name"); got != "test" {
|
|
t.Errorf("GetString(name) = %q, want %q", got, "test")
|
|
}
|
|
}
|
|
|
|
func TestSetBool(t *testing.T) {
|
|
cm := NewConfigManager()
|
|
cm.SetBool("enabled", true)
|
|
|
|
if got := cm.GetBool("enabled"); got != true {
|
|
t.Errorf("GetBool(enabled) = %v, want true", got)
|
|
}
|
|
}
|
|
|
|
func TestWriteConfigUnsupportedType(t *testing.T) {
|
|
dir := t.TempDir()
|
|
configFile := filepath.Join(dir, "test.txt")
|
|
|
|
cm := NewConfigManager()
|
|
cm.SetConfigFile(configFile)
|
|
// Don't set config type
|
|
|
|
err := cm.WriteConfig()
|
|
if err == nil {
|
|
t.Error("WriteConfig() expected error for unsupported type, got nil")
|
|
}
|
|
}
|
|
|
|
func TestSetEnvPrefix(t *testing.T) {
|
|
cm := NewConfigManager()
|
|
cm.SetEnvPrefix("PREFIX_")
|
|
}
|
|
|
|
func TestDeeplyNestedConfig(t *testing.T) {
|
|
const nestedYAML = `
|
|
app:
|
|
name: myapp
|
|
server:
|
|
host: localhost
|
|
port: 8080
|
|
tls:
|
|
enabled: true
|
|
cert: /path/to/cert.pem
|
|
key: /path/to/key.pem
|
|
database:
|
|
primary:
|
|
host: db1.example.com
|
|
port: 5432
|
|
credentials:
|
|
username: admin
|
|
password: secret
|
|
replicas:
|
|
- host: db2.example.com
|
|
port: 5432
|
|
- host: db3.example.com
|
|
port: 5432
|
|
features:
|
|
- name: feature1
|
|
enabled: true
|
|
config:
|
|
timeout: 30s
|
|
retries: 3
|
|
- name: feature2
|
|
enabled: false
|
|
`
|
|
|
|
const nestedTOML = `
|
|
[app]
|
|
name = "myapp"
|
|
|
|
[app.server]
|
|
host = "localhost"
|
|
port = 8080
|
|
|
|
[app.server.tls]
|
|
enabled = true
|
|
cert = "/path/to/cert.pem"
|
|
key = "/path/to/key.pem"
|
|
|
|
[app.database.primary]
|
|
host = "db1.example.com"
|
|
port = 5432
|
|
|
|
[app.database.primary.credentials]
|
|
username = "admin"
|
|
password = "secret"
|
|
|
|
[[app.database.replicas]]
|
|
host = "db2.example.com"
|
|
port = 5432
|
|
|
|
[[app.database.replicas]]
|
|
host = "db3.example.com"
|
|
port = 5432
|
|
|
|
[[app.features]]
|
|
name = "feature1"
|
|
enabled = true
|
|
|
|
[app.features.config]
|
|
timeout = "30s"
|
|
retries = 3
|
|
|
|
[[app.features]]
|
|
name = "feature2"
|
|
enabled = false
|
|
`
|
|
|
|
const nestedJSON = `{
|
|
"app": {
|
|
"name": "myapp",
|
|
"server": {
|
|
"host": "localhost",
|
|
"port": 8080,
|
|
"tls": {
|
|
"enabled": true,
|
|
"cert": "/path/to/cert.pem",
|
|
"key": "/path/to/key.pem"
|
|
}
|
|
},
|
|
"database": {
|
|
"primary": {
|
|
"host": "db1.example.com",
|
|
"port": 5432,
|
|
"credentials": {
|
|
"username": "admin",
|
|
"password": "secret"
|
|
}
|
|
},
|
|
"replicas": [
|
|
{"host": "db2.example.com", "port": 5432},
|
|
{"host": "db3.example.com", "port": 5432}
|
|
]
|
|
},
|
|
"features": [
|
|
{
|
|
"name": "feature1",
|
|
"enabled": true,
|
|
"config": {
|
|
"timeout": "30s",
|
|
"retries": 3
|
|
}
|
|
},
|
|
{
|
|
"name": "feature2",
|
|
"enabled": false
|
|
}
|
|
]
|
|
}
|
|
}`
|
|
|
|
tests := []struct {
|
|
name string
|
|
configType string
|
|
content string
|
|
ext string
|
|
}{
|
|
{"YAML", "yaml", nestedYAML, ".yaml"},
|
|
{"TOML", "toml", nestedTOML, ".toml"},
|
|
{"JSON", "json", nestedJSON, ".json"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
configFile := filepath.Join(dir, "nested"+tt.ext)
|
|
if err := os.WriteFile(configFile, []byte(tt.content), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
cm := NewConfigManager()
|
|
cm.SetConfigFile(configFile)
|
|
if err := cm.SetConfigType(tt.configType); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err := cm.ReadInConfig(); err != nil {
|
|
t.Fatalf("ReadInConfig() error = %v", err)
|
|
}
|
|
|
|
// Test that we can retrieve the top-level nested structure
|
|
app := cm.GetStringMap("app")
|
|
if app == nil {
|
|
t.Fatal("GetStringMap(app) = nil, want nested map")
|
|
}
|
|
|
|
// Verify app.name exists
|
|
if name, ok := app["name"].(string); !ok || name != "myapp" {
|
|
t.Errorf("app.name = %v, want %q", app["name"], "myapp")
|
|
}
|
|
|
|
// Verify nested server config
|
|
server, ok := app["server"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("app.server is not a map: %T", app["server"])
|
|
}
|
|
if server["host"] != "localhost" {
|
|
t.Errorf("app.server.host = %v, want %q", server["host"], "localhost")
|
|
}
|
|
|
|
// Verify deeply nested TLS config
|
|
tls, ok := server["tls"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("app.server.tls is not a map: %T", server["tls"])
|
|
}
|
|
if tls["enabled"] != true {
|
|
t.Errorf("app.server.tls.enabled = %v, want true", tls["enabled"])
|
|
}
|
|
if tls["cert"] != "/path/to/cert.pem" {
|
|
t.Errorf("app.server.tls.cert = %v, want %q", tls["cert"], "/path/to/cert.pem")
|
|
}
|
|
|
|
// Verify database.primary.credentials (4 levels deep)
|
|
database, ok := app["database"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("app.database is not a map: %T", app["database"])
|
|
}
|
|
primary, ok := database["primary"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("app.database.primary is not a map: %T", database["primary"])
|
|
}
|
|
creds, ok := primary["credentials"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("app.database.primary.credentials is not a map: %T", primary["credentials"])
|
|
}
|
|
if creds["username"] != "admin" {
|
|
t.Errorf("credentials.username = %v, want %q", creds["username"], "admin")
|
|
}
|
|
|
|
// Verify array of nested objects (replicas)
|
|
// TOML decodes to []map[string]interface{}, YAML/JSON to []any
|
|
var replicaHost any
|
|
switch r := database["replicas"].(type) {
|
|
case []any:
|
|
if len(r) != 2 {
|
|
t.Errorf("len(replicas) = %d, want 2", len(r))
|
|
}
|
|
if len(r) > 0 {
|
|
replica0, ok := r[0].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("replicas[0] is not a map: %T", r[0])
|
|
}
|
|
replicaHost = replica0["host"]
|
|
}
|
|
case []map[string]any:
|
|
if len(r) != 2 {
|
|
t.Errorf("len(replicas) = %d, want 2", len(r))
|
|
}
|
|
if len(r) > 0 {
|
|
replicaHost = r[0]["host"]
|
|
}
|
|
default:
|
|
t.Fatalf("app.database.replicas unexpected type: %T", database["replicas"])
|
|
}
|
|
if replicaHost != "db2.example.com" {
|
|
t.Errorf("replicas[0].host = %v, want %q", replicaHost, "db2.example.com")
|
|
}
|
|
|
|
// Verify features array with nested config
|
|
// TOML decodes to []map[string]interface{}, YAML/JSON to []any
|
|
var featureName any
|
|
switch f := app["features"].(type) {
|
|
case []any:
|
|
if len(f) < 1 {
|
|
t.Fatal("features slice is empty")
|
|
}
|
|
feature0, ok := f[0].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("features[0] is not a map: %T", f[0])
|
|
}
|
|
featureName = feature0["name"]
|
|
case []map[string]any:
|
|
if len(f) < 1 {
|
|
t.Fatal("features slice is empty")
|
|
}
|
|
featureName = f[0]["name"]
|
|
default:
|
|
t.Fatalf("app.features unexpected type: %T", app["features"])
|
|
}
|
|
if featureName != "feature1" {
|
|
t.Errorf("features[0].name = %v, want %q", featureName, "feature1")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPackageLevelGetIntSlice(t *testing.T) {
|
|
defaultConfigManager = NewConfigManager()
|
|
Set("nums", []int{1, 2, 3})
|
|
got := GetIntSlice("nums")
|
|
if len(got) != 3 || got[0] != 1 {
|
|
t.Errorf("GetIntSlice() = %v, want [1 2 3]", got)
|
|
}
|
|
}
|
|
|
|
func TestPackageLevelSetConfigName(t *testing.T) {
|
|
defaultConfigManager = NewConfigManager()
|
|
dir := t.TempDir()
|
|
if err := os.WriteFile(filepath.Join(dir, "app.json"), []byte(`{"port": 1234}`), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
SetConfigName("app")
|
|
defaultConfigManager.SetConfigDir(dir)
|
|
if err := SetConfigType("json"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := ReadInConfig(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := GetInt("port"); got != 1234 {
|
|
t.Errorf("GetInt(port) = %d, want 1234", got)
|
|
}
|
|
}
|
|
|
|
func TestPackageLevelSetEnvPrefix(t *testing.T) {
|
|
defaultConfigManager = NewConfigManager()
|
|
SetEnvPrefix("JETY_TEST_")
|
|
}
|
|
|
|
func TestPackageLevelWriteConfig(t *testing.T) {
|
|
defaultConfigManager = NewConfigManager()
|
|
dir := t.TempDir()
|
|
f := filepath.Join(dir, "out.json")
|
|
SetConfigFile(f)
|
|
if err := SetConfigType("json"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
Set("key", "value")
|
|
if err := WriteConfig(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
data, _ := os.ReadFile(f)
|
|
if len(data) == 0 {
|
|
t.Error("WriteConfig produced empty file")
|
|
}
|
|
}
|
|
|
|
func TestPackageLevelGetStringMap(t *testing.T) {
|
|
defaultConfigManager = NewConfigManager()
|
|
Set("m", map[string]any{"a": 1})
|
|
got := GetStringMap("m")
|
|
if got == nil || got["a"] != 1 {
|
|
t.Errorf("GetStringMap() = %v", got)
|
|
}
|
|
}
|
|
|
|
func TestPackageLevelGetStringSlice(t *testing.T) {
|
|
defaultConfigManager = NewConfigManager()
|
|
Set("s", []string{"a", "b"})
|
|
got := GetStringSlice("s")
|
|
if len(got) != 2 || got[0] != "a" {
|
|
t.Errorf("GetStringSlice() = %v", got)
|
|
}
|
|
}
|
|
|
|
func TestGetStringNonStringValue(t *testing.T) {
|
|
cm := NewConfigManager()
|
|
cm.Set("num", 42)
|
|
if got := cm.GetString("num"); got != "42" {
|
|
t.Errorf("GetString(num) = %q, want %q", got, "42")
|
|
}
|
|
}
|
|
|
|
func TestGetIntInt64(t *testing.T) {
|
|
cm := NewConfigManager()
|
|
cm.Set("key", int64(999))
|
|
if got := cm.GetInt("key"); got != 999 {
|
|
t.Errorf("GetInt(int64) = %d, want 999", got)
|
|
}
|
|
}
|
|
|
|
func TestGetIntUnknownType(t *testing.T) {
|
|
cm := NewConfigManager()
|
|
cm.Set("key", struct{}{})
|
|
if got := cm.GetInt("key"); got != 0 {
|
|
t.Errorf("GetInt(struct) = %d, want 0", got)
|
|
}
|
|
}
|
|
|
|
func TestGetIntSliceInt64Values(t *testing.T) {
|
|
cm := NewConfigManager()
|
|
cm.Set("key", []any{int64(10), int64(20)})
|
|
got := cm.GetIntSlice("key")
|
|
if len(got) != 2 || got[0] != 10 || got[1] != 20 {
|
|
t.Errorf("GetIntSlice(int64) = %v, want [10 20]", got)
|
|
}
|
|
}
|
|
|
|
func TestGetIntSliceNonSlice(t *testing.T) {
|
|
cm := NewConfigManager()
|
|
cm.Set("key", "notaslice")
|
|
if got := cm.GetIntSlice("key"); got != nil {
|
|
t.Errorf("GetIntSlice(string) = %v, want nil", got)
|
|
}
|
|
}
|
|
|
|
func TestGetIntSliceUnknownInnerType(t *testing.T) {
|
|
cm := NewConfigManager()
|
|
cm.Set("key", []any{struct{}{}, true})
|
|
got := cm.GetIntSlice("key")
|
|
if len(got) != 0 {
|
|
t.Errorf("GetIntSlice(unknown types) = %v, want []", got)
|
|
}
|
|
}
|
|
|
|
func TestDeeplyNestedWriteConfig(t *testing.T) {
|
|
dir := t.TempDir()
|
|
|
|
tests := []struct {
|
|
name string
|
|
configType string
|
|
ext string
|
|
}{
|
|
{"YAML", "yaml", ".yaml"},
|
|
{"TOML", "toml", ".toml"},
|
|
{"JSON", "json", ".json"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
configFile := filepath.Join(dir, "nested_write"+tt.ext)
|
|
|
|
cm := NewConfigManager()
|
|
cm.SetConfigFile(configFile)
|
|
if err := cm.SetConfigType(tt.configType); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Set a deeply nested structure
|
|
nested := map[string]any{
|
|
"server": map[string]any{
|
|
"host": "localhost",
|
|
"port": 8080,
|
|
"tls": map[string]any{
|
|
"enabled": true,
|
|
"cert": "/path/to/cert.pem",
|
|
},
|
|
},
|
|
"database": map[string]any{
|
|
"primary": map[string]any{
|
|
"host": "db.example.com",
|
|
"port": 5432,
|
|
},
|
|
},
|
|
}
|
|
cm.Set("app", nested)
|
|
|
|
if err := cm.WriteConfig(); err != nil {
|
|
t.Fatalf("WriteConfig() error = %v", err)
|
|
}
|
|
|
|
// Read it back
|
|
cm2 := NewConfigManager()
|
|
cm2.SetConfigFile(configFile)
|
|
if err := cm2.SetConfigType(tt.configType); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := cm2.ReadInConfig(); err != nil {
|
|
t.Fatalf("ReadInConfig() error = %v", err)
|
|
}
|
|
|
|
// Verify nested structure was preserved
|
|
app := cm2.GetStringMap("app")
|
|
if app == nil {
|
|
t.Fatal("GetStringMap(app) = nil after read")
|
|
}
|
|
|
|
server, ok := app["server"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("app.server is not a map: %T", app["server"])
|
|
}
|
|
if server["host"] != "localhost" {
|
|
t.Errorf("app.server.host = %v, want %q", server["host"], "localhost")
|
|
}
|
|
|
|
tls, ok := server["tls"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("app.server.tls is not a map: %T", server["tls"])
|
|
}
|
|
if tls["enabled"] != true {
|
|
t.Errorf("app.server.tls.enabled = %v, want true", tls["enabled"])
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSetEnvPrefixOverridesDefault(t *testing.T) {
|
|
// Subprocess test: env vars must exist before NewConfigManager is called.
|
|
if os.Getenv("TEST_SET_ENV_PREFIX") == "1" {
|
|
cm := NewConfigManager()
|
|
cm.SetEnvPrefix("MYAPP_")
|
|
cm.SetDefault("port", 8080)
|
|
|
|
if got := cm.GetInt("port"); got != 9999 {
|
|
fmt.Fprintf(os.Stderr, "GetInt(port) = %d, want 9999\n", got)
|
|
os.Exit(1)
|
|
}
|
|
if got := cm.GetString("host"); got != "envhost" {
|
|
fmt.Fprintf(os.Stderr, "GetString(host) = %q, want %q\n", got, "envhost")
|
|
os.Exit(1)
|
|
}
|
|
// Unprefixed var should not be visible.
|
|
if got := cm.GetString("other"); got != "" {
|
|
fmt.Fprintf(os.Stderr, "GetString(other) = %q, want empty\n", got)
|
|
os.Exit(1)
|
|
}
|
|
os.Exit(0)
|
|
}
|
|
|
|
cmd := exec.Command(os.Args[0], "-test.run=^TestSetEnvPrefixOverridesDefault$")
|
|
cmd.Env = append(os.Environ(),
|
|
"TEST_SET_ENV_PREFIX=1",
|
|
"MYAPP_PORT=9999",
|
|
"MYAPP_HOST=envhost",
|
|
"OTHER=should_not_see",
|
|
)
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
t.Fatalf("subprocess failed: %v\n%s", err, out)
|
|
}
|
|
}
|
|
|
|
func TestSetEnvPrefixWithSetDefault(t *testing.T) {
|
|
// SetDefault should pick up prefixed env vars after SetEnvPrefix.
|
|
if os.Getenv("TEST_SET_ENV_PREFIX_DEFAULT") == "1" {
|
|
cm := NewConfigManager()
|
|
cm.SetEnvPrefix("APP_")
|
|
cm.SetDefault("database_host", "localhost")
|
|
|
|
if got := cm.GetString("database_host"); got != "db.example.com" {
|
|
fmt.Fprintf(os.Stderr, "GetString(database_host) = %q, want %q\n", got, "db.example.com")
|
|
os.Exit(1)
|
|
}
|
|
os.Exit(0)
|
|
}
|
|
|
|
cmd := exec.Command(os.Args[0], "-test.run=^TestSetEnvPrefixWithSetDefault$")
|
|
cmd.Env = append(os.Environ(),
|
|
"TEST_SET_ENV_PREFIX_DEFAULT=1",
|
|
"APP_DATABASE_HOST=db.example.com",
|
|
)
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
t.Fatalf("subprocess failed: %v\n%s", err, out)
|
|
}
|
|
}
|
|
|
|
func TestPackageLevelSetEnvPrefixOverrides(t *testing.T) {
|
|
// Package-level SetEnvPrefix should work the same way.
|
|
if os.Getenv("TEST_PKG_SET_ENV_PREFIX") == "1" {
|
|
// Reset the default manager to pick up our env vars.
|
|
defaultConfigManager = NewConfigManager()
|
|
SetEnvPrefix("PKG_")
|
|
SetDefault("val", "default")
|
|
|
|
if got := GetString("val"); got != "from_env" {
|
|
fmt.Fprintf(os.Stderr, "GetString(val) = %q, want %q\n", got, "from_env")
|
|
os.Exit(1)
|
|
}
|
|
os.Exit(0)
|
|
}
|
|
|
|
cmd := exec.Command(os.Args[0], "-test.run=^TestPackageLevelSetEnvPrefixOverrides$")
|
|
cmd.Env = append(os.Environ(),
|
|
"TEST_PKG_SET_ENV_PREFIX=1",
|
|
"PKG_VAL=from_env",
|
|
)
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
t.Fatalf("subprocess failed: %v\n%s", err, out)
|
|
}
|
|
}
|
|
|
|
func TestPrecedenceChain(t *testing.T) {
|
|
// Verify: Set > env > file > defaults
|
|
os.Setenv("PORT", "5000")
|
|
os.Setenv("HOST", "envhost")
|
|
os.Setenv("LOG", "envlog")
|
|
defer os.Unsetenv("PORT")
|
|
defer os.Unsetenv("HOST")
|
|
defer os.Unsetenv("LOG")
|
|
|
|
dir := t.TempDir()
|
|
configFile := filepath.Join(dir, "config.yaml")
|
|
if err := os.WriteFile(configFile, []byte("port: 9000\nhost: filehost\nlog: filelog\nname: filename"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
cm := NewConfigManager()
|
|
cm.SetDefault("port", 8080)
|
|
cm.SetDefault("host", "defaulthost")
|
|
cm.SetDefault("log", "defaultlog")
|
|
cm.SetDefault("name", "defaultname")
|
|
cm.SetDefault("extra", "defaultextra")
|
|
|
|
cm.SetConfigFile(configFile)
|
|
if err := cm.SetConfigType("yaml"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := cm.ReadInConfig(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
cm.Set("port", 1111) // Set overrides everything
|
|
|
|
// port: Set(1111) > env(5000) > file(9000) > default(8080) → 1111
|
|
if got := cm.GetInt("port"); got != 1111 {
|
|
t.Errorf("port: got %d, want 1111 (Set overrides all)", got)
|
|
}
|
|
// host: env(envhost) > file(filehost) > default(defaulthost) → envhost
|
|
if got := cm.GetString("host"); got != "envhost" {
|
|
t.Errorf("host: got %q, want envhost (env overrides file)", got)
|
|
}
|
|
// name: file(filename) > default(defaultname) → filename
|
|
if got := cm.GetString("name"); got != "filename" {
|
|
t.Errorf("name: got %q, want filename (file overrides default)", got)
|
|
}
|
|
// extra: only default → defaultextra
|
|
if got := cm.GetString("extra"); got != "defaultextra" {
|
|
t.Errorf("extra: got %q, want defaultextra (default)", got)
|
|
}
|
|
}
|