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  }