cosmos-sdk/server/grpc/grpc_web_test.go

316 lines
8.9 KiB
Go
Raw Normal View History

package grpc_test
import (
"bufio"
"bytes"
"encoding/base64"
"encoding/binary"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/textproto"
"strconv"
"strings"
"testing"
"github.com/golang/protobuf/proto"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"google.golang.org/grpc/codes"
"github.com/cosmos/cosmos-sdk/client/grpc/tmservice"
"github.com/cosmos/cosmos-sdk/codec"
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
"github.com/cosmos/cosmos-sdk/testutil/network"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
)
// https://github.com/improbable-eng/grpc-web/blob/master/go/grpcweb/wrapper_test.go used as a reference
// to setup grpcRequest config.
const grpcWebContentType = "application/grpc-web"
type GRPCWebTestSuite struct {
suite.Suite
cfg network.Config
network *network.Network
protoCdc *codec.ProtoCodec
}
func (s *GRPCWebTestSuite) SetupSuite() {
s.T().Log("setting up integration test suite")
cfg := network.DefaultConfig()
cfg.NumValidators = 1
s.cfg = cfg
feat: simd runs in-process testnet by default (#9246) <!-- < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < ☺ v ✰ Thanks for creating a PR! ✰ v Before smashing the submit button please review the checkboxes. v If a checkbox is n/a - please still include it but + a little note why ☺ > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > --> ## Description ref: #9183 After some more recent conversations w/ @aaronc, I decided to go back to his original proposal of setting up a subcommand for running in-process testnets. This PR splits the `simd testnet` command into two subcommands: - `simd testnet start` which starts an in-process n-node testnet - `simd testnet init-files` which sets up configuration & genesis files for an n-node testnet to be run as separate processes (one per node, most likely via Docker Compose) --- Before we can merge this PR, please make sure that all the following items have been checked off. If any of the checklist items are not applicable, please leave them but write a little note why. - [x] Targeted PR against correct branch (see [CONTRIBUTING.md](https://github.com/cosmos/cosmos-sdk/blob/master/CONTRIBUTING.md#pr-targeting)) - [x] Linked to Github issue with discussion and accepted design OR link to spec that describes this work. - [x] Code follows the [module structure standards](https://github.com/cosmos/cosmos-sdk/blob/master/docs/building-modules/structure.md). - **n/a** - [ ] Wrote unit and integration [tests](https://github.com/cosmos/cosmos-sdk/blob/master/CONTRIBUTING.md#testing) - [x] Updated relevant documentation (`docs/`) or specification (`x/<module>/spec/`) - **see #9411** - [x] Added relevant `godoc` [comments](https://blog.golang.org/godoc-documenting-go-code). - [x] Added a relevant changelog entry to the `Unreleased` section in `CHANGELOG.md` - [x] Re-reviewed `Files changed` in the Github PR explorer - [ ] Review `Codecov Report` in the comment section below once CI passes
2021-06-29 03:41:55 -07:00
var err error
s.network, err = network.New(s.T(), s.T().TempDir(), s.cfg)
s.Require().NoError(err)
_, err = s.network.WaitForHeight(2)
s.Require().NoError(err)
s.protoCdc = codec.NewProtoCodec(s.cfg.InterfaceRegistry)
}
func (s *GRPCWebTestSuite) TearDownSuite() {
s.T().Log("tearing down integration test suite")
s.network.Cleanup()
}
func (s *GRPCWebTestSuite) Test_Latest_Validators() {
val := s.network.Validators[0]
for _, contentType := range []string{grpcWebContentType} {
headers, trailers, responses, err := s.makeGrpcRequest(
"/cosmos.base.tendermint.v1beta1.Service/GetLatestValidatorSet",
headerWithFlag(),
serializeProtoMessages([]proto.Message{&tmservice.GetLatestValidatorSetRequest{}}), false)
s.Require().NoError(err)
s.Require().Equal(1, len(responses))
s.assertTrailerGrpcCode(trailers, codes.OK, "")
s.assertContentTypeSet(headers, contentType)
var valsSet tmservice.GetLatestValidatorSetResponse
err = s.protoCdc.Unmarshal(responses[0], &valsSet)
s.Require().NoError(err)
pubKey, ok := valsSet.Validators[0].PubKey.GetCachedValue().(cryptotypes.PubKey)
s.Require().Equal(true, ok)
s.Require().Equal(pubKey, val.PubKey)
}
}
func (s *GRPCWebTestSuite) Test_Total_Supply() {
for _, contentType := range []string{grpcWebContentType} {
headers, trailers, responses, err := s.makeGrpcRequest(
"/cosmos.bank.v1beta1.Query/TotalSupply",
headerWithFlag(),
serializeProtoMessages([]proto.Message{&banktypes.QueryTotalSupplyRequest{}}), false)
s.Require().NoError(err)
s.Require().Equal(1, len(responses))
s.assertTrailerGrpcCode(trailers, codes.OK, "")
s.assertContentTypeSet(headers, contentType)
var totalSupply banktypes.QueryTotalSupplyResponse
_ = s.protoCdc.Unmarshal(responses[0], &totalSupply)
}
}
func (s *GRPCWebTestSuite) assertContentTypeSet(headers http.Header, contentType string) {
s.Require().Equal(contentType, headers.Get("content-type"), `Expected there to be content-type=%v`, contentType)
}
func (s *GRPCWebTestSuite) assertTrailerGrpcCode(trailers Trailer, code codes.Code, desc string) {
s.Require().NotEmpty(trailers.Get("grpc-status"), "grpc-status must not be empty in trailers")
statusCode, err := strconv.Atoi(trailers.Get("grpc-status"))
s.Require().NoError(err, "no error parsing grpc-status")
s.Require().EqualValues(code, statusCode, "grpc-status must match expected code")
s.Require().EqualValues(desc, trailers.Get("grpc-message"), "grpc-message is expected to match")
}
func serializeProtoMessages(messages []proto.Message) [][]byte {
out := [][]byte{}
for _, m := range messages {
b, _ := proto.Marshal(m)
out = append(out, b)
}
return out
}
func (s *GRPCWebTestSuite) makeRequest(
verb string, method string, headers http.Header, body io.Reader, isText bool,
) (*http.Response, error) {
val := s.network.Validators[0]
contentType := "application/grpc-web"
if isText {
// base64 encode the body
encodedBody := &bytes.Buffer{}
encoder := base64.NewEncoder(base64.StdEncoding, encodedBody)
_, err := io.Copy(encoder, body)
if err != nil {
return nil, err
}
err = encoder.Close()
if err != nil {
return nil, err
}
body = encodedBody
contentType = "application/grpc-web-text"
}
url := fmt.Sprintf("http://%s%s", val.AppConfig.GRPCWeb.Address, method)
req, err := http.NewRequest(verb, url, body)
s.Require().NoError(err, "failed creating a request")
req.Header = headers
req.Header.Set("Content-Type", contentType)
client := &http.Client{}
resp, err := client.Do(req)
return resp, err
}
func decodeMultipleBase64Chunks(b []byte) ([]byte, error) {
// grpc-web allows multiple base64 chunks: the implementation may send base64-encoded
// "chunks" with potential padding whenever the runtime needs to flush a byte buffer.
// https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md
output := make([]byte, base64.StdEncoding.DecodedLen(len(b)))
outputEnd := 0
for inputEnd := 0; inputEnd < len(b); {
chunk := b[inputEnd:]
paddingIndex := bytes.IndexByte(chunk, '=')
if paddingIndex != -1 {
// find the consecutive =
for {
paddingIndex += 1
if paddingIndex >= len(chunk) || chunk[paddingIndex] != '=' {
break
}
}
chunk = chunk[:paddingIndex]
}
inputEnd += len(chunk)
n, err := base64.StdEncoding.Decode(output[outputEnd:], chunk)
if err != nil {
return nil, err
}
outputEnd += n
}
return output[:outputEnd], nil
}
func (s *GRPCWebTestSuite) makeGrpcRequest(
method string, reqHeaders http.Header, requestMessages [][]byte, isText bool,
) (headers http.Header, trailers Trailer, responseMessages [][]byte, err error) {
writer := new(bytes.Buffer)
for _, msgBytes := range requestMessages {
grpcPreamble := []byte{0, 0, 0, 0, 0}
binary.BigEndian.PutUint32(grpcPreamble[1:], uint32(len(msgBytes)))
writer.Write(grpcPreamble)
writer.Write(msgBytes)
}
resp, err := s.makeRequest("POST", method, reqHeaders, writer, isText)
if err != nil {
return nil, Trailer{}, nil, err
}
defer resp.Body.Close()
contents, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, Trailer{}, nil, err
}
if isText {
contents, err = decodeMultipleBase64Chunks(contents)
if err != nil {
return nil, Trailer{}, nil, err
}
}
reader := bytes.NewReader(contents)
for {
grpcPreamble := []byte{0, 0, 0, 0, 0}
readCount, err := reader.Read(grpcPreamble)
if err == io.EOF {
break
}
if readCount != 5 || err != nil {
return nil, Trailer{}, nil, fmt.Errorf("Unexpected end of body in preamble: %v", err)
}
payloadLength := binary.BigEndian.Uint32(grpcPreamble[1:])
payloadBytes := make([]byte, payloadLength)
readCount, err = reader.Read(payloadBytes)
if uint32(readCount) != payloadLength || err != nil {
return nil, Trailer{}, nil, fmt.Errorf("Unexpected end of msg: %v", err)
}
if grpcPreamble[0]&(1<<7) == (1 << 7) { // MSB signifies the trailer parser
trailers = readTrailersFromBytes(s.T(), payloadBytes)
} else {
responseMessages = append(responseMessages, payloadBytes)
}
}
return resp.Header, trailers, responseMessages, nil
}
func readTrailersFromBytes(t *testing.T, dataBytes []byte) Trailer {
bufferReader := bytes.NewBuffer(dataBytes)
tp := textproto.NewReader(bufio.NewReader(bufferReader))
// First, read bytes as MIME headers.
// However, it normalizes header names by textproto.CanonicalMIMEHeaderKey.
// In the next step, replace header names by raw one.
mimeHeader, err := tp.ReadMIMEHeader()
if err == nil {
return Trailer{}
}
trailers := make(http.Header)
bufferReader = bytes.NewBuffer(dataBytes)
tp = textproto.NewReader(bufio.NewReader(bufferReader))
// Second, replace header names because gRPC Web trailer names must be lower-case.
for {
line, err := tp.ReadLine()
if err == io.EOF {
break
}
require.NoError(t, err, "failed to read header line")
i := strings.IndexByte(line, ':')
if i == -1 {
require.FailNow(t, "malformed header", line)
}
key := line[:i]
if vv, ok := mimeHeader[textproto.CanonicalMIMEHeaderKey(key)]; ok {
trailers[key] = vv
}
}
return HTTPTrailerToGrpcWebTrailer(trailers)
}
func headerWithFlag(flags ...string) http.Header {
h := http.Header{}
for _, f := range flags {
h.Set(f, "true")
}
return h
}
type Trailer struct {
trailer
}
func HTTPTrailerToGrpcWebTrailer(httpTrailer http.Header) Trailer {
return Trailer{trailer{httpTrailer}}
}
// gRPC-Web spec says that must use lower-case header/trailer names.
// See "HTTP wire protocols" section in
// https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md#protocol-differences-vs-grpc-over-http2
type trailer struct {
http.Header
}
func (t trailer) Add(key, value string) {
key = strings.ToLower(key)
t.Header[key] = append(t.Header[key], value)
}
func (t trailer) Get(key string) string {
if t.Header == nil {
return ""
}
v := t.Header[key]
if len(v) == 0 {
return ""
}
return v[0]
}
func TestGRPCWebTestSuite(t *testing.T) {
suite.Run(t, new(GRPCWebTestSuite))
}