mirror of
https://github.com/taigrr/jety.git
synced 2026-04-02 03:19:03 -07:00
- collapse() now applies env vars for keys present in fileConfig, not just defaultConfig. Previously, env vars couldn't override file values unless a default was also set for that key. - SetDefault no longer pollutes overrideConfig; it correctly resolves the value by checking override > env > file > default. - Remove unused explicitDefaults field and UseExplicitDefaults method. - Add IsSet, AllKeys, AllSettings methods + package-level wrappers. - Add missing package-level wrappers: Get, SetBool, SetString, SetConfigDir, WithEnvPrefix. - Add tests for all new methods and the env-over-file-without-default fix.
1469 lines
35 KiB
Go
1469 lines
35 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 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)
|
|
}
|
|
}
|
|
|
|
func TestIsSet(t *testing.T) {
|
|
cm := NewConfigManager()
|
|
cm.Set("exists", "yes")
|
|
|
|
if !cm.IsSet("exists") {
|
|
t.Error("IsSet(exists) = false, want true")
|
|
}
|
|
if cm.IsSet("nope") {
|
|
t.Error("IsSet(nope) = true, want false")
|
|
}
|
|
}
|
|
|
|
func TestAllKeys(t *testing.T) {
|
|
cm := NewConfigManager()
|
|
cm.SetDefault("a", 1)
|
|
cm.Set("b", 2)
|
|
|
|
keys := cm.AllKeys()
|
|
found := make(map[string]bool)
|
|
for _, k := range keys {
|
|
found[k] = true
|
|
}
|
|
if !found["a"] || !found["b"] {
|
|
t.Errorf("AllKeys() = %v, want to contain a and b", keys)
|
|
}
|
|
}
|
|
|
|
func TestAllSettings(t *testing.T) {
|
|
cm := NewConfigManager()
|
|
cm.Set("port", 8080)
|
|
cm.Set("host", "localhost")
|
|
|
|
settings := cm.AllSettings()
|
|
if settings["port"] != 8080 || settings["host"] != "localhost" {
|
|
t.Errorf("AllSettings() = %v", settings)
|
|
}
|
|
}
|
|
|
|
func TestEnvOverridesFileWithoutDefault(t *testing.T) {
|
|
// Bug fix: env should override file even when no default is set for that key
|
|
os.Setenv("HOST", "envhost")
|
|
defer os.Unsetenv("HOST")
|
|
|
|
dir := t.TempDir()
|
|
configFile := filepath.Join(dir, "config.yaml")
|
|
if err := os.WriteFile(configFile, []byte("host: filehost"), 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.Fatal(err)
|
|
}
|
|
|
|
// No SetDefault("host", ...) was called — env should still win
|
|
if got := cm.GetString("host"); got != "envhost" {
|
|
t.Errorf("GetString(host) = %q, want envhost (env overrides file even without default)", got)
|
|
}
|
|
}
|
|
|
|
func TestPackageLevelIsSet(t *testing.T) {
|
|
defaultConfigManager = NewConfigManager()
|
|
Set("x", 1)
|
|
if !IsSet("x") {
|
|
t.Error("IsSet(x) = false")
|
|
}
|
|
}
|
|
|
|
func TestPackageLevelGet(t *testing.T) {
|
|
defaultConfigManager = NewConfigManager()
|
|
Set("key", "val")
|
|
if Get("key") != "val" {
|
|
t.Error("Get(key) failed")
|
|
}
|
|
}
|