package smartconfig import ( "os" "regexp" "strings" "testing" ) func TestEnvResolver(t *testing.T) { _ = os.Setenv("TEST_VAR", "test-value") defer func() { _ = os.Unsetenv("TEST_VAR") }() resolver := &EnvResolver{} result, err := resolver.Resolve("TEST_VAR") if err != nil { t.Fatalf("Failed to resolve env var: %v", err) } if result != "test-value" { t.Errorf("Expected 'test-value', got '%s'", result) } } func TestExecResolver(t *testing.T) { resolver := &ExecResolver{} result, err := resolver.Resolve("echo hello") if err != nil { t.Fatalf("Failed to execute command: %v", err) } if result != "hello" { t.Errorf("Expected 'hello', got '%s'", result) } } func TestFileResolver(t *testing.T) { resolver := &FileResolver{} result, err := resolver.Resolve("./test/machine-id") if err != nil { t.Fatalf("Failed to read file: %v", err) } if result != "test-machine-123456789" { t.Errorf("Expected 'test-machine-123456789', got '%s'", result) } } func TestJSONResolver(t *testing.T) { resolver := &JSONResolver{} // Test simple path result, err := resolver.Resolve("./test/hosts.json:.") if err != nil { t.Fatalf("Failed to resolve JSON: %v", err) } if !strings.Contains(result, "tls_sni_host") { t.Errorf("Expected JSON output to contain 'tls_sni_host', got '%s'", result) } } func TestYAMLResolver(t *testing.T) { resolver := &YAMLResolver{} // Test simple path result, err := resolver.Resolve("./test/data.yaml:.") if err != nil { t.Fatalf("Failed to resolve YAML: %v", err) } if !strings.Contains(result, "database") { t.Errorf("Expected YAML output to contain 'database', got '%s'", result) } } func TestInterpolateBasic(t *testing.T) { _ = os.Setenv("BASIC", "value") defer func() { _ = os.Unsetenv("BASIC") }() config := New() content := "test: ${ENV:BASIC}" err := config.LoadFromReader(strings.NewReader(content)) if err != nil { t.Fatalf("Failed to load config: %v", err) } value, err := config.GetString("test") if err != nil { t.Fatalf("Failed to get test value: %v", err) } if value != "value" { t.Errorf("Expected 'value', got '%s'", value) } } func TestInterpolateMethod(t *testing.T) { config := New() _ = os.Setenv("FOO", "bar") defer func() { _ = os.Unsetenv("FOO") }() // Test direct interpolation result, _ := config.interpolate("${ENV:FOO}", 0) if result != "\"bar\"" { t.Errorf("Expected '\"bar\"', got '%s'", result) } } func TestInterpolateSimple(t *testing.T) { config := New() _ = os.Setenv("TEST", "value") defer func() { _ = os.Unsetenv("TEST") }() // Verify regex finds the match re := regexp.MustCompile(`\$\{([^:]+?):(.*?)\}`) matches := re.FindAllStringSubmatch("${ENV:TEST}", -1) if len(matches) == 0 { t.Fatal("Regex didn't find any matches") } // Test simple interpolation result, _ := config.interpolate("${ENV:TEST}", 0) if result != "\"value\"" { t.Errorf("Simple interpolation failed: expected '\"value\"', got '%s'", result) } } func TestInterpolateStep(t *testing.T) { config := New() // Set env vars _ = os.Setenv("BAR", "value1") _ = os.Setenv("FOO_value1", "success") defer func() { _ = os.Unsetenv("BAR") _ = os.Unsetenv("FOO_value1") }() // Step 1: Inner interpolation should resolve first step1 := "${ENV:BAR}" result1, _ := config.interpolate(step1, 0) if result1 != "\"value1\"" { t.Errorf("Step 1 failed: expected '\"value1\"', got '%s'", result1) } // Step 2: With the result, build the outer step2 := "${ENV:FOO_value1}" result2, _ := config.interpolate(step2, 0) if result2 != "\"success\"" { t.Errorf("Step 2 failed: expected '\"success\"', got '%s'", result2) } // Step 3: Full nested full := "${ENV:FOO_${ENV:BAR}}" result3, _ := config.interpolate(full, 0) if result3 != "\"success\"" { t.Errorf("Full nested failed: expected '\"success\"', got '%s'", result3) } } func TestRegexPattern(t *testing.T) { re := regexp.MustCompile(`\$\{([^:]+?):(.*?)\}`) // Test simple case matches := re.FindStringSubmatch("${ENV:FOO}") if len(matches) != 3 || matches[1] != "ENV" || matches[2] != "FOO" { t.Errorf("Simple pattern failed: %v", matches) } // Test nested case - this will fail with current regex input := "${ENV:FOO_${ENV:BAR}}" matches = re.FindStringSubmatch(input) if len(matches) == 3 { t.Logf("Nested pattern matches: resolver=%s, value=%s", matches[1], matches[2]) // This will show that value is "FOO_${ENV:BAR" which is wrong } } func TestInterpolateNested(t *testing.T) { config := New() _ = os.Setenv("INNER", "BAR") _ = os.Setenv("FOO_BAR", "success") defer func() { _ = os.Unsetenv("INNER") _ = os.Unsetenv("FOO_BAR") }() // Test nested interpolation directly result, _ := config.interpolate("${ENV:FOO_${ENV:INNER}}", 0) if result != "\"success\"" { t.Errorf("Expected '\"success\"', got '%s'", result) } } func TestSimpleNestedInterpolation(t *testing.T) { _ = os.Setenv("SUFFIX", "BAR") _ = os.Setenv("FOO_BAR", "success") defer func() { _ = os.Unsetenv("SUFFIX") _ = os.Unsetenv("FOO_BAR") }() config := New() content := "test: ${ENV:FOO_${ENV:SUFFIX}}" err := config.LoadFromReader(strings.NewReader(content)) if err != nil { t.Fatalf("Failed to load config: %v", err) } value, err := config.GetString("test") if err != nil { t.Fatalf("Failed to get test value: %v", err) } if value != "success" { t.Errorf("Expected 'success', got '%s'", value) } } func TestConfigInterpolation(t *testing.T) { // Set up environment _ = os.Setenv("TEST_APP_NAME", "myapp") _ = os.Setenv("TEST_PORT", "8080") _ = os.Setenv("TEST_ENV_SUFFIX", "VALUE") _ = os.Setenv("NESTED_VALUE", "nested-result") defer func() { _ = os.Unsetenv("TEST_APP_NAME") _ = os.Unsetenv("TEST_PORT") _ = os.Unsetenv("TEST_ENV_SUFFIX") _ = os.Unsetenv("NESTED_VALUE") }() config := New() err := config.LoadFromFile("./test/config.yaml") if err != nil { t.Fatalf("Failed to load config: %v", err) } // Test basic interpolation name, err := config.GetString("name") if err != nil { t.Fatalf("Failed to get name: %v", err) } if name != "myapp" { t.Errorf("Expected name 'myapp', got '%s'", name) } // Test exec interpolation if serverData, ok := config.data["server"].(map[string]interface{}); ok { if hostname, ok := serverData["hostname"].(string); ok { if hostname == "" { t.Errorf("Expected hostname to be non-empty") } } else { t.Errorf("Expected server.hostname to be a string") } } else { t.Errorf("Expected server to be a map") } // Test file interpolation if machineData, ok := config.data["machine"].(map[string]interface{}); ok { if id, ok := machineData["id"].(string); ok { if id != "test-machine-123456789" { t.Errorf("Expected machine.id 'test-machine-123456789', got '%s'", id) } } } // Test nested interpolation if nestedData, ok := config.data["nested"].(map[string]interface{}); ok { if value, ok := nestedData["value"].(string); ok { if value != "nested-result" { t.Errorf("Expected nested.value 'nested-result', got '%s'", value) } } } // Test environment injection injectedVar := os.Getenv("INJECTED_VAR") if injectedVar != "injected-value" { t.Errorf("Expected INJECTED_VAR to be 'injected-value', got '%s'", injectedVar) } computedVar := os.Getenv("COMPUTED_VAR") if computedVar != "computed" { t.Errorf("Expected COMPUTED_VAR to be 'computed', got '%s'", computedVar) } } func TestRecursionLimit(t *testing.T) { config := New() // Create a deeply nested interpolation that hits the recursion limit // This has 4 levels, but our max is 3 content := "value: ${ENV:LEVEL1_${ENV:LEVEL2_${ENV:LEVEL3_${ENV:LEVEL4}}}}" // Set up environment variables for a complete chain _ = os.Setenv("LEVEL4", "END") _ = os.Setenv("LEVEL3_END", "3") _ = os.Setenv("LEVEL2_3", "2") _ = os.Setenv("LEVEL1_2", "final") // Also set the partial resolution that would be needed at depth 3 _ = os.Setenv("LEVEL3_${ENV:LEVEL4}", "depth3value") _ = os.Setenv("LEVEL2_depth3value", "depth2value") _ = os.Setenv("LEVEL1_depth2value", "final_with_limit") defer func() { _ = os.Unsetenv("LEVEL4") _ = os.Unsetenv("LEVEL3_END") _ = os.Unsetenv("LEVEL2_3") _ = os.Unsetenv("LEVEL1_2") _ = os.Unsetenv("LEVEL3_${ENV:LEVEL4}") _ = os.Unsetenv("LEVEL2_depth3value") _ = os.Unsetenv("LEVEL1_depth2value") }() err := config.LoadFromReader(strings.NewReader(content)) if err != nil { t.Fatalf("Failed to load config: %v", err) } // At depth 3, interpolation stops and returns the partial result // So LEVEL3_${ENV:LEVEL4} is used as the literal key, giving "depth3value" // Then LEVEL2_depth3value gives "depth2value" // Finally LEVEL1_depth2value gives "final_with_limit" value, _ := config.GetString("value") if value != "final_with_limit" { t.Errorf("Expected value to be 'final_with_limit' (recursion limit reached), got '%s'", value) } } func TestInvalidResolverFormat(t *testing.T) { tests := []struct { name string resolver Resolver input string }{ {"JSON missing path", &JSONResolver{}, "file.json"}, {"YAML missing path", &YAMLResolver{}, "file.yaml"}, {"Vault missing key", &VaultResolver{}, "secret/data/myapp"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { _, err := tt.resolver.Resolve(tt.input) if err == nil { t.Errorf("Expected error for invalid format") } }) } } func TestUnknownResolver(t *testing.T) { config := New() content := "value: ${UNKNOWN:test}" // Unknown resolver should cause an error err := config.LoadFromReader(strings.NewReader(content)) if err == nil { t.Fatal("Expected error for unknown resolver, but got none") } if !strings.Contains(err.Error(), "unknown resolver: UNKNOWN") { t.Errorf("Expected error to mention unknown resolver UNKNOWN, got: %v", err) } }