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  }