Skip to content

Commit 891a42c

Browse files
committed
More tests.
1 parent 2636e01 commit 891a42c

File tree

2 files changed

+143
-24
lines changed

2 files changed

+143
-24
lines changed

internal/server/cel.go

+12-24
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import (
2525
"strings"
2626

2727
"github.com/google/cel-go/cel"
28-
"github.com/google/cel-go/checker/decls"
2928
"github.com/google/cel-go/common/types"
3029
"github.com/google/cel-go/common/types/ref"
3130
"github.com/google/cel-go/common/types/traits"
@@ -54,6 +53,9 @@ func newCELProgram(expr string) (cel.Program, error) {
5453
if issues != nil && issues.Err() != nil {
5554
return nil, fmt.Errorf("expression %v check failed: %w", expr, issues.Err())
5655
}
56+
if checked.OutputType() != types.BoolType {
57+
return nil, fmt.Errorf("invalid expression output type %v", checked.OutputType())
58+
}
5759

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

97-
v, ok := out.(types.Bool)
98-
if !ok {
99-
return nil, fmt.Errorf("expression %q did not return a boolean value", expr)
100-
}
101-
102-
result := v.Value().(bool)
99+
result := out.Value().(bool)
103100

104101
return &result, nil
105102
}, nil
106103
}
107104

108105
func makeCELEnv() (*cel.Env, error) {
109-
mapStrDyn := decls.NewMapType(decls.String, decls.Dyn)
110106
return cel.NewEnv(
111107
celext.Strings(),
112108
notifications(),
113-
cel.Declarations(
114-
decls.NewVar("resource", mapStrDyn),
115-
decls.NewVar("request", mapStrDyn),
116-
))
109+
cel.Variable("resource", cel.ObjectType("google.protobuf.Struct")),
110+
cel.Variable("request", cel.ObjectType("google.protobuf.Struct")),
111+
)
117112
}
118113

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

134129
func notifications() cel.EnvOption {
135-
r, err := types.NewRegistry()
136-
if err != nil {
137-
panic(err) // TODO: Do something better?
138-
}
139-
140-
return cel.Lib(&notificationsLib{registry: r})
130+
return cel.Lib(&notificationsLib{})
141131
}
142132

143-
type notificationsLib struct {
144-
registry *types.Registry
145-
}
133+
type notificationsLib struct{}
146134

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

152140
// CompileOptions implements the Library interface method.
153141
func (l *notificationsLib) CompileOptions() []cel.EnvOption {
154-
listStrDyn := cel.ListType(cel.DynType)
142+
listDyn := cel.ListType(cel.DynType)
155143
opts := []cel.EnvOption{
156144
cel.Function("first",
157-
cel.MemberOverload("first_list", []*cel.Type{listStrDyn}, cel.DynType,
145+
cel.MemberOverload("first_list", []*cel.Type{listDyn}, cel.DynType,
158146
cel.UnaryBinding(listFirst))),
159147
cel.Function("last",
160-
cel.MemberOverload("last_list", []*cel.Type{listStrDyn}, cel.DynType,
148+
cel.MemberOverload("last_list", []*cel.Type{listDyn}, cel.DynType,
161149
cel.UnaryBinding(listLast))),
162150
}
163151

internal/server/cel_test.go

+131
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package server
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/json"
7+
"io"
8+
"net/http"
9+
"testing"
10+
11+
apiv1 "github.com/fluxcd/notification-controller/api/v1"
12+
. "github.com/onsi/gomega"
13+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
14+
"sigs.k8s.io/controller-runtime/pkg/client"
15+
)
16+
17+
func TestValidateCELExpressionValidExpressions(t *testing.T) {
18+
validationTests := []string{
19+
"true",
20+
"false",
21+
"request.body.value == 'test'",
22+
}
23+
24+
for _, tt := range validationTests {
25+
t.Run(tt, func(t *testing.T) {
26+
g := NewWithT(t)
27+
g.Expect(ValidateCELExpression(tt)).To(Succeed())
28+
})
29+
}
30+
}
31+
32+
func TestValidateCELExpressionInvalidExpressions(t *testing.T) {
33+
validationTests := []struct {
34+
expression string
35+
wantError string
36+
}{
37+
{
38+
"'test'",
39+
"invalid expression output type string",
40+
},
41+
{
42+
"requrest.body.value",
43+
"undeclared reference to 'requrest'",
44+
},
45+
}
46+
47+
for _, tt := range validationTests {
48+
t.Run(tt.expression, func(t *testing.T) {
49+
g := NewWithT(t)
50+
g.Expect(ValidateCELExpression(tt.expression)).To(MatchError(ContainSubstring(tt.wantError)))
51+
})
52+
}
53+
}
54+
55+
func TestCELEvaluation(t *testing.T) {
56+
evaluationTests := []struct {
57+
expression string
58+
request *http.Request
59+
resource client.Object
60+
wantResult bool
61+
}{
62+
{
63+
expression: `resource.metadata.name == 'test-resource' && request.body.target.repository == 'hello-world'`,
64+
request: testNewHTTPRequest(t, http.MethodPost, "/test", map[string]any{
65+
"target": map[string]any{
66+
"repository": "hello-world",
67+
},
68+
}),
69+
resource: &apiv1.Receiver{
70+
TypeMeta: metav1.TypeMeta{
71+
Kind: apiv1.ReceiverKind,
72+
APIVersion: apiv1.GroupVersion.String(),
73+
},
74+
ObjectMeta: metav1.ObjectMeta{
75+
Name: "test-resource",
76+
},
77+
},
78+
wantResult: true,
79+
},
80+
{
81+
expression: `resource.metadata.name == 'test-resource' && request.body.image.source.split(':').last().startsWith('v')`,
82+
request: testNewHTTPRequest(t, http.MethodPost, "/test", map[string]any{
83+
"image": map[string]any{
84+
"source": "hello-world:v1.0.0",
85+
},
86+
}),
87+
resource: &apiv1.Receiver{
88+
TypeMeta: metav1.TypeMeta{
89+
Kind: apiv1.ReceiverKind,
90+
APIVersion: apiv1.GroupVersion.String(),
91+
},
92+
ObjectMeta: metav1.ObjectMeta{
93+
Name: "test-resource",
94+
},
95+
},
96+
wantResult: true,
97+
},
98+
}
99+
100+
for _, tt := range evaluationTests {
101+
t.Run(tt.expression, func(t *testing.T) {
102+
g := NewWithT(t)
103+
evaluator, err := newCELEvaluator(tt.expression, tt.request)
104+
g.Expect(err).To(Succeed())
105+
106+
result, err := evaluator(context.Background(), tt.resource)
107+
g.Expect(err).To(Succeed())
108+
g.Expect(result).To(Equal(&tt.wantResult))
109+
})
110+
}
111+
}
112+
113+
func testNewHTTPRequest(t *testing.T, method, target string, body map[string]any) *http.Request {
114+
var httpBody io.Reader
115+
g := NewWithT(t)
116+
if body != nil {
117+
b, err := json.Marshal(body)
118+
g.Expect(err).To(Succeed())
119+
httpBody = bytes.NewReader(b)
120+
}
121+
122+
req, err := http.NewRequest(method, target, httpBody)
123+
g.Expect(err).To(Succeed())
124+
125+
if httpBody != nil {
126+
req.Header.Set("Content-Type", "application/json")
127+
}
128+
129+
return req
130+
131+
}

0 commit comments

Comments
 (0)