Skip to content

Commit

Permalink
More tests.
Browse files Browse the repository at this point in the history
This adds more testing for the CEL evaluation mechanism for resource
filtering.

Signed-off-by: Kevin McDermott <bigkevmcd@gmail.com>
  • Loading branch information
bigkevmcd committed Dec 12, 2024
1 parent bda3f0a commit d1a161e
Show file tree
Hide file tree
Showing 2 changed files with 143 additions and 24 deletions.
36 changes: 12 additions & 24 deletions internal/server/cel.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import (
"strings"

"github.com/google/cel-go/cel"
"github.com/google/cel-go/checker/decls"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
"github.com/google/cel-go/common/types/traits"
Expand Down Expand Up @@ -54,6 +53,9 @@ func newCELProgram(expr string) (cel.Program, error) {
if issues != nil && issues.Err() != nil {
return nil, fmt.Errorf("expression %v check failed: %w", expr, issues.Err())
}
if checked.OutputType() != types.BoolType {
return nil, fmt.Errorf("invalid expression output type %v", checked.OutputType())
}

prg, err := env.Program(checked, cel.EvalOptions(cel.OptOptimize), cel.InterruptCheckFrequency(100))
if err != nil {
Expand Down Expand Up @@ -94,26 +96,19 @@ func newCELEvaluator(expr string, req *http.Request) (resourcePredicate, error)
return nil, fmt.Errorf("expression %v failed to evaluate: %w", expr, err)
}

v, ok := out.(types.Bool)
if !ok {
return nil, fmt.Errorf("expression %q did not return a boolean value", expr)
}

result := v.Value().(bool)
result := out.Value().(bool)

return &result, nil
}, nil
}

func makeCELEnv() (*cel.Env, error) {
mapStrDyn := decls.NewMapType(decls.String, decls.Dyn)
return cel.NewEnv(
celext.Strings(),
notifications(),
cel.Declarations(
decls.NewVar("resource", mapStrDyn),
decls.NewVar("request", mapStrDyn),
))
cel.Variable("resource", cel.ObjectType("google.protobuf.Struct")),
cel.Variable("request", cel.ObjectType("google.protobuf.Struct")),
)
}

func isJSONContent(r *http.Request) bool {
Expand All @@ -132,17 +127,10 @@ func isJSONContent(r *http.Request) bool {
}

func notifications() cel.EnvOption {
r, err := types.NewRegistry()
if err != nil {
panic(err) // TODO: Do something better?
}

return cel.Lib(&notificationsLib{registry: r})
return cel.Lib(&notificationsLib{})
}

type notificationsLib struct {
registry *types.Registry
}
type notificationsLib struct{}

// LibraryName implements the SingletonLibrary interface method.
func (*notificationsLib) LibraryName() string {
Expand All @@ -151,13 +139,13 @@ func (*notificationsLib) LibraryName() string {

// CompileOptions implements the Library interface method.
func (l *notificationsLib) CompileOptions() []cel.EnvOption {
listStrDyn := cel.ListType(cel.DynType)
listDyn := cel.ListType(cel.DynType)
opts := []cel.EnvOption{
cel.Function("first",
cel.MemberOverload("first_list", []*cel.Type{listStrDyn}, cel.DynType,
cel.MemberOverload("first_list", []*cel.Type{listDyn}, cel.DynType,
cel.UnaryBinding(listFirst))),
cel.Function("last",
cel.MemberOverload("last_list", []*cel.Type{listStrDyn}, cel.DynType,
cel.MemberOverload("last_list", []*cel.Type{listDyn}, cel.DynType,
cel.UnaryBinding(listLast))),
}

Expand Down
131 changes: 131 additions & 0 deletions internal/server/cel_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package server

import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
"testing"

apiv1 "github.com/fluxcd/notification-controller/api/v1"
. "github.com/onsi/gomega"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
)

func TestValidateCELExpressionValidExpressions(t *testing.T) {
validationTests := []string{
"true",
"false",
"request.body.value == 'test'",
}

for _, tt := range validationTests {
t.Run(tt, func(t *testing.T) {
g := NewWithT(t)
g.Expect(ValidateCELExpression(tt)).To(Succeed())
})
}
}

func TestValidateCELExpressionInvalidExpressions(t *testing.T) {
validationTests := []struct {
expression string
wantError string
}{
{
"'test'",
"invalid expression output type string",
},
{
"requrest.body.value",
"undeclared reference to 'requrest'",
},
}

for _, tt := range validationTests {
t.Run(tt.expression, func(t *testing.T) {
g := NewWithT(t)
g.Expect(ValidateCELExpression(tt.expression)).To(MatchError(ContainSubstring(tt.wantError)))
})
}
}

func TestCELEvaluation(t *testing.T) {
evaluationTests := []struct {
expression string
request *http.Request
resource client.Object
wantResult bool
}{
{
expression: `resource.metadata.name == 'test-resource' && request.body.target.repository == 'hello-world'`,
request: testNewHTTPRequest(t, http.MethodPost, "/test", map[string]any{
"target": map[string]any{
"repository": "hello-world",
},
}),
resource: &apiv1.Receiver{
TypeMeta: metav1.TypeMeta{
Kind: apiv1.ReceiverKind,
APIVersion: apiv1.GroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: "test-resource",
},
},
wantResult: true,
},
{
expression: `resource.metadata.name == 'test-resource' && request.body.image.source.split(':').last().startsWith('v')`,
request: testNewHTTPRequest(t, http.MethodPost, "/test", map[string]any{
"image": map[string]any{
"source": "hello-world:v1.0.0",
},
}),
resource: &apiv1.Receiver{
TypeMeta: metav1.TypeMeta{
Kind: apiv1.ReceiverKind,
APIVersion: apiv1.GroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: "test-resource",
},
},
wantResult: true,
},
}

for _, tt := range evaluationTests {
t.Run(tt.expression, func(t *testing.T) {
g := NewWithT(t)
evaluator, err := newCELEvaluator(tt.expression, tt.request)
g.Expect(err).To(Succeed())

result, err := evaluator(context.Background(), tt.resource)
g.Expect(err).To(Succeed())
g.Expect(result).To(Equal(&tt.wantResult))
})
}
}

func testNewHTTPRequest(t *testing.T, method, target string, body map[string]any) *http.Request {
var httpBody io.Reader
g := NewWithT(t)
if body != nil {
b, err := json.Marshal(body)
g.Expect(err).To(Succeed())
httpBody = bytes.NewReader(b)
}

req, err := http.NewRequest(method, target, httpBody)
g.Expect(err).To(Succeed())

if httpBody != nil {
req.Header.Set("Content-Type", "application/json")
}

return req

}

0 comments on commit d1a161e

Please sign in to comment.