cosmos-sdk/client/grpc_query_test.go
mmsqe f4e2ce0ea4
feat: support for multi gRPC query clients serve with old binary (#25565)
Co-authored-by: Alex | Cosmos Labs <alex@cosmoslabs.io>
2025-11-19 15:46:50 +00:00

333 lines
9.7 KiB
Go

package client_test
import (
"context"
"testing"
abci "github.com/cometbft/cometbft/abci/types"
cmtjson "github.com/cometbft/cometbft/libs/json"
dbm "github.com/cosmos/cosmos-db"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata"
"cosmossdk.io/depinject"
"cosmossdk.io/log"
"cosmossdk.io/math"
"github.com/cosmos/cosmos-sdk/baseapp"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/codec"
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
"github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1"
"github.com/cosmos/cosmos-sdk/runtime"
"github.com/cosmos/cosmos-sdk/server/config"
"github.com/cosmos/cosmos-sdk/testutil/sims"
"github.com/cosmos/cosmos-sdk/testutil/testdata"
sdk "github.com/cosmos/cosmos-sdk/types"
grpctypes "github.com/cosmos/cosmos-sdk/types/grpc"
"github.com/cosmos/cosmos-sdk/x/auth/testutil"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper"
"github.com/cosmos/cosmos-sdk/x/bank/types"
)
type IntegrationTestSuite struct {
suite.Suite
ctx sdk.Context
cdc codec.Codec
genesisAccount *authtypes.BaseAccount
bankClient types.QueryClient
testClient testdata.QueryClient
genesisAccountBalance int64
}
func (s *IntegrationTestSuite) SetupSuite() {
s.T().Log("setting up integration test suite")
var (
interfaceRegistry codectypes.InterfaceRegistry
bankKeeper bankkeeper.BaseKeeper
appBuilder *runtime.AppBuilder
cdc codec.Codec
)
// TODO duplicated from testutils/sims/app_helpers.go
// need more composable startup options for simapp, this test needed a handle to the closed over genesis account
// to query balances
err := depinject.Inject(
depinject.Configs(
testutil.AppConfig,
depinject.Supply(log.NewNopLogger()),
),
&interfaceRegistry, &bankKeeper, &appBuilder, &cdc)
s.NoError(err)
app := appBuilder.Build(dbm.NewMemDB(), nil)
err = app.Load(true)
s.NoError(err)
valSet, err := sims.CreateRandomValidatorSet()
s.NoError(err)
// generate genesis account
s.genesisAccountBalance = 100000000000000
senderPrivKey := secp256k1.GenPrivKey()
acc := authtypes.NewBaseAccount(senderPrivKey.PubKey().Address().Bytes(), senderPrivKey.PubKey(), 0, 0)
balance := types.Balance{
Address: acc.GetAddress().String(),
Coins: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, math.NewInt(s.genesisAccountBalance))),
}
genesisState, err := sims.GenesisStateWithValSet(cdc, app.DefaultGenesis(), valSet, []authtypes.GenesisAccount{acc}, balance)
s.NoError(err)
stateBytes, err := cmtjson.MarshalIndent(genesisState, "", " ")
s.NoError(err)
// init chain will set the validator set and initialize the genesis accounts
_, err = app.InitChain(&abci.RequestInitChain{
Validators: []abci.ValidatorUpdate{},
ConsensusParams: sims.DefaultConsensusParams,
AppStateBytes: stateBytes,
})
s.NoError(err)
_, err = app.FinalizeBlock(&abci.RequestFinalizeBlock{
Height: app.LastBlockHeight() + 1,
Hash: app.LastCommitID().Hash,
NextValidatorsHash: valSet.Hash(),
})
s.NoError(err)
// end of app init
s.ctx = app.NewContext(false)
s.cdc = cdc
queryHelper := baseapp.NewQueryServerTestHelper(s.ctx, interfaceRegistry)
types.RegisterQueryServer(queryHelper, bankKeeper)
testdata.RegisterQueryServer(queryHelper, testdata.QueryImpl{})
s.bankClient = types.NewQueryClient(queryHelper)
s.testClient = testdata.NewQueryClient(queryHelper)
s.genesisAccount = acc
}
func (s *IntegrationTestSuite) TearDownSuite() {
s.T().Log("tearing down integration test suite")
}
func (s *IntegrationTestSuite) TestGRPCQuery() {
denom := sdk.DefaultBondDenom
// gRPC query to test service should work
testRes, err := s.testClient.Echo(context.Background(), &testdata.EchoRequest{Message: "hello"})
s.Require().NoError(err)
s.Require().Equal("hello", testRes.Message)
// gRPC query to bank service should work
var header metadata.MD
res, err := s.bankClient.Balance(
context.Background(),
&types.QueryBalanceRequest{Address: s.genesisAccount.GetAddress().String(), Denom: denom},
grpc.Header(&header), // Also fetch grpc header
)
s.Require().NoError(err)
bal := res.GetBalance()
s.Equal(sdk.NewCoin(denom, math.NewInt(s.genesisAccountBalance)), *bal)
}
func TestIntegrationTestSuite(t *testing.T) {
suite.Run(t, new(IntegrationTestSuite))
}
func (s *IntegrationTestSuite) TestGetGRPCConnWithContext() {
defaultConn, err := grpc.NewClient("localhost:9090",
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
s.Require().NoError(err)
defer defaultConn.Close()
historicalConn, err := grpc.NewClient("localhost:9091",
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
s.Require().NoError(err)
defer historicalConn.Close()
historicalConns := config.HistoricalGRPCConnections{
config.BlockRange{100, 500}: historicalConn,
}
provider := client.NewGRPCConnProvider(defaultConn, historicalConns)
testCases := []struct {
name string
height int64
setupCtx func() client.Context
expectedConn *grpc.ClientConn
}{
{
name: "context with GRPCConnProvider and historical height",
height: 300,
setupCtx: func() client.Context {
return client.Context{}.
WithCodec(s.cdc).
WithGRPCClient(defaultConn).
WithGRPCConnProvider(provider).
WithHeight(300)
},
expectedConn: historicalConn,
},
{
name: "context with GRPCConnProvider and latest height",
height: 0,
setupCtx: func() client.Context {
return client.Context{}.
WithCodec(s.cdc).
WithGRPCClient(defaultConn).
WithGRPCConnProvider(provider).
WithHeight(0)
},
expectedConn: defaultConn,
},
{
name: "context without GRPCConnProvider",
height: 300,
setupCtx: func() client.Context {
return client.Context{}.
WithCodec(s.cdc).
WithGRPCClient(defaultConn).
WithHeight(300)
},
expectedConn: defaultConn,
},
{
name: "context with nil historical connections map",
height: 100,
setupCtx: func() client.Context {
nilProvider := client.NewGRPCConnProvider(defaultConn, nil)
return client.Context{}.
WithCodec(s.cdc).
WithGRPCClient(defaultConn).
WithGRPCConnProvider(nilProvider).
WithHeight(100)
},
expectedConn: defaultConn,
},
{
name: "context with empty historical connections map",
height: 100,
setupCtx: func() client.Context {
emptyProvider := client.NewGRPCConnProvider(defaultConn, config.HistoricalGRPCConnections{})
return client.Context{}.
WithCodec(s.cdc).
WithGRPCClient(defaultConn).
WithGRPCConnProvider(emptyProvider).
WithHeight(100)
},
expectedConn: defaultConn,
},
}
for _, tc := range testCases {
s.Run(tc.name, func() {
ctx := tc.setupCtx()
var actualConn *grpc.ClientConn
if ctx.GRPCConnProvider != nil {
actualConn = ctx.GRPCConnProvider.GetGRPCConn(ctx.Height)
} else {
actualConn = ctx.GRPCClient
}
s.Require().Equal(tc.expectedConn, actualConn)
})
}
}
func TestGetHeightFromMetadata(t *testing.T) {
tests := []struct {
name string
setupContext func() context.Context
expectedHeight int64
}{
{
name: "valid height in metadata",
setupContext: func() context.Context {
md := metadata.Pairs(grpctypes.GRPCBlockHeightHeader, "12345")
return metadata.NewOutgoingContext(context.Background(), md)
},
expectedHeight: 12345,
},
{
name: "zero height in metadata",
setupContext: func() context.Context {
md := metadata.Pairs(grpctypes.GRPCBlockHeightHeader, "0")
return metadata.NewOutgoingContext(context.Background(), md)
},
expectedHeight: 0,
},
{
name: "negative height returns zero",
setupContext: func() context.Context {
md := metadata.Pairs(grpctypes.GRPCBlockHeightHeader, "-100")
return metadata.NewOutgoingContext(context.Background(), md)
},
expectedHeight: 0,
},
{
name: "no metadata returns zero",
setupContext: context.Background,
expectedHeight: 0,
},
{
name: "empty height header returns zero",
setupContext: func() context.Context {
md := metadata.New(map[string]string{})
return metadata.NewOutgoingContext(context.Background(), md)
},
expectedHeight: 0,
},
{
name: "invalid height string returns zero",
setupContext: func() context.Context {
md := metadata.Pairs(grpctypes.GRPCBlockHeightHeader, "not-a-number")
return metadata.NewOutgoingContext(context.Background(), md)
},
expectedHeight: 0,
},
{
name: "multiple height values uses first",
setupContext: func() context.Context {
md := metadata.Pairs(
grpctypes.GRPCBlockHeightHeader, "100",
grpctypes.GRPCBlockHeightHeader, "200",
)
return metadata.NewOutgoingContext(context.Background(), md)
},
expectedHeight: 100,
},
{
name: "very large height",
setupContext: func() context.Context {
md := metadata.Pairs(grpctypes.GRPCBlockHeightHeader, "9223372036854775807") // max int64
return metadata.NewOutgoingContext(context.Background(), md)
},
expectedHeight: 9223372036854775807,
},
{
name: "height exceeding int64 returns zero",
setupContext: func() context.Context {
md := metadata.Pairs(grpctypes.GRPCBlockHeightHeader, "9223372036854775808") // max int64 + 1
return metadata.NewOutgoingContext(context.Background(), md)
},
expectedHeight: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := tt.setupContext()
height := client.GetHeightFromMetadata(ctx)
require.Equal(t, tt.expectedHeight, height)
})
}
}