go.uber.org/yarpc@v1.72.1/encoding/protobuf/v2/error_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 v2
    22  
    23  import (
    24  	"errors"
    25  	"fmt"
    26  	"strings"
    27  	"testing"
    28  
    29  	"github.com/golang/protobuf/ptypes/any"
    30  	"github.com/golang/protobuf/ptypes/wrappers"
    31  	"github.com/stretchr/testify/assert"
    32  	"github.com/stretchr/testify/require"
    33  	"go.uber.org/yarpc/api/transport/transporttest"
    34  	"go.uber.org/yarpc/yarpcerrors"
    35  	rpc_status "google.golang.org/genproto/googleapis/rpc/status"
    36  	"google.golang.org/grpc/codes"
    37  	"google.golang.org/grpc/status"
    38  	"google.golang.org/protobuf/proto"
    39  	anypb "google.golang.org/protobuf/types/known/anypb"
    40  )
    41  
    42  func TestNewOK(t *testing.T) {
    43  	err := NewError(yarpcerrors.CodeOK, "okay")
    44  	assert.Nil(t, err)
    45  
    46  	assert.Equal(t, yarpcerrors.FromError(err).Code(), yarpcerrors.CodeOK)
    47  	assert.Equal(t, yarpcerrors.FromError(err).Message(), "")
    48  }
    49  
    50  func TestNew(t *testing.T) {
    51  	err := NewError(yarpcerrors.CodeNotFound, "unfounded accusation")
    52  	assert.Equal(t, yarpcerrors.FromError(err).Code(), yarpcerrors.CodeNotFound)
    53  	assert.Equal(t, yarpcerrors.FromError(err).Message(), "unfounded accusation")
    54  	assert.Contains(t, err.Error(), "unfounded accusation")
    55  }
    56  
    57  func TestForeignError(t *testing.T) {
    58  	err := errors.New("to err is go")
    59  	assert.Equal(t, yarpcerrors.FromError(err).Code(), yarpcerrors.CodeUnknown)
    60  	assert.Equal(t, yarpcerrors.FromError(err).Message(), "to err is go")
    61  }
    62  
    63  func TestConvertToYARPCErrorWithWrappedError(t *testing.T) {
    64  	errDetail := &wrappers.BytesValue{Value: []byte("err detail bytes")}
    65  
    66  	pbErr := NewError(
    67  		yarpcerrors.CodeAborted,
    68  		"aborted",
    69  		WithErrorDetails(errDetail))
    70  
    71  	wrappedErr := fmt.Errorf("wrapped err 2: %w", fmt.Errorf("wrapped err 1: %w", pbErr))
    72  
    73  	err := convertToYARPCError(Encoding, wrappedErr, &codec{}, nil /* resw */)
    74  	require.True(t, yarpcerrors.IsStatus(err), "unexpected error")
    75  	assert.Equal(t, yarpcerrors.FromError(err).Code(), yarpcerrors.CodeAborted, "unexpected err code")
    76  	assert.Equal(t, yarpcerrors.FromError(err).Message(), "aborted", "unexpected error message")
    77  
    78  	gotDetails := yarpcerrors.FromError(err).Details()
    79  	assert.NotEmpty(t, gotDetails, "no details marshaled")
    80  }
    81  
    82  func TestConvertToYARPCErrorApplicationErrorMeta(t *testing.T) {
    83  	errDetails := []proto.Message{
    84  		&wrappers.StringValue{Value: "detail message"},
    85  		&wrappers.Int32Value{Value: 42},
    86  		&wrappers.BytesValue{Value: []byte("detail bytes")},
    87  	}
    88  
    89  	pbErr := NewError(
    90  		yarpcerrors.CodeAborted,
    91  		"aborted",
    92  		WithErrorDetails(errDetails...))
    93  
    94  	resw := &transporttest.FakeResponseWriter{}
    95  	err := convertToYARPCError(Encoding, pbErr, &codec{}, resw)
    96  	require.Error(t, err)
    97  
    98  	require.NotNil(t, resw.ApplicationErrorMeta)
    99  	assert.Equal(t, "StringValue", resw.ApplicationErrorMeta.Name, "expected first error detail name")
   100  	assert.Equal(t,
   101  		`[]{ StringValue{value:"detail message"} , Int32Value{value:42} , BytesValue{value:"detail bytes"} }`,
   102  		resw.ApplicationErrorMeta.Details,
   103  		"unexpected string of error details")
   104  	assert.Nil(t, resw.ApplicationErrorMeta.Code, "code should nil")
   105  }
   106  
   107  func TestMessageNameWithoutPackage(t *testing.T) {
   108  	tests := []struct {
   109  		name string
   110  		give string
   111  		want string
   112  	}{
   113  		{
   114  			name: "fqn",
   115  			give: "uber.foo.bar.baz.MessageName",
   116  			want: "MessageName",
   117  		},
   118  		{
   119  			name: "not fully qualified",
   120  			give: "MyMessage",
   121  			want: "MyMessage",
   122  		},
   123  	}
   124  
   125  	for _, tt := range tests {
   126  		t.Run(tt.name, func(t *testing.T) {
   127  			assert.Equal(t, tt.want, messageNameWithoutPackage(tt.give), "unexpected trim")
   128  		})
   129  	}
   130  }
   131  
   132  type yarpcError interface{ YARPCError() *yarpcerrors.Status }
   133  
   134  func TestPbErrorToYARPCError(t *testing.T) {
   135  	tests := []struct {
   136  		name             string
   137  		code             yarpcerrors.Code
   138  		message          string
   139  		details          []proto.Message
   140  		expectedGRPCCode codes.Code
   141  	}{
   142  		{
   143  			name:             "pbError without details",
   144  			code:             yarpcerrors.CodeAborted,
   145  			message:          "simple test",
   146  			expectedGRPCCode: codes.Aborted,
   147  		},
   148  		{
   149  			name:             "pbError with single detail",
   150  			code:             yarpcerrors.CodeInternal,
   151  			message:          "internal error",
   152  			expectedGRPCCode: codes.Internal,
   153  			details: []proto.Message{
   154  				&wrappers.StringValue{Value: "test value"},
   155  			},
   156  		},
   157  		{
   158  			name:             "pbError with multiple details",
   159  			code:             yarpcerrors.CodeNotFound,
   160  			message:          "not found error",
   161  			expectedGRPCCode: codes.NotFound,
   162  			details: []proto.Message{
   163  				&wrappers.StringValue{Value: "test value"},
   164  				&wrappers.Int32Value{Value: 45},
   165  				&any.Any{Value: []byte{1, 2, 3, 4, 5}},
   166  			},
   167  		},
   168  	}
   169  
   170  	for _, tt := range tests {
   171  		t.Run(tt.name, func(t *testing.T) {
   172  			var errOpts []ErrorOption
   173  			if len(tt.details) > 0 {
   174  				errOpts = append(errOpts, WithErrorDetails(tt.details...))
   175  			}
   176  			pberror := NewError(tt.code, tt.message, errOpts...)
   177  			st := pberror.(yarpcError).YARPCError()
   178  			assert.Equal(t, st.Code(), tt.code)
   179  			assert.Equal(t, st.Message(), tt.message)
   180  
   181  			statusPb := rpc_status.Status{}
   182  			err := proto.Unmarshal(st.Details(), &statusPb)
   183  			assert.NoError(t, err, "unexpected unmarshal error")
   184  
   185  			status := status.FromProto(&statusPb)
   186  			assert.Equal(t, tt.expectedGRPCCode, status.Code(), "unexpected grpc status code")
   187  			assert.Equal(t, tt.message, status.Message(), "unexpected grpc status message")
   188  			assert.Len(t, status.Details(), len(tt.details), "unexpected details length")
   189  			for i, detail := range tt.details {
   190  				assert.True(t, proto.Equal(detail, status.Details()[i].(proto.Message)))
   191  			}
   192  		})
   193  	}
   194  }
   195  
   196  func TestConvertToYARPCErrorWithIncorrectEncoding(t *testing.T) {
   197  	pberr := &pberror{code: yarpcerrors.CodeAborted, message: "test"}
   198  	err := convertToYARPCError("thrift", pberr, &codec{}, nil)
   199  	assert.Error(t, err, "unexpected empty error")
   200  	assert.Equal(t, err.Error(),
   201  		"code:internal message:encoding.Expect should have handled encoding \"thrift\" but did not")
   202  }
   203  
   204  func TestConvertFromYARPCError(t *testing.T) {
   205  	t.Run("incorrect encoding", func(t *testing.T) {
   206  		yerr := yarpcerrors.Newf(yarpcerrors.CodeAborted, "test").WithDetails([]byte{1, 2})
   207  		err := convertFromYARPCError("thrift", yerr, &codec{})
   208  		assert.Equal(t, err.Error(),
   209  			`code:internal message:encoding.Expect should have handled encoding "thrift" but did not`)
   210  	})
   211  	t.Run("empty details", func(t *testing.T) {
   212  		yerr := yarpcerrors.Newf(yarpcerrors.CodeAborted, "test")
   213  		err := convertFromYARPCError(Encoding, yerr, &codec{})
   214  		assert.Equal(t, err.Error(), "code:aborted message:test")
   215  	})
   216  }
   217  
   218  func TestCreateStatusWithDetailErrors(t *testing.T) {
   219  	t.Run("code ok", func(t *testing.T) {
   220  		pberr := &pberror{code: yarpcerrors.CodeOK, message: "test"}
   221  		_, err := createStatusWithDetail(pberr, Encoding, &codec{})
   222  		assert.Error(t, err, "unexpected empty error")
   223  		assert.Equal(t, err.Error(), "no status error for error with code OK")
   224  	})
   225  
   226  	t.Run("unsupported code", func(t *testing.T) {
   227  		pberr := &pberror{code: 99, message: "test"}
   228  		_, err := createStatusWithDetail(pberr, Encoding, &codec{})
   229  		assert.Error(t, err, "unexpected empty error")
   230  		assert.Equal(t, err.Error(), "no status error for error with code 99")
   231  	})
   232  
   233  	t.Run("unsupported encoding", func(t *testing.T) {
   234  		pberr := &pberror{code: yarpcerrors.CodeAborted}
   235  		_, err := createStatusWithDetail(pberr, "thrift", &codec{})
   236  		assert.Error(t, err, "unexpected empty error")
   237  		assert.Equal(t, err.Error(),
   238  			"code:internal message:encoding.Expect should have handled encoding \"thrift\" but did not")
   239  	})
   240  }
   241  
   242  func TestErrorHandling(t *testing.T) {
   243  	t.Run("GetErrorDetail empty error handling", func(t *testing.T) {
   244  		assert.Nil(t, GetErrorDetails(nil), "unexpected details")
   245  	})
   246  	t.Run("GetErrorDetail non pberror", func(t *testing.T) {
   247  		assert.Nil(t, GetErrorDetails(errors.New("test")), "unexpected details")
   248  	})
   249  	t.Run("GetErrorDetail with no error detail", func(t *testing.T) {
   250  		err := NewError(
   251  			yarpcerrors.CodeAborted,
   252  			"aborted")
   253  		assert.Nil(t, GetErrorDetails(err), "unexpected details")
   254  	})
   255  	t.Run("PbError empty error handling", func(t *testing.T) {
   256  		var pbErr *pberror
   257  		assert.Nil(t, pbErr.YARPCError(), "unexpected yarpcerror")
   258  	})
   259  	t.Run("PbError with nil detail message", func(t *testing.T) {
   260  		pbErr := &pberror{
   261  			details: []*any.Any{nil},
   262  		}
   263  		require.Len(t, GetErrorDetails(pbErr), 1)
   264  		assert.Equal(t, strings.ReplaceAll(GetErrorDetails(pbErr)[0].(error).Error(), "\u00a0", " "),
   265  			fmt.Errorf("proto: invalid empty type URL").Error())
   266  	})
   267  	t.Run("NewError with nil error detail", func(t *testing.T) {
   268  		err := NewError(
   269  			yarpcerrors.CodeAborted,
   270  			"aborted",
   271  			WithErrorDetails([]proto.Message{nil}...))
   272  		assert.Equal(t, strings.ReplaceAll(err.Error(), "\u00a0", " "),
   273  			fmt.Errorf("proto: invalid nil source message").Error())
   274  	})
   275  }
   276  
   277  func TestSetApplicationErrorMeta(t *testing.T) {
   278  	respErr := NewError(
   279  		yarpcerrors.CodeAborted,
   280  		"aborted",
   281  	)
   282  
   283  	anyString, err := anypb.New(&wrappers.StringValue{Value: "baz"})
   284  	require.NoError(t, err)
   285  
   286  	pbErr := respErr.(*pberror)
   287  	pbErr.details = append(pbErr.details, &any.Any{TypeUrl: "foo", Value: []byte("bar")})
   288  	pbErr.details = append(pbErr.details, anyString)
   289  
   290  	resw := &transporttest.FakeResponseWriter{}
   291  	setApplicationErrorMeta(pbErr, resw)
   292  
   293  	assert.Equal(t, "StringValue", resw.ApplicationErrorMeta.Name)
   294  	assert.Equal(t, `[]{ StringValue{value:"baz"} }`, resw.ApplicationErrorMeta.Details)
   295  }