go.uber.org/yarpc@v1.72.1/encoding/json/outbound_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 json 22 23 import ( 24 "bytes" 25 "context" 26 "errors" 27 "io/ioutil" 28 "reflect" 29 "testing" 30 31 "github.com/golang/mock/gomock" 32 "github.com/stretchr/testify/assert" 33 "github.com/stretchr/testify/require" 34 "go.uber.org/yarpc" 35 "go.uber.org/yarpc/api/transport" 36 "go.uber.org/yarpc/api/transport/transporttest" 37 "go.uber.org/yarpc/internal/clientconfig" 38 ) 39 40 var _typeOfMapInterface = reflect.TypeOf(map[string]interface{}{}) 41 42 func TestCall(t *testing.T) { 43 mockCtrl := gomock.NewController(t) 44 defer mockCtrl.Finish() 45 46 ctx := context.Background() 47 48 caller := "caller" 49 service := "service" 50 51 tests := []struct { 52 procedure string 53 headers map[string]string 54 body interface{} 55 encodedRequest string 56 encodedResponse string 57 responseErr error 58 59 // whether the outbound receives the request 60 noCall bool 61 62 // Either want, or wantType and wantErr must be set. 63 want interface{} // expected response body 64 wantHeaders map[string]string 65 wantType reflect.Type // type of response body 66 wantErr string // error message 67 }{ 68 { 69 procedure: "foo", 70 body: []string{"foo", "bar"}, 71 encodedRequest: `["foo","bar"]`, 72 encodedResponse: `{"success": true}`, 73 want: map[string]interface{}{"success": true}, 74 }, 75 { 76 procedure: "foo", 77 body: []string{"foo", "bar"}, 78 encodedRequest: `["foo","bar"]`, 79 encodedResponse: `{"success": true}`, 80 responseErr: errors.New("bar"), 81 want: map[string]interface{}{"success": true}, 82 wantErr: "bar", 83 }, 84 { 85 procedure: "bar", 86 body: []int{1, 2, 3}, 87 encodedRequest: `[1,2,3]`, 88 encodedResponse: `invalid JSON`, 89 wantType: _typeOfMapInterface, 90 wantErr: `failed to decode "json" response body for procedure "bar" of service "service"`, 91 }, 92 { 93 procedure: "baz", 94 body: func() {}, // funcs cannot be json.Marshal'ed 95 noCall: true, 96 wantType: _typeOfMapInterface, 97 wantErr: `failed to encode "json" request body for procedure "baz" of service "service"`, 98 }, 99 { 100 procedure: "requestHeaders", 101 headers: map[string]string{"user-id": "42"}, 102 body: map[string]interface{}{}, 103 encodedRequest: "{}", 104 encodedResponse: "{}", 105 want: map[string]interface{}{}, 106 wantHeaders: map[string]string{"success": "true"}, 107 }, 108 } 109 110 for _, tt := range tests { 111 outbound := transporttest.NewMockUnaryOutbound(mockCtrl) 112 client := New(clientconfig.MultiOutbound(caller, service, 113 transport.Outbounds{ 114 Unary: outbound, 115 })) 116 117 if !tt.noCall { 118 outbound.EXPECT().Call(gomock.Any(), 119 transporttest.NewRequestMatcher(t, 120 &transport.Request{ 121 Caller: caller, 122 Service: service, 123 Procedure: tt.procedure, 124 Encoding: Encoding, 125 Headers: transport.HeadersFromMap(tt.headers), 126 Body: bytes.NewReader([]byte(tt.encodedRequest)), 127 }), 128 ).Return( 129 &transport.Response{ 130 Body: ioutil.NopCloser( 131 bytes.NewReader([]byte(tt.encodedResponse))), 132 Headers: transport.HeadersFromMap(tt.wantHeaders), 133 }, tt.responseErr) 134 } 135 136 var wantType reflect.Type 137 if tt.want != nil { 138 wantType = reflect.TypeOf(tt.want) 139 } else { 140 require.NotNil(t, tt.wantType, "wantType is required if want is nil") 141 wantType = tt.wantType 142 } 143 resBody := reflect.Zero(wantType).Interface() 144 145 var ( 146 opts []yarpc.CallOption 147 resHeaders map[string]string 148 ) 149 150 for k, v := range tt.headers { 151 opts = append(opts, yarpc.WithHeader(k, v)) 152 } 153 opts = append(opts, yarpc.ResponseHeaders(&resHeaders)) 154 155 err := client.Call(ctx, tt.procedure, tt.body, &resBody, opts...) 156 if tt.wantErr != "" { 157 if assert.Error(t, err) { 158 assert.Contains(t, err.Error(), tt.wantErr) 159 } 160 } else { 161 assert.NoError(t, err) 162 } 163 if tt.wantHeaders != nil { 164 assert.Equal(t, tt.wantHeaders, resHeaders) 165 } 166 if tt.want != nil { 167 assert.Equal(t, tt.want, resBody) 168 } 169 } 170 } 171 172 type successAck struct{} 173 174 func (a successAck) String() string { 175 return "success" 176 } 177 178 func TestCallOneway(t *testing.T) { 179 mockCtrl := gomock.NewController(t) 180 defer mockCtrl.Finish() 181 182 ctx := context.Background() 183 184 caller := "caller" 185 service := "service" 186 187 tests := []struct { 188 procedure string 189 headers map[string]string 190 body interface{} 191 encodedRequest string 192 193 // whether the outbound receives the request 194 noCall bool 195 196 wantErr string // error message 197 }{ 198 { 199 procedure: "foo", 200 body: []string{"foo", "bar"}, 201 encodedRequest: `["foo","bar"]` + "\n", 202 }, 203 { 204 procedure: "baz", 205 body: func() {}, // funcs cannot be json.Marshal'ed 206 noCall: true, 207 wantErr: `failed to encode "json" request body for procedure "baz" of service "service"`, 208 }, 209 { 210 procedure: "requestHeaders", 211 headers: map[string]string{"user-id": "42"}, 212 body: map[string]interface{}{}, 213 encodedRequest: "{}\n", 214 }, 215 } 216 217 for _, tt := range tests { 218 outbound := transporttest.NewMockOnewayOutbound(mockCtrl) 219 client := New(clientconfig.MultiOutbound(caller, service, 220 transport.Outbounds{ 221 Oneway: outbound, 222 })) 223 224 if !tt.noCall { 225 reqMatcher := transporttest.NewRequestMatcher(t, 226 &transport.Request{ 227 Caller: caller, 228 Service: service, 229 Procedure: tt.procedure, 230 Encoding: Encoding, 231 Headers: transport.HeadersFromMap(tt.headers), 232 Body: bytes.NewReader([]byte(tt.encodedRequest)), 233 }) 234 235 if tt.wantErr != "" { 236 outbound. 237 EXPECT(). 238 CallOneway(gomock.Any(), reqMatcher). 239 Return(nil, errors.New(tt.wantErr)) 240 } else { 241 outbound. 242 EXPECT(). 243 CallOneway(gomock.Any(), reqMatcher). 244 Return(&successAck{}, nil) 245 } 246 } 247 248 var opts []yarpc.CallOption 249 250 for k, v := range tt.headers { 251 opts = append(opts, yarpc.WithHeader(k, v)) 252 } 253 254 ack, err := client.CallOneway(ctx, tt.procedure, tt.body, opts...) 255 if tt.wantErr != "" { 256 assert.Error(t, err) 257 assert.Contains(t, err.Error(), tt.wantErr) 258 } else { 259 assert.NoError(t, err, "") 260 assert.Equal(t, ack.String(), "success") 261 } 262 } 263 }