go_study/fabric-main/discovery/service_test.go

625 lines
19 KiB
Go

/*
Copyright IBM Corp. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/
package discovery
import (
"context"
"errors"
"fmt"
"reflect"
"testing"
"github.com/hyperledger/fabric-protos-go/peer"
"github.com/golang/protobuf/proto"
"github.com/hyperledger/fabric-protos-go/discovery"
"github.com/hyperledger/fabric-protos-go/gossip"
"github.com/hyperledger/fabric/gossip/api"
gcommon "github.com/hyperledger/fabric/gossip/common"
gdisc "github.com/hyperledger/fabric/gossip/discovery"
"github.com/hyperledger/fabric/gossip/protoext"
"github.com/hyperledger/fabric/protoutil"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
func TestConfig(t *testing.T) {
for _, trueOfFalse := range []bool{true, false} {
conf := Config{
AuthCacheEnabled: trueOfFalse,
AuthCachePurgeRetentionRatio: 0.5,
AuthCacheMaxSize: 42,
}
service := NewService(conf, &mockSupport{})
require.Equal(t, trueOfFalse, service.auth.conf.enabled)
require.Equal(t, 42, service.auth.conf.maxCacheSize)
require.Equal(t, 0.5, service.auth.conf.purgeRetentionRatio)
}
}
func TestService(t *testing.T) {
conf := Config{
AuthCacheEnabled: true,
}
ctx := context.Background()
req := &discovery.Request{
Authentication: &discovery.AuthInfo{
ClientIdentity: []byte{1, 2, 3},
},
Queries: []*discovery.Query{
{
Channel: "noneExistentChannel",
},
},
}
mockSup := &mockSupport{}
mockSup.On("ChannelExists", "noneExistentChannel").Return(false)
mockSup.On("ChannelExists", "channelWithAccessDenied").Return(true)
mockSup.On("ChannelExists", "channelWithAccessGranted").Return(true)
mockSup.On("ChannelExists", "channelWithSomeProblem").Return(true)
mockSup.On("EligibleForService", "channelWithAccessDenied", mock.Anything).Return(errors.New("foo"))
mockSup.On("EligibleForService", "channelWithAccessGranted", mock.Anything).Return(nil)
mockSup.On("EligibleForService", "channelWithSomeProblem", mock.Anything).Return(nil)
ed1 := &discovery.EndorsementDescriptor{
Chaincode: "cc1",
}
ed2 := &discovery.EndorsementDescriptor{
Chaincode: "cc2",
}
ed3 := &discovery.EndorsementDescriptor{
Chaincode: "cc3",
}
mockSup.On("PeersForEndorsement", "unknownCC").Return(nil, errors.New("unknown chaincode"))
mockSup.On("PeersForEndorsement", "cc1").Return(ed1, nil)
mockSup.On("PeersForEndorsement", "cc2").Return(ed2, nil)
mockSup.On("PeersForEndorsement", "cc3").Return(ed3, nil)
service := NewService(conf, mockSup)
// Scenario I: Channel does not exist
resp, err := service.Discover(ctx, toSignedRequest(req))
require.NoError(t, err)
require.Equal(t, wrapResult(&discovery.Error{Content: "access denied"}), resp)
// Scenario II: Channel does not exist
req.Queries[0].Channel = "channelWithAccessDenied"
resp, err = service.Discover(ctx, toSignedRequest(req))
require.NoError(t, err)
require.Equal(t, wrapResult(&discovery.Error{Content: "access denied"}), resp)
// Scenario III: Request with nil query
req.Queries[0].Channel = "channelWithAccessGranted"
req.Queries[0].Query = nil
resp, err = service.Discover(ctx, toSignedRequest(req))
require.NoError(t, err)
require.Contains(t, resp.Results[0].GetError().Content, "unknown or missing request type")
// Scenario IV: Request payload is invalid
signedRequest := toSignedRequest(req)
// Corrupt the payload by appending a zero byte at its end
signedRequest.Payload = append(signedRequest.Payload, 0)
resp, err = service.Discover(ctx, signedRequest)
require.Nil(t, resp)
require.Contains(t, err.Error(), "failed parsing request")
// Scenario V: Request a CC query with no chaincodes at all
req.Queries[0].Query = &discovery.Query_CcQuery{
CcQuery: &discovery.ChaincodeQuery{
Interests: []*peer.ChaincodeInterest{
{},
},
},
}
resp, err = service.Discover(ctx, toSignedRequest(req))
require.NoError(t, err)
require.Contains(t, resp.Results[0].GetError().Content, "chaincode interest must contain at least one chaincode")
// Scenario VI: Request a CC query with no interests at all
req.Queries[0].Query = &discovery.Query_CcQuery{
CcQuery: &discovery.ChaincodeQuery{
Interests: []*peer.ChaincodeInterest{},
},
}
resp, err = service.Discover(ctx, toSignedRequest(req))
require.NoError(t, err)
require.Contains(t, resp.Results[0].GetError().Content, "chaincode query must have at least one chaincode interest")
// Scenario VII: Request a CC query with a chaincode name that is empty
req.Queries[0].Query = &discovery.Query_CcQuery{
CcQuery: &discovery.ChaincodeQuery{
Interests: []*peer.ChaincodeInterest{{
Chaincodes: []*peer.ChaincodeCall{{
Name: "",
}},
}},
},
}
resp, err = service.Discover(ctx, toSignedRequest(req))
require.NoError(t, err)
require.Contains(t, resp.Results[0].GetError().Content, "chaincode name in interest cannot be empty")
// Scenario VIII: Request with a CC query where one chaincode is unavailable
req.Queries[0].Query = &discovery.Query_CcQuery{
CcQuery: &discovery.ChaincodeQuery{
Interests: []*peer.ChaincodeInterest{
{
Chaincodes: []*peer.ChaincodeCall{{Name: "unknownCC"}},
},
{
Chaincodes: []*peer.ChaincodeCall{{Name: "cc1"}},
},
},
},
}
resp, err = service.Discover(ctx, toSignedRequest(req))
require.NoError(t, err)
require.Contains(t, resp.Results[0].GetError().Content, "failed constructing descriptor")
require.Contains(t, resp.Results[0].GetError().Content, "unknownCC")
// Scenario IX: Request with a CC query where all are available
req.Queries[0].Query = &discovery.Query_CcQuery{
CcQuery: &discovery.ChaincodeQuery{
Interests: []*peer.ChaincodeInterest{
{
Chaincodes: []*peer.ChaincodeCall{{Name: "cc1"}},
},
{
Chaincodes: []*peer.ChaincodeCall{{Name: "cc2"}},
},
{
Chaincodes: []*peer.ChaincodeCall{{Name: "cc3"}},
},
},
},
}
resp, err = service.Discover(ctx, toSignedRequest(req))
require.NoError(t, err)
expected := wrapResult(&discovery.ChaincodeQueryResult{
Content: []*discovery.EndorsementDescriptor{ed1, ed2, ed3},
})
require.Equal(t, expected, resp)
// Scenario X: Request with a config query
mockSup.On("Config", mock.Anything).Return(nil, errors.New("failed fetching config")).Once()
req.Queries[0].Query = &discovery.Query_ConfigQuery{
ConfigQuery: &discovery.ConfigQuery{},
}
resp, err = service.Discover(ctx, toSignedRequest(req))
require.NoError(t, err)
require.Contains(t, resp.Results[0].GetError().Content, "failed fetching config for channel channelWithAccessGranted")
// Scenario XI: Request with a config query
mockSup.On("Config", mock.Anything).Return(&discovery.ConfigResult{}, nil).Once()
req.Queries[0].Query = &discovery.Query_ConfigQuery{
ConfigQuery: &discovery.ConfigQuery{},
}
resp, err = service.Discover(ctx, toSignedRequest(req))
require.NoError(t, err)
require.NotNil(t, resp.Results[0].GetConfigResult())
// Scenario XII: Request with a membership query
// Peers in membership view: { p0, p1, p2, p3}
// Peers in channel view: {p1, p2, p4}
// So that means that the returned peers for the channel should be the intersection
// which is: {p1, p2}, but the returned peers for the local query should be
// simply the membership view.
peersInMembershipView := gdisc.Members{
aliveMsg(0), aliveMsg(1), aliveMsg(2), aliveMsg(3),
}
peersInChannelView := gdisc.Members{
stateInfoMsg(1), stateInfoMsg(2), stateInfoMsg(4),
}
// EligibleForService for an "empty" channel
mockSup.On("EligibleForService", "", mock.Anything).Return(nil).Once()
mockSup.On("PeersAuthorizedByCriteria", gcommon.ChannelID("channelWithAccessGranted")).Return(peersInChannelView, nil).Once()
mockSup.On("PeersAuthorizedByCriteria", gcommon.ChannelID("channelWithSomeProblem")).Return(nil, errors.New("an error occurred")).Once()
mockSup.On("Peers").Return(peersInMembershipView).Twice()
mockSup.On("IdentityInfo").Return(api.PeerIdentitySet{
idInfo(0, "O2"), idInfo(1, "O2"), idInfo(2, "O3"),
idInfo(3, "O3"), idInfo(4, "O3"),
}).Twice()
req.Queries = []*discovery.Query{
{
Channel: "channelWithAccessGranted",
Query: &discovery.Query_PeerQuery{
PeerQuery: &discovery.PeerMembershipQuery{},
},
},
{
Query: &discovery.Query_LocalPeers{
LocalPeers: &discovery.LocalPeerQuery{},
},
},
{
Channel: "channelWithSomeProblem",
Query: &discovery.Query_PeerQuery{
PeerQuery: &discovery.PeerMembershipQuery{
Filter: &peer.ChaincodeInterest{},
},
},
},
}
resp, err = service.Discover(ctx, toSignedRequest(req))
require.NoError(t, err)
expectedChannelResponse := &discovery.PeerMembershipResult{
PeersByOrg: map[string]*discovery.Peers{
"O2": {
Peers: []*discovery.Peer{
{
Identity: idInfo(1, "O2").Identity,
StateInfo: stateInfoMsg(1).Envelope,
MembershipInfo: aliveMsg(1).Envelope,
},
},
},
"O3": {
Peers: []*discovery.Peer{
{
Identity: idInfo(2, "O3").Identity,
StateInfo: stateInfoMsg(2).Envelope,
MembershipInfo: aliveMsg(2).Envelope,
},
},
},
},
}
expectedLocalResponse := &discovery.PeerMembershipResult{
PeersByOrg: map[string]*discovery.Peers{
"O2": {
Peers: []*discovery.Peer{
{
Identity: idInfo(0, "O2").Identity,
MembershipInfo: aliveMsg(0).Envelope,
},
{
Identity: idInfo(1, "O2").Identity,
MembershipInfo: aliveMsg(1).Envelope,
},
},
},
"O3": {
Peers: []*discovery.Peer{
{
Identity: idInfo(2, "O3").Identity,
MembershipInfo: aliveMsg(2).Envelope,
},
{
Identity: idInfo(3, "O3").Identity,
MembershipInfo: aliveMsg(3).Envelope,
},
},
},
},
}
require.Len(t, resp.Results, 3)
require.Len(t, resp.Results[0].GetMembers().PeersByOrg, 2)
require.Len(t, resp.Results[1].GetMembers().PeersByOrg, 2)
require.Equal(t, "an error occurred", resp.Results[2].GetError().Content)
for org, responsePeers := range resp.Results[0].GetMembers().PeersByOrg {
err := peers(expectedChannelResponse.PeersByOrg[org].Peers).compare(peers(responsePeers.Peers))
require.NoError(t, err)
}
for org, responsePeers := range resp.Results[1].GetMembers().PeersByOrg {
err := peers(expectedLocalResponse.PeersByOrg[org].Peers).compare(peers(responsePeers.Peers))
require.NoError(t, err)
}
// Scenario XIII: The client is eligible for channel queries but not for channel-less
// since it's not an admin. It sends a query for a channel-less query but puts a channel in the query.
// It should fail because channel-less query types cannot have a channel configured in them.
req.Queries = []*discovery.Query{
{
Channel: "channelWithAccessGranted",
Query: &discovery.Query_LocalPeers{
LocalPeers: &discovery.LocalPeerQuery{},
},
},
}
resp, err = service.Discover(ctx, toSignedRequest(req))
require.NoError(t, err)
require.Contains(t, resp.Results[0].GetError().Content, "unknown or missing request type")
}
func TestValidateStructure(t *testing.T) {
extractHash := func(ctx context.Context) []byte {
return nil
}
// Scenarios I-V without TLS, scenarios VI onwards TLS
// Scenario I: Nil request
res, err := validateStructure(context.Background(), nil, false, extractHash)
require.Nil(t, res)
require.Equal(t, "nil request", err.Error())
// Scenario II: Malformed envelope
res, err = validateStructure(context.Background(), &discovery.SignedRequest{
Payload: []byte{1, 2, 3},
}, false, extractHash)
require.Nil(t, res)
require.Contains(t, err.Error(), "failed parsing request")
// Scenario III: Empty request
res, err = validateStructure(context.Background(), &discovery.SignedRequest{}, false, extractHash)
require.Nil(t, res)
require.Equal(t, "access denied, no authentication info in request", err.Error())
// Scenario IV: request without a client identity
req := &discovery.Request{
Authentication: &discovery.AuthInfo{},
}
b, _ := proto.Marshal(req)
res, err = validateStructure(context.Background(), &discovery.SignedRequest{
Payload: b,
}, false, extractHash)
require.Nil(t, res)
require.Equal(t, "access denied, client identity wasn't supplied", err.Error())
// Scenario V: request with a client identity, should succeed because no TLS is used
req = &discovery.Request{
Authentication: &discovery.AuthInfo{
ClientIdentity: []byte{1, 2, 3},
},
}
b, _ = proto.Marshal(req)
res, err = validateStructure(context.Background(), &discovery.SignedRequest{
Payload: b,
}, false, extractHash)
require.NoError(t, err)
// Ensure returned request is as before serialization to bytes
require.True(t, proto.Equal(req, res))
// Scenario VI: request with a client identity but with TLS enabled but client doesn't send a TLS cert
req = &discovery.Request{
Authentication: &discovery.AuthInfo{
ClientIdentity: []byte{1, 2, 3},
},
}
b, _ = proto.Marshal(req)
res, err = validateStructure(context.Background(), &discovery.SignedRequest{
Payload: b,
}, true, extractHash)
require.Nil(t, res)
require.Equal(t, "client didn't send a TLS certificate", err.Error())
// Scenario VII: request with a client identity and with TLS enabled but the TLS cert hash doesn't match
// the computed one
extractHash = func(ctx context.Context) []byte {
return []byte{1, 2}
}
req = &discovery.Request{
Authentication: &discovery.AuthInfo{
ClientIdentity: []byte{1, 2, 3},
ClientTlsCertHash: []byte{1, 2, 3},
},
}
b, _ = proto.Marshal(req)
res, err = validateStructure(context.Background(), &discovery.SignedRequest{
Payload: b,
}, true, extractHash)
require.Nil(t, res)
require.Equal(t, "client claimed TLS hash doesn't match computed TLS hash from gRPC stream", err.Error())
// Scenario VIII: request with a client identity and with TLS enabled and the TLS cert hash doesn't match
// the computed one
extractHash = func(ctx context.Context) []byte {
return []byte{1, 2, 3}
}
req = &discovery.Request{
Authentication: &discovery.AuthInfo{
ClientIdentity: []byte{1, 2, 3},
ClientTlsCertHash: []byte{1, 2, 3},
},
}
b, _ = proto.Marshal(req)
res, err = validateStructure(context.Background(), &discovery.SignedRequest{
Payload: b,
}, true, extractHash)
require.NoError(t, err)
require.NotNil(t, res)
}
func TestValidateCCQuery(t *testing.T) {
err := validateCCQuery(&discovery.ChaincodeQuery{
Interests: []*peer.ChaincodeInterest{
nil,
},
})
require.Equal(t, "chaincode interest is nil", err.Error())
}
func wrapResult(responses ...interface{}) *discovery.Response {
response := &discovery.Response{}
for _, res := range responses {
response.Results = append(response.Results, wrapQueryResult(res))
}
return response
}
func wrapQueryResult(res interface{}) *discovery.QueryResult {
if err, isErr := res.(*discovery.Error); isErr {
return &discovery.QueryResult{
Result: &discovery.QueryResult_Error{
Error: err,
},
}
}
if ccRes, isCCQuery := res.(*discovery.ChaincodeQueryResult); isCCQuery {
return &discovery.QueryResult{
Result: &discovery.QueryResult_CcQueryRes{
CcQueryRes: ccRes,
},
}
}
if membRes, isMembershipQuery := res.(*discovery.PeerMembershipResult); isMembershipQuery {
return &discovery.QueryResult{
Result: &discovery.QueryResult_Members{
Members: membRes,
},
}
}
if confRes, isConfQuery := res.(*discovery.ConfigResult); isConfQuery {
return &discovery.QueryResult{
Result: &discovery.QueryResult_ConfigResult{
ConfigResult: confRes,
},
}
}
panic(fmt.Sprint("invalid type:", reflect.TypeOf(res)))
}
func toSignedRequest(req *discovery.Request) *discovery.SignedRequest {
b, _ := proto.Marshal(req)
return &discovery.SignedRequest{
Payload: b,
}
}
type mockSupport struct {
mock.Mock
}
func (ms *mockSupport) ConfigSequence(channel string) uint64 {
return 0
}
func (ms *mockSupport) IdentityInfo() api.PeerIdentitySet {
return ms.Called().Get(0).(api.PeerIdentitySet)
}
func (ms *mockSupport) ChannelExists(channel string) bool {
return ms.Called(channel).Get(0).(bool)
}
func (ms *mockSupport) PeersOfChannel(channel gcommon.ChannelID) gdisc.Members {
panic("not implemented")
}
func (ms *mockSupport) Peers() gdisc.Members {
return ms.Called().Get(0).(gdisc.Members)
}
func (ms *mockSupport) PeersForEndorsement(channel gcommon.ChannelID, interest *peer.ChaincodeInterest) (*discovery.EndorsementDescriptor, error) {
cc := interest.Chaincodes[0].Name
args := ms.Called(cc)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*discovery.EndorsementDescriptor), args.Error(1)
}
func (ms *mockSupport) PeersAuthorizedByCriteria(chainID gcommon.ChannelID, interest *peer.ChaincodeInterest) (gdisc.Members, error) {
args := ms.Called(chainID)
if args.Error(1) != nil {
return nil, args.Error(1)
}
return args.Get(0).(gdisc.Members), args.Error(1)
}
func (*mockSupport) Chaincodes(id gcommon.ChannelID) []*gossip.Chaincode {
panic("implement me")
}
func (ms *mockSupport) EligibleForService(channel string, data protoutil.SignedData) error {
return ms.Called(channel, data).Error(0)
}
func (ms *mockSupport) Config(channel string) (*discovery.ConfigResult, error) {
args := ms.Called(channel)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*discovery.ConfigResult), args.Error(1)
}
func idInfo(id int, org string) api.PeerIdentityInfo {
endpoint := fmt.Sprintf("p%d", id)
return api.PeerIdentityInfo{
PKIId: gcommon.PKIidType(endpoint),
Organization: api.OrgIdentityType(org),
Identity: api.PeerIdentityType(endpoint),
}
}
func stateInfoMsg(id int) gdisc.NetworkMember {
endpoint := fmt.Sprintf("p%d", id)
pkiID := gcommon.PKIidType(endpoint)
si := &gossip.StateInfo{
PkiId: pkiID,
}
gm := &gossip.GossipMessage{
Content: &gossip.GossipMessage_StateInfo{
StateInfo: si,
},
}
sm, _ := protoext.NoopSign(gm)
return gdisc.NetworkMember{
PKIid: pkiID,
Envelope: sm.Envelope,
}
}
func aliveMsg(id int) gdisc.NetworkMember {
endpoint := fmt.Sprintf("p%d", id)
pkiID := gcommon.PKIidType(endpoint)
am := &gossip.AliveMessage{
Membership: &gossip.Member{
PkiId: pkiID,
Endpoint: endpoint,
},
}
gm := &gossip.GossipMessage{
Content: &gossip.GossipMessage_AliveMsg{
AliveMsg: am,
},
}
sm, _ := protoext.NoopSign(gm)
return gdisc.NetworkMember{
PKIid: pkiID,
Endpoint: endpoint,
Envelope: sm.Envelope,
}
}
type peers []*discovery.Peer
func (ps peers) exists(p *discovery.Peer) error {
var found bool
for _, q := range ps {
if reflect.DeepEqual(*p, *q) {
found = true
break
}
}
if !found {
return fmt.Errorf("%v wasn't found in %v", ps, p)
}
return nil
}
func (ps peers) compare(otherPeers peers) error {
if len(ps) != len(otherPeers) {
return fmt.Errorf("size mismatch: %d, %d", len(ps), len(otherPeers))
}
for _, p := range otherPeers {
if err := ps.exists(p); err != nil {
return err
}
}
for _, p := range ps {
if err := otherPeers.exists(p); err != nil {
return err
}
}
return nil
}