Files
ConfigKit/internal/engine/path_test.go

159 lines
4.1 KiB
Go

package engine
import (
"strconv"
"testing"
)
func TestPathRoundTripsSemanticKeys(t *testing.T) {
t.Parallel()
tests := []struct {
name string
pointer string
}{
{name: "root", pointer: ""},
{name: "slash and tilde", pointer: "/a~1b/c~0d"},
{name: "empty key", pointer: "/"},
{name: "dotted key", pointer: "/outputs/a.b"},
{name: "numeric key", pointer: "/outputs/0"},
{name: "dot keys", pointer: "/./.."},
{name: "percent text", pointer: "/%2F/%7E"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
parsed, err := ParsePath(test.pointer)
if err != nil {
t.Fatalf("ParsePath() error = %v", err)
}
if got := parsed.String(); got != test.pointer {
t.Fatalf("String() = %q, want %q", got, test.pointer)
}
})
}
if got := Keys("a/b", "c~d", "", "..").String(); got != "/a~1b/c~0d//.." {
t.Fatalf("Keys().String() = %q", got)
}
if got := Keys("items").Index(12).Key("name").String(); got != "/items/12/name" {
t.Fatalf("indexed path = %q", got)
}
maximum := int(^uint(0) >> 1)
for _, index := range []int{100, maximum, -1, -maximum - 1} {
want := "/items/" + strconv.Itoa(index)
if got := Keys("items").Index(index).String(); got != want {
t.Fatalf("indexed path = %q, want %q", got, want)
}
}
}
func TestPathContainmentMatchesPointerSpelling(t *testing.T) {
parse := func(pointer string) Path {
path, err := ParsePath(pointer)
if err != nil {
t.Fatalf("ParsePath(%q) error = %v", pointer, err)
}
return path
}
maximum := int(^uint(0) >> 1)
minimum := -maximum - 1
paths := []Path{
{},
Keys(""),
Keys("a"),
Keys("ab"),
Keys("a", "b"),
Keys("a/b"),
Keys("a~1b"),
Keys("~"),
Keys("0"),
Keys("01"),
Keys("+1"),
Keys("items").Index(0),
Keys("items").Index(maximum),
Keys("items").Index(minimum),
parse("/"),
parse("/a~1b"),
parse("/a~01b"),
parse("/~0"),
parse("/0"),
parse("/01"),
parse("/+1"),
parse("/items/0"),
parse("/items/" + strconv.Itoa(maximum)),
parse("/items/" + strconv.Itoa(minimum)),
Path{}.Index(-1),
}
for _, container := range paths {
for _, descendant := range paths {
got := pathContains(container, descendant)
want := pathContainsByPointer(container, descendant)
if got != want {
t.Errorf("pathContains(%q, %q) = %v, want %v", container.String(), descendant.String(), got, want)
}
}
}
}
func pathContainsByPointer(container, descendant Path) bool {
if !container.valid() || !descendant.valid() || len(container.segments) > len(descendant.segments) {
return false
}
prefix := container.String()
if prefix == "" {
return true
}
value := descendant.String()
return value == prefix || len(value) > len(prefix) && value[:len(prefix)] == prefix && value[len(prefix)] == '/'
}
func TestPathPreservesSegmentIntentAndImmutability(t *testing.T) {
t.Parallel()
key := Keys("items", "0")
index := Keys("items").Index(0)
parsed, err := ParsePath("/items/0")
if err != nil {
t.Fatal(err)
}
if key.String() != index.String() || index.String() != parsed.String() {
t.Fatal("equivalent textual paths should share a JSON Pointer spelling")
}
if key.segments[1].kind != pathSegmentKey {
t.Fatal("Keys numeric segment lost mapping-key intent")
}
if index.segments[1].kind != pathSegmentIndex {
t.Fatal("Index segment lost sequence intent")
}
if parsed.segments[1].kind != pathSegmentParsed {
t.Fatal("parsed numeric segment was resolved before traversal")
}
base := Keys("parent")
child := base.Key("child")
child.segments[0].key = "changed"
if got := base.String(); got != "/parent" {
t.Fatalf("builder mutated parent: %q", got)
}
negative := Path{}.Index(-1)
if negative.valid() {
t.Fatal("negative index must produce an invalid path")
}
}
func TestParsePathRejectsMalformedPointers(t *testing.T) {
t.Parallel()
for _, pointer := range []string{"relative", "~0", "/bad~", "/bad~2", "/bad~01~"} {
if _, err := ParsePath(pointer); err == nil {
t.Errorf("ParsePath(%q) succeeded", pointer)
}
}
if parsed, err := ParsePath("/missing"); err != nil || !parsed.valid() {
t.Fatalf("valid missing path rejected: path=%v error=%v", parsed, err)
}
}