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:
co-authored by
Randy Grok
Julien Robert
parent
8c24b6bef1
commit
62ddd3e939
@@ -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())
|
||||
|
||||
@@ -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
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user