Merge pull request #119453 from cici37/addTest

Refactor jsonpath parser and add tests
This commit is contained in:
Kubernetes Prow Robot 2023-07-21 11:34:07 -07:00 committed by GitHub
commit 5e8dfe5d8b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 261 additions and 194 deletions

View File

@ -1092,9 +1092,6 @@ func ValidateCustomResourceDefinitionOpenAPISchema(schema *apiextensions.JSONSch
allErrs.SchemaErrors = append(allErrs.SchemaErrors, field.Invalid(fldPath.Child("x-kubernetes-validations").Index(i).Child("fieldPath"), rule.FieldPath, "fieldPath must not contain line breaks")) allErrs.SchemaErrors = append(allErrs.SchemaErrors, field.Invalid(fldPath.Child("x-kubernetes-validations").Index(i).Child("fieldPath"), rule.FieldPath, "fieldPath must not contain line breaks"))
} }
if len(rule.FieldPath) > 0 { if len(rule.FieldPath) > 0 {
if errs := validateSimpleJSONPath(rule.FieldPath, fldPath.Child("x-kubernetes-validations").Index(i).Child("fieldPath")); len(errs) > 0 {
allErrs.SchemaErrors = append(allErrs.SchemaErrors, errs...)
}
if !pathValid(schema, rule.FieldPath) { if !pathValid(schema, rule.FieldPath) {
allErrs.SchemaErrors = append(allErrs.SchemaErrors, field.Invalid(fldPath.Child("x-kubernetes-validations").Index(i).Child("fieldPath"), rule.FieldPath, "fieldPath must be a valid path")) allErrs.SchemaErrors = append(allErrs.SchemaErrors, field.Invalid(fldPath.Child("x-kubernetes-validations").Index(i).Child("fieldPath"), rule.FieldPath, "fieldPath must be a valid path"))
} }
@ -1163,7 +1160,7 @@ func ValidateCustomResourceDefinitionOpenAPISchema(schema *apiextensions.JSONSch
func pathValid(schema *apiextensions.JSONSchemaProps, path string) bool { func pathValid(schema *apiextensions.JSONSchemaProps, path string) bool {
// To avoid duplicated code and better maintain, using ValidaFieldPath func to check if the path is valid // To avoid duplicated code and better maintain, using ValidaFieldPath func to check if the path is valid
if ss, err := structuralschema.NewStructural(schema); err == nil { if ss, err := structuralschema.NewStructural(schema); err == nil {
_, err := cel.ValidFieldPath(path, nil, ss) _, err := cel.ValidFieldPath(path, ss)
return err == nil return err == nil
} }
return true return true

View File

@ -76,13 +76,6 @@ func (v validationMatch) matches(err *field.Error) bool {
return err.Type == v.errorType && err.Field == v.path.String() && strings.Contains(err.Error(), v.contains) return err.Type == v.errorType && err.Field == v.path.String() && strings.Contains(err.Error(), v.contains)
} }
func (v validationMatch) empty() bool {
if v.path == nil && v.errorType == "" && v.contains == "" {
return true
}
return false
}
func strPtr(s string) *string { return &s } func strPtr(s string) *string { return &s }
func TestValidateCustomResourceDefinition(t *testing.T) { func TestValidateCustomResourceDefinition(t *testing.T) {
@ -4140,18 +4133,45 @@ func TestValidateCustomResourceDefinition(t *testing.T) {
Type: "object", Type: "object",
XValidations: apiextensions.ValidationRules{ XValidations: apiextensions.ValidationRules{
{ {
Rule: "self.a.b.c > 0.0", Rule: "true",
FieldPath: ".a.b.c", FieldPath: ".foo['b.c']['c\\a']",
},
{
Rule: "true",
FieldPath: "['a.c']",
},
{
Rule: "true",
FieldPath: ".a.c",
},
{
Rule: "true",
FieldPath: ".list[0]",
},
{
Rule: "true",
FieldPath: " ",
},
{
Rule: "true",
FieldPath: ".",
},
{
Rule: "true",
FieldPath: "..",
}, },
}, },
Properties: map[string]apiextensions.JSONSchemaProps{ Properties: map[string]apiextensions.JSONSchemaProps{
"a": { "a.c": {
Type: "number",
},
"foo": {
Type: "object", Type: "object",
Properties: map[string]apiextensions.JSONSchemaProps{ Properties: map[string]apiextensions.JSONSchemaProps{
"b": { "b.c": {
Type: "object", Type: "object",
Properties: map[string]apiextensions.JSONSchemaProps{ Properties: map[string]apiextensions.JSONSchemaProps{
"c": { "c\a": {
Type: "number", Type: "number",
}, },
}, },
@ -4179,7 +4199,14 @@ func TestValidateCustomResourceDefinition(t *testing.T) {
StoredVersions: []string{"version"}, StoredVersions: []string{"version"},
}, },
}, },
errors: []validationMatch{}, errors: []validationMatch{
invalid("spec", "validation", "openAPIV3Schema", "x-kubernetes-validations[2]", "fieldPath"),
invalid("spec", "validation", "openAPIV3Schema", "x-kubernetes-validations[3]", "fieldPath"),
invalid("spec", "validation", "openAPIV3Schema", "x-kubernetes-validations[4]", "fieldPath"),
invalid("spec", "validation", "openAPIV3Schema", "x-kubernetes-validations[4]", "fieldPath"),
invalid("spec", "validation", "openAPIV3Schema", "x-kubernetes-validations[5]", "fieldPath"),
invalid("spec", "validation", "openAPIV3Schema", "x-kubernetes-validations[6]", "fieldPath"),
},
}, },
{ {
name: "x-kubernetes-validations have invalid fieldPath", name: "x-kubernetes-validations have invalid fieldPath",
@ -4368,168 +4395,156 @@ func TestValidateFieldPath(t *testing.T) {
fieldPath string fieldPath string
pathOfFieldPath *field.Path pathOfFieldPath *field.Path
schema *apiextensions.JSONSchemaProps schema *apiextensions.JSONSchemaProps
error validationMatch errMsg string
}{ }{
{ {
name: "Valid .a", name: "Valid .a",
fieldPath: ".a", fieldPath: ".a",
pathOfFieldPath: path, pathOfFieldPath: path,
schema: &schema, schema: &schema,
error: validationMatch{},
}, },
{ {
name: "Valid .a.b", name: "Valid .a.b",
fieldPath: ".a.bbb", fieldPath: ".a.bbb",
pathOfFieldPath: path, pathOfFieldPath: path,
schema: &schema, schema: &schema,
error: validationMatch{},
}, },
{ {
name: "Valid .foo.f1", name: "Valid .foo.f1",
fieldPath: ".foo.f1", fieldPath: ".foo.f1",
pathOfFieldPath: path, pathOfFieldPath: path,
schema: &schema, schema: &schema,
error: validationMatch{},
}, },
{ {
name: "Invalid map syntax .a.b", name: "Invalid map syntax .a.b",
fieldPath: ".a['bbb']", fieldPath: ".a['bbb']",
pathOfFieldPath: path, pathOfFieldPath: path,
schema: &schema, schema: &schema,
error: validationMatch{},
}, },
{ {
name: "Valid .a['bbb.c']", name: "Valid .a['bbb.c']",
fieldPath: ".a['bbb.c']", fieldPath: ".a['bbb.c']",
pathOfFieldPath: path, pathOfFieldPath: path,
schema: &schema, schema: &schema,
error: validationMatch{},
}, },
{ {
name: "Invalid .a['bbb.c'].a-b34", name: "Valid .a['bbb.c'].a-b34",
fieldPath: ".a['bbb.c'].a-b34", fieldPath: ".a['bbb.c'].a-b34",
pathOfFieldPath: path, pathOfFieldPath: path,
schema: &schema, schema: &schema,
error: invalid(path.String()),
}, },
{ {
name: "Valid .a['bbb.c']['a-b34']", name: "Valid .a['bbb.c']['a-b34']",
fieldPath: ".a['bbb.c']['a-b34']", fieldPath: ".a['bbb.c']['a-b34']",
pathOfFieldPath: path, pathOfFieldPath: path,
schema: &schema, schema: &schema,
error: validationMatch{},
}, },
{ {
name: "Valid .a.bbb.c", name: "Valid .a.bbb.c",
fieldPath: ".a.bbb.c", fieldPath: ".a.bbb.c",
pathOfFieldPath: path, pathOfFieldPath: path,
schema: &schema, schema: &schema,
error: validationMatch{},
}, },
{ {
name: "Valid .a.bbb.34", name: "Valid .a.bbb.34",
fieldPath: ".a.bbb['34']", fieldPath: ".a.bbb['34']",
pathOfFieldPath: path, pathOfFieldPath: path,
schema: &schema, schema: &schema,
error: validationMatch{},
}, },
{ {
name: "Invalid map key", name: "Invalid map key",
fieldPath: ".a.foo", fieldPath: ".a.foo",
pathOfFieldPath: path, pathOfFieldPath: path,
schema: &schema, schema: &schema,
error: invalid(path.String()), errMsg: "does not refer to a valid field",
}, },
{ {
name: "Malformed map key", name: "Malformed map key",
fieldPath: ".a.bbb[0]", fieldPath: ".a.bbb[0]",
pathOfFieldPath: path, pathOfFieldPath: path,
schema: &schema, schema: &schema,
error: invalid(path.String()), errMsg: "expected single quoted string but got 0",
}, },
{ {
name: "Special field names", name: "number in field names",
fieldPath: ".a.bbb.34", fieldPath: ".a.bbb.34",
pathOfFieldPath: path, pathOfFieldPath: path,
schema: &schema, schema: &schema,
error: invalid(path.String()),
}, },
{ {
name: "Special field names", name: "Special field names",
fieldPath: ".a.bbb['34']", fieldPath: ".a.bbb['34']",
pathOfFieldPath: path, pathOfFieldPath: path,
schema: &schema, schema: &schema,
error: validationMatch{},
}, },
{ {
name: "Valid .list", name: "Valid .list",
fieldPath: ".list", fieldPath: ".list",
pathOfFieldPath: path, pathOfFieldPath: path,
schema: &schema, schema: &schema,
error: validationMatch{},
}, },
{ {
name: "Invalid .list[1]", name: "Invalid .list[1]",
fieldPath: ".list[1]", fieldPath: ".list[1]",
pathOfFieldPath: path, pathOfFieldPath: path,
schema: &schema, schema: &schema,
error: invalid(path.String()), errMsg: "expected single quoted string but got 1",
}, },
{ {
name: "Unsopported .list.a", name: "Unsopported .list.a",
fieldPath: ".list.a", fieldPath: ".list.a",
pathOfFieldPath: path, pathOfFieldPath: path,
schema: &schema, schema: &schema,
error: invalid(path.String()), errMsg: "does not refer to a valid field",
}, },
{ {
name: "Unsupported .list['a-b.34']", name: "Unsupported .list['a-b.34']",
fieldPath: ".list['a-b.34']", fieldPath: ".list['a-b.34']",
pathOfFieldPath: path, pathOfFieldPath: path,
schema: &schema, schema: &schema,
error: invalid(path.String()), errMsg: "does not refer to a valid field",
}, },
{ {
name: "Invalid .list.a-b.34", name: "Invalid .list.a-b.34",
fieldPath: ".list.a-b.34", fieldPath: ".list.a-b.34",
pathOfFieldPath: path, pathOfFieldPath: path,
schema: &schema, schema: &schema,
error: invalid(path.String()), errMsg: "does not refer to a valid field",
}, },
{ {
name: "Missing leading dot", name: "Missing leading dot",
fieldPath: "a", fieldPath: "a",
pathOfFieldPath: path, pathOfFieldPath: path,
schema: &schema, schema: &schema,
error: invalid(path.String()), errMsg: "expected [ or . but got: a",
}, },
{ {
name: "Nonexistent field", name: "Nonexistent field",
fieldPath: ".c", fieldPath: ".c",
pathOfFieldPath: path, pathOfFieldPath: path,
schema: &schema, schema: &schema,
error: invalid(path.String()), errMsg: "does not refer to a valid field",
}, },
{ {
name: "Duplicate dots", name: "Duplicate dots",
fieldPath: ".a..b", fieldPath: ".a..b",
pathOfFieldPath: path, pathOfFieldPath: path,
schema: &schema, schema: &schema,
error: invalid(path.String()), errMsg: "does not refer to a valid field",
}, },
{ {
name: "Negative array index", name: "Negative array index",
fieldPath: ".list[-1]", fieldPath: ".list[-1]",
pathOfFieldPath: path, pathOfFieldPath: path,
schema: &schema, schema: &schema,
error: invalid(path.String()), errMsg: "expected single quoted string but got -1",
}, },
{ {
name: "Floating-point array index", name: "Floating-point array index",
fieldPath: ".list[1.0]", fieldPath: ".list[1.0]",
pathOfFieldPath: path, pathOfFieldPath: path,
schema: &schema, schema: &schema,
error: invalid(path.String()), errMsg: "expected single quoted string but got 1",
}, },
} }
@ -4539,13 +4554,15 @@ func TestValidateFieldPath(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("error when converting schema to structural schema: %v", err) t.Fatalf("error when converting schema to structural schema: %v", err)
} }
_, e := celschema.ValidFieldPath(tc.fieldPath, tc.pathOfFieldPath, ss) _, err = celschema.ValidFieldPath(tc.fieldPath, ss)
if e == nil && !tc.error.empty() { if err == nil && tc.errMsg != "" {
t.Errorf("expected %v at %v but got nil", tc.error.errorType, tc.error.path.String()) t.Errorf("expected err contains: %v but get nil", tc.errMsg)
} else if e != nil && tc.error.empty() { }
t.Errorf("unexpected error: %v at %v", tc.error.errorType, tc.error.path.String()) if err != nil && tc.errMsg == "" {
} else if !tc.error.empty() && !tc.error.matches(e) { t.Errorf("unexpected error: %v", err)
t.Errorf("expected %v at %v, got %v", tc.error.errorType, tc.error.path.String(), err) }
if err != nil && !strings.Contains(err.Error(), tc.errMsg) {
t.Errorf("expected error to contain: %v, but get: %v", tc.errMsg, err)
} }
}) })
} }

View File

@ -249,7 +249,7 @@ func compileRule(s *schema.Structural, rule apiextensions.ValidationRule, envSet
compilationResult.MessageExpressionMaxCost = costEst.Max compilationResult.MessageExpressionMaxCost = costEst.Max
} }
if rule.FieldPath != "" { if rule.FieldPath != "" {
validFieldPath, err := ValidFieldPath(rule.FieldPath, nil, s) validFieldPath, err := ValidFieldPath(rule.FieldPath, s)
if err == nil { if err == nil {
compilationResult.NormalizedRuleFieldPath = validFieldPath.String() compilationResult.NormalizedRuleFieldPath = validFieldPath.String()
} }

View File

@ -17,13 +17,13 @@ limitations under the License.
package cel package cel
import ( import (
"bufio"
"context" "context"
"fmt" "fmt"
"math" "math"
"reflect" "reflect"
"regexp" "regexp"
"strings" "strings"
"text/scanner"
"time" "time"
celgo "github.com/google/cel-go/cel" celgo "github.com/google/cel-go/cel"
@ -322,101 +322,117 @@ func unescapeSingleQuote(s string) (string, error) {
return unescaped, err return unescaped, err
} }
// ValidFieldPath returns a valid field path. // ValidFieldPath validates that jsonPath is a valid JSON Path containing only field and map accessors
func ValidFieldPath(fieldPath string, pathOfFieldPath *field.Path, schema *schema.Structural) (validFieldPath *field.Path, err *field.Error) { // that are valid for the given schema, and returns a field.Path representation of the validated jsonPath or an error.
validFieldPath = pathOfFieldPath func ValidFieldPath(jsonPath string, schema *schema.Structural) (validFieldPath *field.Path, err error) {
if len(fieldPath) == 0 { appendToPath := func(name string, isNamed bool) error {
return pathOfFieldPath, field.Invalid(pathOfFieldPath, fieldPath, "must not be empty") if !isNamed {
} validFieldPath = validFieldPath.Key(name)
schema = schema.AdditionalProperties.Structural
invalidFieldError := field.Invalid(pathOfFieldPath, fieldPath, "does not refer to an valid field") } else {
validFieldPath = validFieldPath.Child(name)
var s scanner.Scanner val, ok := schema.Properties[name]
s.Init(strings.NewReader(fieldPath)) if !ok {
s.Filename = pathOfFieldPath.String() return fmt.Errorf("does not refer to a valid field")
s.Mode = scanner.ScanInts | scanner.ScanIdents | scanner.ScanChars | scanner.ScanStrings }
s.Error = func(s *scanner.Scanner, msg string) { schema = &val
field.Invalid(pathOfFieldPath, fieldPath, fmt.Sprintf("failed to parse JSON Path: %s", msg))
}
found := false
for true {
tok := s.Scan()
if tok == scanner.EOF {
found = true
break
} }
switch schema.Type { return nil
case "object": }
isMapSyntax := false
if s.TokenText() == "[" {
isMapSyntax = true
} else if s.TokenText() != "." {
return pathOfFieldPath, field.Invalid(pathOfFieldPath, fieldPath, "expected [ or . but got: "+s.TokenText())
}
tok = s.Scan() validFieldPath = nil
if tok == scanner.EOF {
return pathOfFieldPath, field.Invalid(pathOfFieldPath, fieldPath, "unexpected end of JSON path") scanner := bufio.NewScanner(strings.NewReader(jsonPath))
}
fieldName := s.TokenText() // configure the scanner to split the string into tokens.
if isMapSyntax { // The three delimiters ('.', '[', ']') will be returned as single char tokens.
if tok == scanner.Char { // All other text between delimiters is returned as string tokens.
newS := fieldName[1 : len(fieldName)-1] scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
newS, err := unescapeSingleQuote(newS) if len(data) > 0 {
if err != nil { for i := 0; i < len(data); i++ {
return pathOfFieldPath, field.Invalid(pathOfFieldPath, fieldPath, fmt.Sprintf("failed to unescape: %v", err)) // If in a single quoted string, look for the end of string
// ignoring delimiters.
if data[0] == '\'' {
if i > 0 && data[i] == '\'' && data[i-1] != '\\' {
// Return quoted string
return i + 1, data[:i+1], nil
} }
fieldName = newS continue
if schema.AdditionalProperties != nil { }
validFieldPath = validFieldPath.Key(fieldName) switch data[i] {
case '.', '[', ']': // delimiters
if i == 0 {
// Return the delimiter.
return 1, data[:1], nil
} else { } else {
validFieldPath = validFieldPath.Child(fieldName) // Return identifier leading up to the delimiter.
// The next call to split will return the delimiter.
return i, data[:i], nil
} }
} else {
return pathOfFieldPath, field.Invalid(pathOfFieldPath, fieldPath, "unexpected format of fieldName: "+fieldName)
}
} else if tok != scanner.Ident {
return pathOfFieldPath, invalidFieldError
} else {
if schema.AdditionalProperties != nil {
validFieldPath = validFieldPath.Key(fieldName)
} else {
validFieldPath = validFieldPath.Child(fieldName)
} }
} }
if atEOF {
// Return the string.
return len(data), data, nil
}
}
return 0, nil, nil
})
var tok string
var isNamed bool
for scanner.Scan() {
tok = scanner.Text()
switch tok {
case "[":
if !scanner.Scan() {
return nil, fmt.Errorf("unexpected end of JSON path")
}
tok = scanner.Text()
if len(tok) < 2 || tok[0] != '\'' || tok[len(tok)-1] != '\'' {
return nil, fmt.Errorf("expected single quoted string but got %s", tok)
}
unescaped, err := unescapeSingleQuote(tok[1 : len(tok)-1])
if err != nil {
return nil, fmt.Errorf("invalid string literal: %v", err)
}
if schema.Properties != nil { if schema.Properties != nil {
propertySchema, ok := schema.Properties[fieldName] isNamed = true
if ok {
schema = &propertySchema
} else {
return pathOfFieldPath, invalidFieldError
}
} else if schema.AdditionalProperties != nil { } else if schema.AdditionalProperties != nil {
schema = schema.AdditionalProperties.Structural isNamed = false
} else { } else {
return pathOfFieldPath, invalidFieldError return nil, fmt.Errorf("does not refer to a valid field")
} }
if err := appendToPath(unescaped, isNamed); err != nil {
if isMapSyntax { return nil, err
if tok == scanner.EOF { }
return pathOfFieldPath, field.Invalid(pathOfFieldPath, fieldPath, "unexpected end of JSON path") if !scanner.Scan() {
} return nil, fmt.Errorf("unexpected end of JSON path")
s.Scan() }
if s.TokenText() != "]" { tok = scanner.Text()
return pathOfFieldPath, field.Invalid(pathOfFieldPath, fieldPath, "expect ] but get: "+s.TokenText()) if tok != "]" {
} return nil, fmt.Errorf("expected ] but got %s", tok)
}
case ".":
if !scanner.Scan() {
return nil, fmt.Errorf("unexpected end of JSON path")
}
tok = scanner.Text()
if schema.Properties != nil {
isNamed = true
} else if schema.AdditionalProperties != nil {
isNamed = false
} else {
return nil, fmt.Errorf("does not refer to a valid field")
}
if err := appendToPath(tok, isNamed); err != nil {
return nil, err
} }
default: default:
return pathOfFieldPath, invalidFieldError return nil, fmt.Errorf("expected [ or . but got: %s", tok)
} }
} }
if !found {
return pathOfFieldPath, invalidFieldError
}
return validFieldPath, nil return validFieldPath, nil
} }

View File

@ -2634,7 +2634,7 @@ func TestReasonAndFldPath(t *testing.T) {
}, },
}, },
schema: withRulePtr(objectTypePtr(map[string]schema.Structural{ schema: withRulePtr(objectTypePtr(map[string]schema.Structural{
"f": withReasonAndFldPath(objectType(map[string]schema.Structural{"m": integerType}), "self.m == 2", ". m", forbiddenReason), "f": withReasonAndFldPath(objectType(map[string]schema.Structural{"m": integerType}), "self.m == 2", ".m", forbiddenReason),
}), "1 == 1"), }), "1 == 1"),
errorType: field.ErrorTypeForbidden, errorType: field.ErrorTypeForbidden,
errors: []string{"root.f.m: Forbidden"}, errors: []string{"root.f.m: Forbidden"},
@ -2701,6 +2701,16 @@ func TestValidateFieldPath(t *testing.T) {
}, },
}, },
}, },
"white space": {
Generic: schema.Generic{
Type: "number",
},
},
"'foo'bar": {
Generic: schema.Generic{
Type: "number",
},
},
"a": { "a": {
Generic: schema.Generic{ Generic: schema.Generic{
Type: "object", Type: "object",
@ -2716,6 +2726,11 @@ func TestValidateFieldPath(t *testing.T) {
Type: "number", Type: "number",
}, },
}, },
"bb[b": {
Generic: schema.Generic{
Type: "number",
},
},
"bbb": { "bbb": {
Generic: schema.Generic{ Generic: schema.Generic{
Type: "object", Type: "object",
@ -2779,39 +2794,89 @@ func TestValidateFieldPath(t *testing.T) {
fieldPath string fieldPath string
pathOfFieldPath *field.Path pathOfFieldPath *field.Path
schema *schema.Structural schema *schema.Structural
error validationMatch errDetail string
validFieldPath *field.Path validFieldPath *field.Path
}{ }{
{ {
name: "Valid .a", name: "Valid .a",
fieldPath: ". a ", fieldPath: ".a",
pathOfFieldPath: path, pathOfFieldPath: path,
schema: &sts, schema: &sts,
error: validationMatch{},
validFieldPath: path.Child("a"), validFieldPath: path.Child("a"),
}, },
{ {
name: "Valid .a.bbb", name: "Valid 'foo'bar",
fieldPath: ". a. bbb", fieldPath: "['\\'foo\\'bar']",
pathOfFieldPath: path, pathOfFieldPath: path,
schema: &sts, schema: &sts,
error: validationMatch{}, validFieldPath: path.Child("'foo'bar"),
validFieldPath: path.Child("a", "bbb"), },
{
name: "Invalid 'foo'bar",
fieldPath: ".\\'foo\\'bar",
pathOfFieldPath: path,
schema: &sts,
errDetail: "does not refer to a valid field",
},
{
name: "Invalid with whitespace",
fieldPath: ". a",
pathOfFieldPath: path,
schema: &sts,
errDetail: "does not refer to a valid field",
},
{
name: "Valid with whitespace inside field",
fieldPath: ".white space",
pathOfFieldPath: path,
schema: &sts,
},
{
name: "Valid with whitespace inside field",
fieldPath: "['white space']",
pathOfFieldPath: path,
schema: &sts,
},
{
name: "invalid dot annotation",
fieldPath: ".a.bb[b",
pathOfFieldPath: path,
schema: &sts,
errDetail: "does not refer to a valid field",
},
{
name: "valid with .",
fieldPath: ".a['bbb.c']",
pathOfFieldPath: path,
schema: &sts,
validFieldPath: path.Child("a", "bbb.c"),
},
{
name: "Unclosed ]",
fieldPath: ".a['bbb.c'",
pathOfFieldPath: path,
schema: &sts,
errDetail: "unexpected end of JSON path",
},
{
name: "Unexpected end of JSON path",
fieldPath: ".",
pathOfFieldPath: path,
schema: &sts,
errDetail: "unexpected end of JSON path",
}, },
{ {
name: "Valid map syntax .a.bbb", name: "Valid map syntax .a.bbb",
fieldPath: ".a['bbb']", fieldPath: ".a['bbb.c']",
pathOfFieldPath: path, pathOfFieldPath: path,
schema: &sts, schema: &sts,
error: validationMatch{}, validFieldPath: path.Child("a").Child("bbb.c"),
validFieldPath: path.Child("a").Child("bbb"),
}, },
{ {
name: "Valid map key", name: "Valid map key",
fieldPath: ".foo.subAdd", fieldPath: ".foo.subAdd",
pathOfFieldPath: path, pathOfFieldPath: path,
schema: &sts, schema: &sts,
error: validationMatch{},
validFieldPath: path.Child("foo").Key("subAdd"), validFieldPath: path.Child("foo").Key("subAdd"),
}, },
{ {
@ -2819,7 +2884,6 @@ func TestValidateFieldPath(t *testing.T) {
fieldPath: ".foo['subAdd']", fieldPath: ".foo['subAdd']",
pathOfFieldPath: path, pathOfFieldPath: path,
schema: &sts, schema: &sts,
error: validationMatch{},
validFieldPath: path.Child("foo").Key("subAdd"), validFieldPath: path.Child("foo").Key("subAdd"),
}, },
{ {
@ -2827,14 +2891,12 @@ func TestValidateFieldPath(t *testing.T) {
fieldPath: ".a.foo's", fieldPath: ".a.foo's",
pathOfFieldPath: path, pathOfFieldPath: path,
schema: &sts, schema: &sts,
error: invalid(path.String()),
}, },
{ {
name: "Escaping", name: "Escaping",
fieldPath: ".a['foo\\'s']", fieldPath: ".a['foo\\'s']",
pathOfFieldPath: path, pathOfFieldPath: path,
schema: &sts, schema: &sts,
error: validationMatch{},
validFieldPath: path.Child("a").Child("foo's"), validFieldPath: path.Child("a").Child("foo's"),
}, },
{ {
@ -2842,7 +2904,6 @@ func TestValidateFieldPath(t *testing.T) {
fieldPath: ".a['test\\a']", fieldPath: ".a['test\\a']",
pathOfFieldPath: path, pathOfFieldPath: path,
schema: &sts, schema: &sts,
error: validationMatch{},
validFieldPath: path.Child("a").Child("test\a"), validFieldPath: path.Child("a").Child("test\a"),
}, },
@ -2851,35 +2912,33 @@ func TestValidateFieldPath(t *testing.T) {
fieldPath: ".a.foo", fieldPath: ".a.foo",
pathOfFieldPath: path, pathOfFieldPath: path,
schema: &sts, schema: &sts,
error: invalid(path.String()), errDetail: "does not refer to a valid field",
}, },
{ {
name: "Malformed map key", name: "Malformed map key",
fieldPath: ".a.bbb[0]", fieldPath: ".a.bbb[0]",
pathOfFieldPath: path, pathOfFieldPath: path,
schema: &sts, schema: &sts,
error: invalid(path.String()), errDetail: "expected single quoted string but got 0",
}, },
{ {
name: "Invalid refer for special map key", name: "Valid refer for name has number",
fieldPath: ".a.bbb.34", fieldPath: ".a.bbb.34",
pathOfFieldPath: path, pathOfFieldPath: path,
schema: &sts, schema: &sts,
error: invalid(path.String()),
}, },
{ {
name: "Map syntax for special field names", name: "Map syntax for special field names",
fieldPath: ".a.bbb['34']", fieldPath: ".a.bbb['34']",
pathOfFieldPath: path, pathOfFieldPath: path,
schema: &sts, schema: &sts,
error: validationMatch{}, //errDetail: "does not refer to a valid field",
}, },
{ {
name: "Valid .list", name: "Valid .list",
fieldPath: ". list ", fieldPath: ".list",
pathOfFieldPath: path, pathOfFieldPath: path,
schema: &sts, schema: &sts,
error: validationMatch{},
validFieldPath: path.Child("list"), validFieldPath: path.Child("list"),
}, },
{ {
@ -2887,94 +2946,72 @@ func TestValidateFieldPath(t *testing.T) {
fieldPath: ".list[0]", fieldPath: ".list[0]",
pathOfFieldPath: path, pathOfFieldPath: path,
schema: &sts, schema: &sts,
error: invalid(path.String()), errDetail: "expected single quoted string but got 0",
}, },
{ {
name: "Invalid list reference", name: "Invalid list reference",
fieldPath: ".list. a", fieldPath: ".list. a",
pathOfFieldPath: path, pathOfFieldPath: path,
schema: &sts, schema: &sts,
error: invalid(path.String()), errDetail: "does not refer to a valid field",
}, },
{ {
name: "Invalid .list.a", name: "Invalid .list.a",
fieldPath: ".list['a']", fieldPath: ".list['a']",
pathOfFieldPath: path, pathOfFieldPath: path,
schema: &sts, schema: &sts,
error: invalid(path.String()), errDetail: "does not refer to a valid field",
}, },
{ {
name: "Missing leading dot", name: "Missing leading dot",
fieldPath: "a", fieldPath: "a",
pathOfFieldPath: path, pathOfFieldPath: path,
schema: &sts, schema: &sts,
error: invalid(path.String()), errDetail: "expected [ or . but got: a",
}, },
{ {
name: "Nonexistent field", name: "Nonexistent field",
fieldPath: ".c", fieldPath: ".c",
pathOfFieldPath: path, pathOfFieldPath: path,
schema: &sts, schema: &sts,
error: invalid(path.String()), errDetail: "does not refer to a valid field",
}, },
{ {
name: "Duplicate dots", name: "Duplicate dots",
fieldPath: ".a..b", fieldPath: ".a..b",
pathOfFieldPath: path, pathOfFieldPath: path,
schema: &sts, schema: &sts,
error: invalid(path.String()), errDetail: "does not refer to a valid field",
}, },
{ {
name: "object of array", name: "object of array",
fieldPath: ".list.a-b.34", fieldPath: ".list.a-b.34",
pathOfFieldPath: path, pathOfFieldPath: path,
schema: &sts, schema: &sts,
error: invalid(path.String()), errDetail: "does not refer to a valid field",
}, },
} }
for _, tc := range tests { for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
validField, err := ValidFieldPath(tc.fieldPath, tc.pathOfFieldPath, tc.schema) validField, err := ValidFieldPath(tc.fieldPath, tc.schema)
if err == nil && !tc.error.empty() { if err == nil && tc.errDetail != "" {
t.Errorf("expected %v at %v but got nil", tc.error.errorType, tc.error.path.String()) t.Errorf("expected err contains: %v but get nil", tc.errDetail)
} else if err != nil && tc.error.empty() {
t.Errorf("unexpected error: %v at %v", tc.error.errorType, tc.error.path.String())
} else if !tc.error.matches(err) {
t.Errorf("expected %v at %v, got %v", tc.error.errorType, tc.error.path.String(), err)
} }
if tc.validFieldPath != nil && tc.validFieldPath.String() != validField.String() { if err != nil && tc.errDetail == "" {
t.Errorf("expected %v, got %v", tc.validFieldPath, validField) t.Errorf("unexpected error: %v", err)
}
if err != nil && !strings.Contains(err.Error(), tc.errDetail) {
t.Errorf("expected error to contain: %v, but get: %v", tc.errDetail, err)
}
if tc.validFieldPath != nil && tc.validFieldPath.String() != path.Child(validField.String()).String() {
t.Errorf("expected %v, got %v", tc.validFieldPath, path.Child(validField.String()))
} }
}) })
} }
} }
type validationMatch struct {
path *field.Path
errorType field.ErrorType
contains string
}
func invalid(path ...string) validationMatch {
return validationMatch{path: field.NewPath(path[0], path[1:]...), errorType: field.ErrorTypeInvalid}
}
func (v validationMatch) matches(err *field.Error) bool {
if err == nil && v.empty() {
return true
}
return err.Type == v.errorType && err.Field == v.path.String() && strings.Contains(err.Error(), v.contains)
}
func (v validationMatch) empty() bool {
if v.path == nil && v.errorType == "" && v.contains == "" {
return true
}
return false
}
func genString(n int, c rune) string { func genString(n int, c rune) string {
b := strings.Builder{} b := strings.Builder{}
for i := 0; i < n; i++ { for i := 0; i < n; i++ {