// Copyright (c) 2022 Alibaba Group Holding Ltd. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package common import ( "testing" networking "istio.io/api/networking/v1alpha3" "istio.io/istio/pilot/pkg/model" "istio.io/istio/pkg/config" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/alibaba/higress/pkg/ingress/kube/annotations" "github.com/stretchr/testify/assert" ) func TestConstructRouteName(t *testing.T) { testCases := []struct { input *WrapperHTTPRoute expect string }{ { input: &WrapperHTTPRoute{ Host: "test.com", OriginPathType: Exact, OriginPath: "/test", HTTPRoute: &networking.HTTPRoute{}, }, expect: "test.com-exact-/test", }, { input: &WrapperHTTPRoute{ Host: "*.test.com", OriginPathType: PrefixRegex, OriginPath: "/test/(.*)/?[0-9]", HTTPRoute: &networking.HTTPRoute{}, }, expect: "*.test.com-prefixRegex-/test/(.*)/?[0-9]", }, { input: &WrapperHTTPRoute{ Host: "test.com", OriginPathType: Exact, OriginPath: "/test", HTTPRoute: &networking.HTTPRoute{ Match: []*networking.HTTPMatchRequest{ { Headers: map[string]*networking.StringMatch{ "b": { MatchType: &networking.StringMatch_Regex{ Regex: "a?c.*", }, }, "a": { MatchType: &networking.StringMatch_Exact{ Exact: "hello", }, }, }, }, }, }, }, expect: "test.com-exact-/test-exact-a-hello-regex-b-a?c.*", }, { input: &WrapperHTTPRoute{ Host: "test.com", OriginPathType: Prefix, OriginPath: "/test", HTTPRoute: &networking.HTTPRoute{ Match: []*networking.HTTPMatchRequest{ { QueryParams: map[string]*networking.StringMatch{ "b": { MatchType: &networking.StringMatch_Regex{ Regex: "a?c.*", }, }, "a": { MatchType: &networking.StringMatch_Exact{ Exact: "hello", }, }, }, }, }, }, }, expect: "test.com-prefix-/test-exact:a:hello-regex:b:a?c.*", }, { input: &WrapperHTTPRoute{ Host: "test.com", OriginPathType: Prefix, OriginPath: "/test", HTTPRoute: &networking.HTTPRoute{ Match: []*networking.HTTPMatchRequest{ { Headers: map[string]*networking.StringMatch{ "f": { MatchType: &networking.StringMatch_Regex{ Regex: "abc?", }, }, "e": { MatchType: &networking.StringMatch_Exact{ Exact: "bye", }, }, }, QueryParams: map[string]*networking.StringMatch{ "b": { MatchType: &networking.StringMatch_Regex{ Regex: "a?c.*", }, }, "a": { MatchType: &networking.StringMatch_Exact{ Exact: "hello", }, }, }, }, }, }, }, expect: "test.com-prefix-/test-exact-e-bye-regex-f-abc?-exact:a:hello-regex:b:a?c.*", }, } for _, c := range testCases { t.Run("", func(t *testing.T) { out := constructRouteName(c.input) if out != c.expect { t.Fatalf("Expect %s, but is %s", c.expect, out) } }) } } func TestGenerateUniqueRouteName(t *testing.T) { input := &WrapperHTTPRoute{ WrapperConfig: &WrapperConfig{ Config: &config.Config{ Meta: config.Meta{ Name: "foo", Namespace: "bar", }, }, AnnotationsConfig: &annotations.Ingress{}, }, Host: "test.com", OriginPathType: Prefix, OriginPath: "/test", ClusterId: "cluster1", HTTPRoute: &networking.HTTPRoute{ Match: []*networking.HTTPMatchRequest{ { Headers: map[string]*networking.StringMatch{ "f": { MatchType: &networking.StringMatch_Regex{ Regex: "abc?", }, }, "e": { MatchType: &networking.StringMatch_Exact{ Exact: "bye", }, }, }, QueryParams: map[string]*networking.StringMatch{ "b": { MatchType: &networking.StringMatch_Regex{ Regex: "a?c.*", }, }, "a": { MatchType: &networking.StringMatch_Exact{ Exact: "hello", }, }, }, }, }, }, } assert.Equal(t, "bar/foo", GenerateUniqueRouteName("xxx", input)) assert.Equal(t, "foo", GenerateUniqueRouteName("bar", input)) } func TestGetLbStatusList(t *testing.T) { clusterPrefix = "gw-123-" svcName := clusterPrefix svcList := []*v1.Service{ { ObjectMeta: metav1.ObjectMeta{ Name: svcName, }, Spec: v1.ServiceSpec{ Type: v1.ServiceTypeLoadBalancer, }, Status: v1.ServiceStatus{ LoadBalancer: v1.LoadBalancerStatus{ Ingress: []v1.LoadBalancerIngress{ { IP: "2.2.2.2", }, }, }, }, }, { ObjectMeta: metav1.ObjectMeta{ Name: svcName, }, Spec: v1.ServiceSpec{ Type: v1.ServiceTypeLoadBalancer, }, Status: v1.ServiceStatus{ LoadBalancer: v1.LoadBalancerStatus{ Ingress: []v1.LoadBalancerIngress{ { Hostname: "1.1.1.1" + SvcHostNameSuffix, }, }, }, }, }, { ObjectMeta: metav1.ObjectMeta{ Name: svcName, }, Spec: v1.ServiceSpec{ Type: v1.ServiceTypeLoadBalancer, }, Status: v1.ServiceStatus{ LoadBalancer: v1.LoadBalancerStatus{ Ingress: []v1.LoadBalancerIngress{ { Hostname: "4.4.4.4" + SvcHostNameSuffix, }, }, }, }, }, { ObjectMeta: metav1.ObjectMeta{ Name: svcName, }, Spec: v1.ServiceSpec{ Type: v1.ServiceTypeLoadBalancer, }, Status: v1.ServiceStatus{ LoadBalancer: v1.LoadBalancerStatus{ Ingress: []v1.LoadBalancerIngress{ { IP: "3.3.3.3", }, }, }, }, }, { ObjectMeta: metav1.ObjectMeta{ Name: svcName, }, Spec: v1.ServiceSpec{ Type: v1.ServiceTypeClusterIP, }, Status: v1.ServiceStatus{ LoadBalancer: v1.LoadBalancerStatus{ Ingress: []v1.LoadBalancerIngress{ { IP: "5.5.5.5", }, }, }, }, }, } lbiList := GetLbStatusList(svcList) if len(lbiList) != 4 { t.Fatal("len should be 4") } if lbiList[0].IP != "1.1.1.1" { t.Fatal("should be 1.1.1.1") } if lbiList[3].IP != "4.4.4.4" { t.Fatal("should be 4.4.4.4") } } func TestSortRoutes(t *testing.T) { input := []*WrapperHTTPRoute{ { WrapperConfig: &WrapperConfig{ Config: &config.Config{ Meta: config.Meta{ Name: "foo", Namespace: "bar", }, }, AnnotationsConfig: &annotations.Ingress{}, }, Host: "test.com", OriginPathType: Prefix, OriginPath: "/", ClusterId: "cluster1", HTTPRoute: &networking.HTTPRoute{ Name: "test-1", }, }, { WrapperConfig: &WrapperConfig{ Config: &config.Config{ Meta: config.Meta{ Name: "foo", Namespace: "bar", }, }, AnnotationsConfig: &annotations.Ingress{}, }, Host: "test.com", OriginPathType: Prefix, OriginPath: "/a", ClusterId: "cluster1", HTTPRoute: &networking.HTTPRoute{ Name: "test-2", }, }, { WrapperConfig: &WrapperConfig{ Config: &config.Config{ Meta: config.Meta{ Name: "foo", Namespace: "bar", }, }, AnnotationsConfig: &annotations.Ingress{}, }, HTTPRoute: &networking.HTTPRoute{ Name: "test-3", }, IsDefaultBackend: true, }, { WrapperConfig: &WrapperConfig{ Config: &config.Config{ Meta: config.Meta{ Name: "foo", Namespace: "bar", }, }, AnnotationsConfig: &annotations.Ingress{}, }, Host: "test.com", OriginPathType: Exact, OriginPath: "/b", ClusterId: "cluster1", HTTPRoute: &networking.HTTPRoute{ Name: "test-4", }, }, { WrapperConfig: &WrapperConfig{ Config: &config.Config{ Meta: config.Meta{ Name: "foo", Namespace: "bar", }, }, AnnotationsConfig: &annotations.Ingress{}, }, Host: "test.com", OriginPathType: PrefixRegex, OriginPath: "/d(.*)", ClusterId: "cluster1", HTTPRoute: &networking.HTTPRoute{ Name: "test-5", }, }, } SortHTTPRoutes(input) if (input[0].HTTPRoute.Name) != "test-4" { t.Fatal("should be test-4") } if (input[1].HTTPRoute.Name) != "test-2" { t.Fatal("should be test-2") } if (input[2].HTTPRoute.Name) != "test-5" { t.Fatal("should be test-5") } if (input[3].HTTPRoute.Name) != "test-1" { t.Fatal("should be test-1") } if (input[4].HTTPRoute.Name) != "test-3" { t.Fatal("should be test-3") } } // TestSortHTTPRoutesWithMoreRules include headers, query params, methods func TestSortHTTPRoutesWithMoreRules(t *testing.T) { input := []struct { order string pathType PathType path string method *networking.StringMatch header map[string]*networking.StringMatch queryParam map[string]*networking.StringMatch }{ { order: "1", pathType: Exact, path: "/bar", }, { order: "2", pathType: Prefix, path: "/bar", }, { order: "3", pathType: Prefix, path: "/bar", method: &networking.StringMatch{ MatchType: &networking.StringMatch_Regex{Regex: "GET|PUT"}, }, }, { order: "4", pathType: Prefix, path: "/bar", method: &networking.StringMatch{ MatchType: &networking.StringMatch_Regex{Regex: "GET"}, }, }, { order: "5", pathType: Prefix, path: "/bar", header: map[string]*networking.StringMatch{ "foo": { MatchType: &networking.StringMatch_Exact{Exact: "bar"}, }, }, }, { order: "6", pathType: Prefix, path: "/bar", header: map[string]*networking.StringMatch{ "foo": { MatchType: &networking.StringMatch_Exact{Exact: "bar"}, }, "bar": { MatchType: &networking.StringMatch_Exact{Exact: "foo"}, }, }, }, { order: "7", pathType: Prefix, path: "/bar", queryParam: map[string]*networking.StringMatch{ "foo": { MatchType: &networking.StringMatch_Exact{Exact: "bar"}, }, }, }, { order: "8", pathType: Prefix, path: "/bar", queryParam: map[string]*networking.StringMatch{ "foo": { MatchType: &networking.StringMatch_Exact{Exact: "bar"}, }, "bar": { MatchType: &networking.StringMatch_Exact{Exact: "foo"}, }, }, }, { order: "9", pathType: Prefix, path: "/bar", method: &networking.StringMatch{ MatchType: &networking.StringMatch_Regex{Regex: "GET"}, }, queryParam: map[string]*networking.StringMatch{ "foo": { MatchType: &networking.StringMatch_Exact{Exact: "bar"}, }, }, }, { order: "10", pathType: Prefix, path: "/bar", method: &networking.StringMatch{ MatchType: &networking.StringMatch_Regex{Regex: "GET"}, }, queryParam: map[string]*networking.StringMatch{ "bar": { MatchType: &networking.StringMatch_Exact{Exact: "foo"}, }, }, }, } origin := []string{"1", "2", "3", "4", "5", "6", "7", "8", "9", "10"} expect := []string{"1", "9", "10", "4", "3", "6", "5", "8", "7", "2"} var list []*WrapperHTTPRoute for idx, val := range input { list = append(list, &WrapperHTTPRoute{ OriginPath: val.path, OriginPathType: val.pathType, HTTPRoute: &networking.HTTPRoute{ Name: origin[idx], Match: []*networking.HTTPMatchRequest{ { Method: val.method, Headers: val.header, QueryParams: val.queryParam, }, }, }, }) } SortHTTPRoutes(list) for idx, val := range list { if val.HTTPRoute.Name != expect[idx] { t.Fatalf("should be %s, but got %s", expect[idx], val.HTTPRoute.Name) } } } func TestValidateBackendResource(t *testing.T) { groupStr := "networking.higress.io" testCases := []struct { name string resource *v1.TypedLocalObjectReference expected bool }{ { name: "nil resource", resource: nil, expected: false, }, { name: "nil APIGroup", resource: &v1.TypedLocalObjectReference{ APIGroup: nil, Kind: "McpBridge", Name: "default", }, expected: false, }, { name: "wrong APIGroup", resource: &v1.TypedLocalObjectReference{ APIGroup: &groupStr, Kind: "McpBridge", Name: "wrong-name", }, expected: false, }, { name: "wrong Kind", resource: &v1.TypedLocalObjectReference{ APIGroup: &groupStr, Kind: "WrongKind", Name: "default", }, expected: false, }, { name: "valid resource", resource: &v1.TypedLocalObjectReference{ APIGroup: &groupStr, Kind: "McpBridge", Name: "default", }, expected: true, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { result := ValidateBackendResource(tc.resource) assert.Equal(t, tc.expected, result) }) } } func TestCreateOrUpdateAnnotations(t *testing.T) { testCases := []struct { name string annotations map[string]string options Options expected map[string]string }{ { name: "empty annotations", annotations: map[string]string{}, options: Options{ ClusterId: "test-cluster", RawClusterId: "raw-test-cluster", }, expected: map[string]string{ ClusterIdAnnotation: "test-cluster", RawClusterIdAnnotation: "raw-test-cluster", }, }, { name: "existing annotations", annotations: map[string]string{ "key1": "value1", "key2": "value2", }, options: Options{ ClusterId: "test-cluster", RawClusterId: "raw-test-cluster", }, expected: map[string]string{ "key1": "value1", "key2": "value2", ClusterIdAnnotation: "test-cluster", RawClusterIdAnnotation: "raw-test-cluster", }, }, { name: "overwrite existing cluster annotations", annotations: map[string]string{ ClusterIdAnnotation: "old-cluster", RawClusterIdAnnotation: "old-raw-cluster", "key1": "value1", }, options: Options{ ClusterId: "new-cluster", RawClusterId: "new-raw-cluster", }, expected: map[string]string{ ClusterIdAnnotation: "new-cluster", RawClusterIdAnnotation: "new-raw-cluster", "key1": "value1", }, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { result := CreateOrUpdateAnnotations(tc.annotations, tc.options) assert.Equal(t, tc.expected, result) }) } } func TestGetClusterId(t *testing.T) { testCases := []struct { name string annotations map[string]string expected string }{ { name: "nil annotations", annotations: nil, expected: "", }, { name: "empty annotations", annotations: map[string]string{}, expected: "", }, { name: "with cluster id", annotations: map[string]string{ ClusterIdAnnotation: "test-cluster", }, expected: "test-cluster", }, { name: "with other annotations", annotations: map[string]string{ "key1": "value1", "key2": "value2", }, expected: "", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { result := GetClusterId(tc.annotations) assert.Equal(t, tc.expected, string(result)) }) } } func TestConvertToDNSLabelValidAndCleanHost(t *testing.T) { testCases := []struct { name string input string }{ { name: "simple host", input: "example.com", }, { name: "wildcard host", input: "*.example.com", }, { name: "long host", input: "very-long-subdomain.example-service.my-namespace.svc.cluster.local", }, { name: "empty host", input: "", }, { name: "ip address", input: "192.168.1.1", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { // Test internal convertToDNSLabelValid function (through CleanHost) result := CleanHost(tc.input) // Validate result assert.NotEmpty(t, result) assert.Equal(t, 16, len(result)) // MD5 hash format is fixed length of 16 bytes // Consistency check - same input should produce same output result2 := CleanHost(tc.input) assert.Equal(t, result, result2) }) } } func TestSplitServiceFQDN(t *testing.T) { testCases := []struct { name string fqdn string expectedSvc string expectedNs string expectedValid bool }{ { name: "simple fqdn", fqdn: "service.namespace", expectedSvc: "service", expectedNs: "namespace", expectedValid: true, }, { name: "full k8s fqdn", fqdn: "service.namespace.svc.cluster.local", expectedSvc: "service", expectedNs: "namespace", expectedValid: true, }, { name: "just service name", fqdn: "service", expectedSvc: "", expectedNs: "", expectedValid: false, }, { name: "empty string", fqdn: "", expectedSvc: "", expectedNs: "", expectedValid: false, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { svc, ns, valid := SplitServiceFQDN(tc.fqdn) assert.Equal(t, tc.expectedSvc, svc) assert.Equal(t, tc.expectedNs, ns) assert.Equal(t, tc.expectedValid, valid) }) } } func TestConvertBackendService(t *testing.T) { testCases := []struct { name string dest *networking.HTTPRouteDestination expected model.BackendService }{ { name: "simple service", dest: &networking.HTTPRouteDestination{ Destination: &networking.Destination{ Host: "service.namespace", Port: &networking.PortSelector{ Number: 80, }, }, Weight: 100, }, expected: model.BackendService{ Name: "service", Namespace: "namespace", Port: 80, Weight: 100, }, }, { name: "full k8s FQDN", dest: &networking.HTTPRouteDestination{ Destination: &networking.Destination{ Host: "service.namespace.svc.cluster.local", Port: &networking.PortSelector{ Number: 8080, }, }, Weight: 50, }, expected: model.BackendService{ Name: "service", Namespace: "namespace", Port: 8080, Weight: 50, }, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { result := ConvertBackendService(tc.dest) assert.Equal(t, tc.expected.Name, result.Name) assert.Equal(t, tc.expected.Namespace, result.Namespace) assert.Equal(t, tc.expected.Port, result.Port) assert.Equal(t, tc.expected.Weight, result.Weight) }) } } func TestCreateConvertedName(t *testing.T) { testCases := []struct { name string items []string expected string }{ { name: "empty slice", items: []string{}, expected: "", }, { name: "single item", items: []string{"example"}, expected: "example", }, { name: "multiple items", items: []string{"part1", "part2", "part3"}, expected: "part1-part2-part3", }, { name: "with empty strings", items: []string{"part1", "", "part3"}, expected: "part1-part3", }, { name: "all empty strings", items: []string{"", "", ""}, expected: "", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { result := CreateConvertedName(tc.items...) assert.Equal(t, tc.expected, result) }) } } func TestSortIngressByCreationTime(t *testing.T) { configs := []config.Config{ { Meta: config.Meta{ Name: "c-ingress", Namespace: "ns1", }, }, { Meta: config.Meta{ Name: "a-ingress", Namespace: "ns1", }, }, { Meta: config.Meta{ Name: "b-ingress", Namespace: "ns1", }, }, } expected := []string{"a-ingress", "b-ingress", "c-ingress"} SortIngressByCreationTime(configs) var actual []string for _, cfg := range configs { actual = append(actual, cfg.Name) } assert.Equal(t, expected, actual, "When the timestamps are the same, the configuration should be sorted by name") sameNamespaceConfigs := []config.Config{ { Meta: config.Meta{ Name: "same-name", Namespace: "c-ns", }, }, { Meta: config.Meta{ Name: "same-name", Namespace: "a-ns", }, }, { Meta: config.Meta{ Name: "same-name", Namespace: "b-ns", }, }, } expectedNamespace := []string{"a-ns", "b-ns", "c-ns"} SortIngressByCreationTime(sameNamespaceConfigs) var actualNamespace []string for _, cfg := range sameNamespaceConfigs { actualNamespace = append(actualNamespace, cfg.Namespace) } assert.Equal(t, expectedNamespace, actualNamespace, "When the names are the same, the configuration should be sorted by namespace") } func TestPartMd5(t *testing.T) { testCases := []struct { name string input string length int }{ { name: "empty string", input: "", length: 8, }, { name: "simple string", input: "test", length: 8, }, { name: "complex string", input: "this-is-a-long-string-with-special-chars-!@#$%^&*()", length: 8, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { result := partMd5(tc.input) // Check result format assert.Equal(t, tc.length, len(result), "MD5 hash excerpt should be 8 characters") // Run twice to ensure deterministic output result2 := partMd5(tc.input) assert.Equal(t, result, result2, "partMd5 function should be deterministic") }) } } func TestGetLbStatusListV1AndV1Beta1(t *testing.T) { clusterPrefix = "gw-123-" svcName := clusterPrefix svcList := []*v1.Service{ { ObjectMeta: metav1.ObjectMeta{ Name: svcName, }, Spec: v1.ServiceSpec{ Type: v1.ServiceTypeLoadBalancer, }, Status: v1.ServiceStatus{ LoadBalancer: v1.LoadBalancerStatus{ Ingress: []v1.LoadBalancerIngress{ { IP: "2.2.2.2", }, }, }, }, }, { ObjectMeta: metav1.ObjectMeta{ Name: svcName, }, Spec: v1.ServiceSpec{ Type: v1.ServiceTypeLoadBalancer, }, Status: v1.ServiceStatus{ LoadBalancer: v1.LoadBalancerStatus{ Ingress: []v1.LoadBalancerIngress{ { Hostname: "1.1.1.1" + SvcHostNameSuffix, }, }, }, }, }, } // Test the V1 version t.Run("GetLbStatusListV1", func(t *testing.T) { lbiList := GetLbStatusListV1(svcList) assert.Equal(t, 2, len(lbiList), "There should be 2 entry points") assert.Equal(t, "1.1.1.1", lbiList[0].IP, "The first IP should be 1.1.1.1") assert.Equal(t, "2.2.2.2", lbiList[1].IP, "The second IP should be 2.2.2.2") }) // Test the V1Beta1 version t.Run("GetLbStatusListV1Beta1", func(t *testing.T) { lbiList := GetLbStatusListV1Beta1(svcList) assert.Equal(t, 2, len(lbiList), "There should be 2 entry points") assert.Equal(t, "1.1.1.1", lbiList[0].IP, "The first IP should be 1.1.1.1") assert.Equal(t, "2.2.2.2", lbiList[1].IP, "The second IP should be 2.2.2.2") }) }