1
16
17 package validation
18
19 import (
20 "testing"
21
22 "github.com/stretchr/testify/assert"
23 "github.com/stretchr/testify/require"
24 "k8s.io/apimachinery/pkg/util/validation/field"
25
26 gatewayv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
27 )
28
29 func TestValidateGRPCRoute(t *testing.T) {
30 t.Parallel()
31
32 service := "foo.Test.Example"
33 method := "Login"
34 regex := ".*"
35
36 tests := []struct {
37 name string
38 rules []gatewayv1a2.GRPCRouteRule
39 errs field.ErrorList
40 }{
41 {
42 name: "valid GRPCRoute with 1 service in GRPCMethodMatch field",
43 rules: []gatewayv1a2.GRPCRouteRule{
44 {
45 Matches: []gatewayv1a2.GRPCRouteMatch{
46 {
47 Method: &gatewayv1a2.GRPCMethodMatch{
48 Service: &service,
49 },
50 },
51 },
52 },
53 },
54 },
55 {
56 name: "valid GRPCRoute with 1 method in GRPCMethodMatch field",
57 rules: []gatewayv1a2.GRPCRouteRule{
58 {
59 Matches: []gatewayv1a2.GRPCRouteMatch{
60 {
61 Method: &gatewayv1a2.GRPCMethodMatch{
62 Method: &method,
63 },
64 },
65 },
66 },
67 },
68 },
69 {
70 name: "invalid GRPCRoute missing service or method in GRPCMethodMatch field",
71 rules: []gatewayv1a2.GRPCRouteRule{
72 {
73 Matches: []gatewayv1a2.GRPCRouteMatch{
74 {
75 Method: &gatewayv1a2.GRPCMethodMatch{
76 Service: nil,
77 Method: nil,
78 },
79 },
80 },
81 },
82 },
83 errs: field.ErrorList{
84 {
85 Type: field.ErrorTypeRequired,
86 Field: "spec.rules[0].matches[0].method",
87 Detail: "one or both of `service` or `method` must be specified",
88 },
89 },
90 },
91 {
92 name: "GRPCRoute use regex in service and method with undefined match type",
93 rules: []gatewayv1a2.GRPCRouteRule{
94 {
95 Matches: []gatewayv1a2.GRPCRouteMatch{
96 {
97 Method: &gatewayv1a2.GRPCMethodMatch{
98 Service: ®ex,
99 Method: ®ex,
100 },
101 },
102 },
103 },
104 },
105 errs: field.ErrorList{
106 {
107 Type: field.ErrorTypeInvalid,
108 BadValue: regex,
109 Field: "spec.rules[0].matches[0].method",
110 Detail: `must only contain valid characters (matching ^(?i)\.?[a-z_][a-z_0-9]*(\.[a-z_][a-z_0-9]*)*$)`,
111 },
112 {
113 Type: field.ErrorTypeInvalid,
114 BadValue: regex,
115 Field: "spec.rules[0].matches[0].method",
116 Detail: `must only contain valid characters (matching ^[A-Za-z_][A-Za-z_0-9]*$)`,
117 },
118 },
119 },
120 {
121 name: "GRPCRoute use regex in service and method with match type Exact",
122 rules: []gatewayv1a2.GRPCRouteRule{
123 {
124 Matches: []gatewayv1a2.GRPCRouteMatch{
125 {
126 Method: &gatewayv1a2.GRPCMethodMatch{
127 Service: ®ex,
128 Method: ®ex,
129 Type: ptrTo(gatewayv1a2.GRPCMethodMatchExact),
130 },
131 },
132 },
133 },
134 },
135 errs: field.ErrorList{
136 {
137 Type: field.ErrorTypeInvalid,
138 BadValue: regex,
139 Field: "spec.rules[0].matches[0].method",
140 Detail: `must only contain valid characters (matching ^(?i)\.?[a-z_][a-z_0-9]*(\.[a-z_][a-z_0-9]*)*$)`,
141 },
142 {
143 Type: field.ErrorTypeInvalid,
144 BadValue: regex,
145 Field: "spec.rules[0].matches[0].method",
146 Detail: `must only contain valid characters (matching ^[A-Za-z_][A-Za-z_0-9]*$)`,
147 },
148 },
149 },
150 {
151 name: "GRPCRoute use regex in service and method with match type RegularExpression",
152 rules: []gatewayv1a2.GRPCRouteRule{
153 {
154 Matches: []gatewayv1a2.GRPCRouteMatch{
155 {
156 Method: &gatewayv1a2.GRPCMethodMatch{
157 Service: ®ex,
158 Method: ®ex,
159 Type: ptrTo(gatewayv1a2.GRPCMethodMatchRegularExpression),
160 },
161 },
162 },
163 },
164 },
165 errs: field.ErrorList{},
166 },
167 {
168 name: "GRPCRoute use valid service and method with undefined match type",
169 rules: []gatewayv1a2.GRPCRouteRule{
170 {
171 Matches: []gatewayv1a2.GRPCRouteMatch{
172 {
173 Method: &gatewayv1a2.GRPCMethodMatch{
174 Service: &service,
175 Method: &method,
176 },
177 },
178 },
179 },
180 },
181 errs: field.ErrorList{},
182 },
183 {
184 name: "GRPCRoute use valid service and method with match type Exact",
185 rules: []gatewayv1a2.GRPCRouteRule{
186 {
187 Matches: []gatewayv1a2.GRPCRouteMatch{
188 {
189 Method: &gatewayv1a2.GRPCMethodMatch{
190 Service: &service,
191 Method: &method,
192 Type: ptrTo(gatewayv1a2.GRPCMethodMatchExact),
193 },
194 },
195 },
196 },
197 },
198 errs: field.ErrorList{},
199 },
200 {
201 name: "GRPCRoute with duplicate ExtensionRef filters",
202 rules: []gatewayv1a2.GRPCRouteRule{
203 {
204 Filters: []gatewayv1a2.GRPCRouteFilter{{
205 Type: "ExtensionRef",
206 ExtensionRef: &gatewayv1a2.LocalObjectReference{
207 Kind: "Example1",
208 },
209 }, {
210 Type: "ExtensionRef",
211 ExtensionRef: &gatewayv1a2.LocalObjectReference{
212 Kind: "Example2",
213 },
214 }},
215 },
216 },
217 },
218 {
219 name: "GRPCRoute with duplicate RequestMirror filters",
220 rules: []gatewayv1a2.GRPCRouteRule{
221 {
222 Filters: []gatewayv1a2.GRPCRouteFilter{{
223 Type: "RequestMirror",
224 RequestMirror: &gatewayv1a2.HTTPRequestMirrorFilter{
225 BackendRef: gatewayv1a2.BackendObjectReference{
226 Name: "Example1",
227 },
228 },
229 }, {
230 Type: "RequestMirror",
231 RequestMirror: &gatewayv1a2.HTTPRequestMirrorFilter{
232 BackendRef: gatewayv1a2.BackendObjectReference{
233 Name: "Example2",
234 },
235 },
236 }},
237 },
238 },
239 },
240 {
241 name: "invalid GRPCRoute with duplicate RequestHeaderModifier filters",
242 rules: []gatewayv1a2.GRPCRouteRule{
243 {
244 Filters: []gatewayv1a2.GRPCRouteFilter{{
245 Type: "RequestHeaderModifier",
246 RequestHeaderModifier: &gatewayv1a2.HTTPHeaderFilter{
247 Set: []gatewayv1a2.HTTPHeader{
248 {
249 Name: "special-header",
250 Value: "foo",
251 },
252 },
253 },
254 }, {
255 Type: "RequestHeaderModifier",
256 RequestHeaderModifier: &gatewayv1a2.HTTPHeaderFilter{
257 Add: []gatewayv1a2.HTTPHeader{
258 {
259 Name: "my-header",
260 Value: "bar",
261 },
262 },
263 },
264 }},
265 },
266 },
267 errs: field.ErrorList{
268 {
269 Type: field.ErrorTypeInvalid,
270 BadValue: "RequestHeaderModifier",
271 Field: "spec.rules[0].filters",
272 Detail: "cannot be used multiple times in the same rule",
273 },
274 },
275 },
276 }
277
278 for _, tc := range tests {
279 tc := tc
280 t.Run(tc.name, func(t *testing.T) {
281 t.Parallel()
282
283 route := gatewayv1a2.GRPCRoute{Spec: gatewayv1a2.GRPCRouteSpec{Rules: tc.rules}}
284 errs := ValidateGRPCRoute(&route)
285 if len(errs) != len(tc.errs) {
286 t.Errorf("got %d errors, want %d errors: %s", len(errs), len(tc.errs), errs)
287 t.FailNow()
288 }
289 for i := 0; i < len(errs); i++ {
290 realErr := errs[i].Error()
291 expectedErr := tc.errs[i].Error()
292 if realErr != expectedErr {
293 t.Errorf("expect error message: %s, but got: %s", expectedErr, realErr)
294 t.FailNow()
295 }
296 }
297 })
298 }
299 }
300
301 func TestValidateGRPCBackendUniqueFilters(t *testing.T) {
302 var testService gatewayv1a2.ObjectName = "testService"
303 var specialService gatewayv1a2.ObjectName = "specialService"
304 tests := []struct {
305 name string
306 rules []gatewayv1a2.GRPCRouteRule
307 errCount int
308 }{{
309 name: "valid grpcRoute Rules backendref filters",
310 errCount: 0,
311 rules: []gatewayv1a2.GRPCRouteRule{{
312 BackendRefs: []gatewayv1a2.GRPCBackendRef{
313 {
314 BackendRef: gatewayv1a2.BackendRef{
315 BackendObjectReference: gatewayv1a2.BackendObjectReference{
316 Name: testService,
317 Port: ptrTo(gatewayv1a2.PortNumber(8080)),
318 },
319 Weight: ptrTo(int32(100)),
320 },
321 Filters: []gatewayv1a2.GRPCRouteFilter{
322 {
323 Type: gatewayv1a2.GRPCRouteFilterRequestMirror,
324 RequestMirror: &gatewayv1a2.HTTPRequestMirrorFilter{
325 BackendRef: gatewayv1a2.BackendObjectReference{
326 Name: testService,
327 Port: ptrTo(gatewayv1a2.PortNumber(8080)),
328 },
329 },
330 },
331 },
332 },
333 },
334 }},
335 }, {
336 name: "valid grpcRoute Rules duplicate mirror filter",
337 errCount: 0,
338 rules: []gatewayv1a2.GRPCRouteRule{{
339 BackendRefs: []gatewayv1a2.GRPCBackendRef{
340 {
341 BackendRef: gatewayv1a2.BackendRef{
342 BackendObjectReference: gatewayv1a2.BackendObjectReference{
343 Name: testService,
344 Port: ptrTo(gatewayv1a2.PortNumber(8080)),
345 },
346 },
347 Filters: []gatewayv1a2.GRPCRouteFilter{
348 {
349 Type: gatewayv1a2.GRPCRouteFilterRequestMirror,
350 RequestMirror: &gatewayv1a2.HTTPRequestMirrorFilter{
351 BackendRef: gatewayv1a2.BackendObjectReference{
352 Name: testService,
353 Port: ptrTo(gatewayv1a2.PortNumber(8080)),
354 },
355 },
356 },
357 {
358 Type: gatewayv1a2.GRPCRouteFilterRequestMirror,
359 RequestMirror: &gatewayv1a2.HTTPRequestMirrorFilter{
360 BackendRef: gatewayv1a2.BackendObjectReference{
361 Name: specialService,
362 Port: ptrTo(gatewayv1a2.PortNumber(8080)),
363 },
364 },
365 },
366 },
367 },
368 },
369 }},
370 }}
371
372 for _, tc := range tests {
373 t.Run(tc.name, func(t *testing.T) {
374 route := gatewayv1a2.GRPCRoute{Spec: gatewayv1a2.GRPCRouteSpec{Rules: tc.rules}}
375 errs := ValidateGRPCRoute(&route)
376 if len(errs) != tc.errCount {
377 t.Errorf("got %d errors, want %d errors: %s", len(errs), tc.errCount, errs)
378 }
379 })
380 }
381 }
382
383 func TestValidateGRPCHeaderMatches(t *testing.T) {
384 tests := []struct {
385 name string
386 headerMatches []gatewayv1a2.GRPCHeaderMatch
387 expectErr string
388 }{{
389 name: "no header matches",
390 headerMatches: nil,
391 expectErr: "",
392 }, {
393 name: "no header matched more than once",
394 headerMatches: []gatewayv1a2.GRPCHeaderMatch{
395 {Name: "Header-Name-1", Value: "val-1"},
396 {Name: "Header-Name-2", Value: "val-2"},
397 {Name: "Header-Name-3", Value: "val-3"},
398 },
399 expectErr: "",
400 }, {
401 name: "header matched more than once (same case)",
402 headerMatches: []gatewayv1a2.GRPCHeaderMatch{
403 {Name: "Header-Name-1", Value: "val-1"},
404 {Name: "Header-Name-2", Value: "val-2"},
405 {Name: "Header-Name-1", Value: "val-3"},
406 },
407 expectErr: "spec.rules[0].matches[0].headers: Invalid value: \"Header-Name-1\": cannot match the same header multiple times in the same rule",
408 }, {
409 name: "header matched more than once (different case)",
410 headerMatches: []gatewayv1a2.GRPCHeaderMatch{
411 {Name: "Header-Name-1", Value: "val-1"},
412 {Name: "Header-Name-2", Value: "val-2"},
413 {Name: "HEADER-NAME-2", Value: "val-3"},
414 },
415 expectErr: "spec.rules[0].matches[0].headers: Invalid value: \"Header-Name-2\": cannot match the same header multiple times in the same rule",
416 }}
417
418 for _, tc := range tests {
419 t.Run(tc.name, func(t *testing.T) {
420 route := gatewayv1a2.GRPCRoute{Spec: gatewayv1a2.GRPCRouteSpec{
421 Rules: []gatewayv1a2.GRPCRouteRule{{
422 Matches: []gatewayv1a2.GRPCRouteMatch{{
423 Headers: tc.headerMatches,
424 }},
425 BackendRefs: []gatewayv1a2.GRPCBackendRef{{
426 BackendRef: gatewayv1a2.BackendRef{
427 BackendObjectReference: gatewayv1a2.BackendObjectReference{
428 Name: gatewayv1a2.ObjectName("test"),
429 Port: ptrTo(gatewayv1a2.PortNumber(8080)),
430 },
431 },
432 }},
433 }},
434 }}
435
436 errs := ValidateGRPCRoute(&route)
437 if len(tc.expectErr) == 0 {
438 assert.Emptyf(t, errs, "expected no errors, got %d errors: %s", len(errs), errs)
439 } else {
440 require.Lenf(t, errs, 1, "expected one error, got %d errors: %s", len(errs), errs)
441 assert.Equal(t, tc.expectErr, errs[0].Error())
442 }
443 })
444 }
445 }
446
View as plain text