github.com/cosmos/cosmos-sdk@v0.50.10/server/api/server_test.go (about)

     1  package api_test
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"encoding/base64"
     7  	"encoding/binary"
     8  	"fmt"
     9  	"io"
    10  	"net/http"
    11  	"net/textproto"
    12  	"strconv"
    13  	"strings"
    14  	"testing"
    15  
    16  	"github.com/golang/protobuf/proto" //nolint:staticcheck // grpc-gateway uses deprecated golang/protobuf
    17  	"github.com/stretchr/testify/require"
    18  	"github.com/stretchr/testify/suite"
    19  	"google.golang.org/grpc/codes"
    20  
    21  	"github.com/cosmos/cosmos-sdk/client/grpc/cmtservice"
    22  	"github.com/cosmos/cosmos-sdk/codec"
    23  	cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
    24  	"github.com/cosmos/cosmos-sdk/testutil/network"
    25  	_ "github.com/cosmos/cosmos-sdk/x/auth"
    26  	_ "github.com/cosmos/cosmos-sdk/x/auth/tx/config"
    27  	_ "github.com/cosmos/cosmos-sdk/x/bank"
    28  	banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
    29  	_ "github.com/cosmos/cosmos-sdk/x/genutil"
    30  	_ "github.com/cosmos/cosmos-sdk/x/params"
    31  	_ "github.com/cosmos/cosmos-sdk/x/staking"
    32  )
    33  
    34  // https://github.com/improbable-eng/grpc-web/blob/master/go/grpcweb/wrapper_test.go used as a reference
    35  // to setup grpcRequest config.
    36  
    37  const grpcWebContentType = "application/grpc-web"
    38  
    39  type GRPCWebTestSuite struct {
    40  	suite.Suite
    41  
    42  	cfg      network.Config
    43  	network  *network.Network
    44  	protoCdc *codec.ProtoCodec
    45  }
    46  
    47  func (s *GRPCWebTestSuite) SetupSuite() {
    48  	s.T().Log("setting up integration test suite")
    49  
    50  	cfg, err := network.DefaultConfigWithAppConfig(network.MinimumAppConfig())
    51  
    52  	s.NoError(err)
    53  	cfg.NumValidators = 1
    54  	s.cfg = cfg
    55  
    56  	s.network, err = network.New(s.T(), s.T().TempDir(), s.cfg)
    57  	s.Require().NoError(err)
    58  
    59  	_, err = s.network.WaitForHeight(2)
    60  	s.Require().NoError(err)
    61  
    62  	s.protoCdc = codec.NewProtoCodec(s.cfg.InterfaceRegistry)
    63  }
    64  
    65  func (s *GRPCWebTestSuite) TearDownSuite() {
    66  	s.T().Log("tearing down integration test suite")
    67  	s.network.Cleanup()
    68  }
    69  
    70  func (s *GRPCWebTestSuite) Test_Latest_Validators() {
    71  	val := s.network.Validators[0]
    72  	for _, contentType := range []string{grpcWebContentType} {
    73  		headers, trailers, responses, err := s.makeGrpcRequest(
    74  			"/cosmos.base.tendermint.v1beta1.Service/GetLatestValidatorSet",
    75  			headerWithFlag(),
    76  			serializeProtoMessages([]proto.Message{&cmtservice.GetLatestValidatorSetRequest{}}), false)
    77  
    78  		s.Require().NoError(err)
    79  		s.Require().Equal(1, len(responses))
    80  		s.assertTrailerGrpcCode(trailers, codes.OK, "")
    81  		s.assertContentTypeSet(headers, contentType)
    82  		var valsSet cmtservice.GetLatestValidatorSetResponse
    83  		err = s.protoCdc.Unmarshal(responses[0], &valsSet)
    84  		s.Require().NoError(err)
    85  		pubKey, ok := valsSet.Validators[0].PubKey.GetCachedValue().(cryptotypes.PubKey)
    86  		s.Require().Equal(true, ok)
    87  		s.Require().Equal(pubKey, val.PubKey)
    88  	}
    89  }
    90  
    91  func (s *GRPCWebTestSuite) Test_Total_Supply() {
    92  	for _, contentType := range []string{grpcWebContentType} {
    93  		headers, trailers, responses, err := s.makeGrpcRequest(
    94  			"/cosmos.bank.v1beta1.Query/TotalSupply",
    95  			headerWithFlag(),
    96  			serializeProtoMessages([]proto.Message{&banktypes.QueryTotalSupplyRequest{}}), false)
    97  
    98  		s.Require().NoError(err)
    99  		s.Require().Equal(1, len(responses))
   100  		s.assertTrailerGrpcCode(trailers, codes.OK, "")
   101  		s.assertContentTypeSet(headers, contentType)
   102  		var totalSupply banktypes.QueryTotalSupplyResponse
   103  		_ = s.protoCdc.Unmarshal(responses[0], &totalSupply)
   104  	}
   105  }
   106  
   107  func (s *GRPCWebTestSuite) assertContentTypeSet(headers http.Header, contentType string) {
   108  	s.Require().Equal(contentType, headers.Get("content-type"), `Expected there to be content-type=%v`, contentType)
   109  }
   110  
   111  func (s *GRPCWebTestSuite) assertTrailerGrpcCode(trailers Trailer, code codes.Code, desc string) {
   112  	s.Require().NotEmpty(trailers.Get("grpc-status"), "grpc-status must not be empty in trailers")
   113  	statusCode, err := strconv.Atoi(trailers.Get("grpc-status"))
   114  	s.Require().NoError(err, "no error parsing grpc-status")
   115  	s.Require().EqualValues(code, statusCode, "grpc-status must match expected code")
   116  	s.Require().EqualValues(desc, trailers.Get("grpc-message"), "grpc-message is expected to match")
   117  }
   118  
   119  func serializeProtoMessages(messages []proto.Message) [][]byte {
   120  	out := [][]byte{}
   121  	for _, m := range messages {
   122  		b, _ := proto.Marshal(m)
   123  		out = append(out, b)
   124  	}
   125  	return out
   126  }
   127  
   128  func (s *GRPCWebTestSuite) makeRequest(
   129  	verb, method string, headers http.Header, body io.Reader, isText bool,
   130  ) (*http.Response, error) {
   131  	val := s.network.Validators[0]
   132  	contentType := "application/grpc-web"
   133  	if isText {
   134  		// base64 encode the body
   135  		encodedBody := &bytes.Buffer{}
   136  		encoder := base64.NewEncoder(base64.StdEncoding, encodedBody)
   137  		_, err := io.Copy(encoder, body)
   138  		if err != nil {
   139  			return nil, err
   140  		}
   141  		err = encoder.Close()
   142  		if err != nil {
   143  			return nil, err
   144  		}
   145  		body = encodedBody
   146  		contentType = "application/grpc-web-text"
   147  	}
   148  
   149  	url := fmt.Sprintf("http://%s%s", strings.TrimPrefix(val.AppConfig.API.Address, "tcp://"), method)
   150  	req, err := http.NewRequest(verb, url, body)
   151  	s.Require().NoError(err, "failed creating a request")
   152  	req.Header = headers
   153  
   154  	req.Header.Set("Content-Type", contentType)
   155  	client := &http.Client{}
   156  	resp, err := client.Do(req)
   157  	return resp, err
   158  }
   159  
   160  func decodeMultipleBase64Chunks(b []byte) ([]byte, error) {
   161  	// grpc-web allows multiple base64 chunks: the implementation may send base64-encoded
   162  	// "chunks" with potential padding whenever the runtime needs to flush a byte buffer.
   163  	// https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md
   164  	output := make([]byte, base64.StdEncoding.DecodedLen(len(b)))
   165  	outputEnd := 0
   166  
   167  	for inputEnd := 0; inputEnd < len(b); {
   168  		chunk := b[inputEnd:]
   169  		paddingIndex := bytes.IndexByte(chunk, '=')
   170  		if paddingIndex != -1 {
   171  			// find the consecutive =
   172  			for {
   173  				paddingIndex++
   174  				if paddingIndex >= len(chunk) || chunk[paddingIndex] != '=' {
   175  					break
   176  				}
   177  			}
   178  			chunk = chunk[:paddingIndex]
   179  		}
   180  		inputEnd += len(chunk)
   181  
   182  		n, err := base64.StdEncoding.Decode(output[outputEnd:], chunk)
   183  		if err != nil {
   184  			return nil, err
   185  		}
   186  		outputEnd += n
   187  	}
   188  	return output[:outputEnd], nil
   189  }
   190  
   191  func (s *GRPCWebTestSuite) makeGrpcRequest(
   192  	method string, reqHeaders http.Header, requestMessages [][]byte, isText bool,
   193  ) (headers http.Header, trailers Trailer, responseMessages [][]byte, err error) {
   194  	writer := new(bytes.Buffer)
   195  	for _, msgBytes := range requestMessages {
   196  		grpcPreamble := []byte{0, 0, 0, 0, 0}
   197  		binary.BigEndian.PutUint32(grpcPreamble[1:], uint32(len(msgBytes)))
   198  		writer.Write(grpcPreamble)
   199  		writer.Write(msgBytes)
   200  	}
   201  	resp, err := s.makeRequest("POST", method, reqHeaders, writer, isText)
   202  	if err != nil {
   203  		return nil, Trailer{}, nil, err
   204  	}
   205  	defer func() {
   206  		_ = resp.Body.Close()
   207  	}()
   208  	contents, err := io.ReadAll(resp.Body)
   209  	if err != nil {
   210  		return nil, Trailer{}, nil, err
   211  	}
   212  
   213  	if isText {
   214  		contents, err = decodeMultipleBase64Chunks(contents)
   215  		if err != nil {
   216  			return nil, Trailer{}, nil, err
   217  		}
   218  	}
   219  
   220  	reader := bytes.NewReader(contents)
   221  	for {
   222  		grpcPreamble := []byte{0, 0, 0, 0, 0}
   223  		readCount, err := reader.Read(grpcPreamble)
   224  		if err == io.EOF {
   225  			break
   226  		}
   227  		if readCount != 5 || err != nil {
   228  			return nil, Trailer{}, nil, fmt.Errorf("Unexpected end of body in preamble: %v", err)
   229  		}
   230  		payloadLength := binary.BigEndian.Uint32(grpcPreamble[1:])
   231  		payloadBytes := make([]byte, payloadLength)
   232  
   233  		readCount, err = reader.Read(payloadBytes)
   234  		if uint32(readCount) != payloadLength || err != nil {
   235  			return nil, Trailer{}, nil, fmt.Errorf("Unexpected end of msg: %v", err)
   236  		}
   237  		if grpcPreamble[0]&(1<<7) == (1 << 7) { // MSB signifies the trailer parser
   238  			trailers = readTrailersFromBytes(s.T(), payloadBytes)
   239  		} else {
   240  			responseMessages = append(responseMessages, payloadBytes)
   241  		}
   242  	}
   243  	return resp.Header, trailers, responseMessages, nil
   244  }
   245  
   246  func readTrailersFromBytes(t *testing.T, dataBytes []byte) Trailer {
   247  	bufferReader := bytes.NewBuffer(dataBytes)
   248  	tp := textproto.NewReader(bufio.NewReader(bufferReader))
   249  
   250  	// First, read bytes as MIME headers.
   251  	// However, it normalizes header names by textproto.CanonicalMIMEHeaderKey.
   252  	// In the next step, replace header names by raw one.
   253  	mimeHeader, err := tp.ReadMIMEHeader()
   254  	if err == nil {
   255  		return Trailer{}
   256  	}
   257  
   258  	trailers := make(http.Header)
   259  	bufferReader = bytes.NewBuffer(dataBytes)
   260  	tp = textproto.NewReader(bufio.NewReader(bufferReader))
   261  
   262  	// Second, replace header names because gRPC Web trailer names must be lower-case.
   263  	for {
   264  		line, err := tp.ReadLine()
   265  		if err == io.EOF {
   266  			break
   267  		}
   268  		require.NoError(t, err, "failed to read header line")
   269  
   270  		i := strings.IndexByte(line, ':')
   271  		if i == -1 {
   272  			require.FailNow(t, "malformed header", line)
   273  		}
   274  		key := line[:i]
   275  		if vv, ok := mimeHeader[textproto.CanonicalMIMEHeaderKey(key)]; ok {
   276  			trailers[key] = vv
   277  		}
   278  	}
   279  	return HTTPTrailerToGrpcWebTrailer(trailers)
   280  }
   281  
   282  func headerWithFlag(flags ...string) http.Header {
   283  	h := http.Header{}
   284  	for _, f := range flags {
   285  		h.Set(f, "true")
   286  	}
   287  	return h
   288  }
   289  
   290  type Trailer struct {
   291  	trailer
   292  }
   293  
   294  func HTTPTrailerToGrpcWebTrailer(httpTrailer http.Header) Trailer {
   295  	return Trailer{trailer{httpTrailer}}
   296  }
   297  
   298  // gRPC-Web spec says that must use lower-case header/trailer names.
   299  // See "HTTP wire protocols" section in
   300  // https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md#protocol-differences-vs-grpc-over-http2
   301  type trailer struct {
   302  	http.Header
   303  }
   304  
   305  func (t trailer) Add(key, value string) {
   306  	key = strings.ToLower(key)
   307  	t.Header[key] = append(t.Header[key], value)
   308  }
   309  
   310  func (t trailer) Get(key string) string {
   311  	if t.Header == nil {
   312  		return ""
   313  	}
   314  	v := t.Header[key]
   315  	if len(v) == 0 {
   316  		return ""
   317  	}
   318  	return v[0]
   319  }
   320  
   321  func TestGRPCWebTestSuite(t *testing.T) {
   322  	suite.Run(t, new(GRPCWebTestSuite))
   323  }