package smartconfig import ( "fmt" "os" "strings" "testing" ) func TestTypedInterpolation(t *testing.T) { testCases := []struct { name string yaml string env map[string]string checks []struct { key string expectedVal interface{} expectedType string } }{ { name: "unquoted number interpolation", yaml: `port: ${ENV:PORT}`, env: map[string]string{"PORT": "8080"}, checks: []struct { key string expectedVal interface{} expectedType string }{ {key: "port", expectedVal: 8080, expectedType: "int"}, }, }, { name: "force string with prefix", yaml: `port: "port-${ENV:PORT}"`, env: map[string]string{"PORT": "8080"}, checks: []struct { key string expectedVal interface{} expectedType string }{ {key: "port", expectedVal: "port-8080", expectedType: "string"}, }, }, { name: "boolean interpolation", yaml: `enabled: ${ENV:ENABLED}`, env: map[string]string{"ENABLED": "true"}, checks: []struct { key string expectedVal interface{} expectedType string }{ {key: "enabled", expectedVal: true, expectedType: "bool"}, }, }, { name: "float interpolation", yaml: `timeout: ${ENV:TIMEOUT}`, env: map[string]string{"TIMEOUT": "30.5"}, checks: []struct { key string expectedVal interface{} expectedType string }{ {key: "timeout", expectedVal: 30.5, expectedType: "float64"}, }, }, { name: "mixed content stays string", yaml: `message: "Hello ${ENV:NAME}!"`, env: map[string]string{"NAME": "World"}, checks: []struct { key string expectedVal interface{} expectedType string }{ {key: "message", expectedVal: "Hello World!", expectedType: "string"}, }, }, { name: "nested interpolation with type", yaml: `value: ${ENV:PREFIX_${ENV:SUFFIX}}`, env: map[string]string{"SUFFIX": "VAL", "PREFIX_VAL": "42"}, checks: []struct { key string expectedVal interface{} expectedType string }{ {key: "value", expectedVal: 42, expectedType: "int"}, }, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { // Set environment variables for k, v := range tc.env { _ = os.Setenv(k, v) defer func(key string) { _ = os.Unsetenv(key) }(k) } // Load config config, err := NewFromReader(strings.NewReader(tc.yaml)) if err != nil { t.Fatalf("Failed to load config: %v", err) } // Check values and types for _, check := range tc.checks { val, exists := config.Get(check.key) if !exists { t.Errorf("Key %s not found", check.key) continue } // Check type actualType := "" switch val.(type) { case string: actualType = "string" case int: actualType = "int" case float64: actualType = "float64" case bool: actualType = "bool" default: actualType = "unknown" } if actualType != check.expectedType { t.Errorf("Key %s: expected type %s, got %s (value: %v)", check.key, check.expectedType, actualType, val) } // Check value if val != check.expectedVal { t.Errorf("Key %s: expected value %v, got %v", check.key, check.expectedVal, val) } } }) } } func TestNumericConversionSafety(t *testing.T) { tests := []struct { name string input string expected interface{} desc string }{ { name: "integer with leading zeros stays string", input: "0123", expected: "0123", desc: "Leading zeros should prevent integer conversion", }, { name: "float with trailing zeros", input: "1.50", expected: "1.50", desc: "Trailing zeros should prevent float conversion", }, { name: "scientific notation stays string", input: "1e10", expected: "1e10", desc: "Scientific notation should stay as string", }, { name: "integer within range converts", input: "12345", expected: 12345, desc: "Normal integers should convert", }, { name: "float with no trailing zeros converts", input: "123.45", expected: 123.45, desc: "Normal floats should convert", }, { name: "negative integer converts", input: "-123", expected: -123, desc: "Negative integers should convert", }, { name: "negative float converts", input: "-123.45", expected: -123.45, desc: "Negative floats should convert", }, { name: "very large number stays string", input: "999999999999999999999999999999", expected: "999999999999999999999999999999", desc: "Numbers beyond int range should stay as strings", }, { name: "float that looks like int converts to int", input: "123.0", expected: "123.0", desc: "Float notation should be preserved even for whole numbers", }, { name: "plus sign prevents conversion", input: "+123", expected: "+123", desc: "Plus sign should prevent conversion", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Set up environment variable varName := "TEST_NUMERIC_" + strings.ToUpper(strings.ReplaceAll(tt.name, " ", "_")) _ = os.Setenv(varName, tt.input) defer func() { _ = os.Unsetenv(varName) }() yaml := fmt.Sprintf("value: ${ENV:%s}", varName) config, err := NewFromReader(strings.NewReader(yaml)) if err != nil { t.Fatalf("Failed to parse YAML: %v", err) } val, exists := config.Get("value") if !exists { t.Fatal("Expected value to exist") } // Check type and value switch expected := tt.expected.(type) { case int: if i, ok := val.(int); !ok { t.Errorf("%s: Expected int, got %T: %v", tt.desc, val, val) } else if i != expected { t.Errorf("%s: Expected %d, got %d", tt.desc, expected, i) } case float64: if f, ok := val.(float64); !ok { t.Errorf("%s: Expected float64, got %T: %v", tt.desc, val, val) } else if f != expected { t.Errorf("%s: Expected %f, got %f", tt.desc, expected, f) } case string: if s, ok := val.(string); !ok { t.Errorf("%s: Expected string, got %T: %v", tt.desc, val, val) } else if s != expected { t.Errorf("%s: Expected %q, got %q", tt.desc, expected, s) } } }) } }