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

293 lines
6.5 KiB
Go

package smartconfig
import (
"testing"
)
func TestGjsonPathEdgeCasesRefactored(t *testing.T) {
config := loadConfig(t, `
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"
"123": "numeric key"
"": "empty 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
types:
very_large_number: 9223372036854775807
very_small_number: -9223372036854775808
special_values:
inf: .inf
neg_inf: -.inf
nan: .nan
`)
t.Run("Empty and null values", func(t *testing.T) {
// Empty string
val, err := config.GetString("empty_string")
assertNoError(t, err)
assertString(t, val, "")
// Zero value
zero, err := config.GetInt("zero")
assertNoError(t, err)
assertInt(t, zero, 0)
// False boolean
falseBool, err := config.GetBool("false_bool")
assertNoError(t, err)
assertBool(t, falseBool, false)
// Null value should fail for typed getters
_, err = config.GetString("null_value")
assertError(t, err)
_, err = config.GetInt("null_value")
assertError(t, err)
_, err = config.GetBool("null_value")
assertError(t, err)
})
t.Run("Empty collections", func(t *testing.T) {
// Empty map
emptyMap, exists := config.Get("empty_map")
assertExists(t, exists)
m := assertType[map[string]interface{}](t, emptyMap)
if len(m) != 0 {
t.Errorf("empty_map should have 0 elements, has %d", len(m))
}
// Empty array
emptyArray, exists := config.Get("empty_array")
assertExists(t, exists)
a := assertType[[]interface{}](t, emptyArray)
if len(a) != 0 {
t.Errorf("empty_array should have 0 elements, has %d", len(a))
}
})
t.Run("Deep nesting", func(t *testing.T) {
deepValue, err := config.GetString("deeply.nested.structure.with.many.levels.value")
assertNoError(t, err)
assertString(t, deepValue, "deep value")
})
t.Run("Complex arrays", func(t *testing.T) {
// Triple nested array
val, err := config.GetString("arrays.nested.0.0.0.value")
assertNoError(t, err)
assertString(t, val, "triple nested array")
// Mixed structure navigation
id1, err := config.GetInt("arrays.mixed.0.items.0.id")
assertNoError(t, err)
assertInt(t, id1, 1)
id4, err := config.GetInt("arrays.mixed.1.items.1.id")
assertNoError(t, err)
assertInt(t, id4, 4)
})
t.Run("Large numbers", func(t *testing.T) {
largeNum, err := config.GetInt("types.very_large_number")
assertNoError(t, err)
assertInt(t, largeNum, 9223372036854775807)
smallNum, err := config.GetInt("types.very_small_number")
assertNoError(t, err)
assertInt(t, smallNum, -9223372036854775808)
})
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")
assertNoError(t, err)
assertString(t, inf, "Infinity")
negInf, err := config.GetString("special_values.neg_inf")
assertNoError(t, err)
assertString(t, negInf, "-Infinity")
nan, err := config.GetString("special_values.nan")
assertNoError(t, err)
assertString(t, nan, "NaN")
})
t.Run("Non-existent paths", func(t *testing.T) {
paths := []string{
"nonexistent",
"nonexistent.nested.key",
"deeply.nonexistent",
"arrays.mixed.999.name",
"arrays.mixed.-1.name",
}
for _, path := range paths {
t.Run(path, func(t *testing.T) {
_, err := config.GetString(path)
assertError(t, err)
_, err = config.GetInt(path)
assertError(t, err)
_, exists := config.Get(path)
assertNotExists(t, exists)
})
}
})
t.Run("Invalid path syntax", func(t *testing.T) {
// These paths have invalid syntax
paths := []string{
"", // Empty path
"...", // Only dots
"key.", // Trailing dot
".key", // Leading dot
"key..value", // Double dots
}
for _, path := range paths {
t.Run(path, func(t *testing.T) {
_, err := config.GetString(path)
assertError(t, err)
_, exists := config.Get(path)
assertNotExists(t, exists)
})
}
})
}
func TestTypeConversions(t *testing.T) {
config := loadConfig(t, `
string_number: "123"
string_bool: "true"
string_float: "45.67"
string_bytes: "1GB"
not_a_number: "abc"
not_a_bool: "yes"
partial_number: "123abc"
object:
key: value
array:
- item1
- item2
`)
t.Run("Valid conversions", func(t *testing.T) {
// String to int
num, err := config.GetInt("string_number")
assertNoError(t, err)
assertInt(t, num, 123)
// String to bool
b, err := config.GetBool("string_bool")
assertNoError(t, err)
assertBool(t, b, true)
// String to float
f, err := config.GetFloat("string_float")
assertNoError(t, err)
assertFloat(t, f, 45.67)
// String to bytes
bytes, err := config.GetBytes("string_bytes")
assertNoError(t, err)
assertUint64(t, bytes, 1000000000) // 1GB in decimal
})
t.Run("Invalid conversions", func(t *testing.T) {
// Invalid string to int
_, err := config.GetInt("not_a_number")
assertError(t, err)
// Invalid string to bool
_, err = config.GetBool("not_a_bool")
assertError(t, err)
// Partial number
_, err = config.GetInt("partial_number")
assertError(t, err)
// Object as string (should fail)
_, err = config.GetString("object")
assertError(t, err)
// Array as int (should fail)
_, err = config.GetInt("array")
assertError(t, err)
})
}
func TestConcurrentAccess(t *testing.T) {
config := loadConfig(t, `
concurrent:
value1: "test1"
value2: "test2"
nested:
value3: "test3"
`)
// 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:
val, err := config.GetString("concurrent.value1")
if err != nil || val != "test1" {
t.Errorf("concurrent access failed: %v, %v", val, err)
}
case 1:
val, err := config.GetString("concurrent.value2")
if err != nil || val != "test2" {
t.Errorf("concurrent access failed: %v, %v", val, err)
}
case 2:
val, err := config.GetString("concurrent.nested.value3")
if err != nil || val != "test3" {
t.Errorf("concurrent access failed: %v, %v", val, err)
}
}
}
done <- true
}(i)
}
// Wait for all goroutines
for i := 0; i < 10; i++ {
<-done
}
}