blob: 35a5b70989abaeb5a99a0c518955553b1909966c [file] [log] [blame]
// Copyright 2016 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package prpc
import (
"bytes"
"context"
"encoding/base64"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"google.golang.org/genproto/googleapis/rpc/errdetails"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/anypb"
"go.chromium.org/luci/common/logging"
"go.chromium.org/luci/common/logging/memlogger"
"go.chromium.org/luci/common/testing/ftt"
"go.chromium.org/luci/common/testing/truth/assert"
"go.chromium.org/luci/common/testing/truth/convey"
"go.chromium.org/luci/common/testing/truth/should"
"go.chromium.org/luci/grpc/prpc/internal/testpb"
)
func TestEncoding(t *testing.T) {
t.Parallel()
ftt.Run("responseFormat", t, func(t *ftt.Test) {
test := func(acceptHeader string, expectedFormat Format, expectedErr any) {
acceptHeader = strings.Replace(acceptHeader, "{json}", mtPRPCJSONPB, -1)
acceptHeader = strings.Replace(acceptHeader, "{binary}", mtPRPCBinary, -1)
acceptHeader = strings.Replace(acceptHeader, "{text}", mtPRPCText, -1)
t.Run("Accept: "+acceptHeader, func(t *ftt.Test) {
actualFormat, protoErr := responseFormat(acceptHeader)
if expectedErr == nil {
assert.That(t, protoErr, should.Equal[*protocolError](nil))
} else {
assert.That(t, error(protoErr), should.ErrLike(expectedErr))
}
if protoErr == nil {
assert.Loosely(t, actualFormat, should.Equal(expectedFormat))
}
})
}
test("", FormatBinary, nil)
test(ContentTypePRPC, FormatBinary, nil)
test(mtPRPCBinary, FormatBinary, nil)
test(mtPRPCJSONPBLegacy, FormatJSONPB, nil)
test(mtPRPCText, FormatText, nil)
test(ContentTypeJSON, FormatJSONPB, nil)
test("application/*", FormatBinary, nil)
test("*/*", FormatBinary, nil)
// test cases with multiple types
test("{json},{binary}", FormatBinary, nil)
test("{json},{binary};q=0.9", FormatJSONPB, nil)
test("{json};q=1,{binary};q=0.9", FormatJSONPB, nil)
test("{json},{text}", FormatJSONPB, nil)
test("{json};q=0.9,{text}", FormatText, nil)
test("{binary},{json},{text}", FormatBinary, nil)
test("{json},{binary},*/*", FormatBinary, nil)
test("{json},{binary},*/*;q=0.9", FormatBinary, nil)
test("{json},{binary},*/*;x=y", FormatBinary, nil)
test("{json},{binary};q=0.9,*/*", FormatBinary, nil)
test("{json},{binary};q=0.9,*/*;q=0.8", FormatJSONPB, nil)
// supported and unsupported mix
test("{json},foo/bar", FormatJSONPB, nil)
test("{json};q=0.1,foo/bar", FormatJSONPB, nil)
test("foo/bar;q=0.1,{json}", FormatJSONPB, nil)
// only unsupported types
const err406 = "prpc: bad Accept header: specified media types are not not supported"
test(ContentTypePRPC+"; boo=true", 0, err406)
test(ContentTypePRPC+"; encoding=blah", 0, err406)
test("x", 0, err406)
test("x,y", 0, err406)
test("x//y", 0, "prpc: bad Accept header: specified media types are not not supported")
})
ftt.Run("writeResponse", t, func(t *ftt.Test) {
msg := &testpb.HelloReply{Message: "Hi"}
c := context.Background()
test := func(codec protoCodec, contentType string, expected []byte) {
t.Run(contentType, func(t *ftt.Test) {
rec := httptest.NewRecorder()
writeResponse(c, rec, &response{
out: msg,
codec: codec,
})
assert.Loosely(t, rec.Code, should.Equal(http.StatusOK))
assert.Loosely(t, rec.Header().Get(HeaderGRPCCode), should.Equal("0"))
assert.Loosely(t, rec.Header().Get(headerContentType), should.Equal(contentType))
assert.That(t, normalizeEnc(codec, rec.Body.Bytes()), should.Match(normalizeEnc(codec, expected)))
})
}
wirePB, err := codecWireV2.Encode(nil, msg)
assert.That(t, err, should.ErrLike(nil))
test(codecWireV2, mtPRPCBinary, wirePB)
test(codecJSONV2, mtPRPCJSONPB, []byte(")]}'\n{\"message\":\"Hi\"}\n"))
test(codecTextV2, mtPRPCText, []byte(`message:"Hi"`))
t.Run("compression", func(t *ftt.Test) {
rec := httptest.NewRecorder()
msg := &testpb.HelloReply{Message: strings.Repeat("A", 1024)}
writeResponse(c, rec, &response{
out: msg,
codec: codecTextV2,
acceptsGZip: true,
})
assert.Loosely(t, rec.Code, should.Equal(http.StatusOK))
assert.Loosely(t, rec.Header().Get("Content-Encoding"), should.Equal("gzip"))
assert.Loosely(t, rec.Body.Len(), should.BeLessThan(1024))
})
t.Run("maxResponseSize", func(t *ftt.Test) {
rec := httptest.NewRecorder()
msg := &testpb.HelloReply{Message: strings.Repeat("A", 1024)}
writeResponse(c, rec, &response{
out: msg,
codec: codecJSONV2,
maxResponseSize: 123,
})
assert.Loosely(t, rec.Code, should.Equal(http.StatusServiceUnavailable))
assert.Loosely(t, rec.Header().Get(HeaderGRPCCode), should.Equal("14")) // codes.Unavailable
assert.Loosely(t, rec.Header().Get(HeaderStatusDetail), should.NotBeEmpty)
body, _ := io.ReadAll(rec.Body)
assert.Loosely(t, string(body), should.HaveSuffix("exceeds the client limit 123\n"))
})
})
ftt.Run("writeError", t, func(t *ftt.Test) {
c := context.Background()
c = memlogger.Use(c)
log := logging.Get(c).(*memlogger.MemLogger)
rec := httptest.NewRecorder()
t.Run("client error", func(t *ftt.Test) {
writeError(c, rec, status.Error(codes.NotFound, "not found"), codecWireV2)
assert.Loosely(t, rec.Code, should.Equal(http.StatusNotFound))
assert.Loosely(t, rec.Header().Get(HeaderGRPCCode), should.Equal("5"))
assert.Loosely(t, rec.Header().Get(headerContentType), should.Equal("text/plain; charset=utf-8"))
assert.Loosely(t, rec.Body.String(), should.Equal("not found\n"))
assert.Loosely(t, log, convey.Adapt(memlogger.ShouldHaveLog)(logging.Warning, "prpc: responding with NotFound error (HTTP 404): not found"))
})
t.Run("internal error", func(t *ftt.Test) {
writeError(c, rec, status.Error(codes.Internal, "errmsg"), codecWireV2)
assert.Loosely(t, rec.Code, should.Equal(http.StatusInternalServerError))
assert.Loosely(t, rec.Header().Get(HeaderGRPCCode), should.Equal("13"))
assert.Loosely(t, rec.Header().Get(headerContentType), should.Equal("text/plain; charset=utf-8"))
assert.Loosely(t, rec.Body.String(), should.Equal("Internal server error\n"))
assert.Loosely(t, log, convey.Adapt(memlogger.ShouldHaveLog)(logging.Error, "prpc: responding with Internal error (HTTP 500): errmsg"))
})
t.Run("unknown error", func(t *ftt.Test) {
writeError(c, rec, status.Error(codes.Unknown, "errmsg"), codecWireV2)
assert.Loosely(t, rec.Code, should.Equal(http.StatusInternalServerError))
assert.Loosely(t, rec.Header().Get(HeaderGRPCCode), should.Equal("2"))
assert.Loosely(t, rec.Header().Get(headerContentType), should.Equal("text/plain; charset=utf-8"))
assert.Loosely(t, rec.Body.String(), should.Equal("Unknown server error\n"))
assert.Loosely(t, log, convey.Adapt(memlogger.ShouldHaveLog)(logging.Error, "prpc: responding with Unknown error (HTTP 500): errmsg"))
})
t.Run("status details", func(t *ftt.Test) {
errDetails1 := &errdetails.BadRequest{
FieldViolations: []*errdetails.BadRequest_FieldViolation{
{Field: "a"},
},
}
errDetails2 := &errdetails.Help{
Links: []*errdetails.Help_Link{
{Url: "https://example.com"},
},
}
testStatusDetails := func(codec protoCodec, expected [][]byte) {
st := status.New(codes.InvalidArgument, "invalid argument")
st, err := st.WithDetails(errDetails1)
assert.Loosely(t, err, should.BeNil)
st, err = st.WithDetails(errDetails2)
assert.Loosely(t, err, should.BeNil)
writeError(c, rec, st.Err(), codec)
assert.Loosely(t, rec.Header()[HeaderStatusDetail], should.HaveLength(len(expected)))
for i, val := range rec.Header()[HeaderStatusDetail] {
blob, err := base64.StdEncoding.DecodeString(val)
assert.That(t, err, should.ErrLike(nil))
assert.That(t, normalizeEnc(codec, blob), should.Match(normalizeEnc(codec, expected[i])))
}
}
wireAnyPB := func(msg proto.Message) []byte {
apb, err := anypb.New(msg)
if err != nil {
panic(apb)
}
blob, err := codecWireV2.Encode(nil, apb)
if err != nil {
panic(err)
}
return blob
}
t.Run("binary", func(t *ftt.Test) {
testStatusDetails(codecWireV2, [][]byte{
wireAnyPB(errDetails1),
wireAnyPB(errDetails2),
})
})
t.Run("json", func(t *ftt.Test) {
testStatusDetails(codecJSONV2, [][]byte{
[]byte(`{"@type":"type.googleapis.com/google.rpc.BadRequest","fieldViolations":[{"field":"a"}]}`),
[]byte(`{"@type":"type.googleapis.com/google.rpc.Help","links":[{"url":"https://example.com"}]}`),
})
})
t.Run("text", func(t *ftt.Test) {
testStatusDetails(codecTextV2, [][]byte{
[]byte(`[type.googleapis.com/google.rpc.BadRequest]:{field_violations:{field:"a"}}`),
[]byte(`[type.googleapis.com/google.rpc.Help]:{links:{url:"https://example.com"}}`),
})
})
})
})
}
// normalizeEnc does lossy normalization of encoded data for comparisons.
func normalizeEnc(codec protoCodec, blob []byte) []byte {
switch codec {
case codecWireV2:
return blob
case codecJSONV2, codecTextV2:
return bytes.Replace(blob, []byte(" "), nil, -1)
default:
panic("impossible")
}
}