- YAML resolver now supports full path navigation (e.g., production.primary.host) - Both JSON and YAML resolvers return YAML-formatted data for complex types - This allows proper type preservation when loading objects/arrays from files - Updated convertToType to parse YAML returned by resolvers - Added comprehensive tests for YAML path navigation including arrays - Fixed JSON resolver to support "." path for entire document - All README examples now work correctly The key insight was that resolvers should return YAML strings for complex types, which can then be parsed and merged into the configuration structure, preserving the original types (maps, arrays) instead of flattening to strings.
247 lines
5.6 KiB
Go
247 lines
5.6 KiB
Go
package smartconfig
|
|
|
|
import (
|
|
"os"
|
|
"testing"
|
|
)
|
|
|
|
func TestYAMLResolverPathNavigation(t *testing.T) {
|
|
resolver := &YAMLResolver{}
|
|
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
expected string
|
|
wantErr bool
|
|
}{
|
|
// Basic path navigation
|
|
{
|
|
name: "top level key",
|
|
input: "./test/features.yaml:settings",
|
|
expected: "debug: false\nlog_level: info",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "nested path with dot notation",
|
|
input: "./test/database.yaml:production.primary",
|
|
expected: "host: prod-db-primary.example.com\nport: 5432\nusername: prod_user",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "deeply nested path",
|
|
input: "./test/database.yaml:production.replica.host",
|
|
expected: "prod-db-replica.example.com",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "boolean value",
|
|
input: "./test/features.yaml:features.analytics.enabled",
|
|
expected: "true",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "string value",
|
|
input: "./test/features.yaml:features.analytics.provider",
|
|
expected: "google",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "numeric value",
|
|
input: "./test/features.yaml:features.rate_limiting.requests_per_minute",
|
|
expected: "100",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "root document with dot",
|
|
input: "./test/features.yaml:.",
|
|
expected: "features:\n analytics:\n enabled: true\n provider: google\n tracking_id: UA-123456\n new_ui:\n enabled: false\n rollout_percentage: 25\n rate_limiting:\n enabled: true\n requests_per_minute: 100\nsettings:\n debug: false\n log_level: info",
|
|
wantErr: false,
|
|
},
|
|
// Error cases
|
|
{
|
|
name: "non-existent path",
|
|
input: "./test/database.yaml:production.nonexistent.key",
|
|
expected: "",
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "invalid file",
|
|
input: "./test/nonexistent.yaml:some.path",
|
|
expected: "",
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "missing path separator",
|
|
input: "./test/database.yaml",
|
|
expected: "",
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "empty path",
|
|
input: "./test/database.yaml:",
|
|
expected: "",
|
|
wantErr: true,
|
|
},
|
|
// Edge cases with special characters
|
|
{
|
|
name: "path with spaces (if exists)",
|
|
input: "./test/features.yaml:settings.log_level",
|
|
expected: "info",
|
|
wantErr: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result, err := resolver.Resolve(tt.input)
|
|
|
|
if tt.wantErr {
|
|
if err == nil {
|
|
t.Errorf("Expected error but got none, result: %s", result)
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Errorf("Unexpected error: %v", err)
|
|
}
|
|
if result != tt.expected {
|
|
t.Errorf("Expected %q, got %q", tt.expected, result)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestYAMLResolverArrayAccess(t *testing.T) {
|
|
// Create a test YAML file with arrays
|
|
yamlContent := `servers:
|
|
- name: server1
|
|
host: 192.168.1.1
|
|
port: 8080
|
|
- name: server2
|
|
host: 192.168.1.2
|
|
port: 8081
|
|
users:
|
|
- admin
|
|
- user1
|
|
- user2
|
|
`
|
|
|
|
// Write test file
|
|
err := os.WriteFile("./test/arrays.yaml", []byte(yamlContent), 0644)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create test file: %v", err)
|
|
}
|
|
defer func() { _ = os.Remove("./test/arrays.yaml") }()
|
|
|
|
resolver := &YAMLResolver{}
|
|
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
expected string
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "array index access",
|
|
input: "./test/arrays.yaml:servers.0",
|
|
expected: "host: 192.168.1.1\nname: server1\nport: 8080",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "array element property",
|
|
input: "./test/arrays.yaml:servers.1.host",
|
|
expected: "192.168.1.2",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "simple array element",
|
|
input: "./test/arrays.yaml:users.0",
|
|
expected: "admin",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "out of bounds array access",
|
|
input: "./test/arrays.yaml:servers.10",
|
|
expected: "",
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result, err := resolver.Resolve(tt.input)
|
|
|
|
if tt.wantErr {
|
|
if err == nil {
|
|
t.Errorf("Expected error but got none, result: %s", result)
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Errorf("Unexpected error: %v", err)
|
|
}
|
|
if result != tt.expected {
|
|
t.Errorf("Expected %q, got %q", tt.expected, result)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestYAMLResolverComplexPaths(t *testing.T) {
|
|
// Test quoted paths and special cases
|
|
yamlContent := `special:
|
|
"dotted.key": value1
|
|
"key with spaces": value2
|
|
123: numeric_key
|
|
nested:
|
|
level1:
|
|
level2:
|
|
level3:
|
|
level4:
|
|
deepvalue: "found it!"
|
|
`
|
|
|
|
err := os.WriteFile("./test/complex.yaml", []byte(yamlContent), 0644)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create test file: %v", err)
|
|
}
|
|
defer func() { _ = os.Remove("./test/complex.yaml") }()
|
|
|
|
resolver := &YAMLResolver{}
|
|
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
expected string
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "deeply nested value",
|
|
input: "./test/complex.yaml:nested.level1.level2.level3.level4.deepvalue",
|
|
expected: "found it!",
|
|
wantErr: false,
|
|
},
|
|
// Note: Quoted keys would require special handling in the path parser
|
|
// For now, we'll stick to simple dot notation
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result, err := resolver.Resolve(tt.input)
|
|
|
|
if tt.wantErr {
|
|
if err == nil {
|
|
t.Errorf("Expected error but got none, result: %s", result)
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Errorf("Unexpected error: %v", err)
|
|
}
|
|
if result != tt.expected {
|
|
t.Errorf("Expected %q, got %q", tt.expected, result)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|