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 }