blob: 6a34091f1dd2bb2b93fb0e98bdea3902047a74b4 [file] [log] [blame] [edit]
package query
import (
"net/url"
"reflect"
"regexp"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.skia.org/infra/go/deepequal/assertdeep"
"go.skia.org/infra/go/paramtools"
)
func TestValidateKey(t *testing.T) {
testCases := []struct {
key string
valid bool
reason string
}{
{
key: ",arch=x86,config=565,",
valid: true,
reason: "",
},
{
key: ",arch=x86,con-fig=5-65,",
valid: true,
reason: "",
},
{
key: ",arch=x86,config=5 65,",
valid: true,
reason: "",
},
{
key: "arch=x86,config=565,",
valid: false,
reason: "No comma at beginning.",
},
{
key: ",arch=x86,config=565",
valid: false,
reason: "No comma at end.",
},
{
key: ",arch=x86,",
valid: true,
reason: "Short is fine.",
},
{
key: "",
valid: false,
reason: "Empty is invalid.",
},
{
key: ",config=565,arch=x86,",
valid: false,
reason: "Unsorted.",
},
{
key: ",config=565,config=8888,",
valid: false,
reason: "Duplicate param names.",
},
{
key: ",arch=x85,config=565,config=8888,",
valid: false,
reason: "Duplicate param names.",
},
{
key: ",arch=x85,archer=x85,",
valid: true,
reason: "Param name prefix of another param name.",
},
{
key: ",arch=x85,arch,er=x85,",
valid: false,
reason: "No comma in param key.",
},
{
key: ",arch=x85,archer=x,85,",
valid: false,
reason: "No comma in param value.",
},
{
key: ",arch=x85,arch=er=x85,",
valid: false,
reason: "No equal in param key.",
},
{
key: ",arch=x85,archer=x=85,",
valid: false,
reason: "No equal in param value.",
},
{
key: ",arch=x85,archer=;x85,",
valid: false,
reason: "Risk of SQL injection attack.",
},
{
key: ",arch=x85,archer=x85~@#$%^&*()+ :,",
valid: true,
reason: "Special chars are allowed by the validation.",
},
{
key: ",,",
valid: false,
reason: "Degenerate case.",
},
{
key: ",browser=chrome,browser-version=106.0.5196.0,channel=canary,sub-test=date-format-xparb-SP,test=JetStream,type=sub-test,value=score,version=2,",
valid: true,
reason: "Only check sort order on the key values, not on key=value, which breaks on this case between 'browser=' and 'browser-'",
},
}
for _, tc := range testCases {
if got, want := IsValid(tc.key), tc.valid; got != want {
t.Errorf("Failed validation for %q. Got %v Want %v. %s", tc.key, got, want, tc.reason)
}
}
}
func TestMakeKey(t *testing.T) {
testCases := []struct {
m map[string]string
key string
valid bool
reason string
}{
{
m: map[string]string{"arch": "x86", "config": "565"},
key: ",arch=x86,config=565,",
valid: true,
reason: "",
},
{
m: map[string]string{"arch": "x86", "archer": "x86"},
key: ",arch=x86,archer=x86,",
valid: true,
reason: "",
},
{
m: map[string]string{"bad,key": "x86"},
key: "",
valid: false,
reason: "Bad key with comma.",
},
{
m: map[string]string{"bad=key": "x86"},
key: "",
valid: false,
reason: "Bad key with equal.",
},
{
m: map[string]string{"bad;key": "x86"},
key: "",
valid: false,
reason: "Bad key with semicolon.",
},
{
m: map[string]string{"key": "bad,value"},
key: "",
valid: false,
reason: "Bad value with comma.",
},
{
m: map[string]string{"key": "bad=value"},
key: "",
valid: false,
reason: "Bad value with equal.",
},
{
m: map[string]string{"key": "bad;value"},
key: "",
valid: false,
reason: "Bad value with semicolon.",
},
{
m: map[string]string{"key~@#$%^&*()+ :": "value~@#$%^&*()+ :"},
key: ",key~@#$%^&*()+ :=value~@#$%^&*()+ :,",
valid: true,
reason: "Special chars in key and value.",
},
{
m: map[string]string{},
key: "",
valid: false,
reason: "Empty map is invalid.",
},
}
for _, tc := range testCases {
key, err := MakeKey(tc.m)
if tc.valid && err != nil {
t.Errorf("Failed to make key for %#v: %s", tc.m, err)
}
if key != tc.key {
t.Errorf("Failed to make key for %#v. Got %q Want %q. %s", tc.m, key, tc.key, tc.reason)
}
}
}
func TestNew(t *testing.T) {
q, err := New(url.Values{"config": []string{"565", "8888"}})
assert.NoError(t, err)
assert.Equal(t, 1, len(q.params))
assert.Equal(t, false, q.params[0].isWildCard)
q, err = NewFromString("config=565&config=8888")
assert.NoError(t, err)
assert.Equal(t, 1, len(q.params))
assert.Equal(t, false, q.params[0].isWildCard)
q, err = NewFromString("config=%ZZ")
assert.Error(t, err, "Invalid query strings are caught.")
q, err = New(url.Values{"debug": []string{"false"}, "config": []string{"565", "8888"}})
assert.NoError(t, err)
assert.Equal(t, 2, len(q.params))
assert.Equal(t, ",config=", q.params[0].keyMatch)
assert.Equal(t, ",debug=", q.params[1].keyMatch)
assert.Equal(t, false, q.params[0].isWildCard)
assert.Equal(t, false, q.params[1].isWildCard)
assert.Equal(t, false, q.params[0].isNegative)
assert.Equal(t, false, q.params[1].isNegative)
q, err = New(url.Values{"debug": []string{"*"}})
assert.NoError(t, err)
assert.Equal(t, 1, len(q.params))
assert.Equal(t, ",debug=", q.params[0].keyMatch)
assert.Equal(t, true, q.params[0].isWildCard)
assert.Equal(t, false, q.params[0].isNegative)
q, err = New(url.Values{"config": []string{"!565"}})
assert.NoError(t, err)
assert.Equal(t, 1, len(q.params))
assert.Equal(t, ",config=", q.params[0].keyMatch)
assert.Equal(t, false, q.params[0].isWildCard)
assert.Equal(t, true, q.params[0].isNegative)
q, err = New(url.Values{"config": []string{"!565", "!8888"}, "debug": []string{"*"}})
assert.NoError(t, err)
assert.Equal(t, 2, len(q.params))
assert.Equal(t, ",config=", q.params[0].keyMatch)
assert.Equal(t, "565", q.params[0].values[0])
assert.Equal(t, "8888", q.params[0].values[1])
assert.Equal(t, ",debug=", q.params[1].keyMatch)
assert.Equal(t, false, q.params[0].isWildCard)
assert.Equal(t, true, q.params[0].isNegative)
assert.Equal(t, true, q.params[1].isWildCard)
assert.Equal(t, false, q.params[1].isNegative)
q, err = New(url.Values{})
assert.NoError(t, err)
assert.Equal(t, 0, len(q.params))
}
func TestMatches(t *testing.T) {
testCases := []struct {
key string
query url.Values
matches bool
reason string
}{
{
key: ",arch=x86,config=565,debug=true,",
query: url.Values{},
matches: true,
reason: "Empty query matches everything.",
},
{
key: ",arch=x86,config=565,debug=true,",
query: url.Values{"config": []string{"565"}},
matches: true,
reason: "Simple match",
},
{
key: ",arch=x86,config=565,debug=true,",
query: url.Values{"config": []string{"8888"}},
matches: false,
reason: "Simple miss",
},
{
key: ",arch=x86,config=565,debug=true,",
query: url.Values{"config": []string{"565", "8888"}},
matches: true,
reason: "Simple OR",
},
{
key: ",arch=x86,config=565,debug=true,",
query: url.Values{"config": []string{"8888", "565"}},
matches: true,
reason: "Simple OR 2",
},
{
key: ",arch=x86,config=565,debug=true,",
query: url.Values{"config": []string{"565"}, "debug": []string{"true"}},
matches: true,
reason: "Simple AND",
},
{
key: ",arch=x86,config=565,debug=true,",
query: url.Values{"config": []string{"565"}, "debug": []string{"false"}},
matches: false,
reason: "Simple AND miss",
},
{
key: ",arch=x86,config=565,debug=true,",
query: url.Values{"foo": []string{"bar"}},
matches: false,
reason: "Unknown param",
},
{
key: ",arch=x86,config=565,debug=true,",
query: url.Values{"arch": []string{"*"}},
matches: true,
reason: "Wildcard param value match",
},
{
key: ",arch=x86,config=565,debug=true,",
query: url.Values{"foo": []string{"*"}},
matches: false,
reason: "Wildcard param value missing",
},
{
key: ",arch=x86,config=565,debug=true,",
query: url.Values{"config": []string{"!565"}},
matches: false,
reason: "Negative miss",
},
{
key: ",arch=x86,config=8888,debug=true,",
query: url.Values{"config": []string{"!565"}},
matches: true,
reason: "Negative match",
},
{
key: ",arch=x86,config=8888,debug=true,",
query: url.Values{"config": []string{"!565", "!8888"}},
matches: false,
reason: "Negative multi miss",
},
{
key: ",arch=x86,config=8888,debug=true,",
query: url.Values{"config": []string{"!565"}, "debug": []string{"*"}},
matches: true,
reason: "Negative and wildcard",
},
{
key: ",arch=x86,config=8888,",
query: url.Values{"config": []string{"!565"}, "debug": []string{"*"}},
matches: false,
reason: "Negative and wildcard, miss wildcard.",
},
{
key: ",arch=x86,config=565,debug=true,",
query: url.Values{"config": []string{"!565"}, "debug": []string{"*"}},
matches: false,
reason: "Negative and wildcard, miss negative",
},
{
key: ",arch=x86,config=565,debug=true,",
query: url.Values{},
matches: true,
reason: "Empty query matches everything.",
},
{
key: ",arch=x86,config=565,debug=true,",
query: url.Values{"config": []string{"~5.5"}},
matches: true,
reason: "Regexp match",
},
{
key: ",arch=x86,config=565,debug=true,",
query: url.Values{"config": []string{"~8+"}},
matches: false,
reason: "Regexp match",
},
{
key: ",arch=x86,config=8888,debug=true,",
query: url.Values{"arch": []string{"~^x"}, "config": []string{"!565"}, "debug": []string{"*"}},
matches: true,
reason: "Negative, wildcard, and regexp",
},
{
key: ",arch=x86,config=8888,debug=true,",
query: url.Values{"arch": []string{"~^y.*"}, "config": []string{"!565"}, "debug": []string{"*"}},
matches: false,
reason: "Negative, wildcard, and miss regexp",
},
}
for _, tc := range testCases {
q, err := New(tc.query)
assert.NoError(t, err)
if got, want := q.Matches(tc.key), tc.matches; got != want {
t.Errorf("Failed matching %q to %#v. Got %v Want %v. %s", tc.key, tc.query, got, want, tc.reason)
}
}
}
func TestParseKey(t *testing.T) {
testCases := []struct {
key string
parsed map[string]string
hasError bool
reason string
}{
{
key: ",arch=x86,config=565,debug=true,",
parsed: map[string]string{
"arch": "x86",
"config": "565",
"debug": "true",
},
hasError: false,
reason: "Simple parse",
},
{
key: ",arch=x86,config=565,debug~@#$%^&*()+ :=true~@#$%^&*()+ :,",
parsed: map[string]string{
"arch": "x86",
"config": "565",
"debug~@#$%^&*()+ :": "true~@#$%^&*()+ :",
},
hasError: false,
reason: "Special chars are allowed",
},
{
key: ",config=565,arch=x86,",
parsed: map[string]string{},
hasError: true,
reason: "Unsorted",
},
{
key: ",,",
parsed: map[string]string{},
hasError: true,
reason: "Invalid regex",
},
{
key: "x/y",
parsed: map[string]string{},
hasError: true,
reason: "Invalid",
},
{
key: "",
parsed: map[string]string{},
hasError: true,
reason: "Empty string",
},
{
key: ",browser=chrome,browser-version=106.0.5196.0,channel=canary,sub-test=date-format-xparb-SP,test=JetStream,type=sub-test,value=score,version=2,",
parsed: map[string]string{
"browser": "chrome",
"browser-version": "106.0.5196.0",
"channel": "canary",
"sub-test": "date-format-xparb-SP",
"test": "JetStream",
"type": "sub-test",
"value": "score",
"version": "2",
},
hasError: false,
reason: "Only check sort order on the key values, not on key=value, which breaks on this case between 'browser=' and 'browser-'",
},
{
key: ",arc,h=x86,config=565,debug=true,",
parsed: map[string]string{},
hasError: true,
reason: "Comma in key",
},
{
key: ",arch=x,86,config=565,debug=true,",
parsed: map[string]string{},
hasError: true,
reason: "Comma in value",
},
{
key: ",arc=h=x86,config=565,debug=true,",
parsed: map[string]string{},
hasError: true,
reason: "Equal in key",
},
{
key: ",arch=x=86,config=565,debug=true,",
parsed: map[string]string{},
hasError: true,
reason: "Equal in value",
},
{
key: ",arc;h=x86,config=565,debug=true,",
parsed: map[string]string{},
hasError: true,
reason: "Semicolon in key",
},
{
key: ",arch=x;86,config=565,debug=true,",
parsed: map[string]string{},
hasError: true,
reason: "Semicolon in value",
},
}
for _, tc := range testCases {
p, err := ParseKey(tc.key)
if got, want := (err != nil), tc.hasError; got != want {
t.Errorf("Failed error status parsing %q, Got %v Want %v. %s", tc.key, got, want, tc.reason)
}
if err != nil {
continue
}
if got, want := p, tc.parsed; !reflect.DeepEqual(got, want) {
t.Errorf("Failed matching parsed values. Got %v Want %v. %s", got, want, tc.reason)
}
}
}
func TestParseKeyFast(t *testing.T) {
p, err := ParseKeyFast(",arch=x86,config=565,debug=true,")
assert.NoError(t, err)
assert.Equal(t, map[string]string{
"arch": "x86",
"config": "565",
"debug": "true",
}, p)
}
// Test a variety of inputs to make sure the code doesn't panic.
func TestParseKeyFastNoPanics(t *testing.T) {
testCases := []string{
",config=565,arch=x86,", // unsorted
",,",
"x/y",
"",
",",
"foo=bar",
",foo=bar",
",foo,",
",foo,bar,baz,",
",foo=,bar=,",
",=,",
",space=spaces ok here,",
}
for _, tc := range testCases {
_, _ = ParseKeyFast(tc)
}
}
func TestForceValue(t *testing.T) {
testCases := []struct {
input map[string]string
want map[string]string
}{
{
input: map[string]string{"arch": "x86", "config": "565"},
want: map[string]string{"arch": "x86", "config": "565"},
},
{
input: map[string]string{"arch": "x86", "con-fig": "5-65"},
want: map[string]string{"arch": "x86", "con-fig": "5-65"},
},
{
input: map[string]string{"arch": "x86", "config": "5 65"},
want: map[string]string{"arch": "x86", "config": "5_65"},
},
{
input: map[string]string{"arch": "x86", "config+v": "5+65"},
want: map[string]string{"arch": "x86", "config_v": "5_65"},
},
{
input: map[string]string{"arch": "x86", "config": ""},
want: map[string]string{"arch": "x86", "config": "_"},
},
{
input: map[string]string{"arch::this": "x!~@#$%^&*()86"},
want: map[string]string{"arch__this": "x___________86"},
},
{
input: map[string]string{},
want: map[string]string{},
},
}
for _, tc := range testCases {
if got, want := ForceValid(tc.input), tc.want; !reflect.DeepEqual(got, want) {
t.Errorf("Failed to force a map to be valid: Got %#v Want %#v", got, want)
}
}
}
func TestForceValueWithRegex(t *testing.T) {
testCases := []struct {
input map[string]string
want map[string]string
}{
{
input: map[string]string{"arch": "x86", "config": "565"},
want: map[string]string{"arch": "x86", "config": "565"},
},
{
input: map[string]string{"arch": "x86", "con-fig": "5-65"},
want: map[string]string{"arch": "x86", "con-fig": "5-65"},
},
{
input: map[string]string{"arch": "x86", "config": "5 65"},
want: map[string]string{"arch": "x86", "config": "5 65"},
},
{
input: map[string]string{"arch": "x86", "config+v": "5+65"},
want: map[string]string{"arch": "x86", "config+v": "5+65"},
},
{
input: map[string]string{"arch": "x86", "config": ""},
want: map[string]string{"arch": "x86", "config": "_"},
},
{
input: map[string]string{"arch::this": "x!~@#$%^&*()86"},
want: map[string]string{"arch__this": "x!~@#$%^&*()86"},
},
{
input: map[string]string{},
want: map[string]string{},
},
}
invalidCharRegex := regexp.MustCompile("([^a-zA-Z0-9!~@#$%^&*()+ \\._\\-])")
for _, tc := range testCases {
got := ForceValidWithRegex(tc.input, invalidCharRegex)
assertdeep.Equal(t, got, tc.want)
}
}
func TestQueryPlan(t *testing.T) {
rops := paramtools.ReadOnlyParamSet{
"config": []string{"8888", "565", "gpu"},
"arch": []string{"x86", "arm"},
"debug": []string{"true", "false"},
"foo": []string{"bar"},
}
testCases := []struct {
query url.Values
want paramtools.ParamSet
hasError bool
reason string
}{
{
query: url.Values{},
want: paramtools.ParamSet{},
reason: "Empty query matches everything.",
},
{
query: url.Values{"config": []string{"565"}},
want: paramtools.ParamSet{"config": []string{"565"}},
reason: "Simple",
},
{
query: url.Values{"config": []string{"565", "8888"}},
want: paramtools.ParamSet{"config": []string{"8888", "565"}},
reason: "Simple OR",
},
{
query: url.Values{"config": []string{"565"}, "debug": []string{"true"}},
want: paramtools.ParamSet{
"config": []string{"565"},
"debug": []string{"true"},
},
reason: "Simple AND",
},
{
query: url.Values{"fizz": []string{"buzz"}},
want: paramtools.ParamSet{},
reason: "Unknown param",
hasError: true,
},
{
query: url.Values{"config": []string{"buzz"}},
want: paramtools.ParamSet{},
hasError: true,
reason: "Unknown value",
},
{
query: url.Values{"arch": []string{"*"}},
want: paramtools.ParamSet{"arch": []string{"x86", "arm"}},
reason: "Wildcard param value match",
},
{
query: url.Values{"fizz": []string{"*"}},
want: paramtools.ParamSet{},
reason: "Wildcard param value missing",
hasError: true,
},
{
query: url.Values{"foo": []string{"*"}},
want: paramtools.ParamSet{"foo": []string{"bar"}},
reason: "Wildcard param value missing",
},
{
query: url.Values{"config": []string{"!565"}},
want: paramtools.ParamSet{"config": []string{"8888", "gpu"}},
reason: "Negative",
},
{
query: url.Values{"config": []string{"!565", "!8888"}},
want: paramtools.ParamSet{"config": []string{"gpu"}},
reason: "Negative multi miss",
},
{
query: url.Values{"config": []string{"!565"}, "debug": []string{"*"}},
want: paramtools.ParamSet{
"config": []string{"8888", "gpu"},
"debug": []string{"true", "false"},
},
reason: "Negative and wildcard",
},
{
query: url.Values{},
want: paramtools.ParamSet{},
reason: "Empty query matches everything.",
},
{
query: url.Values{"config": []string{"~5.5"}},
want: paramtools.ParamSet{"config": []string{"565"}},
reason: "Regexp match",
},
{
query: url.Values{"config": []string{"~8+"}},
want: paramtools.ParamSet{"config": []string{"8888"}},
reason: "Regexp match",
},
{
query: url.Values{"arch": []string{"~^x"}, "config": []string{"!565"}, "debug": []string{"*"}},
want: paramtools.ParamSet{"arch": []string{"x86"}, "config": []string{"8888", "gpu"}, "debug": []string{"true", "false"}},
reason: "Negative, wildcard, and regexp",
},
{
query: url.Values{"arch": []string{"~^y.*"}, "config": []string{"!565"}, "debug": []string{"*"}},
hasError: true,
reason: "Negative, wildcard, and miss regexp",
},
}
for _, tc := range testCases {
q, err := New(tc.query)
assert.NoError(t, err, tc.reason)
ps, err := q.QueryPlan(rops)
if tc.hasError {
assert.Error(t, err, tc.reason)
} else {
assert.NoError(t, err, tc.reason)
assert.Equal(t, tc.want, ps, tc.reason)
}
}
}
func TestValidateParamSet(t *testing.T) {
assert.NoError(t, ValidateParamSet(nil))
assert.NoError(t, ValidateParamSet(paramtools.ParamSet{}))
assert.Error(t, ValidateParamSet(paramtools.ParamSet{"": []string{}}))
assert.Error(t, ValidateParamSet(paramtools.ParamSet{"); DROP TABLE": []string{}}))
assert.Error(t, ValidateParamSet(paramtools.ParamSet{"good": []string{"); DROP TABLE"}}))
assert.Error(t, ValidateParamSet(paramtools.ParamSet{"ba,d": []string{}}))
assert.Error(t, ValidateParamSet(paramtools.ParamSet{"ba=d": []string{}}))
assert.Error(t, ValidateParamSet(paramtools.ParamSet{"good": []string{"),"}}))
assert.Error(t, ValidateParamSet(paramtools.ParamSet{"good": []string{")="}}))
}
func TestQueryParamKey_HappyPath(t *testing.T) {
require.Equal(t, "arch", queryParam{keyMatch: ",arch="}.Key())
}
func TestQueryParamKey_EmptyKey_ReturnsEmptyString(t *testing.T) {
require.Empty(t, queryParam{keyMatch: ",="}.Key())
}