| /* |
| * |
| * Copyright 2020 gRPC authors. |
| * |
| * 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 keys |
| |
| import ( |
| "fmt" |
| "strings" |
| "testing" |
| |
| "github.com/google/go-cmp/cmp" |
| rlspb "google.golang.org/grpc/balancer/rls/internal/proto/grpc_lookup_v1" |
| "google.golang.org/grpc/metadata" |
| ) |
| |
| var ( |
| goodKeyBuilder1 = &rlspb.GrpcKeyBuilder{ |
| Names: []*rlspb.GrpcKeyBuilder_Name{ |
| {Service: "gFoo"}, |
| }, |
| Headers: []*rlspb.NameMatcher{ |
| {Key: "k1", Names: []string{"n1"}}, |
| {Key: "k2", Names: []string{"n1"}}, |
| }, |
| } |
| goodKeyBuilder2 = &rlspb.GrpcKeyBuilder{ |
| Names: []*rlspb.GrpcKeyBuilder_Name{ |
| {Service: "gBar", Method: "method1"}, |
| {Service: "gFoobar"}, |
| }, |
| Headers: []*rlspb.NameMatcher{ |
| {Key: "k1", Names: []string{"n1", "n2"}}, |
| }, |
| } |
| ) |
| |
| func TestMakeBuilderMap(t *testing.T) { |
| wantBuilderMap1 := map[string]builder{ |
| "/gFoo/": {matchers: []matcher{{key: "k1", names: []string{"n1"}}, {key: "k2", names: []string{"n1"}}}}, |
| } |
| wantBuilderMap2 := map[string]builder{ |
| "/gFoo/": {matchers: []matcher{{key: "k1", names: []string{"n1"}}, {key: "k2", names: []string{"n1"}}}}, |
| "/gBar/method1": {matchers: []matcher{{key: "k1", names: []string{"n1", "n2"}}}}, |
| "/gFoobar/": {matchers: []matcher{{key: "k1", names: []string{"n1", "n2"}}}}, |
| } |
| |
| tests := []struct { |
| desc string |
| cfg *rlspb.RouteLookupConfig |
| wantBuilderMap BuilderMap |
| }{ |
| { |
| desc: "One good GrpcKeyBuilder", |
| cfg: &rlspb.RouteLookupConfig{ |
| GrpcKeybuilders: []*rlspb.GrpcKeyBuilder{goodKeyBuilder1}, |
| }, |
| wantBuilderMap: wantBuilderMap1, |
| }, |
| { |
| desc: "Two good GrpcKeyBuilders", |
| cfg: &rlspb.RouteLookupConfig{ |
| GrpcKeybuilders: []*rlspb.GrpcKeyBuilder{goodKeyBuilder1, goodKeyBuilder2}, |
| }, |
| wantBuilderMap: wantBuilderMap2, |
| }, |
| } |
| |
| for _, test := range tests { |
| t.Run(test.desc, func(t *testing.T) { |
| builderMap, err := MakeBuilderMap(test.cfg) |
| if err != nil || !builderMap.Equal(test.wantBuilderMap) { |
| t.Errorf("MakeBuilderMap(%+v) returned {%v, %v}, want: {%v, nil}", test.cfg, builderMap, err, test.wantBuilderMap) |
| } |
| }) |
| } |
| } |
| |
| func TestMakeBuilderMapErrors(t *testing.T) { |
| emptyServiceKeyBuilder := &rlspb.GrpcKeyBuilder{ |
| Names: []*rlspb.GrpcKeyBuilder_Name{ |
| {Service: "bFoo", Method: "method1"}, |
| {Service: "bBar"}, |
| {Method: "method1"}, |
| }, |
| Headers: []*rlspb.NameMatcher{{Key: "k1", Names: []string{"n1", "n2"}}}, |
| } |
| requiredMatchKeyBuilder := &rlspb.GrpcKeyBuilder{ |
| Names: []*rlspb.GrpcKeyBuilder_Name{{Service: "bFoo", Method: "method1"}}, |
| Headers: []*rlspb.NameMatcher{{Key: "k1", Names: []string{"n1", "n2"}, RequiredMatch: true}}, |
| } |
| repeatedHeadersKeyBuilder := &rlspb.GrpcKeyBuilder{ |
| Names: []*rlspb.GrpcKeyBuilder_Name{ |
| {Service: "gBar", Method: "method1"}, |
| {Service: "gFoobar"}, |
| }, |
| Headers: []*rlspb.NameMatcher{ |
| {Key: "k1", Names: []string{"n1", "n2"}}, |
| {Key: "k1", Names: []string{"n1", "n2"}}, |
| }, |
| } |
| methodNameWithSlashKeyBuilder := &rlspb.GrpcKeyBuilder{ |
| Names: []*rlspb.GrpcKeyBuilder_Name{{Service: "gBar", Method: "method1/foo"}}, |
| Headers: []*rlspb.NameMatcher{{Key: "k1", Names: []string{"n1", "n2"}}}, |
| } |
| |
| tests := []struct { |
| desc string |
| cfg *rlspb.RouteLookupConfig |
| wantErrPrefix string |
| }{ |
| { |
| desc: "No GrpcKeyBuilder", |
| cfg: &rlspb.RouteLookupConfig{}, |
| wantErrPrefix: "rls: RouteLookupConfig does not contain any GrpcKeyBuilder", |
| }, |
| { |
| desc: "Two GrpcKeyBuilders with same Name", |
| cfg: &rlspb.RouteLookupConfig{ |
| GrpcKeybuilders: []*rlspb.GrpcKeyBuilder{goodKeyBuilder1, goodKeyBuilder1}, |
| }, |
| wantErrPrefix: "rls: GrpcKeyBuilder in RouteLookupConfig contains repeated Name field", |
| }, |
| { |
| desc: "GrpcKeyBuilder with empty Service field", |
| cfg: &rlspb.RouteLookupConfig{ |
| GrpcKeybuilders: []*rlspb.GrpcKeyBuilder{emptyServiceKeyBuilder, goodKeyBuilder1}, |
| }, |
| wantErrPrefix: "rls: GrpcKeyBuilder in RouteLookupConfig contains a Name field with no Service", |
| }, |
| { |
| desc: "GrpcKeyBuilder with no Name", |
| cfg: &rlspb.RouteLookupConfig{ |
| GrpcKeybuilders: []*rlspb.GrpcKeyBuilder{{}, goodKeyBuilder1}, |
| }, |
| wantErrPrefix: "rls: GrpcKeyBuilder in RouteLookupConfig does not contain any Name", |
| }, |
| { |
| desc: "GrpcKeyBuilder with requiredMatch field set", |
| cfg: &rlspb.RouteLookupConfig{ |
| GrpcKeybuilders: []*rlspb.GrpcKeyBuilder{requiredMatchKeyBuilder, goodKeyBuilder1}, |
| }, |
| wantErrPrefix: "rls: GrpcKeyBuilder in RouteLookupConfig has required_match field set", |
| }, |
| { |
| desc: "GrpcKeyBuilder two headers with same key", |
| cfg: &rlspb.RouteLookupConfig{ |
| GrpcKeybuilders: []*rlspb.GrpcKeyBuilder{repeatedHeadersKeyBuilder, goodKeyBuilder1}, |
| }, |
| wantErrPrefix: "rls: GrpcKeyBuilder in RouteLookupConfig contains repeated Key field in headers", |
| }, |
| { |
| desc: "GrpcKeyBuilder with slash in method name", |
| cfg: &rlspb.RouteLookupConfig{ |
| GrpcKeybuilders: []*rlspb.GrpcKeyBuilder{methodNameWithSlashKeyBuilder}, |
| }, |
| wantErrPrefix: "rls: GrpcKeyBuilder in RouteLookupConfig contains a method with a slash", |
| }, |
| } |
| |
| for _, test := range tests { |
| t.Run(test.desc, func(t *testing.T) { |
| builderMap, err := MakeBuilderMap(test.cfg) |
| if builderMap != nil || !strings.HasPrefix(fmt.Sprint(err), test.wantErrPrefix) { |
| t.Errorf("MakeBuilderMap(%+v) returned {%v, %v}, want: {nil, %v}", test.cfg, builderMap, err, test.wantErrPrefix) |
| } |
| }) |
| } |
| } |
| |
| func TestRLSKey(t *testing.T) { |
| bm, err := MakeBuilderMap(&rlspb.RouteLookupConfig{ |
| GrpcKeybuilders: []*rlspb.GrpcKeyBuilder{goodKeyBuilder1, goodKeyBuilder2}, |
| }) |
| if err != nil { |
| t.Fatalf("MakeBuilderMap() failed: %v", err) |
| } |
| |
| tests := []struct { |
| desc string |
| path string |
| md metadata.MD |
| wantKM KeyMap |
| }{ |
| { |
| // No keyBuilder is found for the provided service. |
| desc: "service not found in key builder map", |
| path: "/notFoundService/method", |
| md: metadata.Pairs("n1", "v1", "n2", "v2"), |
| wantKM: KeyMap{}, |
| }, |
| { |
| // No keyBuilder is found for the provided method. |
| desc: "method not found in key builder map", |
| path: "/gBar/notFoundMethod", |
| md: metadata.Pairs("n1", "v1", "n2", "v2"), |
| wantKM: KeyMap{}, |
| }, |
| { |
| // A keyBuilder is found, but none of the headers match. |
| desc: "directPathMatch-NoMatchingKey", |
| path: "/gBar/method1", |
| md: metadata.Pairs("notMatchingKey", "v1"), |
| wantKM: KeyMap{Map: map[string]string{}, Str: ""}, |
| }, |
| { |
| // A keyBuilder is found, and a single headers matches. |
| desc: "directPathMatch-SingleKey", |
| path: "/gBar/method1", |
| md: metadata.Pairs("n1", "v1"), |
| wantKM: KeyMap{Map: map[string]string{"k1": "v1"}, Str: "k1=v1"}, |
| }, |
| { |
| // A keyBuilder is found, and multiple headers match, but the first |
| // match is chosen. |
| desc: "directPathMatch-FirstMatchingKey", |
| path: "/gBar/method1", |
| md: metadata.Pairs("n2", "v2", "n1", "v1"), |
| wantKM: KeyMap{Map: map[string]string{"k1": "v1"}, Str: "k1=v1"}, |
| }, |
| { |
| // A keyBuilder is found as a wildcard match, but none of the |
| // headers match. |
| desc: "wildcardPathMatch-NoMatchingKey", |
| path: "/gFoobar/method1", |
| md: metadata.Pairs("notMatchingKey", "v1"), |
| wantKM: KeyMap{Map: map[string]string{}, Str: ""}, |
| }, |
| { |
| // A keyBuilder is found as a wildcard match, and a single headers |
| // matches. |
| desc: "wildcardPathMatch-SingleKey", |
| path: "/gFoobar/method1", |
| md: metadata.Pairs("n1", "v1"), |
| wantKM: KeyMap{Map: map[string]string{"k1": "v1"}, Str: "k1=v1"}, |
| }, |
| { |
| // A keyBuilder is found as a wildcard match, and multiple headers |
| // match, but the first match is chosen. |
| desc: "wildcardPathMatch-FirstMatchingKey", |
| path: "/gFoobar/method1", |
| md: metadata.Pairs("n2", "v2", "n1", "v1"), |
| wantKM: KeyMap{Map: map[string]string{"k1": "v1"}, Str: "k1=v1"}, |
| }, |
| { |
| // Multiple matchers find hits in the provided request headers. |
| desc: "multipleMatchers", |
| path: "/gFoo/method1", |
| md: metadata.Pairs("n2", "v2", "n1", "v1"), |
| wantKM: KeyMap{Map: map[string]string{"k1": "v1", "k2": "v1"}, Str: "k1=v1,k2=v1"}, |
| }, |
| { |
| // A match is found for a header which is specified multiple times. |
| // So, the values are joined with commas separating them. |
| desc: "commaSeparated", |
| path: "/gBar/method1", |
| md: metadata.Pairs("n1", "v1", "n1", "v2", "n1", "v3"), |
| wantKM: KeyMap{Map: map[string]string{"k1": "v1,v2,v3"}, Str: "k1=v1,v2,v3"}, |
| }, |
| } |
| |
| for _, test := range tests { |
| t.Run(test.desc, func(t *testing.T) { |
| if gotKM := bm.RLSKey(test.md, test.path); !cmp.Equal(gotKM, test.wantKM) { |
| t.Errorf("RLSKey(%+v, %s) = %+v, want %+v", test.md, test.path, gotKM, test.wantKM) |
| } |
| }) |
| } |
| } |
| |
| func TestMapToString(t *testing.T) { |
| tests := []struct { |
| desc string |
| input map[string]string |
| wantStr string |
| }{ |
| { |
| desc: "empty map", |
| input: nil, |
| wantStr: "", |
| }, |
| { |
| desc: "one key", |
| input: map[string]string{ |
| "k1": "v1", |
| }, |
| wantStr: "k1=v1", |
| }, |
| { |
| desc: "sorted keys", |
| input: map[string]string{ |
| "k1": "v1", |
| "k2": "v2", |
| "k3": "v3", |
| }, |
| wantStr: "k1=v1,k2=v2,k3=v3", |
| }, |
| { |
| desc: "unsorted keys", |
| input: map[string]string{ |
| "k3": "v3", |
| "k1": "v1", |
| "k2": "v2", |
| }, |
| wantStr: "k1=v1,k2=v2,k3=v3", |
| }, |
| } |
| |
| for _, test := range tests { |
| t.Run(test.desc, func(t *testing.T) { |
| if gotStr := mapToString(test.input); gotStr != test.wantStr { |
| t.Errorf("mapToString(%v) = %s, want %s", test.input, gotStr, test.wantStr) |
| } |
| }) |
| } |
| } |
| |
| func TestBuilderMapEqual(t *testing.T) { |
| tests := []struct { |
| desc string |
| a BuilderMap |
| b BuilderMap |
| wantEqual bool |
| }{ |
| { |
| desc: "nil builder maps", |
| a: nil, |
| b: nil, |
| wantEqual: true, |
| }, |
| { |
| desc: "empty builder maps", |
| a: make(map[string]builder), |
| b: make(map[string]builder), |
| wantEqual: true, |
| }, |
| { |
| desc: "nil and non-nil builder maps", |
| a: nil, |
| b: map[string]builder{"/gFoo/": {matchers: []matcher{{key: "k1", names: []string{"n1"}}}}}, |
| wantEqual: false, |
| }, |
| { |
| desc: "empty and non-empty builder maps", |
| a: make(map[string]builder), |
| b: map[string]builder{"/gFoo/": {matchers: []matcher{{key: "k1", names: []string{"n1"}}}}}, |
| wantEqual: false, |
| }, |
| { |
| desc: "different number of map keys", |
| a: map[string]builder{ |
| "/gFoo/": {matchers: []matcher{{key: "k1", names: []string{"n1"}}}}, |
| "/gBar/": {matchers: []matcher{{key: "k1", names: []string{"n1"}}}}, |
| }, |
| b: map[string]builder{ |
| "/gFoo/": {matchers: []matcher{{key: "k1", names: []string{"n1"}}}}, |
| }, |
| wantEqual: false, |
| }, |
| { |
| desc: "different map keys", |
| a: map[string]builder{ |
| "/gBar/": {matchers: []matcher{{key: "k1", names: []string{"n1"}}}}, |
| }, |
| b: map[string]builder{ |
| "/gFoo/": {matchers: []matcher{{key: "k1", names: []string{"n1"}}}}, |
| }, |
| wantEqual: false, |
| }, |
| { |
| desc: "equal keys different values", |
| a: map[string]builder{ |
| "/gBar/": {matchers: []matcher{{key: "k1", names: []string{"n1"}}}}, |
| "/gFoo/": {matchers: []matcher{{key: "k1", names: []string{"n1", "n2"}}}}, |
| }, |
| b: map[string]builder{ |
| "/gBar/": {matchers: []matcher{{key: "k1", names: []string{"n1"}}}}, |
| "/gFoo/": {matchers: []matcher{{key: "k1", names: []string{"n1"}}}}, |
| }, |
| wantEqual: false, |
| }, |
| { |
| desc: "good match", |
| a: map[string]builder{ |
| "/gBar/": {matchers: []matcher{{key: "k1", names: []string{"n1"}}}}, |
| "/gFoo/": {matchers: []matcher{{key: "k1", names: []string{"n1"}}}}, |
| }, |
| b: map[string]builder{ |
| "/gBar/": {matchers: []matcher{{key: "k1", names: []string{"n1"}}}}, |
| "/gFoo/": {matchers: []matcher{{key: "k1", names: []string{"n1"}}}}, |
| }, |
| wantEqual: true, |
| }, |
| } |
| |
| for _, test := range tests { |
| t.Run(test.desc, func(t *testing.T) { |
| if gotEqual := test.a.Equal(test.b); gotEqual != test.wantEqual { |
| t.Errorf("BuilderMap.Equal(%v, %v) = %v, want %v", test.a, test.b, gotEqual, test.wantEqual) |
| } |
| }) |
| } |
| } |
| |
| func TestBuilderEqual(t *testing.T) { |
| tests := []struct { |
| desc string |
| a builder |
| b builder |
| wantEqual bool |
| }{ |
| { |
| desc: "nil builders", |
| a: builder{matchers: nil}, |
| b: builder{matchers: nil}, |
| wantEqual: true, |
| }, |
| { |
| desc: "empty builders", |
| a: builder{matchers: []matcher{}}, |
| b: builder{matchers: []matcher{}}, |
| wantEqual: true, |
| }, |
| { |
| desc: "nil and non-nil builders", |
| a: builder{matchers: nil}, |
| b: builder{matchers: []matcher{}}, |
| wantEqual: false, |
| }, |
| { |
| desc: "empty and non-empty builders", |
| a: builder{matchers: []matcher{}}, |
| b: builder{matchers: []matcher{{key: "foo"}}}, |
| wantEqual: false, |
| }, |
| { |
| desc: "different number of matchers", |
| a: builder{matchers: []matcher{{key: "foo"}, {key: "bar"}}}, |
| b: builder{matchers: []matcher{{key: "foo"}}}, |
| wantEqual: false, |
| }, |
| { |
| desc: "equal number but differing matchers", |
| a: builder{matchers: []matcher{{key: "bar"}}}, |
| b: builder{matchers: []matcher{{key: "foo"}}}, |
| wantEqual: false, |
| }, |
| { |
| desc: "good match", |
| a: builder{matchers: []matcher{{key: "foo"}}}, |
| b: builder{matchers: []matcher{{key: "foo"}}}, |
| wantEqual: true, |
| }, |
| } |
| |
| for _, test := range tests { |
| t.Run(test.desc, func(t *testing.T) { |
| t.Run(test.desc, func(t *testing.T) { |
| if gotEqual := test.a.Equal(test.b); gotEqual != test.wantEqual { |
| t.Errorf("builder.Equal(%v, %v) = %v, want %v", test.a, test.b, gotEqual, test.wantEqual) |
| } |
| }) |
| }) |
| } |
| } |
| |
| // matcher helps extract a key from request headers based on a given name. |
| func TestMatcherEqual(t *testing.T) { |
| tests := []struct { |
| desc string |
| a matcher |
| b matcher |
| wantEqual bool |
| }{ |
| { |
| desc: "different keys", |
| a: matcher{key: "foo"}, |
| b: matcher{key: "bar"}, |
| wantEqual: false, |
| }, |
| { |
| desc: "different number of names", |
| a: matcher{key: "foo", names: []string{"v1", "v2"}}, |
| b: matcher{key: "foo", names: []string{"v1"}}, |
| wantEqual: false, |
| }, |
| { |
| desc: "equal number but differing names", |
| a: matcher{key: "foo", names: []string{"v1", "v2"}}, |
| b: matcher{key: "foo", names: []string{"v1", "v22"}}, |
| wantEqual: false, |
| }, |
| { |
| desc: "same names in different order", |
| a: matcher{key: "foo", names: []string{"v2", "v1"}}, |
| b: matcher{key: "foo", names: []string{"v1", "v3"}}, |
| wantEqual: false, |
| }, |
| { |
| desc: "good match", |
| a: matcher{key: "foo", names: []string{"v1", "v2"}}, |
| b: matcher{key: "foo", names: []string{"v1", "v2"}}, |
| wantEqual: true, |
| }, |
| } |
| |
| for _, test := range tests { |
| t.Run(test.desc, func(t *testing.T) { |
| if gotEqual := test.a.Equal(test.b); gotEqual != test.wantEqual { |
| t.Errorf("matcher.Equal(%v, %v) = %v, want %v", test.a, test.b, gotEqual, test.wantEqual) |
| } |
| }) |
| } |
| } |