feat: wire new handlers to grpc (#22333)

Co-authored-by: Randy Grok <@faulttolerance.net>
Co-authored-by: Julien Robert <julien@rbrt.fr>
This commit is contained in:
Randy Grok
2024-11-05 15:31:22 +00:00
committed by GitHub
co-authored by Randy Grok Julien Robert
parent 8c24b6bef1
commit 62ddd3e939
9 changed files with 4276 additions and 0 deletions
+3
View File
@@ -74,6 +74,9 @@ func New[T transaction.Tx](
// Reflection allows external clients to see what services and methods the gRPC server exposes.
gogoreflection.Register(grpcSrv, slices.Collect(maps.Keys(queryHandlers)), logger.With("sub-module", "grpc-reflection"))
// Register V2
RegisterServiceServer(grpcSrv, &v2Service{queryHandlers, queryable})
srv.grpcSrv = grpcSrv
srv.config = serverCfg
srv.logger = logger.With(log.ModuleKey, srv.Name())
+73
View File
@@ -0,0 +1,73 @@
package grpc
import (
"context"
"github.com/cosmos/gogoproto/proto"
gogoproto "github.com/cosmos/gogoproto/types/any"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
appmodulev2 "cosmossdk.io/core/appmodule/v2"
"cosmossdk.io/core/transaction"
)
// v2Service implements the gRPC service interface for handling queries and listing handlers.
type v2Service struct {
queryHandlers map[string]appmodulev2.Handler
queryable interface {
Query(ctx context.Context, version uint64, msg transaction.Msg) (transaction.Msg, error)
}
}
// Query handles incoming query requests by unmarshaling the request, processing it,
// and returning the response in an Any protobuf message.
func (s v2Service) Query(ctx context.Context, request *QueryRequest) (*QueryResponse, error) {
if request == nil || request.Request == nil {
return nil, status.Error(codes.InvalidArgument, "empty request")
}
msgName := request.Request.TypeUrl
handler, exists := s.queryHandlers[msgName]
if !exists {
return nil, status.Errorf(codes.NotFound, "handler not found for %s", msgName)
}
protoMsg := handler.MakeMsg()
if err := proto.Unmarshal(request.Request.Value, protoMsg); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "failed to unmarshal request: %v", err)
}
queryResp, err := s.queryable.Query(ctx, 0, protoMsg)
if err != nil {
return nil, status.Errorf(codes.Internal, "query failed: %v", err)
}
respBytes, err := proto.Marshal(queryResp)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to marshal response: %v", err)
}
anyResp := &gogoproto.Any{
TypeUrl: "/" + proto.MessageName(queryResp),
Value: respBytes,
}
return &QueryResponse{Response: anyResp}, nil
}
func (s v2Service) ListQueryHandlers(_ context.Context, _ *ListQueryHandlersRequest) (*ListQueryHandlersResponse, error) {
var handlerDescriptors []*Handler
for handlerName := range s.queryHandlers {
msg := s.queryHandlers[handlerName].MakeMsg()
resp := s.queryHandlers[handlerName].MakeMsgResp()
handlerDescriptors = append(handlerDescriptors, &Handler{
RequestName: proto.MessageName(msg),
ResponseName: proto.MessageName(resp),
})
}
return &ListQueryHandlersResponse{Handlers: handlerDescriptors}, nil
}
File diff suppressed because it is too large Load Diff
+227
View File
@@ -0,0 +1,227 @@
package grpc
import (
"context"
"fmt"
"testing"
"github.com/cosmos/gogoproto/proto"
gogoproto "github.com/cosmos/gogoproto/types/any"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
appmodulev2 "cosmossdk.io/core/appmodule/v2"
"cosmossdk.io/core/transaction"
serverv2 "cosmossdk.io/server/v2"
)
type MockRequestMessage struct {
Data string `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"`
}
func (m *MockRequestMessage) XXX_MessageName() string {
return "MockRequestMessage"
}
func (m *MockRequestMessage) Reset() {}
func (m *MockRequestMessage) String() string { return "" }
func (m *MockRequestMessage) ProtoMessage() {}
func (m *MockRequestMessage) ValidateBasic() error {
return nil
}
type MockResponseMessage struct {
Data string `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"`
}
func (m *MockResponseMessage) Reset() {}
func (m *MockResponseMessage) String() string { return "" }
func (m *MockResponseMessage) ProtoMessage() {}
func (m *MockResponseMessage) ValidateBasic() error {
return nil
}
type mockApp[T transaction.Tx] struct {
mock.Mock
serverv2.AppI[T]
}
func (m *mockApp[T]) QueryHandlers() map[string]appmodulev2.Handler {
args := m.Called()
return args.Get(0).(map[string]appmodulev2.Handler)
}
func (m *mockApp[T]) Query(ctx context.Context, height uint64, msg transaction.Msg) (transaction.Msg, error) {
args := m.Called(ctx, height, msg)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(transaction.Msg), args.Error(1)
}
func TestQuery(t *testing.T) {
tests := []struct {
name string
setupMock func(app *mockApp[transaction.Tx])
request *QueryRequest
expectError bool
expectedError string
}{
{
name: "successful query",
setupMock: func(app *mockApp[transaction.Tx]) {
reqMsg := &MockRequestMessage{Data: "request"}
respMsg := &MockResponseMessage{Data: "response"}
handlers := map[string]appmodulev2.Handler{
"/" + proto.MessageName(&MockRequestMessage{}): {
Func: func(ctx context.Context, msg transaction.Msg) (transaction.Msg, error) {
return respMsg, nil
},
MakeMsg: func() transaction.Msg {
return reqMsg
},
MakeMsgResp: func() transaction.Msg {
return respMsg
},
},
}
app.On("QueryHandlers").Return(handlers)
app.On("Query", mock.Anything, uint64(0), reqMsg).Return(respMsg, nil)
},
request: createTestRequest(t),
expectError: false,
},
{
name: "handler not found",
setupMock: func(app *mockApp[transaction.Tx]) {
handlers := map[string]appmodulev2.Handler{}
app.On("QueryHandlers").Return(handlers)
},
request: createTestRequest(t),
expectError: true,
expectedError: "rpc error: code = NotFound desc = handler not found for /MockRequestMessage",
},
{
name: "query error",
setupMock: func(app *mockApp[transaction.Tx]) {
reqMsg := &MockRequestMessage{Data: "request"}
respMsg := &MockRequestMessage{Data: "response"}
handlers := map[string]appmodulev2.Handler{
"/" + proto.MessageName(&MockRequestMessage{}): {
Func: func(ctx context.Context, msg transaction.Msg) (transaction.Msg, error) {
return respMsg, nil
},
MakeMsg: func() transaction.Msg {
return reqMsg
},
MakeMsgResp: func() transaction.Msg {
return respMsg
},
},
}
app.On("QueryHandlers").Return(handlers)
app.On("Query", mock.Anything, uint64(0), reqMsg).Return(nil, assert.AnError)
},
request: createTestRequest(t),
expectError: true,
expectedError: fmt.Sprintf("rpc error: code = Internal desc = query failed: %s", assert.AnError.Error()),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockApp := &mockApp[transaction.Tx]{}
if tt.setupMock != nil {
tt.setupMock(mockApp)
}
service := &v2Service{mockApp.QueryHandlers(), mockApp}
resp, err := service.Query(context.Background(), tt.request)
if tt.expectError {
assert.Error(t, err)
if tt.expectedError != "" {
assert.Equal(t, tt.expectedError, err.Error())
}
} else {
assert.NoError(t, err)
assert.NotNil(t, resp)
assert.NotNil(t, resp.Response)
}
mockApp.AssertExpectations(t)
})
}
}
func TestV2Service_ListQueryHandlers(t *testing.T) {
tests := []struct {
name string
setupMock func(app *mockApp[transaction.Tx])
}{
{
name: "successful list query handlers",
setupMock: func(app *mockApp[transaction.Tx]) {
reqMsg := &MockRequestMessage{Data: "request"}
respMsg := &MockResponseMessage{Data: "response"}
handlers := map[string]appmodulev2.Handler{
"/test.Query": {
Func: func(ctx context.Context, msg transaction.Msg) (transaction.Msg, error) {
return respMsg, nil
},
MakeMsg: func() transaction.Msg {
return reqMsg
},
MakeMsgResp: func() transaction.Msg {
return respMsg
},
},
}
app.On("QueryHandlers").Return(handlers)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockApp := &mockApp[transaction.Tx]{}
if tt.setupMock != nil {
tt.setupMock(mockApp)
}
service := &v2Service{mockApp.QueryHandlers(), mockApp}
resp, err := service.ListQueryHandlers(context.Background(), &ListQueryHandlersRequest{})
assert.NoError(t, err)
assert.NotNil(t, resp)
assert.Len(t, resp.Handlers, 1)
resp.Handlers[0].RequestName = "/MockRequestMessage"
resp.Handlers[0].ResponseName = "/MockResponseMessage"
mockApp.AssertExpectations(t)
})
}
}
func createTestRequest(t *testing.T) *QueryRequest {
t.Helper()
reqMsg := &MockRequestMessage{Data: "request"}
reqBytes, err := proto.Marshal(reqMsg)
if err != nil {
t.Fatalf("failed to marshal request: %v", err)
}
return &QueryRequest{
Request: &gogoproto.Any{
TypeUrl: "/" + proto.MessageName(reqMsg),
Value: reqBytes,
},
}
}