Files
capsule/pkg/runtime/jsonpath/truth_test.go
T

868 lines
19 KiB
Go

// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0
package jsonpath
import (
"strings"
"testing"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
func TestEvaluateTruthyFromCompiled(t *testing.T) {
t.Parallel()
tests := []struct {
name string
path string
object unstructured.Unstructured
want bool
wantErr bool
}{
{
name: "empty missing path is false",
path: ".missing",
object: newUnstructured(map[string]any{
"spec": map[string]any{
"type": "ClusterIP",
},
}),
want: false,
},
{
name: "empty string is false",
path: ".spec.value",
object: newUnstructured(map[string]any{
"spec": map[string]any{
"value": "",
},
}),
want: false,
},
{
name: "whitespace string is false",
path: ".spec.value",
object: newUnstructured(map[string]any{
"spec": map[string]any{
"value": " ",
},
}),
want: false,
},
{
name: "false string is false",
path: ".spec.value",
object: newUnstructured(map[string]any{
"spec": map[string]any{
"value": "false",
},
}),
want: false,
},
{
name: "false string case insensitive is false",
path: ".spec.value",
object: newUnstructured(map[string]any{
"spec": map[string]any{
"value": "FALSE",
},
}),
want: false,
},
{
name: "false string with whitespace is false",
path: ".spec.value",
object: newUnstructured(map[string]any{
"spec": map[string]any{
"value": " false ",
},
}),
want: false,
},
{
name: "zero string is false",
path: ".spec.value",
object: newUnstructured(map[string]any{
"spec": map[string]any{
"value": "0",
},
}),
want: false,
},
{
name: "true string is true",
path: ".spec.value",
object: newUnstructured(map[string]any{
"spec": map[string]any{
"value": "true",
},
}),
want: true,
},
{
name: "non-empty scalar string is true",
path: ".spec.type",
object: newUnstructured(map[string]any{
"spec": map[string]any{
"type": "ClusterIP",
},
}),
want: true,
},
{
name: "one string is true",
path: ".spec.value",
object: newUnstructured(map[string]any{
"spec": map[string]any{
"value": "1",
},
}),
want: true,
},
{
name: "jsonpath filter match is true",
path: ".spec.accessModes[?(@==\"ReadWriteOnce\")]",
object: newUnstructured(map[string]any{
"spec": map[string]any{
"accessModes": []any{
"ReadWriteOnce",
"ReadOnlyMany",
},
},
}),
want: true,
},
{
name: "jsonpath filter no match is false",
path: ".spec.accessModes[?(@==\"ReadWriteMany\")]",
object: newUnstructured(map[string]any{
"spec": map[string]any{
"accessModes": []any{
"ReadWriteOnce",
"ReadOnlyMany",
},
},
}),
want: false,
},
{
name: "invalid jsonpath execution returns error",
path: ".spec.accessModes[999]",
object: newUnstructured(map[string]any{
"spec": map[string]any{
"accessModes": []any{
"ReadWriteOnce",
},
},
}),
wantErr: true,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
compiled, err := CompileJSONPath(tt.path)
if err != nil {
t.Fatalf("expected jsonpath %q to compile, got %v", tt.path, err)
}
got, err := EvaluateTruthyFromCompiled(tt.object, compiled)
if tt.wantErr {
if err == nil {
t.Fatal("expected error, got nil")
}
return
}
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if got != tt.want {
t.Fatalf("expected %t, got %t", tt.want, got)
}
})
}
}
func TestEvaluateTruthyFromCompiledRejectsNilCompiledJSONPath(t *testing.T) {
t.Parallel()
got, err := EvaluateTruthyFromCompiled(newUnstructured(nil), nil)
if err == nil {
t.Fatal("expected error, got nil")
}
if got {
t.Fatal("expected false result for nil compiled jsonpath")
}
if !strings.Contains(err.Error(), "compiled jsonpath is nil") {
t.Fatalf("expected nil compiled jsonpath error, got %v", err)
}
}
func TestSplitFieldSelectorEquals(t *testing.T) {
t.Parallel()
tests := []struct {
name string
raw string
wantPath string
wantValue string
wantOK bool
}{
{
name: "single equals",
raw: `.spec.type=ClusterIP`,
wantPath: `.spec.type`,
wantValue: `ClusterIP`,
wantOK: true,
},
{
name: "double equals",
raw: `.spec.type==ClusterIP`,
wantPath: `.spec.type`,
wantValue: `ClusterIP`,
wantOK: true,
},
{
name: "double equals with quoted value",
raw: `.spec.type=="ClusterIP"`,
wantPath: `.spec.type`,
wantValue: `ClusterIP`,
wantOK: true,
},
{
name: "double equals with single quoted value",
raw: `.spec.type=='ClusterIP'`,
wantPath: `.spec.type`,
wantValue: `ClusterIP`,
wantOK: true,
},
{
name: "trims whitespace around expression",
raw: ` .spec.type == "ClusterIP" `,
wantPath: `.spec.type`,
wantValue: `ClusterIP`,
wantOK: true,
},
{
name: "keeps spaces inside quoted value",
raw: `.metadata.annotations["example.com/value"]=="hello world"`,
wantPath: `.metadata.annotations["example.com/value"]`,
wantValue: `hello world`,
wantOK: true,
},
{
name: "quoted value containing equals",
raw: `.metadata.annotations["example.com/check"]=="a=b"`,
wantPath: `.metadata.annotations["example.com/check"]`,
wantValue: `a=b`,
wantOK: true,
},
{
name: "single quoted value containing equals",
raw: `.metadata.annotations["example.com/check"]=='a=b'`,
wantPath: `.metadata.annotations["example.com/check"]`,
wantValue: `a=b`,
wantOK: true,
},
{
name: "top level equality after bracket expression",
raw: `.metadata.labels["app.kubernetes.io/name"]=="nginx"`,
wantPath: `.metadata.labels["app.kubernetes.io/name"]`,
wantValue: `nginx`,
wantOK: true,
},
{
name: "jsonpath filter equality is not top level equality",
raw: `.spec.accessModes[?(@=="ReadWriteOnce")]`,
wantOK: false,
},
{
name: "jsonpath filter single equals is not top level equality",
raw: `.spec.accessModes[?(@="ReadWriteOnce")]`,
wantOK: false,
},
{
name: "truthy path without equals",
raw: `.spec.storageClassName`,
wantOK: false,
},
{
name: "empty string",
raw: ``,
wantOK: false,
},
{
name: "whitespace string",
raw: ` `,
wantOK: false,
},
{
name: "missing path",
raw: `=ClusterIP`,
wantOK: false,
},
{
name: "missing value",
raw: `.spec.type=`,
wantOK: false,
},
{
name: "missing value with double equals",
raw: `.spec.type==`,
wantOK: false,
},
{
name: "does not split equals inside double quoted path segment",
raw: `.metadata.annotations["example.com/a=b"]=="value"`,
wantPath: `.metadata.annotations["example.com/a=b"]`,
wantValue: `value`,
wantOK: true,
},
{
name: "does not split equals inside single quoted path segment",
raw: `.metadata.annotations['example.com/a=b']=="value"`,
wantPath: `.metadata.annotations['example.com/a=b']`,
wantValue: `value`,
wantOK: true,
},
{
name: "does not split equals inside parentheses",
raw: `.spec.values(@=="ignored")=="value"`,
wantPath: `.spec.values(@=="ignored")`,
wantValue: `value`,
wantOK: true,
},
{
name: "does not split equals inside braces",
raw: `.spec.values{"a==b"}=="value"`,
wantPath: `.spec.values{"a==b"}`,
wantValue: `value`,
wantOK: true,
},
{
name: "escaped quote inside quoted value",
raw: `.metadata.annotations["example.com/check"]=="a\"=b"`,
wantPath: `.metadata.annotations["example.com/check"]`,
wantValue: `a\"=b`,
wantOK: true,
},
{
name: "unmatched quote after equality remains part of value",
raw: `.spec.type=="ClusterIP`,
wantPath: `.spec.type`,
wantValue: `"ClusterIP`,
wantOK: true,
},
{
name: "unmatched quote before equality suppresses split",
raw: `.metadata.annotations["broken]==value`,
wantOK: false,
},
{
name: "unmatched bracket suppresses split",
raw: `.metadata.annotations["key"=="value"`,
wantOK: false,
},
{
name: "top level equality before later malformed content still splits",
raw: `.spec.type=ClusterIP[?(@=="ignored")]`,
wantPath: `.spec.type`,
wantValue: `ClusterIP[?(@=="ignored")]`,
wantOK: true,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
gotPath, gotValue, gotOK := SplitFieldSelectorEquals(tt.raw)
if gotOK != tt.wantOK {
t.Fatalf("expected ok=%t, got %t", tt.wantOK, gotOK)
}
if gotPath != tt.wantPath {
t.Fatalf("expected path %q, got %q", tt.wantPath, gotPath)
}
if gotValue != tt.wantValue {
t.Fatalf("expected value %q, got %q", tt.wantValue, gotValue)
}
})
}
}
func TestFindTopLevelEquals(t *testing.T) {
t.Parallel()
tests := []struct {
name string
raw string
wantIdx int
wantWidth int
}{
{
name: "single equals",
raw: `.spec.type=ClusterIP`,
wantIdx: len(`.spec.type`),
wantWidth: 1,
},
{
name: "double equals",
raw: `.spec.type==ClusterIP`,
wantIdx: len(`.spec.type`),
wantWidth: 2,
},
{
name: "ignores equals inside brackets",
raw: `.spec.accessModes[?(@=="ReadWriteOnce")]`,
wantIdx: -1,
wantWidth: 0,
},
{
name: "finds top level equals after brackets",
raw: `.metadata.labels["app.kubernetes.io/name"]=="nginx"`,
wantIdx: len(`.metadata.labels["app.kubernetes.io/name"]`),
wantWidth: 2,
},
{
name: "ignores equals inside double quotes",
raw: `.metadata.annotations["a=b"]`,
wantIdx: -1,
wantWidth: 0,
},
{
name: "ignores equals inside single quotes",
raw: `.metadata.annotations['a=b']`,
wantIdx: -1,
wantWidth: 0,
},
{
name: "ignores escaped quote before equals inside quoted string",
raw: `.metadata.annotations["a\"=b"]`,
wantIdx: -1,
wantWidth: 0,
},
{
name: "ignores equals inside parentheses",
raw: `.spec.values(@=="ignored")`,
wantIdx: -1,
wantWidth: 0,
},
{
name: "ignores equals inside braces",
raw: `.spec.values{"a==b"}`,
wantIdx: -1,
wantWidth: 0,
},
{
name: "finds first top level equals",
raw: `.spec.type=ClusterIP=ignored`,
wantIdx: len(`.spec.type`),
wantWidth: 1,
},
{
name: "empty string",
raw: ``,
wantIdx: -1,
wantWidth: 0,
},
{
name: "no equals",
raw: `.spec.type`,
wantIdx: -1,
wantWidth: 0,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
gotIdx, gotWidth := findTopLevelEquals(tt.raw)
if gotIdx != tt.wantIdx {
t.Fatalf("expected idx %d, got %d", tt.wantIdx, gotIdx)
}
if gotWidth != tt.wantWidth {
t.Fatalf("expected width %d, got %d", tt.wantWidth, gotWidth)
}
})
}
}
func TestTrimMatchingQuotes(t *testing.T) {
t.Parallel()
tests := []struct {
name string
value string
want string
}{
{
name: "empty",
value: "",
want: "",
},
{
name: "single character",
value: `"`,
want: `"`,
},
{
name: "double quoted",
value: `"ClusterIP"`,
want: "ClusterIP",
},
{
name: "single quoted",
value: `'ClusterIP'`,
want: "ClusterIP",
},
{
name: "trims inner whitespace",
value: `" ClusterIP "`,
want: "ClusterIP",
},
{
name: "unmatched starting quote",
value: `"ClusterIP`,
want: `"ClusterIP`,
},
{
name: "unmatched ending quote",
value: `ClusterIP"`,
want: `ClusterIP"`,
},
{
name: "different quote types",
value: `"ClusterIP'`,
want: `"ClusterIP'`,
},
{
name: "unquoted",
value: `ClusterIP`,
want: `ClusterIP`,
},
{
name: "quoted value containing equals",
value: `"a=b"`,
want: `a=b`,
},
{
name: "quoted value containing escaped quote",
value: `"a\"=b"`,
want: `a\"=b`,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if got := trimMatchingQuotes(tt.value); got != tt.want {
t.Fatalf("expected %q, got %q", tt.want, got)
}
})
}
}
func TestSplitFieldSelectorNotEquals(t *testing.T) {
t.Parallel()
tests := []struct {
name string
raw string
wantPath string
wantValue string
wantOK bool
}{
{
name: "simple not-equals",
raw: `.status.phase!=Succeeded`,
wantPath: `.status.phase`,
wantValue: `Succeeded`,
wantOK: true,
},
{
name: "not-equals with double-quoted value",
raw: `.status.phase!="Succeeded"`,
wantPath: `.status.phase`,
wantValue: `Succeeded`,
wantOK: true,
},
{
name: "not-equals with single-quoted value",
raw: `.status.phase!='Succeeded'`,
wantPath: `.status.phase`,
wantValue: `Succeeded`,
wantOK: true,
},
{
name: "trims whitespace around expression",
raw: ` .status.phase != "Succeeded" `,
wantPath: `.status.phase`,
wantValue: `Succeeded`,
wantOK: true,
},
{
name: "keeps spaces inside quoted value",
raw: `.metadata.annotations["example.com/value"]!="hello world"`,
wantPath: `.metadata.annotations["example.com/value"]`,
wantValue: `hello world`,
wantOK: true,
},
{
name: "value containing not-equals in quotes",
raw: `.metadata.annotations["example.com/check"]!="a!=b"`,
wantPath: `.metadata.annotations["example.com/check"]`,
wantValue: `a!=b`,
wantOK: true,
},
{
name: "top level not-equals after bracket expression",
raw: `.metadata.labels["app.kubernetes.io/name"]!="nginx"`,
wantPath: `.metadata.labels["app.kubernetes.io/name"]`,
wantValue: `nginx`,
wantOK: true,
},
{
name: "bang without equals is not not-equals",
raw: `.spec.type!ClusterIP`,
wantOK: false,
},
{
name: "truthy path without not-equals",
raw: `.spec.storageClassName`,
wantOK: false,
},
{
name: "plain equals is not not-equals",
raw: `.spec.type=ClusterIP`,
wantOK: false,
},
{
name: "double equals is not not-equals",
raw: `.spec.type==ClusterIP`,
wantOK: false,
},
{
name: "empty string",
raw: ``,
wantOK: false,
},
{
name: "whitespace string",
raw: ` `,
wantOK: false,
},
{
name: "missing path",
raw: `!=Succeeded`,
wantOK: false,
},
{
name: "missing value",
raw: `.status.phase!=`,
wantOK: false,
},
{
name: "does not split not-equals inside double-quoted path segment",
raw: `.metadata.annotations["example.com/a!=b"]!="value"`,
wantPath: `.metadata.annotations["example.com/a!=b"]`,
wantValue: `value`,
wantOK: true,
},
{
name: "does not split not-equals inside single-quoted path segment",
raw: `.metadata.annotations['example.com/a!=b']!="value"`,
wantPath: `.metadata.annotations['example.com/a!=b']`,
wantValue: `value`,
wantOK: true,
},
{
name: "does not split not-equals inside brackets",
raw: `.spec.accessModes[?(@!="ReadWriteOnce")]!="value"`,
wantPath: `.spec.accessModes[?(@!="ReadWriteOnce")]`,
wantValue: `value`,
wantOK: true,
},
{
name: "does not split not-equals inside parentheses",
raw: `.spec.values(@!="ignored")!="value"`,
wantPath: `.spec.values(@!="ignored")`,
wantValue: `value`,
wantOK: true,
},
{
name: "does not split not-equals inside braces",
raw: `.spec.values{"a!=b"}!="value"`,
wantPath: `.spec.values{"a!=b"}`,
wantValue: `value`,
wantOK: true,
},
{
name: "unmatched bracket suppresses split",
raw: `.metadata.annotations["key"!="value"`,
wantOK: false,
},
{
name: "unmatched quote after not-equals remains part of value",
raw: `.status.phase!="Succeeded`,
wantPath: `.status.phase`,
wantValue: `"Succeeded`,
wantOK: true,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
gotPath, gotValue, gotOK := SplitFieldSelectorNotEquals(tt.raw)
if gotOK != tt.wantOK {
t.Fatalf("expected ok=%t, got %t", tt.wantOK, gotOK)
}
if gotPath != tt.wantPath {
t.Fatalf("expected path %q, got %q", tt.wantPath, gotPath)
}
if gotValue != tt.wantValue {
t.Fatalf("expected value %q, got %q", tt.wantValue, gotValue)
}
})
}
}
func TestFindTopLevelNotEquals(t *testing.T) {
t.Parallel()
tests := []struct {
name string
raw string
wantIdx int
wantWidth int
}{
{
name: "simple not-equals",
raw: `.status.phase!=Succeeded`,
wantIdx: len(`.status.phase`),
wantWidth: 2,
},
{
name: "ignores not-equals inside brackets",
raw: `.spec.accessModes[?(@!="ReadWriteOnce")]`,
wantIdx: -1,
wantWidth: 0,
},
{
name: "finds top level not-equals after brackets",
raw: `.metadata.labels["app.kubernetes.io/name"]!="nginx"`,
wantIdx: len(`.metadata.labels["app.kubernetes.io/name"]`),
wantWidth: 2,
},
{
name: "ignores not-equals inside double quotes",
raw: `.metadata.annotations["a!=b"]`,
wantIdx: -1,
wantWidth: 0,
},
{
name: "ignores not-equals inside single quotes",
raw: `.metadata.annotations['a!=b']`,
wantIdx: -1,
wantWidth: 0,
},
{
name: "ignores escaped quote before not-equals inside quoted string",
raw: `.metadata.annotations["a\"!=b"]`,
wantIdx: -1,
wantWidth: 0,
},
{
name: "ignores not-equals inside parentheses",
raw: `.spec.values(@!="ignored")`,
wantIdx: -1,
wantWidth: 0,
},
{
name: "ignores not-equals inside braces",
raw: `.spec.values{"a!=b"}`,
wantIdx: -1,
wantWidth: 0,
},
{
name: "finds first top level not-equals",
raw: `.status.phase!=Succeeded!=ignored`,
wantIdx: len(`.status.phase`),
wantWidth: 2,
},
{
name: "bang without equals is not not-equals",
raw: `.spec.type!ClusterIP`,
wantIdx: -1,
wantWidth: 0,
},
{
name: "plain equals is not found",
raw: `.spec.type=ClusterIP`,
wantIdx: -1,
wantWidth: 0,
},
{
name: "empty string",
raw: ``,
wantIdx: -1,
wantWidth: 0,
},
{
name: "no operator",
raw: `.spec.type`,
wantIdx: -1,
wantWidth: 0,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
gotIdx, gotWidth := findTopLevelNotEquals(tt.raw)
if gotIdx != tt.wantIdx {
t.Fatalf("expected idx %d, got %d", tt.wantIdx, gotIdx)
}
if gotWidth != tt.wantWidth {
t.Fatalf("expected width %d, got %d", tt.wantWidth, gotWidth)
}
})
}
}
func newUnstructured(object map[string]any) unstructured.Unstructured {
return unstructured.Unstructured{
Object: object,
}
}