sigs.k8s.io/gateway-api@v1.0.0/apis/v1alpha2/validation/httproute_test.go (about) 1 /* 2 Copyright 2021 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 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 gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" 27 gatewayv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" 28 ) 29 30 func TestValidateHTTPRoute(t *testing.T) { 31 testService := gatewayv1a2.ObjectName("test-service") 32 pathPrefixMatchType := gatewayv1.PathMatchPathPrefix 33 34 tests := []struct { 35 name string 36 rules []gatewayv1a2.HTTPRouteRule 37 errCount int 38 }{{ 39 name: "valid httpRoute with no filters", 40 errCount: 0, 41 rules: []gatewayv1a2.HTTPRouteRule{ 42 { 43 Matches: []gatewayv1a2.HTTPRouteMatch{ 44 { 45 Path: &gatewayv1a2.HTTPPathMatch{ 46 Type: ptrTo(gatewayv1.PathMatchType("PathPrefix")), 47 Value: ptrTo("/"), 48 }, 49 }, 50 }, 51 BackendRefs: []gatewayv1a2.HTTPBackendRef{ 52 { 53 BackendRef: gatewayv1a2.BackendRef{ 54 BackendObjectReference: gatewayv1a2.BackendObjectReference{ 55 Name: testService, 56 Port: ptrTo(gatewayv1.PortNumber(8080)), 57 }, 58 Weight: ptrTo(int32(100)), 59 }, 60 }, 61 }, 62 }, 63 }, 64 }, { 65 name: "valid httpRoute with 1 filter", 66 errCount: 0, 67 rules: []gatewayv1a2.HTTPRouteRule{ 68 { 69 Matches: []gatewayv1a2.HTTPRouteMatch{ 70 { 71 Path: &gatewayv1a2.HTTPPathMatch{ 72 Type: ptrTo(gatewayv1.PathMatchType("PathPrefix")), 73 Value: ptrTo("/"), 74 }, 75 }, 76 }, 77 Filters: []gatewayv1a2.HTTPRouteFilter{ 78 { 79 Type: gatewayv1.HTTPRouteFilterRequestMirror, 80 RequestMirror: &gatewayv1a2.HTTPRequestMirrorFilter{ 81 BackendRef: gatewayv1a2.BackendObjectReference{ 82 Name: testService, 83 Port: ptrTo(gatewayv1.PortNumber(8081)), 84 }, 85 }, 86 }, 87 }, 88 }, 89 }, 90 }, { 91 name: "invalid httpRoute with 2 extended filters", 92 errCount: 1, 93 rules: []gatewayv1a2.HTTPRouteRule{ 94 { 95 Matches: []gatewayv1a2.HTTPRouteMatch{ 96 { 97 Path: &gatewayv1a2.HTTPPathMatch{ 98 Type: ptrTo(gatewayv1.PathMatchType("PathPrefix")), 99 Value: ptrTo("/"), 100 }, 101 }, 102 }, 103 Filters: []gatewayv1a2.HTTPRouteFilter{ 104 { 105 Type: gatewayv1.HTTPRouteFilterURLRewrite, 106 URLRewrite: &gatewayv1.HTTPURLRewriteFilter{ 107 Path: &gatewayv1.HTTPPathModifier{ 108 Type: gatewayv1.PrefixMatchHTTPPathModifier, 109 ReplacePrefixMatch: ptrTo("foo"), 110 }, 111 }, 112 }, 113 { 114 Type: gatewayv1.HTTPRouteFilterURLRewrite, 115 URLRewrite: &gatewayv1.HTTPURLRewriteFilter{ 116 Path: &gatewayv1.HTTPPathModifier{ 117 Type: gatewayv1.PrefixMatchHTTPPathModifier, 118 ReplacePrefixMatch: ptrTo("bar"), 119 }, 120 }, 121 }, 122 }, 123 }, 124 }, 125 }, { 126 name: "invalid httpRoute with mix of filters and one duplicate", 127 errCount: 1, 128 rules: []gatewayv1a2.HTTPRouteRule{ 129 { 130 Matches: []gatewayv1a2.HTTPRouteMatch{ 131 { 132 Path: &gatewayv1a2.HTTPPathMatch{ 133 Type: ptrTo(gatewayv1.PathMatchType("PathPrefix")), 134 Value: ptrTo("/"), 135 }, 136 }, 137 }, 138 Filters: []gatewayv1a2.HTTPRouteFilter{ 139 { 140 Type: gatewayv1.HTTPRouteFilterRequestHeaderModifier, 141 RequestHeaderModifier: &gatewayv1a2.HTTPHeaderFilter{ 142 Set: []gatewayv1a2.HTTPHeader{ 143 { 144 Name: "special-header", 145 Value: "foo", 146 }, 147 }, 148 }, 149 }, 150 { 151 Type: gatewayv1.HTTPRouteFilterRequestMirror, 152 RequestMirror: &gatewayv1a2.HTTPRequestMirrorFilter{ 153 BackendRef: gatewayv1a2.BackendObjectReference{ 154 Name: testService, 155 Port: ptrTo(gatewayv1.PortNumber(8080)), 156 }, 157 }, 158 }, 159 { 160 Type: gatewayv1.HTTPRouteFilterRequestHeaderModifier, 161 RequestHeaderModifier: &gatewayv1a2.HTTPHeaderFilter{ 162 Add: []gatewayv1a2.HTTPHeader{ 163 { 164 Name: "my-header", 165 Value: "bar", 166 }, 167 }, 168 }, 169 }, 170 }, 171 }, 172 }, 173 }, { 174 name: "invalid httpRoute with multiple duplicate filters", 175 errCount: 2, 176 rules: []gatewayv1a2.HTTPRouteRule{ 177 { 178 Matches: []gatewayv1a2.HTTPRouteMatch{ 179 { 180 Path: &gatewayv1a2.HTTPPathMatch{ 181 Type: ptrTo(gatewayv1.PathMatchType("PathPrefix")), 182 Value: ptrTo("/"), 183 }, 184 }, 185 }, 186 Filters: []gatewayv1a2.HTTPRouteFilter{ 187 { 188 Type: gatewayv1.HTTPRouteFilterResponseHeaderModifier, 189 ResponseHeaderModifier: &gatewayv1.HTTPHeaderFilter{ 190 Add: []gatewayv1.HTTPHeader{ 191 { 192 Name: "extra-header", 193 Value: "foo", 194 }, 195 }, 196 }, 197 }, 198 { 199 Type: gatewayv1.HTTPRouteFilterRequestHeaderModifier, 200 RequestHeaderModifier: &gatewayv1a2.HTTPHeaderFilter{ 201 Set: []gatewayv1a2.HTTPHeader{ 202 { 203 Name: "special-header", 204 Value: "foo", 205 }, 206 }, 207 }, 208 }, 209 { 210 Type: gatewayv1.HTTPRouteFilterResponseHeaderModifier, 211 ResponseHeaderModifier: &gatewayv1.HTTPHeaderFilter{ 212 Set: []gatewayv1.HTTPHeader{ 213 { 214 Name: "other-header", 215 Value: "bat", 216 }, 217 }, 218 }, 219 }, 220 { 221 Type: gatewayv1.HTTPRouteFilterRequestHeaderModifier, 222 RequestHeaderModifier: &gatewayv1a2.HTTPHeaderFilter{ 223 Add: []gatewayv1a2.HTTPHeader{ 224 { 225 Name: "my-header", 226 Value: "bar", 227 }, 228 }, 229 }, 230 }, 231 }, 232 }, 233 }, 234 }, { 235 name: "valid httpRoute with duplicate ExtensionRef filters", 236 errCount: 0, 237 rules: []gatewayv1a2.HTTPRouteRule{ 238 { 239 Matches: []gatewayv1a2.HTTPRouteMatch{ 240 { 241 Path: &gatewayv1a2.HTTPPathMatch{ 242 Type: ptrTo(gatewayv1.PathMatchType("PathPrefix")), 243 Value: ptrTo("/"), 244 }, 245 }, 246 }, 247 Filters: []gatewayv1a2.HTTPRouteFilter{ 248 { 249 Type: gatewayv1.HTTPRouteFilterRequestHeaderModifier, 250 RequestHeaderModifier: &gatewayv1a2.HTTPHeaderFilter{ 251 Set: []gatewayv1a2.HTTPHeader{ 252 { 253 Name: "special-header", 254 Value: "foo", 255 }, 256 }, 257 }, 258 }, 259 { 260 Type: gatewayv1.HTTPRouteFilterRequestMirror, 261 RequestMirror: &gatewayv1a2.HTTPRequestMirrorFilter{ 262 BackendRef: gatewayv1a2.BackendObjectReference{ 263 Name: testService, 264 Port: ptrTo(gatewayv1.PortNumber(8080)), 265 }, 266 }, 267 }, 268 { 269 Type: "ExtensionRef", 270 ExtensionRef: &gatewayv1a2.LocalObjectReference{ 271 Kind: "Service", 272 Name: "test", 273 }, 274 }, 275 { 276 Type: "ExtensionRef", 277 ExtensionRef: &gatewayv1a2.LocalObjectReference{ 278 Kind: "Service", 279 Name: "test", 280 }, 281 }, 282 { 283 Type: "ExtensionRef", 284 ExtensionRef: &gatewayv1a2.LocalObjectReference{ 285 Kind: "Service", 286 Name: "test", 287 }, 288 }, 289 }, 290 }, 291 }, 292 }, { 293 name: "valid redirect path modifier", 294 errCount: 0, 295 rules: []gatewayv1a2.HTTPRouteRule{ 296 { 297 Filters: []gatewayv1a2.HTTPRouteFilter{ 298 { 299 Type: gatewayv1.HTTPRouteFilterRequestRedirect, 300 RequestRedirect: &gatewayv1a2.HTTPRequestRedirectFilter{ 301 Path: &gatewayv1a2.HTTPPathModifier{ 302 Type: gatewayv1.FullPathHTTPPathModifier, 303 ReplaceFullPath: ptrTo("foo"), 304 }, 305 }, 306 }, 307 }, 308 }, 309 }, 310 }, { 311 name: "redirect path modifier with type mismatch", 312 errCount: 2, 313 rules: []gatewayv1a2.HTTPRouteRule{{ 314 Filters: []gatewayv1a2.HTTPRouteFilter{{ 315 Type: gatewayv1.HTTPRouteFilterRequestRedirect, 316 RequestRedirect: &gatewayv1a2.HTTPRequestRedirectFilter{ 317 Path: &gatewayv1a2.HTTPPathModifier{ 318 Type: gatewayv1.PrefixMatchHTTPPathModifier, 319 ReplaceFullPath: ptrTo("foo"), 320 }, 321 }, 322 }}, 323 }}, 324 }, { 325 name: "valid rewrite path modifier", 326 errCount: 0, 327 rules: []gatewayv1a2.HTTPRouteRule{{ 328 Matches: []gatewayv1a2.HTTPRouteMatch{{ 329 Path: &gatewayv1a2.HTTPPathMatch{ 330 Type: &pathPrefixMatchType, 331 Value: ptrTo("/bar"), 332 }, 333 }}, 334 Filters: []gatewayv1a2.HTTPRouteFilter{{ 335 Type: gatewayv1.HTTPRouteFilterURLRewrite, 336 URLRewrite: &gatewayv1a2.HTTPURLRewriteFilter{ 337 Path: &gatewayv1a2.HTTPPathModifier{ 338 Type: gatewayv1.PrefixMatchHTTPPathModifier, 339 ReplacePrefixMatch: ptrTo("foo"), 340 }, 341 }, 342 }}, 343 }}, 344 }, { 345 name: "rewrite path modifier missing path match", 346 errCount: 1, 347 rules: []gatewayv1a2.HTTPRouteRule{{ 348 Filters: []gatewayv1a2.HTTPRouteFilter{{ 349 Type: gatewayv1.HTTPRouteFilterURLRewrite, 350 URLRewrite: &gatewayv1a2.HTTPURLRewriteFilter{ 351 Path: &gatewayv1a2.HTTPPathModifier{ 352 Type: gatewayv1.PrefixMatchHTTPPathModifier, 353 ReplacePrefixMatch: ptrTo("foo"), 354 }, 355 }, 356 }}, 357 }}, 358 }, { 359 name: "rewrite path too many matches", 360 errCount: 1, 361 rules: []gatewayv1a2.HTTPRouteRule{{ 362 Matches: []gatewayv1a2.HTTPRouteMatch{{ 363 Path: &gatewayv1a2.HTTPPathMatch{ 364 Type: &pathPrefixMatchType, 365 Value: ptrTo("/foo"), 366 }, 367 }, { 368 Path: &gatewayv1a2.HTTPPathMatch{ 369 Type: &pathPrefixMatchType, 370 Value: ptrTo("/bar"), 371 }, 372 }}, 373 Filters: []gatewayv1a2.HTTPRouteFilter{{ 374 Type: gatewayv1.HTTPRouteFilterURLRewrite, 375 URLRewrite: &gatewayv1a2.HTTPURLRewriteFilter{ 376 Path: &gatewayv1a2.HTTPPathModifier{ 377 Type: gatewayv1.PrefixMatchHTTPPathModifier, 378 ReplacePrefixMatch: ptrTo("foo"), 379 }, 380 }, 381 }}, 382 }}, 383 }, { 384 name: "redirect path modifier with type mismatch", 385 errCount: 2, 386 rules: []gatewayv1a2.HTTPRouteRule{{ 387 Filters: []gatewayv1a2.HTTPRouteFilter{{ 388 Type: gatewayv1.HTTPRouteFilterURLRewrite, 389 URLRewrite: &gatewayv1a2.HTTPURLRewriteFilter{ 390 Path: &gatewayv1a2.HTTPPathModifier{ 391 Type: gatewayv1.FullPathHTTPPathModifier, 392 ReplacePrefixMatch: ptrTo("foo"), 393 }, 394 }, 395 }}, 396 }}, 397 }, { 398 name: "rewrite and redirect filters combined (invalid)", 399 errCount: 3, 400 rules: []gatewayv1a2.HTTPRouteRule{{ 401 Filters: []gatewayv1a2.HTTPRouteFilter{{ 402 Type: gatewayv1.HTTPRouteFilterURLRewrite, 403 URLRewrite: &gatewayv1a2.HTTPURLRewriteFilter{ 404 Path: &gatewayv1a2.HTTPPathModifier{ 405 Type: gatewayv1.PrefixMatchHTTPPathModifier, 406 ReplacePrefixMatch: ptrTo("foo"), 407 }, 408 }, 409 }, { 410 Type: gatewayv1.HTTPRouteFilterRequestRedirect, 411 RequestRedirect: &gatewayv1a2.HTTPRequestRedirectFilter{ 412 Path: &gatewayv1a2.HTTPPathModifier{ 413 Type: gatewayv1.PrefixMatchHTTPPathModifier, 414 ReplacePrefixMatch: ptrTo("foo"), 415 }, 416 }, 417 }}, 418 }}, 419 }} 420 421 for _, tc := range tests { 422 t.Run(tc.name, func(t *testing.T) { 423 var errs field.ErrorList 424 route := gatewayv1a2.HTTPRoute{Spec: gatewayv1a2.HTTPRouteSpec{Rules: tc.rules}} 425 errs = ValidateHTTPRoute(&route) 426 if len(errs) != tc.errCount { 427 t.Errorf("got %d errors, want %d errors: %s", len(errs), tc.errCount, errs) 428 } 429 }) 430 } 431 } 432 433 func TestValidateHTTPBackendUniqueFilters(t *testing.T) { 434 var testService gatewayv1a2.ObjectName = "testService" 435 var specialService gatewayv1a2.ObjectName = "specialService" 436 tests := []struct { 437 name string 438 rules []gatewayv1a2.HTTPRouteRule 439 errCount int 440 }{{ 441 name: "valid httpRoute Rules backendref filters", 442 errCount: 0, 443 rules: []gatewayv1a2.HTTPRouteRule{{ 444 BackendRefs: []gatewayv1a2.HTTPBackendRef{ 445 { 446 BackendRef: gatewayv1a2.BackendRef{ 447 BackendObjectReference: gatewayv1a2.BackendObjectReference{ 448 Name: testService, 449 Port: ptrTo(gatewayv1.PortNumber(8080)), 450 }, 451 Weight: ptrTo(int32(100)), 452 }, 453 Filters: []gatewayv1a2.HTTPRouteFilter{ 454 { 455 Type: gatewayv1.HTTPRouteFilterRequestMirror, 456 RequestMirror: &gatewayv1a2.HTTPRequestMirrorFilter{ 457 BackendRef: gatewayv1a2.BackendObjectReference{ 458 Name: testService, 459 Port: ptrTo(gatewayv1.PortNumber(8080)), 460 }, 461 }, 462 }, 463 }, 464 }, 465 }, 466 }}, 467 }, { 468 name: "valid httpRoute Rules duplicate mirror filter", 469 errCount: 0, 470 rules: []gatewayv1a2.HTTPRouteRule{{ 471 BackendRefs: []gatewayv1a2.HTTPBackendRef{ 472 { 473 BackendRef: gatewayv1a2.BackendRef{ 474 BackendObjectReference: gatewayv1a2.BackendObjectReference{ 475 Name: testService, 476 Port: ptrTo(gatewayv1.PortNumber(8080)), 477 }, 478 }, 479 Filters: []gatewayv1a2.HTTPRouteFilter{ 480 { 481 Type: gatewayv1.HTTPRouteFilterRequestMirror, 482 RequestMirror: &gatewayv1a2.HTTPRequestMirrorFilter{ 483 BackendRef: gatewayv1a2.BackendObjectReference{ 484 Name: testService, 485 Port: ptrTo(gatewayv1.PortNumber(8080)), 486 }, 487 }, 488 }, 489 { 490 Type: gatewayv1.HTTPRouteFilterRequestMirror, 491 RequestMirror: &gatewayv1a2.HTTPRequestMirrorFilter{ 492 BackendRef: gatewayv1a2.BackendObjectReference{ 493 Name: specialService, 494 Port: ptrTo(gatewayv1.PortNumber(8080)), 495 }, 496 }, 497 }, 498 }, 499 }, 500 }, 501 }}, 502 }} 503 504 for _, tc := range tests { 505 t.Run(tc.name, func(t *testing.T) { 506 route := gatewayv1a2.HTTPRoute{Spec: gatewayv1a2.HTTPRouteSpec{Rules: tc.rules}} 507 errs := ValidateHTTPRoute(&route) 508 if len(errs) != tc.errCount { 509 t.Errorf("got %d errors, want %d errors: %s", len(errs), tc.errCount, errs) 510 } 511 }) 512 } 513 } 514 515 func TestValidateHTTPPathMatch(t *testing.T) { 516 tests := []struct { 517 name string 518 path *gatewayv1a2.HTTPPathMatch 519 errCount int 520 }{{ 521 name: "invalid httpRoute prefix", 522 path: &gatewayv1a2.HTTPPathMatch{ 523 Type: ptrTo(gatewayv1.PathMatchType("PathPrefix")), 524 Value: ptrTo("/."), 525 }, 526 errCount: 1, 527 }, { 528 name: "invalid httpRoute Exact", 529 path: &gatewayv1a2.HTTPPathMatch{ 530 Type: ptrTo(gatewayv1.PathMatchType("Exact")), 531 Value: ptrTo("/foo/./bar"), 532 }, 533 errCount: 1, 534 }, { 535 name: "invalid httpRoute prefix", 536 path: &gatewayv1a2.HTTPPathMatch{ 537 Type: ptrTo(gatewayv1.PathMatchType("PathPrefix")), 538 Value: ptrTo("/"), 539 }, 540 errCount: 0, 541 }} 542 543 for _, tc := range tests { 544 t.Run(tc.name, func(t *testing.T) { 545 route := gatewayv1a2.HTTPRoute{Spec: gatewayv1a2.HTTPRouteSpec{ 546 Rules: []gatewayv1a2.HTTPRouteRule{{ 547 Matches: []gatewayv1a2.HTTPRouteMatch{{ 548 Path: tc.path, 549 }}, 550 BackendRefs: []gatewayv1a2.HTTPBackendRef{{ 551 BackendRef: gatewayv1a2.BackendRef{ 552 BackendObjectReference: gatewayv1a2.BackendObjectReference{ 553 Name: gatewayv1a2.ObjectName("test"), 554 Port: ptrTo(gatewayv1.PortNumber(8080)), 555 }, 556 }, 557 }}, 558 }}, 559 }} 560 561 errs := ValidateHTTPRoute(&route) 562 if len(errs) != tc.errCount { 563 t.Errorf("got %d errors, want %d errors: %s", len(errs), tc.errCount, errs) 564 } 565 }) 566 } 567 } 568 569 func TestValidateHTTPHeaderMatches(t *testing.T) { 570 tests := []struct { 571 name string 572 headerMatches []gatewayv1a2.HTTPHeaderMatch 573 expectErr string 574 }{{ 575 name: "no header matches", 576 headerMatches: nil, 577 expectErr: "", 578 }, { 579 name: "no header matched more than once", 580 headerMatches: []gatewayv1a2.HTTPHeaderMatch{ 581 {Name: "Header-Name-1", Value: "val-1"}, 582 {Name: "Header-Name-2", Value: "val-2"}, 583 {Name: "Header-Name-3", Value: "val-3"}, 584 }, 585 expectErr: "", 586 }, { 587 name: "header matched more than once (same case)", 588 headerMatches: []gatewayv1a2.HTTPHeaderMatch{ 589 {Name: "Header-Name-1", Value: "val-1"}, 590 {Name: "Header-Name-2", Value: "val-2"}, 591 {Name: "Header-Name-1", Value: "val-3"}, 592 }, 593 expectErr: "spec.rules[0].matches[0].headers: Invalid value: \"Header-Name-1\": cannot match the same header multiple times in the same rule", 594 }, { 595 name: "header matched more than once (different case)", 596 headerMatches: []gatewayv1a2.HTTPHeaderMatch{ 597 {Name: "Header-Name-1", Value: "val-1"}, 598 {Name: "Header-Name-2", Value: "val-2"}, 599 {Name: "HEADER-NAME-2", Value: "val-3"}, 600 }, 601 expectErr: "spec.rules[0].matches[0].headers: Invalid value: \"Header-Name-2\": cannot match the same header multiple times in the same rule", 602 }} 603 604 for _, tc := range tests { 605 t.Run(tc.name, func(t *testing.T) { 606 route := gatewayv1a2.HTTPRoute{Spec: gatewayv1a2.HTTPRouteSpec{ 607 Rules: []gatewayv1a2.HTTPRouteRule{{ 608 Matches: []gatewayv1a2.HTTPRouteMatch{{ 609 Headers: tc.headerMatches, 610 }}, 611 BackendRefs: []gatewayv1a2.HTTPBackendRef{{ 612 BackendRef: gatewayv1a2.BackendRef{ 613 BackendObjectReference: gatewayv1a2.BackendObjectReference{ 614 Name: gatewayv1a2.ObjectName("test"), 615 Port: ptrTo(gatewayv1.PortNumber(8080)), 616 }, 617 }, 618 }}, 619 }}, 620 }} 621 622 errs := ValidateHTTPRoute(&route) 623 if len(tc.expectErr) == 0 { 624 assert.Emptyf(t, errs, "expected no errors, got %d errors: %s", len(errs), errs) 625 } else { 626 require.Lenf(t, errs, 1, "expected one error, got %d errors: %s", len(errs), errs) 627 assert.Equal(t, tc.expectErr, errs[0].Error()) 628 } 629 }) 630 } 631 } 632 633 func TestValidateHTTPQueryParamMatches(t *testing.T) { 634 tests := []struct { 635 name string 636 queryParamMatches []gatewayv1a2.HTTPQueryParamMatch 637 expectErr string 638 }{{ 639 name: "no query param matches", 640 queryParamMatches: nil, 641 expectErr: "", 642 }, { 643 name: "no query param matched more than once", 644 queryParamMatches: []gatewayv1a2.HTTPQueryParamMatch{ 645 {Name: "query-param-1", Value: "val-1"}, 646 {Name: "query-param-2", Value: "val-2"}, 647 {Name: "query-param-3", Value: "val-3"}, 648 }, 649 expectErr: "", 650 }, { 651 name: "query param matched more than once", 652 queryParamMatches: []gatewayv1a2.HTTPQueryParamMatch{ 653 {Name: "query-param-1", Value: "val-1"}, 654 {Name: "query-param-2", Value: "val-2"}, 655 {Name: "query-param-1", Value: "val-3"}, 656 }, 657 expectErr: "spec.rules[0].matches[0].queryParams: Invalid value: \"query-param-1\": cannot match the same query parameter multiple times in the same rule", 658 }, { 659 name: "query param names with different casing are not considered duplicates", 660 queryParamMatches: []gatewayv1a2.HTTPQueryParamMatch{ 661 {Name: "query-param-1", Value: "val-1"}, 662 {Name: "query-param-2", Value: "val-2"}, 663 {Name: "QUERY-PARAM-1", Value: "val-3"}, 664 }, 665 expectErr: "", 666 }} 667 668 for _, tc := range tests { 669 t.Run(tc.name, func(t *testing.T) { 670 route := gatewayv1a2.HTTPRoute{Spec: gatewayv1a2.HTTPRouteSpec{ 671 Rules: []gatewayv1a2.HTTPRouteRule{{ 672 Matches: []gatewayv1a2.HTTPRouteMatch{{ 673 QueryParams: tc.queryParamMatches, 674 }}, 675 BackendRefs: []gatewayv1a2.HTTPBackendRef{{ 676 BackendRef: gatewayv1a2.BackendRef{ 677 BackendObjectReference: gatewayv1a2.BackendObjectReference{ 678 Name: gatewayv1a2.ObjectName("test"), 679 Port: ptrTo(gatewayv1.PortNumber(8080)), 680 }, 681 }, 682 }}, 683 }}, 684 }} 685 686 errs := ValidateHTTPRoute(&route) 687 if len(tc.expectErr) == 0 { 688 assert.Emptyf(t, errs, "expected no errors, got %d errors: %s", len(errs), errs) 689 } else { 690 require.Lenf(t, errs, 1, "expected one error, got %d errors: %s", len(errs), errs) 691 assert.Equal(t, tc.expectErr, errs[0].Error()) 692 } 693 }) 694 } 695 } 696 697 func TestValidateServicePort(t *testing.T) { 698 portPtr := func(n int) *gatewayv1a2.PortNumber { 699 p := gatewayv1a2.PortNumber(n) 700 return &p 701 } 702 703 groupPtr := func(g string) *gatewayv1a2.Group { 704 p := gatewayv1a2.Group(g) 705 return &p 706 } 707 708 kindPtr := func(k string) *gatewayv1a2.Kind { 709 p := gatewayv1a2.Kind(k) 710 return &p 711 } 712 713 tests := []struct { 714 name string 715 rules []gatewayv1a2.HTTPRouteRule 716 errCount int 717 }{{ 718 name: "default groupkind with port", 719 errCount: 0, 720 rules: []gatewayv1a2.HTTPRouteRule{{ 721 BackendRefs: []gatewayv1a2.HTTPBackendRef{{ 722 BackendRef: gatewayv1a2.BackendRef{ 723 BackendObjectReference: gatewayv1a2.BackendObjectReference{ 724 Name: "backend", 725 Port: portPtr(99), 726 }, 727 }, 728 }}, 729 }}, 730 }, { 731 name: "default groupkind with no port", 732 errCount: 1, 733 rules: []gatewayv1a2.HTTPRouteRule{{ 734 BackendRefs: []gatewayv1a2.HTTPBackendRef{{ 735 BackendRef: gatewayv1a2.BackendRef{ 736 BackendObjectReference: gatewayv1a2.BackendObjectReference{ 737 Name: "backend", 738 }, 739 }, 740 }}, 741 }}, 742 }, { 743 name: "explicit service with port", 744 errCount: 0, 745 rules: []gatewayv1a2.HTTPRouteRule{{ 746 BackendRefs: []gatewayv1a2.HTTPBackendRef{{ 747 BackendRef: gatewayv1a2.BackendRef{ 748 BackendObjectReference: gatewayv1a2.BackendObjectReference{ 749 Group: groupPtr(""), 750 Kind: kindPtr("Service"), 751 Name: "backend", 752 Port: portPtr(99), 753 }, 754 }, 755 }}, 756 }}, 757 }, { 758 name: "explicit service with no port", 759 errCount: 1, 760 rules: []gatewayv1a2.HTTPRouteRule{{ 761 BackendRefs: []gatewayv1a2.HTTPBackendRef{{ 762 BackendRef: gatewayv1a2.BackendRef{ 763 BackendObjectReference: gatewayv1a2.BackendObjectReference{ 764 Group: groupPtr(""), 765 Kind: kindPtr("Service"), 766 Name: "backend", 767 }, 768 }, 769 }}, 770 }}, 771 }, { 772 name: "explicit ref with no port", 773 errCount: 0, 774 rules: []gatewayv1a2.HTTPRouteRule{{ 775 BackendRefs: []gatewayv1a2.HTTPBackendRef{{ 776 BackendRef: gatewayv1a2.BackendRef{ 777 BackendObjectReference: gatewayv1a2.BackendObjectReference{ 778 Group: groupPtr("foo.example.com"), 779 Kind: kindPtr("Foo"), 780 Name: "backend", 781 }, 782 }, 783 }}, 784 }}, 785 }} 786 787 for _, tc := range tests { 788 t.Run(tc.name, func(t *testing.T) { 789 route := gatewayv1a2.HTTPRoute{Spec: gatewayv1a2.HTTPRouteSpec{Rules: tc.rules}} 790 errs := ValidateHTTPRoute(&route) 791 if len(errs) != tc.errCount { 792 t.Errorf("got %d errors, want %d errors: %s", len(errs), tc.errCount, errs) 793 } 794 }) 795 } 796 } 797 798 func TestValidateHTTPRouteTypeMatchesField(t *testing.T) { 799 tests := []struct { 800 name string 801 routeFilter gatewayv1a2.HTTPRouteFilter 802 errCount int 803 }{{ 804 name: "valid HTTPRouteFilterRequestHeaderModifier route filter", 805 routeFilter: gatewayv1a2.HTTPRouteFilter{ 806 Type: gatewayv1.HTTPRouteFilterRequestHeaderModifier, 807 RequestHeaderModifier: &gatewayv1a2.HTTPHeaderFilter{ 808 Set: []gatewayv1a2.HTTPHeader{{Name: "name"}}, 809 Add: []gatewayv1a2.HTTPHeader{{Name: "add"}}, 810 Remove: []string{"remove"}, 811 }, 812 }, 813 errCount: 0, 814 }, { 815 name: "invalid HTTPRouteFilterRequestHeaderModifier type filter with non-matching field", 816 routeFilter: gatewayv1a2.HTTPRouteFilter{ 817 Type: gatewayv1.HTTPRouteFilterRequestHeaderModifier, 818 RequestMirror: &gatewayv1a2.HTTPRequestMirrorFilter{}, 819 }, 820 errCount: 2, 821 }, { 822 name: "invalid HTTPRouteFilterRequestHeaderModifier type filter with empty value field", 823 routeFilter: gatewayv1a2.HTTPRouteFilter{ 824 Type: gatewayv1.HTTPRouteFilterRequestHeaderModifier, 825 }, 826 errCount: 1, 827 }, { 828 name: "valid HTTPRouteFilterRequestMirror route filter", 829 routeFilter: gatewayv1a2.HTTPRouteFilter{ 830 Type: gatewayv1.HTTPRouteFilterRequestMirror, 831 RequestMirror: &gatewayv1a2.HTTPRequestMirrorFilter{BackendRef: gatewayv1a2.BackendObjectReference{ 832 Group: new(gatewayv1a2.Group), 833 Kind: new(gatewayv1a2.Kind), 834 Name: "name", 835 Namespace: new(gatewayv1a2.Namespace), 836 Port: ptrTo(gatewayv1.PortNumber(22)), 837 }}, 838 }, 839 errCount: 0, 840 }, { 841 name: "invalid HTTPRouteFilterRequestMirror type filter with non-matching field", 842 routeFilter: gatewayv1a2.HTTPRouteFilter{ 843 Type: gatewayv1.HTTPRouteFilterRequestMirror, 844 RequestHeaderModifier: &gatewayv1a2.HTTPHeaderFilter{}, 845 }, 846 errCount: 2, 847 }, { 848 name: "invalid HTTPRouteFilterRequestMirror type filter with empty value field", 849 routeFilter: gatewayv1a2.HTTPRouteFilter{ 850 Type: gatewayv1.HTTPRouteFilterRequestMirror, 851 }, 852 errCount: 1, 853 }, { 854 name: "valid HTTPRouteFilterRequestRedirect route filter", 855 routeFilter: gatewayv1a2.HTTPRouteFilter{ 856 Type: gatewayv1.HTTPRouteFilterRequestRedirect, 857 RequestRedirect: &gatewayv1a2.HTTPRequestRedirectFilter{ 858 Scheme: new(string), 859 Hostname: new(gatewayv1a2.PreciseHostname), 860 Path: &gatewayv1a2.HTTPPathModifier{}, 861 Port: new(gatewayv1a2.PortNumber), 862 StatusCode: new(int), 863 }, 864 }, 865 errCount: 1, 866 }, { 867 name: "invalid HTTPRouteFilterRequestRedirect type filter with non-matching field", 868 routeFilter: gatewayv1a2.HTTPRouteFilter{ 869 Type: gatewayv1.HTTPRouteFilterRequestRedirect, 870 RequestMirror: &gatewayv1a2.HTTPRequestMirrorFilter{}, 871 }, 872 errCount: 2, 873 }, { 874 name: "invalid HTTPRouteFilterRequestRedirect type filter with empty value field", 875 routeFilter: gatewayv1a2.HTTPRouteFilter{ 876 Type: gatewayv1.HTTPRouteFilterRequestRedirect, 877 }, 878 errCount: 1, 879 }, { 880 name: "valid HTTPRouteFilterExtensionRef filter", 881 routeFilter: gatewayv1a2.HTTPRouteFilter{ 882 Type: gatewayv1.HTTPRouteFilterExtensionRef, 883 ExtensionRef: &gatewayv1a2.LocalObjectReference{ 884 Group: "group", 885 Kind: "kind", 886 Name: "name", 887 }, 888 }, 889 errCount: 0, 890 }, { 891 name: "invalid HTTPRouteFilterExtensionRef type filter with non-matching field", 892 routeFilter: gatewayv1a2.HTTPRouteFilter{ 893 Type: gatewayv1.HTTPRouteFilterExtensionRef, 894 RequestMirror: &gatewayv1a2.HTTPRequestMirrorFilter{}, 895 }, 896 errCount: 2, 897 }, { 898 name: "invalid HTTPRouteFilterExtensionRef type filter with empty value field", 899 routeFilter: gatewayv1a2.HTTPRouteFilter{ 900 Type: gatewayv1.HTTPRouteFilterExtensionRef, 901 }, 902 errCount: 1, 903 }, { 904 name: "valid HTTPRouteFilterURLRewrite route filter", 905 routeFilter: gatewayv1a2.HTTPRouteFilter{ 906 Type: gatewayv1.HTTPRouteFilterURLRewrite, 907 URLRewrite: &gatewayv1a2.HTTPURLRewriteFilter{ 908 Hostname: new(gatewayv1a2.PreciseHostname), 909 Path: &gatewayv1a2.HTTPPathModifier{}, 910 }, 911 }, 912 errCount: 0, 913 }, { 914 name: "invalid HTTPRouteFilterURLRewrite type filter with non-matching field", 915 routeFilter: gatewayv1a2.HTTPRouteFilter{ 916 Type: gatewayv1.HTTPRouteFilterURLRewrite, 917 RequestMirror: &gatewayv1a2.HTTPRequestMirrorFilter{}, 918 }, 919 errCount: 2, 920 }, { 921 name: "invalid HTTPRouteFilterURLRewrite type filter with empty value field", 922 routeFilter: gatewayv1a2.HTTPRouteFilter{ 923 Type: gatewayv1.HTTPRouteFilterURLRewrite, 924 }, 925 errCount: 1, 926 }, { 927 name: "empty type filter is valid (caught by CRD validation)", 928 routeFilter: gatewayv1a2.HTTPRouteFilter{}, 929 errCount: 0, 930 }} 931 932 for _, tc := range tests { 933 t.Run(tc.name, func(t *testing.T) { 934 route := gatewayv1a2.HTTPRoute{ 935 Spec: gatewayv1a2.HTTPRouteSpec{ 936 Rules: []gatewayv1a2.HTTPRouteRule{{ 937 Filters: []gatewayv1a2.HTTPRouteFilter{tc.routeFilter}, 938 BackendRefs: []gatewayv1a2.HTTPBackendRef{{ 939 BackendRef: gatewayv1a2.BackendRef{ 940 BackendObjectReference: gatewayv1a2.BackendObjectReference{ 941 Name: gatewayv1a2.ObjectName("test"), 942 Port: ptrTo(gatewayv1.PortNumber(8080)), 943 }, 944 }, 945 }}, 946 }}, 947 }, 948 } 949 errs := ValidateHTTPRoute(&route) 950 if len(errs) != tc.errCount { 951 t.Errorf("got %d errors, want %d errors: %s", len(errs), tc.errCount, errs) 952 } 953 }) 954 } 955 }