smartconfig/gjson_malformed_test.go
sneak 47801ce852 Add gjson path support to all typed getters
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.
2025-07-22 12:24:59 +02:00

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)
}
})
}