go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/grpc/prpc/encoding_test.go (about)

     1  // Copyright 2016 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package prpc
    16  
    17  import (
    18  	"context"
    19  	"net/http"
    20  	"net/http/httptest"
    21  	"strings"
    22  	"testing"
    23  
    24  	"github.com/golang/protobuf/proto"
    25  	"google.golang.org/genproto/googleapis/rpc/errdetails"
    26  
    27  	"google.golang.org/grpc/codes"
    28  	"google.golang.org/grpc/status"
    29  
    30  	"go.chromium.org/luci/common/logging"
    31  	"go.chromium.org/luci/common/logging/memlogger"
    32  
    33  	. "github.com/smartystreets/goconvey/convey"
    34  	. "go.chromium.org/luci/common/testing/assertions"
    35  )
    36  
    37  func TestEncoding(t *testing.T) {
    38  	t.Parallel()
    39  
    40  	Convey("responseFormat", t, func() {
    41  		test := func(acceptHeader string, expectedFormat Format, expectedErr any) {
    42  			acceptHeader = strings.Replace(acceptHeader, "{json}", mtPRPCJSONPB, -1)
    43  			acceptHeader = strings.Replace(acceptHeader, "{binary}", mtPRPCBinary, -1)
    44  			acceptHeader = strings.Replace(acceptHeader, "{text}", mtPRPCText, -1)
    45  
    46  			Convey("Accept: "+acceptHeader, func() {
    47  				actualFormat, err := responseFormat(acceptHeader)
    48  				So(err, ShouldErrLike, expectedErr)
    49  				if err == nil {
    50  					So(actualFormat, ShouldEqual, expectedFormat)
    51  				}
    52  			})
    53  		}
    54  
    55  		test("", FormatBinary, nil)
    56  		test(ContentTypePRPC, FormatBinary, nil)
    57  		test(mtPRPCBinary, FormatBinary, nil)
    58  		test(mtPRPCJSONPBLegacy, FormatJSONPB, nil)
    59  		test(mtPRPCText, FormatText, nil)
    60  		test(ContentTypeJSON, FormatJSONPB, nil)
    61  
    62  		test("application/*", FormatBinary, nil)
    63  		test("*/*", FormatBinary, nil)
    64  
    65  		// test cases with multiple types
    66  		test("{json},{binary}", FormatBinary, nil)
    67  		test("{json},{binary};q=0.9", FormatJSONPB, nil)
    68  		test("{json};q=1,{binary};q=0.9", FormatJSONPB, nil)
    69  		test("{json},{text}", FormatJSONPB, nil)
    70  		test("{json};q=0.9,{text}", FormatText, nil)
    71  		test("{binary},{json},{text}", FormatBinary, nil)
    72  
    73  		test("{json},{binary},*/*", FormatBinary, nil)
    74  		test("{json},{binary},*/*;q=0.9", FormatBinary, nil)
    75  		test("{json},{binary},*/*;x=y", FormatBinary, nil)
    76  		test("{json},{binary};q=0.9,*/*", FormatBinary, nil)
    77  		test("{json},{binary};q=0.9,*/*;q=0.8", FormatJSONPB, nil)
    78  
    79  		// supported and unsupported mix
    80  		test("{json},foo/bar", FormatJSONPB, nil)
    81  		test("{json};q=0.1,foo/bar", FormatJSONPB, nil)
    82  		test("foo/bar;q=0.1,{json}", FormatJSONPB, nil)
    83  
    84  		// only unsupported types
    85  		const err406 = "pRPC: bad Accept header: specified media types are not not supported"
    86  		test(ContentTypePRPC+"; boo=true", 0, err406)
    87  		test(ContentTypePRPC+"; encoding=blah", 0, err406)
    88  		test("x", 0, err406)
    89  		test("x,y", 0, err406)
    90  
    91  		test("x//y", 0, "pRPC: bad Accept header: specified media types are not not supported")
    92  	})
    93  
    94  	Convey("writeMessage", t, func() {
    95  		msg := &HelloReply{Message: "Hi"}
    96  		c := context.Background()
    97  
    98  		test := func(f Format, body []byte, contentType string) {
    99  			Convey(contentType, func() {
   100  				rec := httptest.NewRecorder()
   101  				writeMessage(c, rec, msg, f, false)
   102  				So(rec.Code, ShouldEqual, http.StatusOK)
   103  				So(rec.Header().Get(HeaderGRPCCode), ShouldEqual, "0")
   104  				So(rec.Header().Get(headerContentType), ShouldEqual, contentType)
   105  				So(rec.Body.Bytes(), ShouldResemble, body)
   106  			})
   107  		}
   108  
   109  		msgBytes, err := proto.Marshal(msg)
   110  		So(err, ShouldBeNil)
   111  
   112  		test(FormatBinary, msgBytes, mtPRPCBinary)
   113  		test(FormatJSONPB, []byte(JSONPBPrefix+"{\"message\":\"Hi\"}\n"), mtPRPCJSONPB)
   114  		test(FormatText, []byte("message: \"Hi\"\n"), mtPRPCText)
   115  
   116  		Convey("compression", func() {
   117  			rec := httptest.NewRecorder()
   118  			msg := &HelloReply{Message: strings.Repeat("A", 1024)}
   119  			writeMessage(c, rec, msg, FormatText, true)
   120  			So(rec.Code, ShouldEqual, http.StatusOK)
   121  			So(rec.Header().Get("Content-Encoding"), ShouldEqual, "gzip")
   122  			So(rec.Body.Len(), ShouldBeLessThan, 1024)
   123  		})
   124  	})
   125  
   126  	Convey("writeError", t, func() {
   127  		c := context.Background()
   128  		c = memlogger.Use(c)
   129  		log := logging.Get(c).(*memlogger.MemLogger)
   130  
   131  		rec := httptest.NewRecorder()
   132  
   133  		Convey("client error", func() {
   134  			writeError(c, rec, status.Error(codes.NotFound, "not found"), FormatBinary)
   135  			So(rec.Code, ShouldEqual, http.StatusNotFound)
   136  			So(rec.Header().Get(HeaderGRPCCode), ShouldEqual, "5")
   137  			So(rec.Header().Get(headerContentType), ShouldEqual, "text/plain")
   138  			So(rec.Body.String(), ShouldEqual, "not found\n")
   139  			So(log, memlogger.ShouldHaveLog, logging.Warning, "prpc: responding with NotFound error (HTTP 404): not found")
   140  		})
   141  
   142  		Convey("internal error", func() {
   143  			writeError(c, rec, status.Error(codes.Internal, "errmsg"), FormatBinary)
   144  			So(rec.Code, ShouldEqual, http.StatusInternalServerError)
   145  			So(rec.Header().Get(HeaderGRPCCode), ShouldEqual, "13")
   146  			So(rec.Header().Get(headerContentType), ShouldEqual, "text/plain")
   147  			So(rec.Body.String(), ShouldEqual, "Internal server error\n")
   148  			So(log, memlogger.ShouldHaveLog, logging.Error, "prpc: responding with Internal error (HTTP 500): errmsg")
   149  		})
   150  
   151  		Convey("unknown error", func() {
   152  			writeError(c, rec, status.Error(codes.Unknown, "errmsg"), FormatBinary)
   153  			So(rec.Code, ShouldEqual, http.StatusInternalServerError)
   154  			So(rec.Header().Get(HeaderGRPCCode), ShouldEqual, "2")
   155  			So(rec.Header().Get(headerContentType), ShouldEqual, "text/plain")
   156  			So(rec.Body.String(), ShouldEqual, "Unknown server error\n")
   157  			So(log, memlogger.ShouldHaveLog, logging.Error, "prpc: responding with Unknown error (HTTP 500): errmsg")
   158  		})
   159  
   160  		Convey("status details", func() {
   161  			testStatusDetails := func(format Format, expected []string) {
   162  				st := status.New(codes.InvalidArgument, "invalid argument")
   163  
   164  				st, err := st.WithDetails(&errdetails.BadRequest{
   165  					FieldViolations: []*errdetails.BadRequest_FieldViolation{
   166  						{Field: "a"},
   167  					},
   168  				})
   169  				So(err, ShouldBeNil)
   170  
   171  				st, err = st.WithDetails(&errdetails.Help{
   172  					Links: []*errdetails.Help_Link{
   173  						{Url: "https://example.com"},
   174  					},
   175  				})
   176  				So(err, ShouldBeNil)
   177  
   178  				writeError(c, rec, st.Err(), format)
   179  				So(rec.Header()[HeaderStatusDetail], ShouldResemble, expected)
   180  			}
   181  
   182  			Convey("binary", func() {
   183  				testStatusDetails(FormatBinary, []string{
   184  					"Cil0eXBlLmdvb2dsZWFwaXMuY29tL2dvb2dsZS5ycGMuQmFkUmVxdWVzdBIFCgMKAWE=",
   185  					"CiN0eXBlLmdvb2dsZWFwaXMuY29tL2dvb2dsZS5ycGMuSGVscBIXChUSE2h0dHBzOi8vZXhhbXBsZS5jb20=",
   186  				})
   187  			})
   188  
   189  			Convey("json", func() {
   190  				testStatusDetails(FormatJSONPB, []string{
   191  					"eyJAdHlwZSI6InR5cGUuZ29vZ2xlYXBpcy5jb20vZ29vZ2xlLnJwYy5CYWRSZXF1ZXN0IiwiZmllbGRWaW9sYXRpb25zIjpbeyJmaWVsZCI6ImEifV19",
   192  					"eyJAdHlwZSI6InR5cGUuZ29vZ2xlYXBpcy5jb20vZ29vZ2xlLnJwYy5IZWxwIiwibGlua3MiOlt7InVybCI6Imh0dHBzOi8vZXhhbXBsZS5jb20ifV19",
   193  				})
   194  			})
   195  
   196  			Convey("text", func() {
   197  				testStatusDetails(FormatText, []string{
   198  					"dHlwZV91cmw6ICJ0eXBlLmdvb2dsZWFwaXMuY29tL2dvb2dsZS5ycGMuQmFkUmVxdWVzdCIKdmFsdWU6ICJcblwwMDNcblwwMDFhIgo=",
   199  					"dHlwZV91cmw6ICJ0eXBlLmdvb2dsZWFwaXMuY29tL2dvb2dsZS5ycGMuSGVscCIKdmFsdWU6ICJcblwwMjVcMDIyXDAyM2h0dHBzOi8vZXhhbXBsZS5jb20iCg==",
   200  				})
   201  			})
   202  		})
   203  	})
   204  }