go.uber.org/yarpc@v1.72.1/inject_test.go (about)

     1  // Copyright (c) 2022 Uber Technologies, Inc.
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to deal
     5  // in the Software without restriction, including without limitation the rights
     6  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  // copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    18  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    19  // THE SOFTWARE.
    20  
    21  package yarpc_test
    22  
    23  import (
    24  	"fmt"
    25  	"reflect"
    26  	"testing"
    27  
    28  	"github.com/golang/mock/gomock"
    29  	"github.com/stretchr/testify/assert"
    30  	"github.com/stretchr/testify/require"
    31  	"go.uber.org/yarpc"
    32  	"go.uber.org/yarpc/api/transport"
    33  	"go.uber.org/yarpc/api/transport/transporttest"
    34  	"go.uber.org/yarpc/encoding/json"
    35  	"go.uber.org/yarpc/encoding/raw"
    36  	"go.uber.org/yarpc/internal/clientconfig"
    37  )
    38  
    39  func TestRegisterClientBuilderPanics(t *testing.T) {
    40  	tests := []struct {
    41  		name string
    42  		give interface{}
    43  	}{
    44  		{name: "nil", give: nil},
    45  		{name: "wrong kind", give: 42},
    46  		{
    47  			name: "already registered",
    48  			give: func(transport.ClientConfig) json.Client { return nil },
    49  		},
    50  		{
    51  			name: "wrong argument type",
    52  			give: func(int) json.Client { return nil },
    53  		},
    54  		{
    55  			name: "wrong return type",
    56  			give: func(transport.ClientConfig) string { return "" },
    57  		},
    58  		{
    59  			name: "no arguments",
    60  			give: func() json.Client { return nil },
    61  		},
    62  		{
    63  			name: "too many arguments",
    64  			give: func(transport.ClientConfig, reflect.StructField, string) json.Client { return nil },
    65  		},
    66  		{
    67  			name: "wrong number of arguments",
    68  			give: func(transport.ClientConfig, ...string) json.Client { return nil },
    69  		},
    70  		{
    71  			name: "wrong number of returns",
    72  			give: func(transport.ClientConfig) (json.Client, error) { return nil, nil },
    73  		},
    74  	}
    75  
    76  	for _, tt := range tests {
    77  		assert.Panics(t, func() { yarpc.RegisterClientBuilder(tt.give) }, tt.name)
    78  	}
    79  }
    80  
    81  func TestInjectClientsPanics(t *testing.T) {
    82  	mockCtrl := gomock.NewController(t)
    83  	defer mockCtrl.Finish()
    84  
    85  	type unknownClient interface{}
    86  
    87  	tests := []struct {
    88  		name           string
    89  		failOnServices []string
    90  		target         interface{}
    91  	}{
    92  		{
    93  			name:   "not a pointer to a struct",
    94  			target: struct{}{},
    95  		},
    96  		{
    97  			name:           "unknown service",
    98  			failOnServices: []string{"foo"},
    99  			target: &struct {
   100  				Client json.Client `service:"foo"`
   101  			}{},
   102  		},
   103  		{
   104  			name: "unknown client",
   105  			target: &struct {
   106  				Client unknownClient `service:"bar"`
   107  			}{},
   108  		},
   109  	}
   110  
   111  	for _, tt := range tests {
   112  		cp := newMockClientConfigProvider(mockCtrl)
   113  		for _, s := range tt.failOnServices {
   114  			cp.EXPECT().ClientConfig(s).Do(func(s string) {
   115  				panic(fmt.Sprintf("unknown service %q", s))
   116  			})
   117  		}
   118  
   119  		assert.Panics(t, func() {
   120  			yarpc.InjectClients(cp, tt.target)
   121  		}, tt.name)
   122  	}
   123  }
   124  
   125  type someClient interface{}
   126  
   127  // Helps build client builders (of type someClient) which verify the
   128  // ClientConfig and optionally, the StructField.
   129  type clientBuilderConfig struct {
   130  	ClientConfig gomock.Matcher
   131  	StructField  gomock.Matcher
   132  }
   133  
   134  func (c clientBuilderConfig) clientConfigBuilder(t *testing.T) func(cc transport.ClientConfig) someClient {
   135  	return func(cc transport.ClientConfig) someClient {
   136  		require.True(t, c.ClientConfig.Matches(cc), "client config %v did not match %v", cc, c.ClientConfig)
   137  		return someClient(struct{}{})
   138  	}
   139  }
   140  
   141  func (c clientBuilderConfig) Get(t *testing.T) interface{} {
   142  	ccBuilder := c.clientConfigBuilder(t)
   143  	if c.StructField == nil {
   144  		return ccBuilder
   145  	}
   146  
   147  	return func(cc transport.ClientConfig, f reflect.StructField) someClient {
   148  		require.True(t, c.StructField.Matches(f), "struct field %#v did not match %v", f, c.StructField)
   149  		return ccBuilder(cc)
   150  	}
   151  }
   152  
   153  func TestInjectClientSuccess(t *testing.T) {
   154  	type testCase struct {
   155  		target interface{}
   156  
   157  		// list of client builders to register using RegisterClientBuilder.
   158  		//
   159  		// Test instances of these can be built with
   160  		// clientBuilderConfig.Get(t).
   161  		clientBuilders []interface{}
   162  
   163  		// list of services for which ClientConfig() should return successfully
   164  		knownServices []string
   165  
   166  		// list of field names in target we expect to be nil or non-nil
   167  		wantNil    []string
   168  		wantNonNil []string
   169  	}
   170  
   171  	tests := []struct {
   172  		name  string
   173  		build func(*testing.T, *gomock.Controller) testCase
   174  	}{
   175  		{
   176  			name: "empty",
   177  			build: func(t *testing.T, mockCtrl *gomock.Controller) (tt testCase) {
   178  				tt.target = &struct{}{}
   179  				return
   180  			},
   181  		},
   182  		{
   183  			name: "unknown service non-nil",
   184  			build: func(t *testing.T, mockCtrl *gomock.Controller) (tt testCase) {
   185  				tt.target = &struct {
   186  					Client json.Client `service:"foo"`
   187  				}{
   188  					Client: json.New(clientconfig.MultiOutbound(
   189  						"foo",
   190  						"bar",
   191  						transport.Outbounds{
   192  							Unary: transporttest.NewMockUnaryOutbound(mockCtrl),
   193  						})),
   194  				}
   195  				tt.wantNonNil = []string{"Client"}
   196  				return
   197  			},
   198  		},
   199  		{
   200  			name: "unknown type untagged",
   201  			build: func(t *testing.T, mockCtrl *gomock.Controller) (tt testCase) {
   202  				tt.target = &struct {
   203  					Client someClient `notservice:"foo"`
   204  				}{}
   205  				tt.wantNil = []string{"Client"}
   206  				return
   207  			},
   208  		},
   209  		{
   210  			name: "unknown type non-nil",
   211  			build: func(t *testing.T, mockCtrl *gomock.Controller) (tt testCase) {
   212  				tt.target = &struct {
   213  					Client someClient `service:"foo"`
   214  				}{Client: someClient(struct{}{})}
   215  				tt.wantNonNil = []string{"Client"}
   216  				return
   217  			},
   218  		},
   219  		{
   220  			name: "known type",
   221  			build: func(t *testing.T, mockCtrl *gomock.Controller) (tt testCase) {
   222  				tt.knownServices = []string{"foo"}
   223  				tt.clientBuilders = []interface{}{
   224  					clientBuilderConfig{ClientConfig: gomock.Any()}.Get(t),
   225  				}
   226  				tt.target = &struct {
   227  					Client someClient `service:"foo"`
   228  				}{}
   229  				tt.wantNonNil = []string{"Client"}
   230  				return
   231  			},
   232  		},
   233  		{
   234  			name: "known type with struct field",
   235  			build: func(t *testing.T, mockCtrl *gomock.Controller) (tt testCase) {
   236  				tt.knownServices = []string{"foo"}
   237  				tt.clientBuilders = []interface{}{
   238  					clientBuilderConfig{
   239  						ClientConfig: gomock.Any(),
   240  						StructField: gomock.Eq(reflect.StructField{
   241  							Name:  "Client",
   242  							Type:  reflect.TypeOf((*someClient)(nil)).Elem(),
   243  							Index: []int{0},
   244  							Tag:   `service:"foo" thrift:"bar"`,
   245  						}),
   246  					}.Get(t),
   247  				}
   248  				tt.target = &struct {
   249  					Client someClient `service:"foo" thrift:"bar"`
   250  				}{}
   251  				tt.wantNonNil = []string{"Client"}
   252  				return
   253  			},
   254  		},
   255  		{
   256  			name: "default encodings",
   257  			build: func(t *testing.T, mockCtrl *gomock.Controller) (tt testCase) {
   258  				tt.knownServices = []string{"jsontest", "rawtest"}
   259  				tt.target = &struct {
   260  					JSON json.Client `service:"jsontest"`
   261  					Raw  raw.Client  `service:"rawtest"`
   262  				}{}
   263  				tt.wantNonNil = []string{"JSON", "Raw"}
   264  				return
   265  			},
   266  		},
   267  		{
   268  			name: "unexported field",
   269  			build: func(t *testing.T, mockCtrl *gomock.Controller) (tt testCase) {
   270  				tt.target = &struct {
   271  					rawClient raw.Client `service:"rawtest"`
   272  				}{}
   273  				tt.wantNil = []string{"rawClient"}
   274  				return
   275  			},
   276  		},
   277  	}
   278  
   279  	for _, testCase := range tests {
   280  		t.Run(testCase.name, func(t *testing.T) {
   281  			mockCtrl := gomock.NewController(t)
   282  			defer mockCtrl.Finish()
   283  			tt := testCase.build(t, mockCtrl)
   284  
   285  			for _, builder := range tt.clientBuilders {
   286  				forget := yarpc.RegisterClientBuilder(builder)
   287  				defer forget()
   288  			}
   289  
   290  			cp := newMockClientConfigProvider(mockCtrl, tt.knownServices...)
   291  			assert.NotPanics(t, func() {
   292  				yarpc.InjectClients(cp, tt.target)
   293  			})
   294  
   295  			for _, fieldName := range tt.wantNil {
   296  				field := reflect.ValueOf(tt.target).Elem().FieldByName(fieldName)
   297  				assert.True(t, field.IsNil(), "expected %q to be nil", fieldName)
   298  			}
   299  
   300  			for _, fieldName := range tt.wantNonNil {
   301  				field := reflect.ValueOf(tt.target).Elem().FieldByName(fieldName)
   302  				assert.False(t, field.IsNil(), "expected %q to be non-nil", fieldName)
   303  			}
   304  		})
   305  	}
   306  }
   307  
   308  // newMockClientConfigProvider builds a MockClientConfigProvider which expects ClientConfig()
   309  // calls for the given services and returns mock ClientConfigs for them.
   310  func newMockClientConfigProvider(ctrl *gomock.Controller, services ...string) *transporttest.MockClientConfigProvider {
   311  	cp := transporttest.NewMockClientConfigProvider(ctrl)
   312  	for _, s := range services {
   313  		cp.EXPECT().ClientConfig(s).Return(transporttest.NewMockClientConfig(ctrl))
   314  	}
   315  	return cp
   316  }