blob: fb31c94c0b839944abb3591255f09cfbeba7154b [file] [edit]
// Copyright 2023 Google LLC
//
// 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.
package cel
import (
"reflect"
"testing"
celenv "cel.dev/cel-go/common/env"
"cel.dev/cel-go/common/operators"
"cel.dev/cel-go/common/overloads"
"cel.dev/cel-go/common/types"
"cel.dev/cel-go/common/types/ref"
"cel.dev/cel-go/common/types/traits"
"cel.dev/cel-go/test"
)
func TestValidateDurationLiterals(t *testing.T) {
env, err := NewEnv(
Variable("x", types.StringType),
ASTValidators(ValidateDurationLiterals()))
if err != nil {
t.Fatalf("NewEnv(ValidateDurationLiterals()) failed: %v", err)
}
tests := []struct {
expr string
iss string
}{
{
expr: `duration('1')`,
iss: `ERROR: <input>:1:10: invalid duration argument
| duration('1')
| .........^`,
},
{
expr: `duration('1d')`,
iss: `ERROR: <input>:1:10: invalid duration argument
| duration('1d')
| .........^`,
},
{
expr: "duration('1us')\n < duration('1nns')",
iss: `ERROR: <input>:2:13: invalid duration argument
| < duration('1nns')
| ............^`,
},
{
expr: `duration('2h3m4s5us')`,
},
{
expr: `duration(x)`,
},
}
for _, tst := range tests {
tc := tst
t.Run(tc.expr, func(t *testing.T) {
_, iss := env.Compile(tc.expr)
if tc.iss != "" {
if iss.Err() == nil {
t.Fatalf("e.Compile(%v) returned ast, expected error: %v", tc.expr, tc.iss)
}
if !test.Compare(iss.Err().Error(), tc.iss) {
t.Fatalf("e.Compile(%v) returned %v, expected error: %v", tc.expr, iss.Err(), tc.iss)
}
return
}
if iss.Err() != nil {
t.Fatalf("e.Compile(%v) failed: %v", tc.expr, iss.Err())
}
})
}
}
func TestValidateTimestampLiterals(t *testing.T) {
env, err := NewEnv(
Variable("x", types.StringType),
ASTValidators(ValidateTimestampLiterals()))
if err != nil {
t.Fatalf("NewEnv(ValidateTimestampLiterals()) failed: %v", err)
}
tests := []struct {
expr string
iss string
}{
{
expr: `timestamp('1000-00-00T00:00:00Z')`,
iss: `ERROR: <input>:1:11: invalid timestamp argument
| timestamp('1000-00-00T00:00:00Z')
| ..........^`,
},
{
expr: `timestamp('1000-01-01T00:00:00ZZ')`,
iss: `ERROR: <input>:1:11: invalid timestamp argument
| timestamp('1000-01-01T00:00:00ZZ')
| ..........^`,
},
{
expr: `timestamp('1000-01-01T00:00:00Z')`,
},
{
expr: `timestamp(-6213559680)`, // min unix epoch time.
},
{
expr: `timestamp(-62135596801)`,
iss: `ERROR: <input>:1:11: invalid timestamp argument
| timestamp(-62135596801)
| ..........^`,
},
{
expr: `timestamp(x)`,
},
}
for _, tst := range tests {
tc := tst
t.Run(tc.expr, func(t *testing.T) {
_, iss := env.Compile(tc.expr)
if tc.iss != "" {
if iss.Err() == nil {
t.Fatalf("e.Compile(%v) returned ast, expected error: %v", tc.expr, tc.iss)
}
if !test.Compare(iss.Err().Error(), tc.iss) {
t.Fatalf("e.Compile(%v) returned %v, expected error: %v", tc.expr, iss.Err(), tc.iss)
}
return
}
if iss.Err() != nil {
t.Fatalf("e.Compile(%v) failed: %v", tc.expr, iss.Err())
}
})
}
}
func TestValidateRegexLiterals(t *testing.T) {
env, err := NewEnv(
Variable("x", types.StringType),
ASTValidators(ValidateRegexLiterals()))
if err != nil {
t.Fatalf("NewEnv(ValidateRegexLiterals()) failed: %v", err)
}
tests := []struct {
expr string
iss string
}{
{
expr: `'hello'.matches('el*')`,
},
{
expr: `'hello'.matches('x++')`,
iss: `
ERROR: <input>:1:17: invalid matches argument
| 'hello'.matches('x++')
| ................^`,
},
{
expr: `'hello'.matches('(?<name%>el*)')`,
iss: `
ERROR: <input>:1:17: invalid matches argument
| 'hello'.matches('(?<name%>el*)')
| ................^`,
},
{
expr: `'hello'.matches('??el*')`,
iss: `
ERROR: <input>:1:17: invalid matches argument
| 'hello'.matches('??el*')
| ................^`,
},
{
expr: `'hello'.matches(x)`,
},
}
for _, tst := range tests {
tc := tst
t.Run(tc.expr, func(t *testing.T) {
_, iss := env.Compile(tc.expr)
if tc.iss != "" {
if iss.Err() == nil {
t.Fatalf("e.Compile(%v) returned ast, expected error: %v", tc.expr, tc.iss)
}
if !test.Compare(iss.Err().Error(), tc.iss) {
t.Fatalf("e.Compile(%v) returned %v, expected error: %v", tc.expr, iss.Err(), tc.iss)
}
return
}
if iss.Err() != nil {
t.Fatalf("e.Compile(%v) failed: %v", tc.expr, iss.Err())
}
})
}
}
func TestValidateRegexProgramSizeLimit(t *testing.T) {
opts := []EnvOption{
Variable("x", types.StringType),
ASTValidators(ValidateRegexProgramSizeLimit(5)),
}
tests := []struct {
expr string
iss string
}{
{
expr: `'hello'.matches('el*')`,
},
{
expr: `'hello'.matches('(a|b)*[0-9]+')`,
iss: `
ERROR: <input>:1:17: regex program size 8 exceeds limit of 5
| 'hello'.matches('(a|b)*[0-9]+')
| ................^`,
},
{
expr: `'hello'.matches(x)`,
},
}
for _, tst := range tests {
tc := tst
t.Run(tc.expr, func(t *testing.T) {
_, err := Compile(tc.expr, opts...)
if tc.iss != "" {
if err == nil {
t.Fatalf("Compile(%v) returned ast, expected error: %v", tc.expr, tc.iss)
}
if !test.Compare(err.Error(), tc.iss) {
t.Fatalf("Compile(%v) returned %v, expected error: %v", tc.expr, err, tc.iss)
}
return
}
if err != nil {
t.Fatalf("Compile(%v) failed: %v", tc.expr, err)
}
})
}
}
func TestValidateRegexProgramSizeLimitToConfig(t *testing.T) {
val := ValidateRegexProgramSizeLimit(5)
cfg := val.(ConfigurableASTValidator).ToConfig()
if cfg.Name != regexProgramSizeLimitValidatorName {
t.Errorf("ToConfig().Name = %s, wanted %s", cfg.Name, regexProgramSizeLimitValidatorName)
}
if limit, ok := cfg.ConfigValue("limit"); !ok || limit != 5 {
t.Errorf("ToConfig().ConfigValue('limit') = %v, wanted 5", limit)
}
}
func TestValidateRegexProgramSizeLimitFactory(t *testing.T) {
val := ValidateRegexProgramSizeLimit(5)
cfg := val.(ConfigurableASTValidator).ToConfig()
fac, ok := astValidatorFactories[regexProgramSizeLimitValidatorName]
if !ok {
t.Fatalf("missing factory for %s", regexProgramSizeLimitValidatorName)
}
vFromCfg, err := fac(cfg)
if err != nil {
t.Fatalf("fac(cfg) failed: %v", err)
}
if vFromCfg.Name() != regexProgramSizeLimitValidatorName {
t.Errorf("vFromCfg.Name() = %s, wanted %s", vFromCfg.Name(), regexProgramSizeLimitValidatorName)
}
}
func TestValidateHomogeneousAggregateLiterals(t *testing.T) {
env, err := NewCustomEnv(
Variable("name", StringType),
Function(operators.In,
Overload(overloads.InList, []*Type{StringType, ListType(StringType)}, BoolType,
BinaryBinding(func(lhs, rhs ref.Val) ref.Val {
return rhs.(traits.Container).Contains(lhs)
}),
),
Overload(overloads.InMap, []*Type{StringType, MapType(StringType, BoolType)}, BoolType,
BinaryBinding(func(lhs, rhs ref.Val) ref.Val {
return rhs.(traits.Container).Contains(lhs)
}),
),
),
OptionalTypes(),
HomogeneousAggregateLiterals(),
ASTValidators(ValidateHomogeneousAggregateLiterals()),
)
if err != nil {
t.Fatalf("NewCustomEnv() failed: %v", err)
}
tests := []struct {
expr string
iss string
}{
{
expr: `name in ['hello', 0]`,
iss: `
ERROR: <input>:1:19: expected type 'string' but found 'int'
| name in ['hello', 0]
| ..................^`,
},
{
expr: `{'hello':'world', 1:'!'}`,
iss: `
ERROR: <input>:1:19: expected type 'string' but found 'int'
| {'hello':'world', 1:'!'}
| ..................^`,
},
{
expr: `name in {'hello':'world', 'goodbye':true}`,
iss: `
ERROR: <input>:1:37: expected type 'string' but found 'bool'
| name in {'hello':'world', 'goodbye':true}
| ....................................^`,
},
{
expr: `name in ['hello', 'world']`,
},
{
expr: `name in ['hello', ?optional.ofNonZeroValue('')]`,
},
{
expr: `name in [?optional.ofNonZeroValue(''), 'hello', ?optional.of('')]`,
},
{
expr: `name in {'hello': false, 'world': true}`,
},
{
expr: `{'hello': false, ?'world': optional.ofNonZeroValue(true)}`,
},
{
expr: `{?'hello': optional.ofNonZeroValue(false), 'world': true}`,
},
}
for _, tst := range tests {
tc := tst
t.Run(tc.expr, func(t *testing.T) {
_, iss := env.Compile(tc.expr)
if tc.iss != "" {
if iss.Err() == nil {
t.Fatalf("e.Compile(%v) returned ast, expected error: %v", tc.expr, tc.iss)
}
if !test.Compare(iss.Err().Error(), tc.iss) {
t.Fatalf("e.Compile(%v) returned %v, expected error: %v", tc.expr, iss.Err(), tc.iss)
}
return
}
if iss.Err() != nil {
t.Fatalf("e.Compile(%v) failed: %v", tc.expr, iss.Err())
}
})
}
}
func TestValidateComprehensionNestingLimit(t *testing.T) {
env, err := NewEnv(
ASTValidators(ValidateComprehensionNestingLimit(2)),
)
if err != nil {
t.Fatalf("NewEnv() failed: %v", err)
}
tests := []struct {
expr string
iss string
}{
{
expr: `[1, 2, 3].exists(i, i < 1)`,
},
{
expr: `[1, 2, 3].exists(i, [4, 5, 6].filter(j, j % i != 0).size() > 0)`,
},
{
// three comprehensions, but not three levels deep
expr: `[1, 2, 3].exists(i, [4, 5, 6].filter(j, j % i != 0).size() > 0) && [1, 2, 3].exists(i, i < 1)`,
},
{
// the empty iteration range in [].all(k, k) does not impact the actual runtime complexity,
// so it does not trip the comprehension limit.
expr: `[1, 2, 3].exists(i, [4, 5, 6].filter(j, [].all(k, k) && j % i != 0).size() > 0)`,
},
{
// three comprehensions, three levels deep
expr: `[1, 2, 3].map(i, [4, 5, 6].map(j, [7, 8, 9].map(k, i * j * k)))`,
iss: `
ERROR: <input>:1:48: comprehension exceeds nesting limit
| [1, 2, 3].map(i, [4, 5, 6].map(j, [7, 8, 9].map(k, i * j * k)))
| ...............................................^`,
},
}
for _, tst := range tests {
tc := tst
t.Run(tc.expr, func(t *testing.T) {
_, iss := env.Compile(tc.expr)
if tc.iss != "" {
if iss.Err() == nil {
t.Fatalf("e.Compile(%v) returned ast, expected error: %v", tc.expr, tc.iss)
}
if !test.Compare(iss.Err().Error(), tc.iss) {
t.Fatalf("e.Compile(%v) returned %v, expected error: %v", tc.expr, iss.Err(), tc.iss)
}
return
}
if iss.Err() != nil {
t.Fatalf("e.Compile(%v) failed: %v", tc.expr, iss.Err())
}
})
}
}
func TestExtendedValidations(t *testing.T) {
env, err := NewEnv(
Variable("x", types.StringType),
ExtendedValidations(),
)
if err != nil {
t.Fatalf("NewEnv(ExtendedValidations()) failed: %v", err)
}
tests := []struct {
expr string
iss string
}{
{
expr: `x in ['hello', 0]
&& duration(x) < duration('1d')
&& timestamp(x) != timestamp('1000-01-00T00:00:00Z')
&& x.matches('x++')`,
iss: `
ERROR: <input>:1:16: expected type 'string' but found 'int'
| x in ['hello', 0]
| ...............^
ERROR: <input>:2:30: invalid duration argument
| && duration(x) < duration('1d')
| .............................^
ERROR: <input>:3:33: invalid timestamp argument
| && timestamp(x) != timestamp('1000-01-00T00:00:00Z')
| ................................^
ERROR: <input>:4:17: invalid matches argument
| && x.matches('x++')
| ................^`,
},
}
for _, tst := range tests {
tc := tst
t.Run(tc.expr, func(t *testing.T) {
_, iss := env.Compile(tc.expr)
if tc.iss != "" {
if iss.Err() == nil {
t.Fatalf("e.Compile(%v) returned ast, expected error: %v", tc.expr, tc.iss)
}
if !test.Compare(iss.Err().Error(), tc.iss) {
t.Fatalf("e.Compile(%v) returned %v, expected error: %v", tc.expr, iss.Err(), tc.iss)
}
return
}
if iss.Err() != nil {
t.Fatalf("e.Compile(%v) failed: %v", tc.expr, iss.Err())
}
})
}
}
func TestValidatorConfig(t *testing.T) {
config := newValidatorConfig()
result := config.GetOrDefault("ext.validate.custom", 2)
if result != 2 {
t.Errorf("config.GetOrDefault() got %v, wanted default of 2", result)
}
result = config.GetOrDefault(HomogeneousAggregateLiteralExemptFunctions, []string{})
if reflect.TypeOf(result) != reflect.TypeOf([]string{}) {
t.Errorf("config.GetOrDefault() got %T, wanted type %T", result, []string{})
}
err := config.Set(HomogeneousAggregateLiteralExemptFunctions, []string{"_==_"})
if err != nil {
t.Errorf("config.Set() failed: %v", err)
}
err = config.Set(HomogeneousAggregateLiteralExemptFunctions, map[string]any{})
if err == nil {
t.Error("config.Set() with incorrect value type did not fail")
}
}
func TestOverrideValidator(t *testing.T) {
// Base environment configured with comprehension nesting limit of 2.
baseEnv, err := NewEnv(
ASTValidators(ValidateComprehensionNestingLimit(2)),
)
if err != nil {
t.Fatalf("NewEnv() failed: %v", err)
}
expr := `[1, 2, 3].map(i, [4, 5, 6].map(j, [7, 8, 9].map(k, i * j * k)))`
// Fails in base environment with limit 2.
_, iss := baseEnv.Compile(expr)
if iss.Err() == nil {
t.Fatalf("baseEnv.Compile() succeeded, expected nesting limit error")
}
// Extend environment overriding limit to 3.
extEnv, err := baseEnv.Extend(
ASTValidators(ValidateComprehensionNestingLimit(3)),
)
if err != nil {
t.Fatalf("baseEnv.Extend() failed: %v", err)
}
// Succeeds in extended environment with limit 3.
_, iss = extEnv.Compile(expr)
if iss.Err() != nil {
t.Fatalf("extEnv.Compile() failed: %v", iss.Err())
}
// Verify base environment still fails (immutability check).
_, iss = baseEnv.Compile(expr)
if iss.Err() == nil {
t.Fatalf("baseEnv.Compile() succeeded after Extend, expected baseEnv to remain unchanged")
}
// Extend environment overriding limit to 1 (stricter limit).
stricterEnv, err := extEnv.Extend(
ASTValidators(ValidateComprehensionNestingLimit(1)),
)
if err != nil {
t.Fatalf("extEnv.Extend() failed: %v", err)
}
expr2 := `[1, 2, 3].exists(i, [4, 5, 6].filter(j, j % i != 0).size() > 0)`
// 2 levels deep: succeeds in extEnv (limit 3), fails in stricterEnv (limit 1).
_, iss = extEnv.Compile(expr2)
if iss.Err() != nil {
t.Fatalf("extEnv.Compile() for 2 levels failed: %v", iss.Err())
}
_, iss = stricterEnv.Compile(expr2)
if iss.Err() == nil {
t.Fatalf("stricterEnv.Compile() for 2 levels succeeded, expected error")
}
}
func TestOverrideValidatorFromConfig(t *testing.T) {
baseEnv, err := NewEnv(
ASTValidators(ValidateComprehensionNestingLimit(2)),
)
if err != nil {
t.Fatalf("NewEnv() failed: %v", err)
}
conf := celenv.NewConfig("override_validator").
AddValidators(celenv.NewValidator(nestingLimitValidatorName).SetConfig(map[string]any{"limit": 3}))
extEnv, err := baseEnv.Extend(FromConfig(conf))
if err != nil {
t.Fatalf("baseEnv.Extend(FromConfig) failed: %v", err)
}
expr := `[1, 2, 3].map(i, [4, 5, 6].map(j, [7, 8, 9].map(k, i * j * k)))`
_, iss := extEnv.Compile(expr)
if iss.Err() != nil {
t.Fatalf("extEnv.Compile() failed: %v", iss.Err())
}
}
func TestOverrideValidatorPreservesOrder(t *testing.T) {
v1 := ValidateDurationLiterals()
v2 := ValidateComprehensionNestingLimit(2)
v3 := ValidateTimestampLiterals()
env1, err := NewEnv(ASTValidators(v1, v2, v3))
if err != nil {
t.Fatalf("NewEnv() failed: %v", err)
}
v2New := ValidateComprehensionNestingLimit(5)
env2, err := env1.Extend(ASTValidators(v2New))
if err != nil {
t.Fatalf("Extend() failed: %v", err)
}
validators := env2.Validators()
if len(validators) != 3 {
t.Fatalf("got %d validators, wanted 3", len(validators))
}
if validators[0].Name() != v1.Name() || validators[1].Name() != v2.Name() || validators[2].Name() != v3.Name() {
t.Fatalf("validator order or names unexpected: %v, %v, %v", validators[0].Name(), validators[1].Name(), validators[2].Name())
}
if val, ok := validators[1].(nestingLimitValidator); !ok || val.limit != 5 {
t.Fatalf("expected overridden nesting limit validator with limit 5, got %v", validators[1])
}
}