From c626a5c8ee256fa3f60a4e7d5aa1096aadb3c1e0 Mon Sep 17 00:00:00 2001 From: Daniel Burckhardt Date: Sat, 23 Jul 2022 11:16:23 +0200 Subject: [PATCH] testing(json rpc): Add backend test suite with mock grpc query client (#1199) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * tests(json-rpc): wip evm_backend unit test setup * tests(json-rpc): wip evm_backend unit test setup * fix viper * wip query client mock * fix first backend test except error message * clean up * wip Context with Height * fix JSON RPC backend test setup * typo * refactor folder structure * Update rpc/backend/evm_backend_test.go Co-authored-by: Federico Kunze Küllmer <31522760+fedekunze@users.noreply.github.com> --- go.mod | 1 + rpc/backend/backend_suite_test.go | 73 ++++++ rpc/backend/evm_backend_test.go | 30 +++ rpc/backend/mocks/query_client.go | 393 ++++++++++++++++++++++++++++++ 4 files changed, 497 insertions(+) create mode 100644 rpc/backend/backend_suite_test.go create mode 100644 rpc/backend/evm_backend_test.go create mode 100644 rpc/backend/mocks/query_client.go diff --git a/go.mod b/go.mod index daa61d5f..e52e7d87 100644 --- a/go.mod +++ b/go.mod @@ -133,6 +133,7 @@ require ( github.com/spf13/afero v1.8.2 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/stretchr/objx v0.4.0 // indirect github.com/subosito/gotenv v1.3.0 // indirect github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect github.com/tendermint/btcd v0.1.1 // indirect diff --git a/rpc/backend/backend_suite_test.go b/rpc/backend/backend_suite_test.go new file mode 100644 index 00000000..4839936c --- /dev/null +++ b/rpc/backend/backend_suite_test.go @@ -0,0 +1,73 @@ +package backend + +import ( + "testing" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/server" + grpctypes "github.com/cosmos/cosmos-sdk/types/grpc" + mock "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" + + "github.com/evmos/ethermint/rpc/backend/mocks" + rpc "github.com/evmos/ethermint/rpc/types" + evmtypes "github.com/evmos/ethermint/x/evm/types" +) + +type BackendTestSuite struct { + suite.Suite + backend *Backend +} + +func TestBackendTestSuite(t *testing.T) { + suite.Run(t, new(BackendTestSuite)) +} + +// SetupTest is executed before every BackendTestSuite test +func (suite *BackendTestSuite) SetupTest() { + ctx := server.NewDefaultContext() + ctx.Viper.Set("telemetry.global-labels", []interface{}{}) + clientCtx := client.Context{}.WithChainID("ethermint_9000-1").WithHeight(1) + allowUnprotectedTxs := false + + suite.backend = NewBackend(ctx, ctx.Logger, clientCtx, allowUnprotectedTxs) + + queryClient := mocks.NewQueryClient(suite.T()) + var header metadata.MD + RegisterMockQueries(queryClient, &header) + + suite.backend.queryClient.QueryClient = queryClient + suite.backend.ctx = rpc.ContextWithHeight(1) +} + +// QueryClient defines a mocked object that implements the grpc QueryCLient +// interface. It's used on tests to test the JSON-RPC without running a grpc +// client server. E.g. JSON-PRC-CLIENT -> BACKEND -> Mock GRPC CLIENT -> APP +var _ evmtypes.QueryClient = &mocks.QueryClient{} + +// RegisterMockQueries registers the queries and their respective responses, +// so that they can be called in tests using the queryClient +func RegisterMockQueries(queryClient *mocks.QueryClient, header *metadata.MD) { + queryClient.On("Params", rpc.ContextWithHeight(1), &evmtypes.QueryParamsRequest{}, grpc.Header(header)). + Return(&evmtypes.QueryParamsResponse{}, nil). + Run(func(args mock.Arguments) { + // If Params call is successful, also update the header height + arg := args.Get(2).(grpc.HeaderCallOption) + h := metadata.MD{} + h.Set(grpctypes.GRPCBlockHeightHeader, "1") + *arg.HeaderAddr = h + }) +} + +func TestQueryClient(t *testing.T) { + queryClient := mocks.NewQueryClient(t) + var header metadata.MD + RegisterMockQueries(queryClient, &header) + + // mock calls for abstraction + _, err := queryClient.Params(rpc.ContextWithHeight(1), &evmtypes.QueryParamsRequest{}, grpc.Header(&header)) + require.NoError(t, err) +} diff --git a/rpc/backend/evm_backend_test.go b/rpc/backend/evm_backend_test.go new file mode 100644 index 00000000..f05323d0 --- /dev/null +++ b/rpc/backend/evm_backend_test.go @@ -0,0 +1,30 @@ +package backend + +import "github.com/ethereum/go-ethereum/common/hexutil" + +func (suite *BackendTestSuite) TestBlockNumber() { + testCases := []struct { + mame string + malleate func() + expBlockNumber hexutil.Uint64 + expPass bool + }{ + { + "pass", + func() { + }, + 0x1, + true, + }, + } + for _, tc := range testCases { + blockNumber, err := suite.backend.BlockNumber() + + if tc.expPass { + suite.Require().NoError(err) + suite.Require().Equal(tc.expBlockNumber, blockNumber) + } else { + suite.Require().Error(err) + } + } +} diff --git a/rpc/backend/mocks/query_client.go b/rpc/backend/mocks/query_client.go new file mode 100644 index 00000000..c2d41844 --- /dev/null +++ b/rpc/backend/mocks/query_client.go @@ -0,0 +1,393 @@ +// Code generated by mockery v2.14.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + grpc "google.golang.org/grpc" + + mock "github.com/stretchr/testify/mock" + + types "github.com/evmos/ethermint/x/evm/types" +) + +// QueryClient is an autogenerated mock type for the QueryClient type +type QueryClient struct { + mock.Mock +} + +// Account provides a mock function with given fields: ctx, in, opts +func (_m *QueryClient) Account(ctx context.Context, in *types.QueryAccountRequest, opts ...grpc.CallOption) (*types.QueryAccountResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *types.QueryAccountResponse + if rf, ok := ret.Get(0).(func(context.Context, *types.QueryAccountRequest, ...grpc.CallOption) *types.QueryAccountResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.QueryAccountResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *types.QueryAccountRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Balance provides a mock function with given fields: ctx, in, opts +func (_m *QueryClient) Balance(ctx context.Context, in *types.QueryBalanceRequest, opts ...grpc.CallOption) (*types.QueryBalanceResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *types.QueryBalanceResponse + if rf, ok := ret.Get(0).(func(context.Context, *types.QueryBalanceRequest, ...grpc.CallOption) *types.QueryBalanceResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.QueryBalanceResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *types.QueryBalanceRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// BaseFee provides a mock function with given fields: ctx, in, opts +func (_m *QueryClient) BaseFee(ctx context.Context, in *types.QueryBaseFeeRequest, opts ...grpc.CallOption) (*types.QueryBaseFeeResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *types.QueryBaseFeeResponse + if rf, ok := ret.Get(0).(func(context.Context, *types.QueryBaseFeeRequest, ...grpc.CallOption) *types.QueryBaseFeeResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.QueryBaseFeeResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *types.QueryBaseFeeRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Code provides a mock function with given fields: ctx, in, opts +func (_m *QueryClient) Code(ctx context.Context, in *types.QueryCodeRequest, opts ...grpc.CallOption) (*types.QueryCodeResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *types.QueryCodeResponse + if rf, ok := ret.Get(0).(func(context.Context, *types.QueryCodeRequest, ...grpc.CallOption) *types.QueryCodeResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.QueryCodeResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *types.QueryCodeRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CosmosAccount provides a mock function with given fields: ctx, in, opts +func (_m *QueryClient) CosmosAccount(ctx context.Context, in *types.QueryCosmosAccountRequest, opts ...grpc.CallOption) (*types.QueryCosmosAccountResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *types.QueryCosmosAccountResponse + if rf, ok := ret.Get(0).(func(context.Context, *types.QueryCosmosAccountRequest, ...grpc.CallOption) *types.QueryCosmosAccountResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.QueryCosmosAccountResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *types.QueryCosmosAccountRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// EstimateGas provides a mock function with given fields: ctx, in, opts +func (_m *QueryClient) EstimateGas(ctx context.Context, in *types.EthCallRequest, opts ...grpc.CallOption) (*types.EstimateGasResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *types.EstimateGasResponse + if rf, ok := ret.Get(0).(func(context.Context, *types.EthCallRequest, ...grpc.CallOption) *types.EstimateGasResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.EstimateGasResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *types.EthCallRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// EthCall provides a mock function with given fields: ctx, in, opts +func (_m *QueryClient) EthCall(ctx context.Context, in *types.EthCallRequest, opts ...grpc.CallOption) (*types.MsgEthereumTxResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *types.MsgEthereumTxResponse + if rf, ok := ret.Get(0).(func(context.Context, *types.EthCallRequest, ...grpc.CallOption) *types.MsgEthereumTxResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.MsgEthereumTxResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *types.EthCallRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Params provides a mock function with given fields: ctx, in, opts +func (_m *QueryClient) Params(ctx context.Context, in *types.QueryParamsRequest, opts ...grpc.CallOption) (*types.QueryParamsResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *types.QueryParamsResponse + if rf, ok := ret.Get(0).(func(context.Context, *types.QueryParamsRequest, ...grpc.CallOption) *types.QueryParamsResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.QueryParamsResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *types.QueryParamsRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Storage provides a mock function with given fields: ctx, in, opts +func (_m *QueryClient) Storage(ctx context.Context, in *types.QueryStorageRequest, opts ...grpc.CallOption) (*types.QueryStorageResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *types.QueryStorageResponse + if rf, ok := ret.Get(0).(func(context.Context, *types.QueryStorageRequest, ...grpc.CallOption) *types.QueryStorageResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.QueryStorageResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *types.QueryStorageRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// TraceBlock provides a mock function with given fields: ctx, in, opts +func (_m *QueryClient) TraceBlock(ctx context.Context, in *types.QueryTraceBlockRequest, opts ...grpc.CallOption) (*types.QueryTraceBlockResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *types.QueryTraceBlockResponse + if rf, ok := ret.Get(0).(func(context.Context, *types.QueryTraceBlockRequest, ...grpc.CallOption) *types.QueryTraceBlockResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.QueryTraceBlockResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *types.QueryTraceBlockRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// TraceTx provides a mock function with given fields: ctx, in, opts +func (_m *QueryClient) TraceTx(ctx context.Context, in *types.QueryTraceTxRequest, opts ...grpc.CallOption) (*types.QueryTraceTxResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *types.QueryTraceTxResponse + if rf, ok := ret.Get(0).(func(context.Context, *types.QueryTraceTxRequest, ...grpc.CallOption) *types.QueryTraceTxResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.QueryTraceTxResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *types.QueryTraceTxRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ValidatorAccount provides a mock function with given fields: ctx, in, opts +func (_m *QueryClient) ValidatorAccount(ctx context.Context, in *types.QueryValidatorAccountRequest, opts ...grpc.CallOption) (*types.QueryValidatorAccountResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *types.QueryValidatorAccountResponse + if rf, ok := ret.Get(0).(func(context.Context, *types.QueryValidatorAccountRequest, ...grpc.CallOption) *types.QueryValidatorAccountResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.QueryValidatorAccountResponse) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *types.QueryValidatorAccountRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +type mockConstructorTestingTNewQueryClient interface { + mock.TestingT + Cleanup(func()) +} + +// NewQueryClient creates a new instance of QueryClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewQueryClient(t mockConstructorTestingTNewQueryClient) *QueryClient { + mock := &QueryClient{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +}