| // 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" |
| |
| "github.com/google/cel-go/common/operators" |
| "github.com/google/cel-go/common/overloads" |
| "github.com/google/cel-go/common/types" |
| "github.com/google/cel-go/common/types/ref" |
| "github.com/google/cel-go/common/types/traits" |
| "github.com/google/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 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") |
| } |
| } |