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  }