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 }