1
2
3
4
19
20 package main
21
22 import (
23 "context"
24 "fmt"
25 "strings"
26 "testing"
27 "time"
28
29 gatewayv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
30
31 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
32 )
33
34 func TestGRPCRouteFilter(t *testing.T) {
35 tests := []struct {
36 name string
37 wantErrors []string
38 routeFilter gatewayv1a2.GRPCRouteFilter
39 }{
40 {
41 name: "valid GRPCRouteFilterRequestHeaderModifier route filter",
42 routeFilter: gatewayv1a2.GRPCRouteFilter{
43 Type: gatewayv1a2.GRPCRouteFilterRequestHeaderModifier,
44 RequestHeaderModifier: &gatewayv1a2.HTTPHeaderFilter{
45 Set: []gatewayv1a2.HTTPHeader{{Name: "name", Value: "foo"}},
46 Add: []gatewayv1a2.HTTPHeader{{Name: "add", Value: "foo"}},
47 Remove: []string{"remove"},
48 },
49 },
50 },
51 {
52 name: "invalid GRPCRouteFilterRequestHeaderModifier type filter with non-matching field",
53 routeFilter: gatewayv1a2.GRPCRouteFilter{
54 Type: gatewayv1a2.GRPCRouteFilterRequestHeaderModifier,
55 RequestMirror: &gatewayv1a2.HTTPRequestMirrorFilter{},
56 },
57 wantErrors: []string{"filter.requestHeaderModifier must be specified for RequestHeaderModifier filter.type", "filter.requestMirror must be nil if the filter.type is not RequestMirror"},
58 },
59 {
60 name: "invalid GRPCRouteFilterRequestHeaderModifier type filter with empty value field",
61 routeFilter: gatewayv1a2.GRPCRouteFilter{
62 Type: gatewayv1a2.GRPCRouteFilterRequestHeaderModifier,
63 },
64 wantErrors: []string{"filter.requestHeaderModifier must be specified for RequestHeaderModifier filter.type"},
65 },
66 {
67 name: "valid GRPCRouteFilterResponseHeaderModifier route filter",
68 routeFilter: gatewayv1a2.GRPCRouteFilter{
69 Type: gatewayv1a2.GRPCRouteFilterResponseHeaderModifier,
70 ResponseHeaderModifier: &gatewayv1a2.HTTPHeaderFilter{
71 Set: []gatewayv1a2.HTTPHeader{{Name: "name", Value: "foo"}},
72 Add: []gatewayv1a2.HTTPHeader{{Name: "add", Value: "foo"}},
73 Remove: []string{"remove"},
74 },
75 },
76 },
77 {
78 name: "invalid GRPCRouteFilterResponseHeaderModifier type filter with non-matching field",
79 routeFilter: gatewayv1a2.GRPCRouteFilter{
80 Type: gatewayv1a2.GRPCRouteFilterResponseHeaderModifier,
81 RequestMirror: &gatewayv1a2.HTTPRequestMirrorFilter{},
82 },
83 wantErrors: []string{"filter.responseHeaderModifier must be specified for ResponseHeaderModifier filter.type", "filter.requestMirror must be nil if the filter.type is not RequestMirror"},
84 },
85 {
86 name: "invalid GRPCRouteFilterResponseHeaderModifier type filter with empty value field",
87 routeFilter: gatewayv1a2.GRPCRouteFilter{
88 Type: gatewayv1a2.GRPCRouteFilterResponseHeaderModifier,
89 },
90 wantErrors: []string{"filter.responseHeaderModifier must be specified for ResponseHeaderModifier filter.type"},
91 },
92 {
93 name: "valid GRPCRouteFilterRequestMirror route filter",
94 routeFilter: gatewayv1a2.GRPCRouteFilter{
95 Type: gatewayv1a2.GRPCRouteFilterRequestMirror,
96 RequestMirror: &gatewayv1a2.HTTPRequestMirrorFilter{BackendRef: gatewayv1a2.BackendObjectReference{
97 Group: ptrTo(gatewayv1a2.Group("group")),
98 Kind: ptrTo(gatewayv1a2.Kind("kind")),
99 Name: "name",
100 Namespace: ptrTo(gatewayv1a2.Namespace("ns")),
101 Port: ptrTo(gatewayv1a2.PortNumber(22)),
102 }},
103 },
104 },
105 {
106 name: "invalid GRPCRouteFilterRequestMirror type filter with non-matching field",
107 routeFilter: gatewayv1a2.GRPCRouteFilter{
108 Type: gatewayv1a2.GRPCRouteFilterRequestMirror,
109 RequestHeaderModifier: &gatewayv1a2.HTTPHeaderFilter{},
110 },
111 wantErrors: []string{"filter.requestHeaderModifier must be nil if the filter.type is not RequestHeaderModifier", "filter.requestMirror must be specified for RequestMirror filter.type"},
112 },
113 {
114 name: "invalid GRPCRouteFilterRequestMirror type filter with empty value field",
115 routeFilter: gatewayv1a2.GRPCRouteFilter{
116 Type: gatewayv1a2.GRPCRouteFilterRequestMirror,
117 },
118 wantErrors: []string{"filter.requestMirror must be specified for RequestMirror filter.type"},
119 },
120 {
121 name: "valid GRPCRouteFilterExtensionRef filter",
122 routeFilter: gatewayv1a2.GRPCRouteFilter{
123 Type: gatewayv1a2.GRPCRouteFilterExtensionRef,
124 ExtensionRef: &gatewayv1a2.LocalObjectReference{
125 Group: "group",
126 Kind: "kind",
127 Name: "name",
128 },
129 },
130 },
131 {
132 name: "invalid GRPCRouteFilterExtensionRef type filter with non-matching field",
133 routeFilter: gatewayv1a2.GRPCRouteFilter{
134 Type: gatewayv1a2.GRPCRouteFilterExtensionRef,
135 RequestMirror: &gatewayv1a2.HTTPRequestMirrorFilter{},
136 },
137 wantErrors: []string{"filter.requestMirror must be nil if the filter.type is not RequestMirror", "filter.extensionRef must be specified for ExtensionRef filter.type"},
138 },
139 {
140 name: "invalid GRPCRouteFilterExtensionRef type filter with empty value field",
141 routeFilter: gatewayv1a2.GRPCRouteFilter{
142 Type: gatewayv1a2.GRPCRouteFilterExtensionRef,
143 },
144 wantErrors: []string{"filter.extensionRef must be specified for ExtensionRef filter.type"},
145 },
146 }
147 for _, tc := range tests {
148 t.Run(tc.name, func(t *testing.T) {
149 route := &gatewayv1a2.GRPCRoute{
150 ObjectMeta: metav1.ObjectMeta{
151 Name: fmt.Sprintf("foo-%v", time.Now().UnixNano()),
152 Namespace: metav1.NamespaceDefault,
153 },
154 Spec: gatewayv1a2.GRPCRouteSpec{
155 Rules: []gatewayv1a2.GRPCRouteRule{{
156 Filters: []gatewayv1a2.GRPCRouteFilter{tc.routeFilter},
157 }},
158 },
159 }
160 validateGRPCRoute(t, route, tc.wantErrors)
161 })
162 }
163 }
164
165 func TestGRPCRouteRule(t *testing.T) {
166 testService := gatewayv1a2.ObjectName("test-service")
167 tests := []struct {
168 name string
169 wantErrors []string
170 rules []gatewayv1a2.GRPCRouteRule
171 }{
172 {
173 name: "valid GRPCRoute with no filters",
174 rules: []gatewayv1a2.GRPCRouteRule{
175 {
176 Matches: []gatewayv1a2.GRPCRouteMatch{
177 {
178 Method: &gatewayv1a2.GRPCMethodMatch{
179 Type: ptrTo(gatewayv1a2.GRPCMethodMatchType("Exact")),
180 Service: ptrTo("helloworld.Greeter"),
181 },
182 },
183 },
184 BackendRefs: []gatewayv1a2.GRPCBackendRef{
185 {
186 BackendRef: gatewayv1a2.BackendRef{
187 BackendObjectReference: gatewayv1a2.BackendObjectReference{
188 Name: testService,
189 Port: ptrTo(gatewayv1a2.PortNumber(8080)),
190 },
191 Weight: ptrTo(int32(100)),
192 },
193 },
194 },
195 },
196 },
197 },
198 {
199 name: "valid GRPCRoute with only Method specified",
200 rules: []gatewayv1a2.GRPCRouteRule{
201 {
202 Matches: []gatewayv1a2.GRPCRouteMatch{
203 {
204 Method: &gatewayv1a2.GRPCMethodMatch{
205 Type: ptrTo(gatewayv1a2.GRPCMethodMatchType("Exact")),
206 Method: ptrTo("SayHello"),
207 },
208 },
209 },
210 BackendRefs: []gatewayv1a2.GRPCBackendRef{
211 {
212 BackendRef: gatewayv1a2.BackendRef{
213 BackendObjectReference: gatewayv1a2.BackendObjectReference{
214 Name: testService,
215 Port: ptrTo(gatewayv1a2.PortNumber(8080)),
216 },
217 Weight: ptrTo(int32(100)),
218 },
219 },
220 },
221 },
222 },
223 },
224 {
225 name: "invalid because multiple filters are repeated",
226 wantErrors: []string{"RequestHeaderModifier filter cannot be repeated", "ResponseHeaderModifier filter cannot be repeated"},
227 rules: []gatewayv1a2.GRPCRouteRule{
228 {
229 Matches: []gatewayv1a2.GRPCRouteMatch{
230 {
231 Method: &gatewayv1a2.GRPCMethodMatch{
232 Type: ptrTo(gatewayv1a2.GRPCMethodMatchType("Exact")),
233 Service: ptrTo("helloworld.Greeter"),
234 },
235 },
236 },
237 Filters: []gatewayv1a2.GRPCRouteFilter{
238 {
239 Type: gatewayv1a2.GRPCRouteFilterRequestHeaderModifier,
240 RequestHeaderModifier: &gatewayv1a2.HTTPHeaderFilter{
241 Set: []gatewayv1a2.HTTPHeader{
242 {
243 Name: "special-header",
244 Value: "foo",
245 },
246 },
247 },
248 },
249 {
250 Type: gatewayv1a2.GRPCRouteFilterRequestHeaderModifier,
251 RequestHeaderModifier: &gatewayv1a2.HTTPHeaderFilter{
252 Add: []gatewayv1a2.HTTPHeader{
253 {
254 Name: "my-header",
255 Value: "bar",
256 },
257 },
258 },
259 },
260 {
261 Type: gatewayv1a2.GRPCRouteFilterResponseHeaderModifier,
262 ResponseHeaderModifier: &gatewayv1a2.HTTPHeaderFilter{
263 Set: []gatewayv1a2.HTTPHeader{
264 {
265 Name: "special-header",
266 Value: "foo",
267 },
268 },
269 },
270 },
271 {
272 Type: gatewayv1a2.GRPCRouteFilterResponseHeaderModifier,
273 ResponseHeaderModifier: &gatewayv1a2.HTTPHeaderFilter{
274 Add: []gatewayv1a2.HTTPHeader{
275 {
276 Name: "my-header",
277 Value: "bar",
278 },
279 },
280 },
281 },
282 },
283 },
284 },
285 },
286 }
287 for _, tc := range tests {
288 t.Run(tc.name, func(t *testing.T) {
289 route := &gatewayv1a2.GRPCRoute{
290 ObjectMeta: metav1.ObjectMeta{
291 Name: fmt.Sprintf("foo-%v", time.Now().UnixNano()),
292 Namespace: metav1.NamespaceDefault,
293 },
294 Spec: gatewayv1a2.GRPCRouteSpec{Rules: tc.rules},
295 }
296 validateGRPCRoute(t, route, tc.wantErrors)
297 })
298 }
299 }
300
301 func TestGRPCMethodMatch(t *testing.T) {
302 tests := []struct {
303 name string
304 method gatewayv1a2.GRPCMethodMatch
305 wantErrors []string
306 }{
307 {
308 name: "valid GRPCRoute with 1 service in GRPCMethodMatch field",
309 method: gatewayv1a2.GRPCMethodMatch{
310 Service: ptrTo("foo.Test.Example"),
311 },
312 },
313 {
314 name: "valid GRPCRoute with 1 method in GRPCMethodMatch field",
315 method: gatewayv1a2.GRPCMethodMatch{
316 Method: ptrTo("Login"),
317 },
318 },
319 {
320 name: "invalid GRPCRoute missing service or method in GRPCMethodMatch field",
321 method: gatewayv1a2.GRPCMethodMatch{
322 Service: nil,
323 Method: nil,
324 },
325 wantErrors: []string{"One or both of 'service' or 'method"},
326 },
327 {
328 name: "GRPCRoute uses regex in service and method with undefined match type",
329 method: gatewayv1a2.GRPCMethodMatch{
330 Service: ptrTo(".*"),
331 Method: ptrTo(".*"),
332 },
333 wantErrors: []string{"service must only contain valid characters (matching ^(?i)\\.?[a-z_][a-z_0-9]*(\\.[a-z_][a-z_0-9]*)*$)", "method must only contain valid characters (matching ^[A-Za-z_][A-Za-z_0-9]*$)"},
334 },
335 {
336 name: "GRPCRoute uses regex in service and method with match type Exact",
337 method: gatewayv1a2.GRPCMethodMatch{
338 Type: ptrTo(gatewayv1a2.GRPCMethodMatchExact),
339 Service: ptrTo(".*"),
340 Method: ptrTo(".*"),
341 },
342 wantErrors: []string{"service must only contain valid characters (matching ^(?i)\\.?[a-z_][a-z_0-9]*(\\.[a-z_][a-z_0-9]*)*$)", "method must only contain valid characters (matching ^[A-Za-z_][A-Za-z_0-9]*$)"},
343 },
344 {
345 name: "GRPCRoute uses regex in method with undefined match type",
346 method: gatewayv1a2.GRPCMethodMatch{
347 Method: ptrTo(".*"),
348 },
349 wantErrors: []string{"method must only contain valid characters (matching ^[A-Za-z_][A-Za-z_0-9]*$)"},
350 },
351 {
352 name: "GRPCRoute uses regex in service with match type Exact",
353 method: gatewayv1a2.GRPCMethodMatch{
354 Type: ptrTo(gatewayv1a2.GRPCMethodMatchExact),
355 Service: ptrTo(".*"),
356 },
357 wantErrors: []string{"service must only contain valid characters (matching ^(?i)\\.?[a-z_][a-z_0-9]*(\\.[a-z_][a-z_0-9]*)*$)"},
358 },
359 {
360 name: "GRPCRoute uses regex in service and method with match type RegularExpression",
361 method: gatewayv1a2.GRPCMethodMatch{
362 Type: ptrTo(gatewayv1a2.GRPCMethodMatchRegularExpression),
363 Service: ptrTo(".*"),
364 Method: ptrTo(".*"),
365 },
366 },
367 {
368 name: "GRPCRoute uses valid service and method with undefined match type",
369 method: gatewayv1a2.GRPCMethodMatch{
370 Service: ptrTo("foo.Test.Example"),
371 Method: ptrTo("Login"),
372 },
373 },
374 {
375 name: "GRPCRoute uses valid service and method with match type Exact",
376 method: gatewayv1a2.GRPCMethodMatch{
377 Type: ptrTo(gatewayv1a2.GRPCMethodMatchExact),
378 Service: ptrTo("foo.Test.Example"),
379 Method: ptrTo("Login"),
380 },
381 },
382 {
383 name: "GRPCRoute uses a valid service with a leading dot when match type is Exact",
384 method: gatewayv1a2.GRPCMethodMatch{
385 Type: ptrTo(gatewayv1a2.GRPCMethodMatchExact),
386 Service: ptrTo(".foo.Test.Example"),
387 },
388 },
389 }
390
391 for _, tc := range tests {
392 tc := tc
393 t.Run(tc.name, func(t *testing.T) {
394 route := gatewayv1a2.GRPCRoute{
395 ObjectMeta: metav1.ObjectMeta{
396 Name: fmt.Sprintf("foo-%v", time.Now().UnixNano()),
397 Namespace: metav1.NamespaceDefault,
398 },
399 Spec: gatewayv1a2.GRPCRouteSpec{
400 Rules: []gatewayv1a2.GRPCRouteRule{
401 {
402 Matches: []gatewayv1a2.GRPCRouteMatch{
403 {
404 Method: &tc.method,
405 },
406 },
407 },
408 },
409 },
410 }
411 validateGRPCRoute(t, &route, tc.wantErrors)
412 })
413 }
414 }
415
416 func validateGRPCRoute(t *testing.T, route *gatewayv1a2.GRPCRoute, wantErrors []string) {
417 t.Helper()
418
419 ctx := context.Background()
420 err := k8sClient.Create(ctx, route)
421
422 if (len(wantErrors) != 0) != (err != nil) {
423 t.Fatalf("Unexpected response while creating GRPCRoute %q; got err=\n%v\n;want error=%v", fmt.Sprintf("%v/%v", route.Namespace, route.Name), err, wantErrors)
424 }
425
426 var missingErrorStrings []string
427 for _, wantError := range wantErrors {
428 if !strings.Contains(strings.ToLower(err.Error()), strings.ToLower(wantError)) {
429 missingErrorStrings = append(missingErrorStrings, wantError)
430 }
431 }
432 if len(missingErrorStrings) != 0 {
433 t.Errorf("Unexpected response while creating GRPCRoute %q; got err=\n%v\n;missing strings within error=%q", fmt.Sprintf("%v/%v", route.Namespace, route.Name), err, missingErrorStrings)
434 }
435 }
436
View as plain text