1
0
mirror of https://github.com/rancher/steve.git synced 2025-08-19 14:47:36 +00:00
steve/pkg/stores/sqlpartition/queryparser/selector_test.go
Eric Promislow 18ac23afe7
Support quoting values in queries. (#601)
* Support quoting values in queries.

* Correct test: 'informer' => 'sqltypes', and update description.

* Enable backslash-escaping of quoted strings.
2025-05-13 11:40:54 -07:00

435 lines
12 KiB
Go

/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/*
This file is derived from
https://github.com/kubernetes/apimachinery/blob/master/pkg/labels/selector_test.go
*/
package queryparser
import (
"fmt"
"reflect"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/rancher/steve/pkg/stores/sqlpartition/selection"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/validation/field"
)
var (
ignoreDetail = cmpopts.IgnoreFields(field.Error{}, "Detail")
)
func TestSelectorParse(t *testing.T) {
testGoodStrings := []string{
"x=a,y=b,z=c",
"",
"x!=a,y=b",
"close ~ value",
"notclose !~ value",
"x>1",
"x>1,z<5",
"x gt 1,z lt 5",
`y == def`,
"metadata.labels.im-here",
"!metadata.labels.im-not-here",
"metadata.labels[im.here]",
"!metadata.labels[im.not.here]",
"metadata.labels[k8s.io/meta-stuff] ~ has-dashes_underscores.dots.only",
`metadata.labels[k8s.io/meta-stuff] ~ "m!a@t#c$h%e^v&e*r(y)t-_i=n+g)t{o[$]c}o]m|m\\a:;'<.>"`,
`x="double quotes ok"`,
`x='single quotes ok'`,
`x="double quotes with \\ and \" ok"`,
`x='single quotes with \\ and \' ok'`,
}
testBadStrings := []string{
"!no-label-absence-test",
"no-label-presence-test",
"x=a||y=b",
"x==a==b",
"!x=a",
"x<a",
"x=",
"x= ",
"x=,z= ",
"x= ,z= ",
"x ~",
"x !~",
"~ val",
"!~ val",
"= val",
"== val",
"metadata.labels-im.here",
"metadata.labels[missing/close-bracket",
"!metadata.labels(im.not.here)",
}
for _, test := range testGoodStrings {
_, err := Parse(test)
if err != nil {
t.Errorf("%v: error %v (%#v)\n", test, err, err)
}
}
for _, test := range testBadStrings {
_, err := Parse(test)
if err == nil {
t.Errorf("%v: did not get expected error\n", test)
}
}
}
func TestLexer(t *testing.T) {
testcases := []struct {
s string
t Token
}{
{"", EndOfStringToken},
{",", CommaToken},
{"notin", NotInToken},
{"in", InToken},
{"=", EqualsToken},
{"==", DoubleEqualsToken},
{">", GreaterThanToken},
{"<", LessThanToken},
//Note that Lex returns the longest valid token found
{"!", DoesNotExistToken},
{"!=", NotEqualsToken},
{"(", OpenParToken},
{")", ClosedParToken},
{`'sq string'`, QuotedStringToken},
{`"dq string"`, QuotedStringToken},
{"~", PartialEqualsToken},
{"!~", NotPartialEqualsToken},
{"||", ErrorToken},
{`"double-quoted string"`, QuotedStringToken},
{`'single-quoted string'`, QuotedStringToken},
}
for _, v := range testcases {
l := &Lexer{s: v.s, pos: 0}
token, lit := l.Lex()
if token != v.t {
t.Errorf("Got %d it should be %d for '%s'", token, v.t, v.s)
}
if v.t != ErrorToken {
exp := v.s
if v.t == QuotedStringToken {
exp = exp[1 : len(exp)-1]
}
if lit != exp {
t.Errorf("Got '%s' it should be '%s'", lit, exp)
}
}
}
}
func min(l, r int) (m int) {
m = r
if l < r {
m = l
}
return m
}
func TestLexerSequence(t *testing.T) {
testcases := []struct {
s string
t []Token
}{
{"key in ( value )", []Token{IdentifierToken, InToken, OpenParToken, IdentifierToken, ClosedParToken}},
{"key notin ( value )", []Token{IdentifierToken, NotInToken, OpenParToken, IdentifierToken, ClosedParToken}},
{"key in ( value1, value2 )", []Token{IdentifierToken, InToken, OpenParToken, IdentifierToken, CommaToken, IdentifierToken, ClosedParToken}},
{"key", []Token{IdentifierToken}},
{"!key", []Token{DoesNotExistToken, IdentifierToken}},
{"()", []Token{OpenParToken, ClosedParToken}},
{"x in (),y", []Token{IdentifierToken, InToken, OpenParToken, ClosedParToken, CommaToken, IdentifierToken}},
{"== != (), = notin", []Token{DoubleEqualsToken, NotEqualsToken, OpenParToken, ClosedParToken, CommaToken, EqualsToken, NotInToken}},
{"key>2", []Token{IdentifierToken, GreaterThanToken, IdentifierToken}},
{"key<1", []Token{IdentifierToken, LessThanToken, IdentifierToken}},
{"key gt 3", []Token{IdentifierToken, IdentifierToken, IdentifierToken}},
{"key lt 4", []Token{IdentifierToken, IdentifierToken, IdentifierToken}},
{`key = 'sqs'`, []Token{IdentifierToken, EqualsToken, QuotedStringToken}},
{`key = "dqs"`, []Token{IdentifierToken, EqualsToken, QuotedStringToken}},
{"key=value", []Token{IdentifierToken, EqualsToken, IdentifierToken}},
{"key == value", []Token{IdentifierToken, DoubleEqualsToken, IdentifierToken}},
{"key ~ value", []Token{IdentifierToken, PartialEqualsToken, IdentifierToken}},
{"key~ value", []Token{IdentifierToken, PartialEqualsToken, IdentifierToken}},
{"key ~value", []Token{IdentifierToken, PartialEqualsToken, IdentifierToken}},
{"key~value", []Token{IdentifierToken, PartialEqualsToken, IdentifierToken}},
{"key !~ value", []Token{IdentifierToken, NotPartialEqualsToken, IdentifierToken}},
{"key!~ value", []Token{IdentifierToken, NotPartialEqualsToken, IdentifierToken}},
{"key !~value", []Token{IdentifierToken, NotPartialEqualsToken, IdentifierToken}},
{"key!~value", []Token{IdentifierToken, NotPartialEqualsToken, IdentifierToken}},
}
for _, v := range testcases {
var tokens []Token
l := &Lexer{s: v.s, pos: 0}
for {
token, _ := l.Lex()
if token == EndOfStringToken {
break
}
tokens = append(tokens, token)
}
if len(tokens) != len(v.t) {
t.Errorf("Bad number of tokens for '%s': got %d, wanted %d (got %v)", v.s, len(tokens), len(v.t), tokens)
}
for i := 0; i < min(len(tokens), len(v.t)); i++ {
if tokens[i] != v.t[i] {
t.Errorf("Test '%s': Mismatching in token type found '%v' it should be '%v'", v.s, tokens[i], v.t[i])
}
}
}
}
func TestParserLookahead(t *testing.T) {
testcases := []struct {
s string
t []Token
}{
{"key in ( value )", []Token{IdentifierToken, InToken, OpenParToken, IdentifierToken, ClosedParToken, EndOfStringToken}},
{"key notin ( value )", []Token{IdentifierToken, NotInToken, OpenParToken, IdentifierToken, ClosedParToken, EndOfStringToken}},
{"key in ( value1, value2 )", []Token{IdentifierToken, InToken, OpenParToken, IdentifierToken, CommaToken, IdentifierToken, ClosedParToken, EndOfStringToken}},
{"key", []Token{IdentifierToken, EndOfStringToken}},
{"!key", []Token{DoesNotExistToken, IdentifierToken, EndOfStringToken}},
{"()", []Token{OpenParToken, ClosedParToken, EndOfStringToken}},
{"", []Token{EndOfStringToken}},
{"x in (),y", []Token{IdentifierToken, InToken, OpenParToken, ClosedParToken, CommaToken, IdentifierToken, EndOfStringToken}},
{"== != (), = notin", []Token{DoubleEqualsToken, NotEqualsToken, OpenParToken, ClosedParToken, CommaToken, EqualsToken, NotInToken, EndOfStringToken}},
{"key>2", []Token{IdentifierToken, GreaterThanToken, IdentifierToken, EndOfStringToken}},
{"key<1", []Token{IdentifierToken, LessThanToken, IdentifierToken, EndOfStringToken}},
{"key gt 3", []Token{IdentifierToken, IdentifierToken, IdentifierToken, EndOfStringToken}},
{"key lt 4", []Token{IdentifierToken, IdentifierToken, IdentifierToken, EndOfStringToken}},
{`key = "multi-word-string"`, []Token{IdentifierToken, EqualsToken, QuotedStringToken, EndOfStringToken}},
}
for _, v := range testcases {
p := &Parser{l: &Lexer{s: v.s, pos: 0}, position: 0}
p.scan()
if len(p.scannedItems) != len(v.t) {
t.Errorf("Expected %d items for test %s, found %d", len(v.t), v.s, len(p.scannedItems))
}
for i, entry := range v.t {
token, _ := p.consume(KeyAndOperator)
if token == EndOfStringToken {
if i != len(v.t)-1 {
t.Errorf("Expected end of string token at position %d for test '%s', but length is %d", i, v.s, len(v.t))
}
break
}
if token != entry {
t.Errorf("Expected token %v at position %d for test '%s', but got %v", entry, i, v.s, token)
}
}
}
}
func TestParseOperator(t *testing.T) {
testcases := []struct {
token string
expectedError error
}{
{"in", nil},
{"=", nil},
{"==", nil},
{"~", nil},
{">", nil},
{"<", nil},
{"lt", nil},
{"gt", nil},
{"notin", nil},
{"!=", nil},
{"!~", nil},
{"!", fmt.Errorf("found '%s', expected: %v", selection.DoesNotExist, strings.Join(binaryOperators, ", "))},
{"exists", fmt.Errorf("found '%s', expected: %v", selection.Exists, strings.Join(binaryOperators, ", "))},
{"(", fmt.Errorf("found '%s', expected: %v", "(", strings.Join(binaryOperators, ", "))},
}
for _, testcase := range testcases {
p := &Parser{l: &Lexer{s: testcase.token, pos: 0}, position: 0}
p.scan()
_, err := p.parseOperator()
if ok := reflect.DeepEqual(testcase.expectedError, err); !ok {
t.Errorf("\nexpect err [%v], \nactual err [%v]", testcase.expectedError, err)
}
}
}
// Some error fields are commented out here because this fork no longer
// enforces k8s label expression lexical and length restrictions
func TestRequirementConstructor(t *testing.T) {
requirementConstructorTests := []struct {
Key string
Op selection.Operator
Vals sets.String
WantErr field.ErrorList
}{
{
Key: "x1",
Op: selection.In,
WantErr: field.ErrorList{
&field.Error{
Type: field.ErrorTypeInvalid,
Field: "values",
BadValue: []string{},
},
},
},
{
Key: "x2",
Op: selection.NotIn,
Vals: sets.NewString(),
WantErr: field.ErrorList{
&field.Error{
Type: field.ErrorTypeInvalid,
Field: "values",
BadValue: []string{},
},
},
},
{
Key: "x3",
Op: selection.In,
Vals: sets.NewString("foo"),
},
{
Key: "x4",
Op: selection.NotIn,
Vals: sets.NewString("foo"),
},
{
Key: "x5",
Op: selection.Equals,
Vals: sets.NewString("foo", "bar"),
WantErr: field.ErrorList{
&field.Error{
Type: field.ErrorTypeInvalid,
Field: "values",
BadValue: []string{"bar", "foo"},
},
},
},
{
Key: "x6",
Op: selection.Exists,
},
{
Key: "x7",
Op: selection.DoesNotExist,
},
{
Key: "x8",
Op: selection.Exists,
Vals: sets.NewString("foo"),
WantErr: field.ErrorList{
&field.Error{
Type: field.ErrorTypeInvalid,
Field: "values",
BadValue: []string{"foo"},
},
},
},
{
Key: "x9",
Op: selection.In,
Vals: sets.NewString("bar"),
},
{
Key: "x10",
Op: selection.In,
Vals: sets.NewString("bar"),
},
{
Key: "x11",
Op: selection.GreaterThan,
Vals: sets.NewString("1"),
},
{
Key: "x12",
Op: selection.LessThan,
Vals: sets.NewString("6"),
},
{
Key: "x13",
Op: selection.GreaterThan,
WantErr: field.ErrorList{
&field.Error{
Type: field.ErrorTypeInvalid,
Field: "values",
BadValue: []string{},
},
},
},
{
Key: "x14",
Op: selection.GreaterThan,
Vals: sets.NewString("bar"),
WantErr: field.ErrorList{
&field.Error{
Type: field.ErrorTypeInvalid,
Field: "values[0]",
BadValue: "bar",
},
},
},
{
Key: "x15",
Op: selection.LessThan,
Vals: sets.NewString("bar"),
WantErr: field.ErrorList{
&field.Error{
Type: field.ErrorTypeInvalid,
Field: "values[0]",
BadValue: "bar",
},
},
},
{
Key: strings.Repeat("a", 254), //breaks DNS rule that len(key) <= 253
Op: selection.Exists,
},
{
Key: "x16",
Op: selection.Equals,
Vals: sets.NewString(strings.Repeat("a", 254)),
},
{
Key: "x17",
Op: selection.Equals,
Vals: sets.NewString("a b"),
},
{
Key: "x18",
Op: "unsupportedOp",
WantErr: field.ErrorList{
&field.Error{
Type: field.ErrorTypeNotSupported,
Field: "operator",
BadValue: selection.Operator("unsupportedOp"),
},
},
},
}
for _, rc := range requirementConstructorTests {
_, err := NewRequirement(rc.Key, rc.Op, rc.Vals.List())
if diff := cmp.Diff(rc.WantErr.ToAggregate(), err, ignoreDetail); diff != "" {
t.Errorf("NewRequirement test %v returned unexpected error (-want,+got):\n%s", rc.Key, diff)
}
}
}