smartconfig/gjson_edge_cases_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

394 lines
10 KiB
Go

package smartconfig
import (
"fmt"
"strconv"
"strings"
"testing"
)
func TestGjsonPathEdgeCases(t *testing.T) {
yamlContent := `
# Edge case test data
empty_string: ""
null_value: null
zero: 0
false_bool: false
empty_map: {}
empty_array: []
special_keys:
"key.with.dots": "dotted key value"
"key with spaces": "spaced key value"
"key[with]brackets": "bracketed key value"
"key{with}braces": "braced key value"
"123": "numeric key"
"": "empty key"
"key\"with\"quotes": "quoted key"
"key'with'quotes": "single quoted key"
"key\nwith\nnewlines": "newline key"
"key\twith\ttabs": "tab key"
deeply:
nested:
structure:
with:
many:
levels:
value: "deep value"
arrays:
nested:
- - - value: "triple nested array"
mixed:
- name: "first"
items:
- id: 1
- id: 2
- name: "second"
items:
- id: 3
- id: 4
unicode:
"emoji🎉key": "emoji value"
"中文键": "chinese value"
"مفتاح": "arabic value"
"ключ": "russian value"
types:
very_large_number: 9223372036854775807 # max int64
very_small_number: -9223372036854775808 # min int64
float_edge: 1.7976931348623157e+308 # near max float64
tiny_float: 2.2250738585072014e-308 # near min positive float64
special_values:
inf: .inf
neg_inf: -.inf
nan: .nan
inf_string: "Infinity"
neg_inf_string: "-Infinity"
nan_string: "NaN"
`
config, err := NewFromReader(strings.NewReader(yamlContent))
if err != nil {
t.Fatalf("Failed to load config: %v", err)
}
t.Run("Empty and null values", func(t *testing.T) {
// Empty string
val, err := config.GetString("empty_string")
if err != nil || val != "" {
t.Errorf("GetString(empty_string) = %q, %v; want empty string, nil", val, err)
}
// Zero value
zero, err := config.GetInt("zero")
if err != nil || zero != 0 {
t.Errorf("GetInt(zero) = %d, %v; want 0, nil", zero, err)
}
// False boolean
falseBool, err := config.GetBool("false_bool")
if err != nil || falseBool != false {
t.Errorf("GetBool(false_bool) = %v, %v; want false, nil", falseBool, err)
}
// Null value should fail for typed getters
_, err = config.GetString("null_value")
if err == nil {
t.Error("GetString(null_value) should return error for null")
}
})
t.Run("Special character keys", func(t *testing.T) {
// Keys with dots should be accessible with escaped syntax
// Note: gjson doesn't support accessing keys with dots directly
// This is a known limitation
// Empty map
emptyMap, exists := config.Get("empty_map")
if !exists {
t.Error("empty_map should exist")
}
if m, ok := emptyMap.(map[string]interface{}); !ok || len(m) != 0 {
t.Errorf("empty_map should be an empty map, got %v", emptyMap)
}
// Empty array
emptyArray, exists := config.Get("empty_array")
if !exists {
t.Error("empty_array should exist")
}
if a, ok := emptyArray.([]interface{}); !ok || len(a) != 0 {
t.Errorf("empty_array should be an empty array, got %v", emptyArray)
}
})
t.Run("Very deep nesting", func(t *testing.T) {
deepValue, err := config.GetString("deeply.nested.structure.with.many.levels.value")
if err != nil || deepValue != "deep value" {
t.Errorf("Deep nested access failed: %v, %v", deepValue, err)
}
})
t.Run("Complex array access", func(t *testing.T) {
// Triple nested array
val, err := config.GetString("arrays.nested.0.0.0.value")
if err != nil || val != "triple nested array" {
t.Errorf("Triple nested array access = %q, %v; want 'triple nested array', nil", val, err)
}
// Mixed structure navigation
id1, err := config.GetInt("arrays.mixed.0.items.0.id")
if err != nil || id1 != 1 {
t.Errorf("Mixed array access = %d, %v; want 1, nil", id1, err)
}
id4, err := config.GetInt("arrays.mixed.1.items.1.id")
if err != nil || id4 != 4 {
t.Errorf("Mixed array access = %d, %v; want 4, nil", id4, err)
}
})
t.Run("Unicode keys", func(t *testing.T) {
// Unicode keys should work normally
chineseVal, exists := config.Get("unicode.中文键")
if !exists || chineseVal != "chinese value" {
t.Errorf("Unicode key access failed: %v", chineseVal)
}
})
t.Run("Type edge cases", func(t *testing.T) {
// Very large numbers
largeNum, err := config.GetInt("types.very_large_number")
if err != nil {
t.Errorf("GetInt(very_large_number) failed: %v", err)
}
if largeNum != 9223372036854775807 {
t.Errorf("Large number = %d, want 9223372036854775807", largeNum)
}
// Very small numbers
smallNum, err := config.GetInt("types.very_small_number")
if err != nil {
t.Errorf("GetInt(very_small_number) failed: %v", err)
}
if smallNum != -9223372036854775808 {
t.Errorf("Small number = %d, want -9223372036854775808", smallNum)
}
})
t.Run("Special float values", func(t *testing.T) {
// These special float values should be converted to strings
inf, err := config.GetString("special_values.inf")
if err != nil || inf != "Infinity" {
t.Errorf("GetString(inf) = %q, %v; want 'Infinity', nil", inf, err)
}
negInf, err := config.GetString("special_values.neg_inf")
if err != nil || negInf != "-Infinity" {
t.Errorf("GetString(neg_inf) = %q, %v; want '-Infinity', nil", negInf, err)
}
nan, err := config.GetString("special_values.nan")
if err != nil || nan != "NaN" {
t.Errorf("GetString(nan) = %q, %v; want 'NaN', nil", nan, err)
}
})
t.Run("Invalid paths", func(t *testing.T) {
// Various invalid path formats
invalidPaths := []string{
"...", // Only dots
"key.", // Trailing dot
".key", // Leading dot
"key..value", // Double dots
"key..", // Double trailing dots
"key[", // Unclosed bracket
"key]", // Unmatched bracket
// "key.[0]", // Actually valid gjson syntax
"arrays.mixed.-1.name", // Negative array index
"arrays.mixed.999.name", // Out of bounds array index
"", // Empty path
"nonexistent", // Simple non-existent key
"nonexistent.nested.key", // Nested non-existent
}
for _, path := range invalidPaths {
t.Run(path, func(t *testing.T) {
_, err := config.GetString(path)
if err == nil {
t.Errorf("GetString(%q) should return error", path)
}
_, exists := config.Get(path)
if exists {
t.Errorf("Get(%q) should return false for exists", path)
}
})
}
})
t.Run("Type conversion edge cases", func(t *testing.T) {
// Try to get string as various types
strConfig := `
string_number: "123"
string_bool: "true"
string_float: "45.67"
string_bytes: "1GB"
not_a_number: "abc"
not_a_bool: "yes"
partial_number: "123abc"
`
c, _ := NewFromReader(strings.NewReader(strConfig))
// String to int
num, err := c.GetInt("string_number")
if err != nil || num != 123 {
t.Errorf("GetInt(string_number) = %d, %v; want 123, nil", num, err)
}
// String to bool
b, err := c.GetBool("string_bool")
if err != nil || b != true {
t.Errorf("GetBool(string_bool) = %v, %v; want true, nil", b, err)
}
// Invalid conversions
_, err = c.GetInt("not_a_number")
if err == nil {
t.Error("GetInt(not_a_number) should fail")
}
_, err = c.GetBool("not_a_bool")
if err == nil {
t.Error("GetBool(not_a_bool) should fail")
}
_, err = c.GetInt("partial_number")
if err == nil {
t.Error("GetInt(partial_number) should fail")
}
})
t.Run("Gjson special syntax", func(t *testing.T) {
// Test that gjson special syntax doesn't cause issues
dangerousPaths := []string{
"*", // Wildcard
"special_keys.#", // Array length operator
"special_keys.@reverse", // Modifier
"special_keys|@pretty", // Pipe operator
"arrays.mixed.#.name", // Multi-value syntax
"arrays.mixed.[0,1]", // Union syntax
}
for _, path := range dangerousPaths {
t.Run(path, func(t *testing.T) {
// These should either work correctly or fail gracefully
_, _ = config.GetString(path)
_, _ = config.Get(path)
// We're just ensuring no panic occurs
})
}
})
}
func TestGjsonPathSecurity(t *testing.T) {
yamlContent := `
sensitive:
password: "secret123"
api_key: "sk-1234567890"
public:
version: "1.0.0"
`
config, err := NewFromReader(strings.NewReader(yamlContent))
if err != nil {
t.Fatalf("Failed to load config: %v", err)
}
t.Run("Path traversal attempts", func(t *testing.T) {
// Ensure path traversal doesn't work
traversalPaths := []string{
"../sensitive/password",
"public/../sensitive/password",
"./sensitive/password",
"public/../../sensitive/password",
}
for _, path := range traversalPaths {
t.Run(path, func(t *testing.T) {
// These should not access sensitive data through traversal
val, _ := config.GetString(path)
if val == "secret123" {
t.Errorf("Path traversal %q should not access sensitive data", path)
}
})
}
})
}
func TestGjsonPerformanceEdgeCases(t *testing.T) {
// Test with large configurations
var largeYaml strings.Builder
largeYaml.WriteString("large_array:\n")
for i := 0; i < 1000; i++ {
largeYaml.WriteString(" - id: ")
largeYaml.WriteString(strconv.Itoa(i))
largeYaml.WriteString("\n")
}
largeYaml.WriteString("large_map:\n")
for i := 0; i < 100; i++ {
largeYaml.WriteString(fmt.Sprintf(" key%d: value%d\n", i, i))
}
config, err := NewFromReader(strings.NewReader(largeYaml.String()))
if err != nil {
t.Fatalf("Failed to load large config: %v", err)
}
// Access various elements
_, err = config.GetString("large_array.999.id")
if err != nil {
t.Errorf("Failed to access last array element: %v", err)
}
_, err = config.GetString("large_map.key99")
if err != nil {
t.Errorf("Failed to access map element: %v", err)
}
}
func TestResolverArgumentValidation(t *testing.T) {
yamlContent := `
test: value
`
config, err := NewFromReader(strings.NewReader(yamlContent))
if err != nil {
t.Fatalf("Failed to load config: %v", err)
}
t.Run("Path with interpolation syntax", func(t *testing.T) {
// Paths that look like interpolation should be handled correctly
suspiciousPaths := []string{
"${ENV:PATH}", // Interpolation syntax in path
"test${ENV:SUFFIX}", // Partial interpolation
"${test}", // Brace syntax
"$test", // Dollar sign
"test$", // Trailing dollar
}
for _, path := range suspiciousPaths {
t.Run(path, func(t *testing.T) {
_, _ = config.GetString(path)
// Should not panic or cause issues
})
}
})
}