This change enhances the API to support gjson path syntax for accessing nested configuration values. Users can now use paths like 'server.ssl.enabled' or 'database.replicas.0.host' to directly access nested values without manual navigation. Key changes: - All typed getters now support gjson path syntax - Backward compatibility maintained for top-level keys - Proper error handling for null values and non-existent paths - Special float values (Infinity, NaN) handled correctly - Comprehensive test coverage for edge cases This makes the API much more intuitive and reduces boilerplate code when working with nested configuration structures.
283 lines
6.8 KiB
Go
283 lines
6.8 KiB
Go
package smartconfig
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestMalformedDataHandling(t *testing.T) {
|
|
t.Run("Config with circular references", func(t *testing.T) {
|
|
// YAML doesn't support circular references directly, but we can test
|
|
// configurations that might cause issues during JSON conversion
|
|
yamlContent := `
|
|
a: &anchor
|
|
b: *anchor
|
|
c: test
|
|
`
|
|
config, err := NewFromReader(strings.NewReader(yamlContent))
|
|
if err != nil {
|
|
// It's ok if this fails, we just want to ensure no panic
|
|
return
|
|
}
|
|
|
|
// Try to access the circular structure
|
|
_, _ = config.GetString("a.b.c")
|
|
_, _ = config.Get("a.b.b.b.b")
|
|
// Should handle gracefully without stack overflow
|
|
})
|
|
|
|
t.Run("Very long paths", func(t *testing.T) {
|
|
yamlContent := `
|
|
root:
|
|
level1:
|
|
level2:
|
|
level3:
|
|
level4:
|
|
level5:
|
|
value: "deep"
|
|
`
|
|
config, err := NewFromReader(strings.NewReader(yamlContent))
|
|
if err != nil {
|
|
t.Fatalf("Failed to load config: %v", err)
|
|
}
|
|
|
|
// Build a very long path
|
|
longPath := "root"
|
|
for i := 0; i < 100; i++ {
|
|
longPath += ".level1"
|
|
}
|
|
|
|
// Should handle gracefully
|
|
_, _ = config.GetString(longPath)
|
|
|
|
// Extremely long path
|
|
veryLongPath := strings.Repeat("a.", 10000) + "b"
|
|
_, _ = config.GetString(veryLongPath)
|
|
})
|
|
|
|
t.Run("Special YAML values", func(t *testing.T) {
|
|
yamlContent := `
|
|
special:
|
|
yes_value: yes
|
|
no_value: no
|
|
on_value: on
|
|
off_value: off
|
|
tilde: ~
|
|
empty:
|
|
quoted_yes: "yes"
|
|
quoted_no: "no"
|
|
multiline: |
|
|
line1
|
|
line2
|
|
line3
|
|
folded: >
|
|
this is
|
|
a folded
|
|
string
|
|
`
|
|
config, err := NewFromReader(strings.NewReader(yamlContent))
|
|
if err != nil {
|
|
t.Fatalf("Failed to load config: %v", err)
|
|
}
|
|
|
|
// YAML's "yes" and "no" are parsed as strings by the YAML parser
|
|
// Go's strconv.ParseBool doesn't handle these, so they should fail
|
|
_, err = config.GetBool("special.yes_value")
|
|
if err == nil {
|
|
t.Error("GetBool(yes_value) should fail - 'yes' is not a valid Go boolean")
|
|
}
|
|
|
|
_, err = config.GetBool("special.no_value")
|
|
if err == nil {
|
|
t.Error("GetBool(no_value) should fail - 'no' is not a valid Go boolean")
|
|
}
|
|
|
|
// But we should be able to get them as strings
|
|
yesStr, err := config.GetString("special.yes_value")
|
|
if err != nil || yesStr != "yes" {
|
|
t.Errorf("GetString(yes_value) = %q, %v; want 'yes', nil", yesStr, err)
|
|
}
|
|
|
|
// Same for "on" and "off"
|
|
_, err = config.GetBool("special.on_value")
|
|
if err == nil {
|
|
t.Error("GetBool(on_value) should fail - 'on' is not a valid Go boolean")
|
|
}
|
|
|
|
_, err = config.GetBool("special.off_value")
|
|
if err == nil {
|
|
t.Error("GetBool(off_value) should fail - 'off' is not a valid Go boolean")
|
|
}
|
|
|
|
// Quoted versions should be strings
|
|
quotedYes, err := config.GetString("special.quoted_yes")
|
|
if err != nil || quotedYes != "yes" {
|
|
t.Errorf("GetString(quoted_yes) = %q, %v; want 'yes', nil", quotedYes, err)
|
|
}
|
|
|
|
// Multiline strings
|
|
multiline, err := config.GetString("special.multiline")
|
|
if err != nil || !strings.Contains(multiline, "line1") {
|
|
t.Errorf("Multiline string handling failed: %q", multiline)
|
|
}
|
|
})
|
|
|
|
t.Run("Invalid type conversions", func(t *testing.T) {
|
|
yamlContent := `
|
|
data:
|
|
object:
|
|
key: value
|
|
array:
|
|
- item1
|
|
- item2
|
|
string: "hello"
|
|
number: 42
|
|
boolean: true
|
|
`
|
|
config, err := NewFromReader(strings.NewReader(yamlContent))
|
|
if err != nil {
|
|
t.Fatalf("Failed to load config: %v", err)
|
|
}
|
|
|
|
// Try to get object as string
|
|
_, err = config.GetString("data.object")
|
|
if err == nil {
|
|
t.Error("GetString on object should fail")
|
|
}
|
|
|
|
// Try to get array as int
|
|
_, err = config.GetInt("data.array")
|
|
if err == nil {
|
|
t.Error("GetInt on array should fail")
|
|
}
|
|
|
|
// Try to get string as bytes (should work if valid)
|
|
_, err = config.GetBytes("data.string")
|
|
if err == nil {
|
|
t.Error("GetBytes on non-size string should fail")
|
|
}
|
|
})
|
|
|
|
t.Run("JSON marshaling edge cases", func(t *testing.T) {
|
|
yamlContent := `
|
|
tricky:
|
|
large_int: 18446744073709551615 # max uint64
|
|
float_like_int: 1.0
|
|
scientific: 1e10
|
|
hex: 0xFF # YAML treats as integer 255
|
|
octal: 0o777 # YAML treats as integer 511
|
|
binary: 0b1111 # YAML treats as integer 15
|
|
date: 2023-12-25
|
|
datetime: 2023-12-25T10:30:00Z
|
|
`
|
|
config, err := NewFromReader(strings.NewReader(yamlContent))
|
|
if err != nil {
|
|
// Some YAML parsers might not support all these formats
|
|
t.Skipf("YAML parser doesn't support test formats: %v", err)
|
|
}
|
|
|
|
// Large integers might lose precision in JSON
|
|
_, _ = config.GetString("tricky.large_int")
|
|
|
|
// Float that looks like int
|
|
floatInt, err := config.GetFloat("tricky.float_like_int")
|
|
if err != nil {
|
|
t.Errorf("GetFloat(float_like_int) failed: %v", err)
|
|
}
|
|
if floatInt != 1.0 {
|
|
t.Errorf("float_like_int = %f, want 1.0", floatInt)
|
|
}
|
|
|
|
// Scientific notation
|
|
sci, err := config.GetFloat("tricky.scientific")
|
|
if err != nil {
|
|
t.Errorf("GetFloat(scientific) failed: %v", err)
|
|
}
|
|
if sci != 1e10 {
|
|
t.Errorf("scientific = %f, want 1e10", sci)
|
|
}
|
|
})
|
|
|
|
t.Run("Concurrent access", func(t *testing.T) {
|
|
yamlContent := `
|
|
concurrent:
|
|
value1: "test1"
|
|
value2: "test2"
|
|
nested:
|
|
value3: "test3"
|
|
`
|
|
config, err := NewFromReader(strings.NewReader(yamlContent))
|
|
if err != nil {
|
|
t.Fatalf("Failed to load config: %v", err)
|
|
}
|
|
|
|
// Run multiple goroutines accessing the config
|
|
done := make(chan bool, 10)
|
|
for i := 0; i < 10; i++ {
|
|
go func(id int) {
|
|
for j := 0; j < 100; j++ {
|
|
switch id % 3 {
|
|
case 0:
|
|
_, _ = config.GetString("concurrent.value1")
|
|
case 1:
|
|
_, _ = config.GetString("concurrent.value2")
|
|
case 2:
|
|
_, _ = config.GetString("concurrent.nested.value3")
|
|
}
|
|
}
|
|
done <- true
|
|
}(i)
|
|
}
|
|
|
|
// Wait for all goroutines
|
|
for i := 0; i < 10; i++ {
|
|
<-done
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestGjsonPathNormalization(t *testing.T) {
|
|
yamlContent := `
|
|
data:
|
|
value: "test"
|
|
array:
|
|
- first
|
|
- second
|
|
`
|
|
config, err := NewFromReader(strings.NewReader(yamlContent))
|
|
if err != nil {
|
|
t.Fatalf("Failed to load config: %v", err)
|
|
}
|
|
|
|
t.Run("Different path formats", func(t *testing.T) {
|
|
// These should all access the same value
|
|
paths := []string{
|
|
"data.value",
|
|
"data.value", // With spaces (trimmed in real usage)
|
|
"data\\.value", // Escaped dot (gjson might interpret differently)
|
|
}
|
|
|
|
for _, path := range paths {
|
|
t.Run(path, func(t *testing.T) {
|
|
trimmedPath := strings.TrimSpace(path)
|
|
_, _ = config.GetString(trimmedPath)
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("Array access formats", func(t *testing.T) {
|
|
// Different ways to access array elements
|
|
val1, err := config.GetString("data.array.0")
|
|
if err != nil || val1 != "first" {
|
|
t.Errorf("Array access with .0 failed: %v, %v", val1, err)
|
|
}
|
|
|
|
// gjson also supports [index] syntax
|
|
// but our YAML structure uses .index
|
|
val2, err := config.GetString("data.array.1")
|
|
if err != nil || val2 != "second" {
|
|
t.Errorf("Array access with .1 failed: %v, %v", val2, err)
|
|
}
|
|
})
|
|
}
|