sigs.k8s.io/gateway-api@v1.0.0/apis/v1/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 ) 28 29 func TestValidateHTTPRoute(t *testing.T) { 30 testService := gatewayv1.ObjectName("test-service") 31 pathPrefixMatchType := gatewayv1.PathMatchPathPrefix 32 33 tests := []struct { 34 name string 35 rules []gatewayv1.HTTPRouteRule 36 errCount int 37 }{{ 38 name: "valid httpRoute with no filters", 39 errCount: 0, 40 rules: []gatewayv1.HTTPRouteRule{ 41 { 42 Matches: []gatewayv1.HTTPRouteMatch{ 43 { 44 Path: &gatewayv1.HTTPPathMatch{ 45 Type: ptrTo(gatewayv1.PathMatchType("PathPrefix")), 46 Value: ptrTo("/"), 47 }, 48 }, 49 }, 50 BackendRefs: []gatewayv1.HTTPBackendRef{ 51 { 52 BackendRef: gatewayv1.BackendRef{ 53 BackendObjectReference: gatewayv1.BackendObjectReference{ 54 Name: testService, 55 Port: ptrTo(gatewayv1.PortNumber(8080)), 56 }, 57 Weight: ptrTo(int32(100)), 58 }, 59 }, 60 }, 61 }, 62 }, 63 }, { 64 name: "valid httpRoute with 1 filter", 65 errCount: 0, 66 rules: []gatewayv1.HTTPRouteRule{ 67 { 68 Matches: []gatewayv1.HTTPRouteMatch{ 69 { 70 Path: &gatewayv1.HTTPPathMatch{ 71 Type: ptrTo(gatewayv1.PathMatchType("PathPrefix")), 72 Value: ptrTo("/"), 73 }, 74 }, 75 }, 76 Filters: []gatewayv1.HTTPRouteFilter{ 77 { 78 Type: gatewayv1.HTTPRouteFilterRequestMirror, 79 RequestMirror: &gatewayv1.HTTPRequestMirrorFilter{ 80 BackendRef: gatewayv1.BackendObjectReference{ 81 Name: testService, 82 Port: ptrTo(gatewayv1.PortNumber(8081)), 83 }, 84 }, 85 }, 86 }, 87 }, 88 }, 89 }, { 90 name: "invalid httpRoute with 2 extended filters", 91 errCount: 1, 92 rules: []gatewayv1.HTTPRouteRule{ 93 { 94 Matches: []gatewayv1.HTTPRouteMatch{ 95 { 96 Path: &gatewayv1.HTTPPathMatch{ 97 Type: ptrTo(gatewayv1.PathMatchType("PathPrefix")), 98 Value: ptrTo("/"), 99 }, 100 }, 101 }, 102 Filters: []gatewayv1.HTTPRouteFilter{ 103 { 104 Type: gatewayv1.HTTPRouteFilterURLRewrite, 105 URLRewrite: &gatewayv1.HTTPURLRewriteFilter{ 106 Path: &gatewayv1.HTTPPathModifier{ 107 Type: gatewayv1.PrefixMatchHTTPPathModifier, 108 ReplacePrefixMatch: ptrTo("foo"), 109 }, 110 }, 111 }, 112 { 113 Type: gatewayv1.HTTPRouteFilterURLRewrite, 114 URLRewrite: &gatewayv1.HTTPURLRewriteFilter{ 115 Path: &gatewayv1.HTTPPathModifier{ 116 Type: gatewayv1.PrefixMatchHTTPPathModifier, 117 ReplacePrefixMatch: ptrTo("bar"), 118 }, 119 }, 120 }, 121 }, 122 }, 123 }, 124 }, { 125 name: "invalid httpRoute with mix of filters and one duplicate", 126 errCount: 1, 127 rules: []gatewayv1.HTTPRouteRule{ 128 { 129 Matches: []gatewayv1.HTTPRouteMatch{ 130 { 131 Path: &gatewayv1.HTTPPathMatch{ 132 Type: ptrTo(gatewayv1.PathMatchType("PathPrefix")), 133 Value: ptrTo("/"), 134 }, 135 }, 136 }, 137 Filters: []gatewayv1.HTTPRouteFilter{ 138 { 139 Type: gatewayv1.HTTPRouteFilterRequestHeaderModifier, 140 RequestHeaderModifier: &gatewayv1.HTTPHeaderFilter{ 141 Set: []gatewayv1.HTTPHeader{ 142 { 143 Name: "special-header", 144 Value: "foo", 145 }, 146 }, 147 }, 148 }, 149 { 150 Type: gatewayv1.HTTPRouteFilterRequestMirror, 151 RequestMirror: &gatewayv1.HTTPRequestMirrorFilter{ 152 BackendRef: gatewayv1.BackendObjectReference{ 153 Name: testService, 154 Port: ptrTo(gatewayv1.PortNumber(8080)), 155 }, 156 }, 157 }, 158 { 159 Type: gatewayv1.HTTPRouteFilterRequestHeaderModifier, 160 RequestHeaderModifier: &gatewayv1.HTTPHeaderFilter{ 161 Add: []gatewayv1.HTTPHeader{ 162 { 163 Name: "my-header", 164 Value: "bar", 165 }, 166 }, 167 }, 168 }, 169 }, 170 }, 171 }, 172 }, { 173 name: "invalid httpRoute with multiple duplicate filters", 174 errCount: 2, 175 rules: []gatewayv1.HTTPRouteRule{ 176 { 177 Matches: []gatewayv1.HTTPRouteMatch{ 178 { 179 Path: &gatewayv1.HTTPPathMatch{ 180 Type: ptrTo(gatewayv1.PathMatchType("PathPrefix")), 181 Value: ptrTo("/"), 182 }, 183 }, 184 }, 185 Filters: []gatewayv1.HTTPRouteFilter{ 186 { 187 Type: gatewayv1.HTTPRouteFilterRequestHeaderModifier, 188 RequestHeaderModifier: &gatewayv1.HTTPHeaderFilter{ 189 Set: []gatewayv1.HTTPHeader{ 190 { 191 Name: "special-header", 192 Value: "foo", 193 }, 194 }, 195 }, 196 }, 197 { 198 Type: gatewayv1.HTTPRouteFilterRequestHeaderModifier, 199 RequestHeaderModifier: &gatewayv1.HTTPHeaderFilter{ 200 Add: []gatewayv1.HTTPHeader{ 201 { 202 Name: "my-header", 203 Value: "bar", 204 }, 205 }, 206 }, 207 }, 208 { 209 Type: gatewayv1.HTTPRouteFilterResponseHeaderModifier, 210 ResponseHeaderModifier: &gatewayv1.HTTPHeaderFilter{ 211 Add: []gatewayv1.HTTPHeader{ 212 { 213 Name: "extra-header", 214 Value: "foo", 215 }, 216 }, 217 }, 218 }, 219 { 220 Type: gatewayv1.HTTPRouteFilterResponseHeaderModifier, 221 ResponseHeaderModifier: &gatewayv1.HTTPHeaderFilter{ 222 Set: []gatewayv1.HTTPHeader{ 223 { 224 Name: "other-header", 225 Value: "bat", 226 }, 227 }, 228 }, 229 }, 230 }, 231 }, 232 }, 233 }, { 234 name: "valid httpRoute with duplicate ExtensionRef filters", 235 errCount: 0, 236 rules: []gatewayv1.HTTPRouteRule{ 237 { 238 Matches: []gatewayv1.HTTPRouteMatch{ 239 { 240 Path: &gatewayv1.HTTPPathMatch{ 241 Type: ptrTo(gatewayv1.PathMatchType("PathPrefix")), 242 Value: ptrTo("/"), 243 }, 244 }, 245 }, 246 Filters: []gatewayv1.HTTPRouteFilter{ 247 { 248 Type: gatewayv1.HTTPRouteFilterRequestHeaderModifier, 249 RequestHeaderModifier: &gatewayv1.HTTPHeaderFilter{ 250 Set: []gatewayv1.HTTPHeader{ 251 { 252 Name: "special-header", 253 Value: "foo", 254 }, 255 }, 256 }, 257 }, 258 { 259 Type: gatewayv1.HTTPRouteFilterRequestMirror, 260 RequestMirror: &gatewayv1.HTTPRequestMirrorFilter{ 261 BackendRef: gatewayv1.BackendObjectReference{ 262 Name: testService, 263 Port: ptrTo(gatewayv1.PortNumber(8080)), 264 }, 265 }, 266 }, 267 { 268 Type: "ExtensionRef", 269 ExtensionRef: &gatewayv1.LocalObjectReference{ 270 Kind: "Service", 271 Name: "test", 272 }, 273 }, 274 { 275 Type: "ExtensionRef", 276 ExtensionRef: &gatewayv1.LocalObjectReference{ 277 Kind: "Service", 278 Name: "test", 279 }, 280 }, 281 { 282 Type: "ExtensionRef", 283 ExtensionRef: &gatewayv1.LocalObjectReference{ 284 Kind: "Service", 285 Name: "test", 286 }, 287 }, 288 }, 289 }, 290 }, 291 }, { 292 name: "valid redirect path modifier", 293 errCount: 0, 294 rules: []gatewayv1.HTTPRouteRule{ 295 { 296 Filters: []gatewayv1.HTTPRouteFilter{ 297 { 298 Type: gatewayv1.HTTPRouteFilterRequestRedirect, 299 RequestRedirect: &gatewayv1.HTTPRequestRedirectFilter{ 300 Path: &gatewayv1.HTTPPathModifier{ 301 Type: gatewayv1.FullPathHTTPPathModifier, 302 ReplaceFullPath: ptrTo("foo"), 303 }, 304 }, 305 }, 306 }, 307 }, 308 }, 309 }, { 310 name: "redirect path modifier with type mismatch", 311 errCount: 2, 312 rules: []gatewayv1.HTTPRouteRule{{ 313 Filters: []gatewayv1.HTTPRouteFilter{{ 314 Type: gatewayv1.HTTPRouteFilterRequestRedirect, 315 RequestRedirect: &gatewayv1.HTTPRequestRedirectFilter{ 316 Path: &gatewayv1.HTTPPathModifier{ 317 Type: gatewayv1.PrefixMatchHTTPPathModifier, 318 ReplaceFullPath: ptrTo("foo"), 319 }, 320 }, 321 }}, 322 }}, 323 }, { 324 name: "valid rewrite path modifier", 325 errCount: 0, 326 rules: []gatewayv1.HTTPRouteRule{{ 327 Matches: []gatewayv1.HTTPRouteMatch{{ 328 Path: &gatewayv1.HTTPPathMatch{ 329 Type: &pathPrefixMatchType, 330 Value: ptrTo("/bar"), 331 }, 332 }}, 333 Filters: []gatewayv1.HTTPRouteFilter{{ 334 Type: gatewayv1.HTTPRouteFilterURLRewrite, 335 URLRewrite: &gatewayv1.HTTPURLRewriteFilter{ 336 Path: &gatewayv1.HTTPPathModifier{ 337 Type: gatewayv1.PrefixMatchHTTPPathModifier, 338 ReplacePrefixMatch: ptrTo("foo"), 339 }, 340 }, 341 }}, 342 }}, 343 }, { 344 name: "rewrite path modifier missing path match", 345 errCount: 1, 346 rules: []gatewayv1.HTTPRouteRule{{ 347 Filters: []gatewayv1.HTTPRouteFilter{{ 348 Type: gatewayv1.HTTPRouteFilterURLRewrite, 349 URLRewrite: &gatewayv1.HTTPURLRewriteFilter{ 350 Path: &gatewayv1.HTTPPathModifier{ 351 Type: gatewayv1.PrefixMatchHTTPPathModifier, 352 ReplacePrefixMatch: ptrTo("foo"), 353 }, 354 }, 355 }}, 356 }}, 357 }, { 358 name: "rewrite path too many matches", 359 errCount: 1, 360 rules: []gatewayv1.HTTPRouteRule{{ 361 Matches: []gatewayv1.HTTPRouteMatch{{ 362 Path: &gatewayv1.HTTPPathMatch{ 363 Type: &pathPrefixMatchType, 364 Value: ptrTo("/foo"), 365 }, 366 }, { 367 Path: &gatewayv1.HTTPPathMatch{ 368 Type: &pathPrefixMatchType, 369 Value: ptrTo("/bar"), 370 }, 371 }}, 372 Filters: []gatewayv1.HTTPRouteFilter{{ 373 Type: gatewayv1.HTTPRouteFilterURLRewrite, 374 URLRewrite: &gatewayv1.HTTPURLRewriteFilter{ 375 Path: &gatewayv1.HTTPPathModifier{ 376 Type: gatewayv1.PrefixMatchHTTPPathModifier, 377 ReplacePrefixMatch: ptrTo("foo"), 378 }, 379 }, 380 }}, 381 }}, 382 }, { 383 name: "redirect path modifier with type mismatch", 384 errCount: 2, 385 rules: []gatewayv1.HTTPRouteRule{{ 386 Filters: []gatewayv1.HTTPRouteFilter{{ 387 Type: gatewayv1.HTTPRouteFilterURLRewrite, 388 URLRewrite: &gatewayv1.HTTPURLRewriteFilter{ 389 Path: &gatewayv1.HTTPPathModifier{ 390 Type: gatewayv1.FullPathHTTPPathModifier, 391 ReplacePrefixMatch: ptrTo("foo"), 392 }, 393 }, 394 }}, 395 }}, 396 }, { 397 name: "rewrite and redirect filters combined (invalid)", 398 errCount: 3, 399 rules: []gatewayv1.HTTPRouteRule{{ 400 Filters: []gatewayv1.HTTPRouteFilter{{ 401 Type: gatewayv1.HTTPRouteFilterURLRewrite, 402 URLRewrite: &gatewayv1.HTTPURLRewriteFilter{ 403 Path: &gatewayv1.HTTPPathModifier{ 404 Type: gatewayv1.PrefixMatchHTTPPathModifier, 405 ReplacePrefixMatch: ptrTo("foo"), 406 }, 407 }, 408 }, { 409 Type: gatewayv1.HTTPRouteFilterRequestRedirect, 410 RequestRedirect: &gatewayv1.HTTPRequestRedirectFilter{ 411 Path: &gatewayv1.HTTPPathModifier{ 412 Type: gatewayv1.PrefixMatchHTTPPathModifier, 413 ReplacePrefixMatch: ptrTo("foo"), 414 }, 415 }, 416 }}, 417 }}, 418 }, { 419 name: "multiple actions for the same request header (invalid)", 420 errCount: 2, 421 rules: []gatewayv1.HTTPRouteRule{{ 422 Filters: []gatewayv1.HTTPRouteFilter{{ 423 Type: gatewayv1.HTTPRouteFilterRequestHeaderModifier, 424 RequestHeaderModifier: &gatewayv1.HTTPHeaderFilter{ 425 Add: []gatewayv1.HTTPHeader{ 426 { 427 Name: gatewayv1.HTTPHeaderName("x-fruit"), 428 Value: "apple", 429 }, 430 { 431 Name: gatewayv1.HTTPHeaderName("x-vegetable"), 432 Value: "carrot", 433 }, 434 { 435 Name: gatewayv1.HTTPHeaderName("x-grain"), 436 Value: "rye", 437 }, 438 }, 439 Set: []gatewayv1.HTTPHeader{ 440 { 441 Name: gatewayv1.HTTPHeaderName("x-fruit"), 442 Value: "watermelon", 443 }, 444 { 445 Name: gatewayv1.HTTPHeaderName("x-grain"), 446 Value: "wheat", 447 }, 448 { 449 Name: gatewayv1.HTTPHeaderName("x-spice"), 450 Value: "coriander", 451 }, 452 }, 453 }, 454 }}, 455 }}, 456 }, { 457 name: "multiple actions for the same request header with inconsistent case (invalid)", 458 errCount: 1, 459 rules: []gatewayv1.HTTPRouteRule{{ 460 Filters: []gatewayv1.HTTPRouteFilter{{ 461 Type: gatewayv1.HTTPRouteFilterRequestHeaderModifier, 462 RequestHeaderModifier: &gatewayv1.HTTPHeaderFilter{ 463 Add: []gatewayv1.HTTPHeader{ 464 { 465 Name: gatewayv1.HTTPHeaderName("x-fruit"), 466 Value: "apple", 467 }, 468 }, 469 Set: []gatewayv1.HTTPHeader{ 470 { 471 Name: gatewayv1.HTTPHeaderName("X-Fruit"), 472 Value: "watermelon", 473 }, 474 }, 475 }, 476 }}, 477 }}, 478 }, { 479 name: "multiple of the same action for the same request header (invalid)", 480 errCount: 1, 481 rules: []gatewayv1.HTTPRouteRule{{ 482 Filters: []gatewayv1.HTTPRouteFilter{{ 483 Type: gatewayv1.HTTPRouteFilterRequestHeaderModifier, 484 RequestHeaderModifier: &gatewayv1.HTTPHeaderFilter{ 485 Add: []gatewayv1.HTTPHeader{ 486 { 487 Name: gatewayv1.HTTPHeaderName("x-fruit"), 488 Value: "apple", 489 }, 490 { 491 Name: gatewayv1.HTTPHeaderName("x-fruit"), 492 Value: "plum", 493 }, 494 }, 495 }, 496 }}, 497 }}, 498 }, { 499 name: "multiple actions for different request headers", 500 errCount: 0, 501 rules: []gatewayv1.HTTPRouteRule{{ 502 Filters: []gatewayv1.HTTPRouteFilter{{ 503 Type: gatewayv1.HTTPRouteFilterRequestHeaderModifier, 504 RequestHeaderModifier: &gatewayv1.HTTPHeaderFilter{ 505 Add: []gatewayv1.HTTPHeader{ 506 { 507 Name: gatewayv1.HTTPHeaderName("x-vegetable"), 508 Value: "carrot", 509 }, 510 { 511 Name: gatewayv1.HTTPHeaderName("x-grain"), 512 Value: "rye", 513 }, 514 }, 515 Set: []gatewayv1.HTTPHeader{ 516 { 517 Name: gatewayv1.HTTPHeaderName("x-fruit"), 518 Value: "watermelon", 519 }, 520 { 521 Name: gatewayv1.HTTPHeaderName("x-spice"), 522 Value: "coriander", 523 }, 524 }, 525 }, 526 }}, 527 }}, 528 }, { 529 name: "multiple actions for the same response header (invalid)", 530 errCount: 1, 531 rules: []gatewayv1.HTTPRouteRule{{ 532 Filters: []gatewayv1.HTTPRouteFilter{{ 533 Type: gatewayv1.HTTPRouteFilterResponseHeaderModifier, 534 ResponseHeaderModifier: &gatewayv1.HTTPHeaderFilter{ 535 Add: []gatewayv1.HTTPHeader{{ 536 Name: gatewayv1.HTTPHeaderName("x-example"), 537 Value: "blueberry", 538 }}, 539 Set: []gatewayv1.HTTPHeader{{ 540 Name: gatewayv1.HTTPHeaderName("x-example"), 541 Value: "turnip", 542 }}, 543 }, 544 }}, 545 }}, 546 }, { 547 name: "multiple actions for different response headers", 548 errCount: 0, 549 rules: []gatewayv1.HTTPRouteRule{{ 550 Filters: []gatewayv1.HTTPRouteFilter{{ 551 Type: gatewayv1.HTTPRouteFilterResponseHeaderModifier, 552 ResponseHeaderModifier: &gatewayv1.HTTPHeaderFilter{ 553 Add: []gatewayv1.HTTPHeader{{ 554 Name: gatewayv1.HTTPHeaderName("x-example"), 555 Value: "blueberry", 556 }}, 557 Set: []gatewayv1.HTTPHeader{{ 558 Name: gatewayv1.HTTPHeaderName("x-different"), 559 Value: "turnip", 560 }}, 561 }, 562 }}, 563 }}, 564 }} 565 566 for _, tc := range tests { 567 t.Run(tc.name, func(t *testing.T) { 568 var errs field.ErrorList 569 route := gatewayv1.HTTPRoute{Spec: gatewayv1.HTTPRouteSpec{Rules: tc.rules}} 570 errs = ValidateHTTPRoute(&route) 571 if len(errs) != tc.errCount { 572 t.Errorf("got %d errors, want %d errors: %s", len(errs), tc.errCount, errs) 573 } 574 }) 575 } 576 } 577 578 func TestValidateHTTPBackendUniqueFilters(t *testing.T) { 579 var testService gatewayv1.ObjectName = "testService" 580 var specialService gatewayv1.ObjectName = "specialService" 581 tests := []struct { 582 name string 583 rules []gatewayv1.HTTPRouteRule 584 errCount int 585 }{{ 586 name: "valid httpRoute Rules backendref filters", 587 errCount: 0, 588 rules: []gatewayv1.HTTPRouteRule{{ 589 BackendRefs: []gatewayv1.HTTPBackendRef{ 590 { 591 BackendRef: gatewayv1.BackendRef{ 592 BackendObjectReference: gatewayv1.BackendObjectReference{ 593 Name: testService, 594 Port: ptrTo(gatewayv1.PortNumber(8080)), 595 }, 596 Weight: ptrTo(int32(100)), 597 }, 598 Filters: []gatewayv1.HTTPRouteFilter{ 599 { 600 Type: gatewayv1.HTTPRouteFilterRequestMirror, 601 RequestMirror: &gatewayv1.HTTPRequestMirrorFilter{ 602 BackendRef: gatewayv1.BackendObjectReference{ 603 Name: testService, 604 Port: ptrTo(gatewayv1.PortNumber(8080)), 605 }, 606 }, 607 }, 608 }, 609 }, 610 }, 611 }}, 612 }, { 613 name: "valid httpRoute Rules duplicate mirror filter", 614 errCount: 0, 615 rules: []gatewayv1.HTTPRouteRule{{ 616 BackendRefs: []gatewayv1.HTTPBackendRef{ 617 { 618 BackendRef: gatewayv1.BackendRef{ 619 BackendObjectReference: gatewayv1.BackendObjectReference{ 620 Name: testService, 621 Port: ptrTo(gatewayv1.PortNumber(8080)), 622 }, 623 }, 624 Filters: []gatewayv1.HTTPRouteFilter{ 625 { 626 Type: gatewayv1.HTTPRouteFilterRequestMirror, 627 RequestMirror: &gatewayv1.HTTPRequestMirrorFilter{ 628 BackendRef: gatewayv1.BackendObjectReference{ 629 Name: testService, 630 Port: ptrTo(gatewayv1.PortNumber(8080)), 631 }, 632 }, 633 }, 634 { 635 Type: gatewayv1.HTTPRouteFilterRequestMirror, 636 RequestMirror: &gatewayv1.HTTPRequestMirrorFilter{ 637 BackendRef: gatewayv1.BackendObjectReference{ 638 Name: specialService, 639 Port: ptrTo(gatewayv1.PortNumber(8080)), 640 }, 641 }, 642 }, 643 }, 644 }, 645 }, 646 }}, 647 }} 648 649 for _, tc := range tests { 650 t.Run(tc.name, func(t *testing.T) { 651 route := gatewayv1.HTTPRoute{Spec: gatewayv1.HTTPRouteSpec{Rules: tc.rules}} 652 errs := ValidateHTTPRoute(&route) 653 if len(errs) != tc.errCount { 654 t.Errorf("got %d errors, want %d errors: %s", len(errs), tc.errCount, errs) 655 } 656 }) 657 } 658 } 659 660 func TestValidateHTTPPathMatch(t *testing.T) { 661 tests := []struct { 662 name string 663 path *gatewayv1.HTTPPathMatch 664 errCount int 665 }{{ 666 name: "invalid httpRoute prefix (/.)", 667 path: &gatewayv1.HTTPPathMatch{ 668 Type: ptrTo(gatewayv1.PathMatchType("PathPrefix")), 669 Value: ptrTo("/."), 670 }, 671 errCount: 1, 672 }, { 673 name: "invalid exact (/./)", 674 path: &gatewayv1.HTTPPathMatch{ 675 Type: ptrTo(gatewayv1.PathMatchType("Exact")), 676 Value: ptrTo("/foo/./bar"), 677 }, 678 errCount: 1, 679 }, { 680 name: "valid httpRoute prefix", 681 path: &gatewayv1.HTTPPathMatch{ 682 Type: ptrTo(gatewayv1.PathMatchType("PathPrefix")), 683 Value: ptrTo("/"), 684 }, 685 errCount: 0, 686 }, { 687 name: "invalid httpRoute prefix (/[])", 688 path: &gatewayv1.HTTPPathMatch{ 689 Type: ptrTo(gatewayv1.PathMatchType("PathPrefix")), 690 Value: ptrTo("/[]"), 691 }, 692 errCount: 1, 693 }, { 694 name: "invalid httpRoute exact (/^)", 695 path: &gatewayv1.HTTPPathMatch{ 696 Type: ptrTo(gatewayv1.PathMatchType("Exact")), 697 Value: ptrTo("/^"), 698 }, 699 errCount: 1, 700 }} 701 702 for _, tc := range tests { 703 t.Run(tc.name, func(t *testing.T) { 704 route := gatewayv1.HTTPRoute{Spec: gatewayv1.HTTPRouteSpec{ 705 Rules: []gatewayv1.HTTPRouteRule{{ 706 Matches: []gatewayv1.HTTPRouteMatch{{ 707 Path: tc.path, 708 }}, 709 BackendRefs: []gatewayv1.HTTPBackendRef{{ 710 BackendRef: gatewayv1.BackendRef{ 711 BackendObjectReference: gatewayv1.BackendObjectReference{ 712 Name: gatewayv1.ObjectName("test"), 713 Port: ptrTo(gatewayv1.PortNumber(8080)), 714 }, 715 }, 716 }}, 717 }}, 718 }} 719 720 errs := ValidateHTTPRoute(&route) 721 if len(errs) != tc.errCount { 722 t.Errorf("got %d errors, want %d errors: %s", len(errs), tc.errCount, errs) 723 } 724 }) 725 } 726 } 727 728 func TestValidateHTTPHeaderMatches(t *testing.T) { 729 tests := []struct { 730 name string 731 headerMatches []gatewayv1.HTTPHeaderMatch 732 expectErr string 733 }{{ 734 name: "no header matches", 735 headerMatches: nil, 736 expectErr: "", 737 }, { 738 name: "no header matched more than once", 739 headerMatches: []gatewayv1.HTTPHeaderMatch{ 740 {Name: "Header-Name-1", Value: "val-1"}, 741 {Name: "Header-Name-2", Value: "val-2"}, 742 {Name: "Header-Name-3", Value: "val-3"}, 743 }, 744 expectErr: "", 745 }, { 746 name: "header matched more than once (same case)", 747 headerMatches: []gatewayv1.HTTPHeaderMatch{ 748 {Name: "Header-Name-1", Value: "val-1"}, 749 {Name: "Header-Name-2", Value: "val-2"}, 750 {Name: "Header-Name-1", Value: "val-3"}, 751 }, 752 expectErr: "spec.rules[0].matches[0].headers: Invalid value: \"Header-Name-1\": cannot match the same header multiple times in the same rule", 753 }, { 754 name: "header matched more than once (different case)", 755 headerMatches: []gatewayv1.HTTPHeaderMatch{ 756 {Name: "Header-Name-1", Value: "val-1"}, 757 {Name: "Header-Name-2", Value: "val-2"}, 758 {Name: "HEADER-NAME-2", Value: "val-3"}, 759 }, 760 expectErr: "spec.rules[0].matches[0].headers: Invalid value: \"Header-Name-2\": cannot match the same header multiple times in the same rule", 761 }} 762 763 for _, tc := range tests { 764 t.Run(tc.name, func(t *testing.T) { 765 route := gatewayv1.HTTPRoute{Spec: gatewayv1.HTTPRouteSpec{ 766 Rules: []gatewayv1.HTTPRouteRule{{ 767 Matches: []gatewayv1.HTTPRouteMatch{{ 768 Headers: tc.headerMatches, 769 }}, 770 BackendRefs: []gatewayv1.HTTPBackendRef{{ 771 BackendRef: gatewayv1.BackendRef{ 772 BackendObjectReference: gatewayv1.BackendObjectReference{ 773 Name: gatewayv1.ObjectName("test"), 774 Port: ptrTo(gatewayv1.PortNumber(8080)), 775 }, 776 }, 777 }}, 778 }}, 779 }} 780 781 errs := ValidateHTTPRoute(&route) 782 if len(tc.expectErr) == 0 { 783 assert.Emptyf(t, errs, "expected no errors, got %d errors: %s", len(errs), errs) 784 } else { 785 require.Lenf(t, errs, 1, "expected one error, got %d errors: %s", len(errs), errs) 786 assert.Equal(t, tc.expectErr, errs[0].Error()) 787 } 788 }) 789 } 790 } 791 792 func TestValidateHTTPQueryParamMatches(t *testing.T) { 793 tests := []struct { 794 name string 795 queryParamMatches []gatewayv1.HTTPQueryParamMatch 796 expectErr string 797 }{{ 798 name: "no query param matches", 799 queryParamMatches: nil, 800 expectErr: "", 801 }, { 802 name: "no query param matched more than once", 803 queryParamMatches: []gatewayv1.HTTPQueryParamMatch{ 804 {Name: "query-param-1", Value: "val-1"}, 805 {Name: "query-param-2", Value: "val-2"}, 806 {Name: "query-param-3", Value: "val-3"}, 807 }, 808 expectErr: "", 809 }, { 810 name: "query param matched more than once", 811 queryParamMatches: []gatewayv1.HTTPQueryParamMatch{ 812 {Name: "query-param-1", Value: "val-1"}, 813 {Name: "query-param-2", Value: "val-2"}, 814 {Name: "query-param-1", Value: "val-3"}, 815 }, 816 expectErr: "spec.rules[0].matches[0].queryParams: Invalid value: \"query-param-1\": cannot match the same query parameter multiple times in the same rule", 817 }, { 818 name: "query param names with different casing are not considered duplicates", 819 queryParamMatches: []gatewayv1.HTTPQueryParamMatch{ 820 {Name: "query-param-1", Value: "val-1"}, 821 {Name: "query-param-2", Value: "val-2"}, 822 {Name: "QUERY-PARAM-1", Value: "val-3"}, 823 }, 824 expectErr: "", 825 }} 826 827 for _, tc := range tests { 828 t.Run(tc.name, func(t *testing.T) { 829 route := gatewayv1.HTTPRoute{Spec: gatewayv1.HTTPRouteSpec{ 830 Rules: []gatewayv1.HTTPRouteRule{{ 831 Matches: []gatewayv1.HTTPRouteMatch{{ 832 QueryParams: tc.queryParamMatches, 833 }}, 834 BackendRefs: []gatewayv1.HTTPBackendRef{{ 835 BackendRef: gatewayv1.BackendRef{ 836 BackendObjectReference: gatewayv1.BackendObjectReference{ 837 Name: gatewayv1.ObjectName("test"), 838 Port: ptrTo(gatewayv1.PortNumber(8080)), 839 }, 840 }, 841 }}, 842 }}, 843 }} 844 845 errs := ValidateHTTPRoute(&route) 846 if len(tc.expectErr) == 0 { 847 assert.Emptyf(t, errs, "expected no errors, got %d errors: %s", len(errs), errs) 848 } else { 849 require.Lenf(t, errs, 1, "expected one error, got %d errors: %s", len(errs), errs) 850 assert.Equal(t, tc.expectErr, errs[0].Error()) 851 } 852 }) 853 } 854 } 855 856 func TestValidateServicePort(t *testing.T) { 857 portPtr := func(n int) *gatewayv1.PortNumber { 858 p := gatewayv1.PortNumber(n) 859 return &p 860 } 861 862 groupPtr := func(g string) *gatewayv1.Group { 863 p := gatewayv1.Group(g) 864 return &p 865 } 866 867 kindPtr := func(k string) *gatewayv1.Kind { 868 p := gatewayv1.Kind(k) 869 return &p 870 } 871 872 tests := []struct { 873 name string 874 rules []gatewayv1.HTTPRouteRule 875 errCount int 876 }{{ 877 name: "default groupkind with port", 878 errCount: 0, 879 rules: []gatewayv1.HTTPRouteRule{{ 880 BackendRefs: []gatewayv1.HTTPBackendRef{{ 881 BackendRef: gatewayv1.BackendRef{ 882 BackendObjectReference: gatewayv1.BackendObjectReference{ 883 Name: "backend", 884 Port: portPtr(99), 885 }, 886 }, 887 }}, 888 }}, 889 }, { 890 name: "default groupkind with no port", 891 errCount: 1, 892 rules: []gatewayv1.HTTPRouteRule{{ 893 BackendRefs: []gatewayv1.HTTPBackendRef{{ 894 BackendRef: gatewayv1.BackendRef{ 895 BackendObjectReference: gatewayv1.BackendObjectReference{ 896 Name: "backend", 897 }, 898 }, 899 }}, 900 }}, 901 }, { 902 name: "explicit service with port", 903 errCount: 0, 904 rules: []gatewayv1.HTTPRouteRule{{ 905 BackendRefs: []gatewayv1.HTTPBackendRef{{ 906 BackendRef: gatewayv1.BackendRef{ 907 BackendObjectReference: gatewayv1.BackendObjectReference{ 908 Group: groupPtr(""), 909 Kind: kindPtr("Service"), 910 Name: "backend", 911 Port: portPtr(99), 912 }, 913 }, 914 }}, 915 }}, 916 }, { 917 name: "explicit service with no port", 918 errCount: 1, 919 rules: []gatewayv1.HTTPRouteRule{{ 920 BackendRefs: []gatewayv1.HTTPBackendRef{{ 921 BackendRef: gatewayv1.BackendRef{ 922 BackendObjectReference: gatewayv1.BackendObjectReference{ 923 Group: groupPtr(""), 924 Kind: kindPtr("Service"), 925 Name: "backend", 926 }, 927 }, 928 }}, 929 }}, 930 }, { 931 name: "explicit ref with no port", 932 errCount: 0, 933 rules: []gatewayv1.HTTPRouteRule{{ 934 BackendRefs: []gatewayv1.HTTPBackendRef{{ 935 BackendRef: gatewayv1.BackendRef{ 936 BackendObjectReference: gatewayv1.BackendObjectReference{ 937 Group: groupPtr("foo.example.com"), 938 Kind: kindPtr("Foo"), 939 Name: "backend", 940 }, 941 }, 942 }}, 943 }}, 944 }} 945 946 for _, tc := range tests { 947 t.Run(tc.name, func(t *testing.T) { 948 route := gatewayv1.HTTPRoute{Spec: gatewayv1.HTTPRouteSpec{Rules: tc.rules}} 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 } 956 957 func TestValidateHTTPRouteTypeMatchesField(t *testing.T) { 958 tests := []struct { 959 name string 960 routeFilter gatewayv1.HTTPRouteFilter 961 errCount int 962 }{{ 963 name: "valid HTTPRouteFilterRequestHeaderModifier route filter", 964 routeFilter: gatewayv1.HTTPRouteFilter{ 965 Type: gatewayv1.HTTPRouteFilterRequestHeaderModifier, 966 RequestHeaderModifier: &gatewayv1.HTTPHeaderFilter{ 967 Set: []gatewayv1.HTTPHeader{{Name: "name"}}, 968 Add: []gatewayv1.HTTPHeader{{Name: "add"}}, 969 Remove: []string{"remove"}, 970 }, 971 }, 972 errCount: 0, 973 }, { 974 name: "invalid HTTPRouteFilterRequestHeaderModifier type filter with non-matching field", 975 routeFilter: gatewayv1.HTTPRouteFilter{ 976 Type: gatewayv1.HTTPRouteFilterRequestHeaderModifier, 977 RequestMirror: &gatewayv1.HTTPRequestMirrorFilter{}, 978 }, 979 errCount: 2, 980 }, { 981 name: "invalid HTTPRouteFilterRequestHeaderModifier type filter with empty value field", 982 routeFilter: gatewayv1.HTTPRouteFilter{ 983 Type: gatewayv1.HTTPRouteFilterRequestHeaderModifier, 984 }, 985 errCount: 1, 986 }, { 987 name: "valid HTTPRouteFilterRequestMirror route filter", 988 routeFilter: gatewayv1.HTTPRouteFilter{ 989 Type: gatewayv1.HTTPRouteFilterRequestMirror, 990 RequestMirror: &gatewayv1.HTTPRequestMirrorFilter{BackendRef: gatewayv1.BackendObjectReference{ 991 Group: new(gatewayv1.Group), 992 Kind: new(gatewayv1.Kind), 993 Name: "name", 994 Namespace: new(gatewayv1.Namespace), 995 Port: ptrTo(gatewayv1.PortNumber(22)), 996 }}, 997 }, 998 errCount: 0, 999 }, { 1000 name: "invalid HTTPRouteFilterRequestMirror type filter with non-matching field", 1001 routeFilter: gatewayv1.HTTPRouteFilter{ 1002 Type: gatewayv1.HTTPRouteFilterRequestMirror, 1003 RequestHeaderModifier: &gatewayv1.HTTPHeaderFilter{}, 1004 }, 1005 errCount: 2, 1006 }, { 1007 name: "invalid HTTPRouteFilterRequestMirror type filter with empty value field", 1008 routeFilter: gatewayv1.HTTPRouteFilter{ 1009 Type: gatewayv1.HTTPRouteFilterRequestMirror, 1010 }, 1011 errCount: 1, 1012 }, { 1013 name: "valid HTTPRouteFilterRequestRedirect route filter", 1014 routeFilter: gatewayv1.HTTPRouteFilter{ 1015 Type: gatewayv1.HTTPRouteFilterRequestRedirect, 1016 RequestRedirect: &gatewayv1.HTTPRequestRedirectFilter{ 1017 Scheme: new(string), 1018 Hostname: new(gatewayv1.PreciseHostname), 1019 Path: &gatewayv1.HTTPPathModifier{}, 1020 Port: new(gatewayv1.PortNumber), 1021 StatusCode: new(int), 1022 }, 1023 }, 1024 errCount: 1, 1025 }, { 1026 name: "invalid HTTPRouteFilterRequestRedirect type filter with non-matching field", 1027 routeFilter: gatewayv1.HTTPRouteFilter{ 1028 Type: gatewayv1.HTTPRouteFilterRequestRedirect, 1029 RequestMirror: &gatewayv1.HTTPRequestMirrorFilter{}, 1030 }, 1031 errCount: 2, 1032 }, { 1033 name: "invalid HTTPRouteFilterRequestRedirect type filter with empty value field", 1034 routeFilter: gatewayv1.HTTPRouteFilter{ 1035 Type: gatewayv1.HTTPRouteFilterRequestRedirect, 1036 }, 1037 errCount: 1, 1038 }, { 1039 name: "valid HTTPRouteFilterExtensionRef filter", 1040 routeFilter: gatewayv1.HTTPRouteFilter{ 1041 Type: gatewayv1.HTTPRouteFilterExtensionRef, 1042 ExtensionRef: &gatewayv1.LocalObjectReference{ 1043 Group: "group", 1044 Kind: "kind", 1045 Name: "name", 1046 }, 1047 }, 1048 errCount: 0, 1049 }, { 1050 name: "invalid HTTPRouteFilterExtensionRef type filter with non-matching field", 1051 routeFilter: gatewayv1.HTTPRouteFilter{ 1052 Type: gatewayv1.HTTPRouteFilterExtensionRef, 1053 RequestMirror: &gatewayv1.HTTPRequestMirrorFilter{}, 1054 }, 1055 errCount: 2, 1056 }, { 1057 name: "invalid HTTPRouteFilterExtensionRef type filter with empty value field", 1058 routeFilter: gatewayv1.HTTPRouteFilter{ 1059 Type: gatewayv1.HTTPRouteFilterExtensionRef, 1060 }, 1061 errCount: 1, 1062 }, { 1063 name: "valid HTTPRouteFilterURLRewrite route filter", 1064 routeFilter: gatewayv1.HTTPRouteFilter{ 1065 Type: gatewayv1.HTTPRouteFilterURLRewrite, 1066 URLRewrite: &gatewayv1.HTTPURLRewriteFilter{ 1067 Hostname: new(gatewayv1.PreciseHostname), 1068 Path: &gatewayv1.HTTPPathModifier{}, 1069 }, 1070 }, 1071 errCount: 0, 1072 }, { 1073 name: "invalid HTTPRouteFilterURLRewrite type filter with non-matching field", 1074 routeFilter: gatewayv1.HTTPRouteFilter{ 1075 Type: gatewayv1.HTTPRouteFilterURLRewrite, 1076 RequestMirror: &gatewayv1.HTTPRequestMirrorFilter{}, 1077 }, 1078 errCount: 2, 1079 }, { 1080 name: "invalid HTTPRouteFilterURLRewrite type filter with empty value field", 1081 routeFilter: gatewayv1.HTTPRouteFilter{ 1082 Type: gatewayv1.HTTPRouteFilterURLRewrite, 1083 }, 1084 errCount: 1, 1085 }, { 1086 name: "empty type filter is valid (caught by CRD validation)", 1087 routeFilter: gatewayv1.HTTPRouteFilter{}, 1088 errCount: 0, 1089 }} 1090 1091 for _, tc := range tests { 1092 t.Run(tc.name, func(t *testing.T) { 1093 route := gatewayv1.HTTPRoute{ 1094 Spec: gatewayv1.HTTPRouteSpec{ 1095 Rules: []gatewayv1.HTTPRouteRule{{ 1096 Filters: []gatewayv1.HTTPRouteFilter{tc.routeFilter}, 1097 BackendRefs: []gatewayv1.HTTPBackendRef{{ 1098 BackendRef: gatewayv1.BackendRef{ 1099 BackendObjectReference: gatewayv1.BackendObjectReference{ 1100 Name: gatewayv1.ObjectName("test"), 1101 Port: ptrTo(gatewayv1.PortNumber(8080)), 1102 }, 1103 }, 1104 }}, 1105 }}, 1106 }, 1107 } 1108 errs := ValidateHTTPRoute(&route) 1109 if len(errs) != tc.errCount { 1110 t.Errorf("got %d errors, want %d errors: %s", len(errs), tc.errCount, errs) 1111 } 1112 }) 1113 } 1114 } 1115 1116 func TestValidateRequestRedirectFiltersWithNoBackendRef(t *testing.T) { 1117 testService := gatewayv1.ObjectName("test-service") 1118 tests := []struct { 1119 name string 1120 rules []gatewayv1.HTTPRouteRule 1121 errCount int 1122 }{ 1123 { 1124 name: "backendref with request redirect httpRoute filter", 1125 errCount: 1, 1126 rules: []gatewayv1.HTTPRouteRule{ 1127 { 1128 Filters: []gatewayv1.HTTPRouteFilter{ 1129 { 1130 Type: gatewayv1.HTTPRouteFilterRequestRedirect, 1131 RequestRedirect: &gatewayv1.HTTPRequestRedirectFilter{ 1132 Scheme: ptrTo("https"), 1133 StatusCode: ptrTo(301), 1134 }, 1135 }, 1136 }, 1137 BackendRefs: []gatewayv1.HTTPBackendRef{ 1138 { 1139 BackendRef: gatewayv1.BackendRef{ 1140 BackendObjectReference: gatewayv1.BackendObjectReference{ 1141 Name: testService, 1142 Port: ptrTo(gatewayv1.PortNumber(80)), 1143 }, 1144 }, 1145 }, 1146 }, 1147 }, 1148 }, 1149 }, { 1150 name: "request redirect without backendref in httpRoute filter", 1151 errCount: 0, 1152 rules: []gatewayv1.HTTPRouteRule{ 1153 { 1154 Filters: []gatewayv1.HTTPRouteFilter{ 1155 { 1156 Type: gatewayv1.HTTPRouteFilterRequestRedirect, 1157 RequestRedirect: &gatewayv1.HTTPRequestRedirectFilter{ 1158 Scheme: ptrTo("https"), 1159 StatusCode: ptrTo(301), 1160 }, 1161 }, 1162 }, 1163 }, 1164 }, 1165 }, 1166 } 1167 1168 for _, tc := range tests { 1169 t.Run(tc.name, func(t *testing.T) { 1170 var errs field.ErrorList 1171 route := gatewayv1.HTTPRoute{Spec: gatewayv1.HTTPRouteSpec{Rules: tc.rules}} 1172 errs = ValidateHTTPRoute(&route) 1173 if len(errs) != tc.errCount { 1174 t.Errorf("got %d errors, want %d errors: %s", len(errs), tc.errCount, errs) 1175 } 1176 }) 1177 } 1178 } 1179 1180 func toDuration(durationString string) *gatewayv1.Duration { 1181 return (*gatewayv1.Duration)(&durationString) 1182 } 1183 1184 func TestValidateHTTPTimeouts(t *testing.T) { 1185 tests := []struct { 1186 name string 1187 rules []gatewayv1.HTTPRouteRule 1188 errCount int 1189 }{ 1190 { 1191 name: "valid httpRoute Rules timeouts", 1192 errCount: 0, 1193 rules: []gatewayv1.HTTPRouteRule{ 1194 { 1195 Timeouts: &gatewayv1.HTTPRouteTimeouts{ 1196 Request: toDuration("1ms"), 1197 }, 1198 }, 1199 }, 1200 }, { 1201 name: "valid httpRoute Rules timeout set to 0s (disabled)", 1202 errCount: 0, 1203 rules: []gatewayv1.HTTPRouteRule{ 1204 { 1205 Timeouts: &gatewayv1.HTTPRouteTimeouts{ 1206 Request: toDuration("0s"), 1207 }, 1208 }, 1209 }, 1210 }, { 1211 name: "valid httpRoute Rules timeout set to 0ms (disabled)", 1212 errCount: 0, 1213 rules: []gatewayv1.HTTPRouteRule{ 1214 { 1215 Timeouts: &gatewayv1.HTTPRouteTimeouts{ 1216 Request: toDuration("0ms"), 1217 }, 1218 }, 1219 }, 1220 }, {}, { 1221 name: "valid httpRoute Rules timeout set to 0h (disabled)", 1222 errCount: 0, 1223 rules: []gatewayv1.HTTPRouteRule{ 1224 { 1225 Timeouts: &gatewayv1.HTTPRouteTimeouts{ 1226 Request: toDuration("0h"), 1227 }, 1228 }, 1229 }, 1230 }, { 1231 name: "valid httpRoute Rules timeout and backendRequest have the same value", 1232 errCount: 0, 1233 rules: []gatewayv1.HTTPRouteRule{ 1234 { 1235 Timeouts: &gatewayv1.HTTPRouteTimeouts{ 1236 Request: toDuration("1ms"), 1237 BackendRequest: toDuration("1ms"), 1238 }, 1239 }, 1240 }, 1241 }, { 1242 name: "invalid httpRoute Rules backendRequest timeout cannot be longer than request timeout", 1243 errCount: 1, 1244 rules: []gatewayv1.HTTPRouteRule{ 1245 { 1246 Timeouts: &gatewayv1.HTTPRouteTimeouts{ 1247 Request: toDuration("1ms"), 1248 BackendRequest: toDuration("2ms"), 1249 }, 1250 }, 1251 }, 1252 }, { 1253 name: "valid httpRoute Rules request timeout 1s and backendRequest timeout 200ms", 1254 errCount: 0, 1255 rules: []gatewayv1.HTTPRouteRule{ 1256 { 1257 Timeouts: &gatewayv1.HTTPRouteTimeouts{ 1258 Request: toDuration("1s"), 1259 BackendRequest: toDuration("200ms"), 1260 }, 1261 }, 1262 }, 1263 }, { 1264 name: "valid httpRoute Rules request timeout 10s and backendRequest timeout 10s", 1265 errCount: 0, 1266 rules: []gatewayv1.HTTPRouteRule{ 1267 { 1268 Timeouts: &gatewayv1.HTTPRouteTimeouts{ 1269 Request: toDuration("10s"), 1270 BackendRequest: toDuration("10s"), 1271 }, 1272 }, 1273 }, 1274 }, { 1275 name: "invalid httpRoute Rules backendRequest timeout cannot be greater than request timeout", 1276 errCount: 1, 1277 rules: []gatewayv1.HTTPRouteRule{ 1278 { 1279 Timeouts: &gatewayv1.HTTPRouteTimeouts{ 1280 Request: toDuration("200ms"), 1281 BackendRequest: toDuration("1s"), 1282 }, 1283 }, 1284 }, 1285 }, { 1286 name: "valid httpRoute Rules request 0s (infinite) and backendRequest 100ms", 1287 errCount: 0, 1288 rules: []gatewayv1.HTTPRouteRule{ 1289 { 1290 Timeouts: &gatewayv1.HTTPRouteTimeouts{ 1291 Request: toDuration("0s"), 1292 BackendRequest: toDuration("100ms"), 1293 }, 1294 }, 1295 }, 1296 }, 1297 } 1298 for _, tc := range tests { 1299 t.Run(tc.name, func(t *testing.T) { 1300 route := gatewayv1.HTTPRoute{Spec: gatewayv1.HTTPRouteSpec{Rules: tc.rules}} 1301 errs := ValidateHTTPRoute(&route) 1302 if len(errs) != tc.errCount { 1303 t.Errorf("got %d errors, want %d errors: %s", len(errs), tc.errCount, errs) 1304 } 1305 }) 1306 } 1307 }