475 lines
14 KiB
Go
475 lines
14 KiB
Go
/*
|
|
Copyright 2021 IBM All Rights Reserved.
|
|
|
|
SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
package gateway
|
|
|
|
import (
|
|
"io"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/golang/protobuf/proto"
|
|
"github.com/golang/protobuf/ptypes/timestamp"
|
|
cp "github.com/hyperledger/fabric-protos-go/common"
|
|
pb "github.com/hyperledger/fabric-protos-go/gateway"
|
|
ab "github.com/hyperledger/fabric-protos-go/orderer"
|
|
"github.com/hyperledger/fabric-protos-go/peer"
|
|
"github.com/hyperledger/fabric/protoutil"
|
|
"github.com/pkg/errors"
|
|
"github.com/stretchr/testify/require"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
)
|
|
|
|
func TestChaincodeEvents(t *testing.T) {
|
|
now := time.Now()
|
|
lastTransactionID := "LAST_TX_ID"
|
|
|
|
newChaincodeEvent := func(chaincodeName string, transactionID string) *peer.ChaincodeEvent {
|
|
return &peer.ChaincodeEvent{
|
|
ChaincodeId: chaincodeName,
|
|
TxId: transactionID,
|
|
EventName: "EVENT_NAME",
|
|
Payload: []byte("PAYLOAD"),
|
|
}
|
|
}
|
|
|
|
newTransactionHeader := func(transactionID string) *cp.Header {
|
|
return &cp.Header{
|
|
ChannelHeader: protoutil.MarshalOrPanic(&cp.ChannelHeader{
|
|
Type: int32(cp.HeaderType_ENDORSER_TRANSACTION),
|
|
Timestamp: ×tamp.Timestamp{
|
|
Seconds: now.Unix(),
|
|
Nanos: int32(now.Nanosecond()),
|
|
},
|
|
TxId: transactionID,
|
|
}),
|
|
}
|
|
}
|
|
|
|
newTransactionEnvelope := func(event *peer.ChaincodeEvent) *cp.Envelope {
|
|
return &cp.Envelope{
|
|
Payload: protoutil.MarshalOrPanic(&cp.Payload{
|
|
Header: newTransactionHeader(event.GetTxId()),
|
|
Data: protoutil.MarshalOrPanic(&peer.Transaction{
|
|
Actions: []*peer.TransactionAction{
|
|
{
|
|
Payload: protoutil.MarshalOrPanic(&peer.ChaincodeActionPayload{
|
|
Action: &peer.ChaincodeEndorsedAction{
|
|
ProposalResponsePayload: protoutil.MarshalOrPanic(&peer.ProposalResponsePayload{
|
|
Extension: protoutil.MarshalOrPanic(&peer.ChaincodeAction{
|
|
Events: protoutil.MarshalOrPanic(event),
|
|
}),
|
|
}),
|
|
},
|
|
}),
|
|
},
|
|
},
|
|
}),
|
|
}),
|
|
}
|
|
}
|
|
|
|
newBlock := func(number uint64) *cp.Block {
|
|
return &cp.Block{
|
|
Header: &cp.BlockHeader{
|
|
Number: number,
|
|
},
|
|
Metadata: &cp.BlockMetadata{
|
|
Metadata: make([][]byte, 5),
|
|
},
|
|
Data: &cp.BlockData{
|
|
Data: [][]byte{},
|
|
},
|
|
}
|
|
}
|
|
|
|
addTransaction := func(block *cp.Block, transaction *cp.Envelope, status peer.TxValidationCode) {
|
|
metadata := block.GetMetadata().GetMetadata()
|
|
metadata[cp.BlockMetadataIndex_TRANSACTIONS_FILTER] = append(metadata[cp.BlockMetadataIndex_TRANSACTIONS_FILTER], byte(status))
|
|
|
|
blockData := block.GetData()
|
|
blockData.Data = append(blockData.Data, protoutil.MarshalOrPanic(transaction))
|
|
}
|
|
|
|
matchEvent := newChaincodeEvent(testChaincode, "EXPECTED_TX_ID")
|
|
wrongChaincodeEvent := newChaincodeEvent("WRONG_CHAINCODE", "WRONG__TX_ID")
|
|
oldTransactionEvent := newChaincodeEvent(testChaincode, "OLD_TX_ID")
|
|
lastTransactionEvent := newChaincodeEvent(testChaincode, lastTransactionID)
|
|
lastTransactionWrongChaincodeEvent := newChaincodeEvent("WRONG_CHAINCODE", lastTransactionID)
|
|
|
|
configTxEnvelope := &cp.Envelope{
|
|
Payload: protoutil.MarshalOrPanic(&cp.Payload{
|
|
Header: &cp.Header{
|
|
ChannelHeader: protoutil.MarshalOrPanic(&cp.ChannelHeader{
|
|
Type: int32(cp.HeaderType_CONFIG_UPDATE),
|
|
}),
|
|
},
|
|
}),
|
|
}
|
|
|
|
noMatchingEventsBlock := newBlock(100)
|
|
addTransaction(noMatchingEventsBlock, newTransactionEnvelope(wrongChaincodeEvent), peer.TxValidationCode_VALID)
|
|
|
|
matchingEventBlock := newBlock(101)
|
|
addTransaction(matchingEventBlock, configTxEnvelope, peer.TxValidationCode_VALID)
|
|
addTransaction(matchingEventBlock, newTransactionEnvelope(wrongChaincodeEvent), peer.TxValidationCode_VALID)
|
|
addTransaction(matchingEventBlock, newTransactionEnvelope(matchEvent), peer.TxValidationCode_VALID)
|
|
|
|
partReadBlock := newBlock(200)
|
|
addTransaction(partReadBlock, newTransactionEnvelope(oldTransactionEvent), peer.TxValidationCode_VALID)
|
|
addTransaction(partReadBlock, newTransactionEnvelope(lastTransactionEvent), peer.TxValidationCode_VALID)
|
|
addTransaction(partReadBlock, newTransactionEnvelope(matchEvent), peer.TxValidationCode_VALID)
|
|
|
|
differentChaincodePartReadBlock := newBlock(300)
|
|
addTransaction(differentChaincodePartReadBlock, newTransactionEnvelope(oldTransactionEvent), peer.TxValidationCode_VALID)
|
|
addTransaction(differentChaincodePartReadBlock, newTransactionEnvelope(lastTransactionWrongChaincodeEvent), peer.TxValidationCode_VALID)
|
|
addTransaction(differentChaincodePartReadBlock, newTransactionEnvelope(matchEvent), peer.TxValidationCode_VALID)
|
|
|
|
tests := []testDef{
|
|
{
|
|
name: "error reading events",
|
|
eventErr: errors.New("EVENT_ERROR"),
|
|
errCode: codes.Aborted,
|
|
errString: "EVENT_ERROR",
|
|
},
|
|
{
|
|
name: "returns chaincode events",
|
|
blocks: []*cp.Block{
|
|
matchingEventBlock,
|
|
},
|
|
expectedResponses: []proto.Message{
|
|
&pb.ChaincodeEventsResponse{
|
|
BlockNumber: matchingEventBlock.GetHeader().GetNumber(),
|
|
Events: []*peer.ChaincodeEvent{
|
|
{
|
|
ChaincodeId: testChaincode,
|
|
TxId: matchEvent.GetTxId(),
|
|
EventName: matchEvent.GetEventName(),
|
|
Payload: matchEvent.GetPayload(),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "skips blocks containing only non-matching chaincode events",
|
|
blocks: []*cp.Block{
|
|
noMatchingEventsBlock,
|
|
matchingEventBlock,
|
|
},
|
|
expectedResponses: []proto.Message{
|
|
&pb.ChaincodeEventsResponse{
|
|
BlockNumber: matchingEventBlock.GetHeader().GetNumber(),
|
|
Events: []*peer.ChaincodeEvent{
|
|
{
|
|
ChaincodeId: testChaincode,
|
|
TxId: matchEvent.GetTxId(),
|
|
EventName: matchEvent.GetEventName(),
|
|
Payload: matchEvent.GetPayload(),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "skips previously seen transactions",
|
|
blocks: []*cp.Block{
|
|
partReadBlock,
|
|
},
|
|
afterTxID: lastTransactionID,
|
|
expectedResponses: []proto.Message{
|
|
&pb.ChaincodeEventsResponse{
|
|
BlockNumber: partReadBlock.GetHeader().GetNumber(),
|
|
Events: []*peer.ChaincodeEvent{
|
|
{
|
|
ChaincodeId: testChaincode,
|
|
TxId: matchEvent.GetTxId(),
|
|
EventName: matchEvent.GetEventName(),
|
|
Payload: matchEvent.GetPayload(),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "identifies specified transaction if from different chaincode",
|
|
blocks: []*cp.Block{
|
|
differentChaincodePartReadBlock,
|
|
},
|
|
afterTxID: lastTransactionID,
|
|
expectedResponses: []proto.Message{
|
|
&pb.ChaincodeEventsResponse{
|
|
BlockNumber: differentChaincodePartReadBlock.GetHeader().GetNumber(),
|
|
Events: []*peer.ChaincodeEvent{
|
|
{
|
|
ChaincodeId: testChaincode,
|
|
TxId: matchEvent.GetTxId(),
|
|
EventName: matchEvent.GetEventName(),
|
|
Payload: matchEvent.GetPayload(),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "identifies specified transaction if not in first read block",
|
|
blocks: []*cp.Block{
|
|
noMatchingEventsBlock,
|
|
partReadBlock,
|
|
},
|
|
afterTxID: lastTransactionID,
|
|
expectedResponses: []proto.Message{
|
|
&pb.ChaincodeEventsResponse{
|
|
BlockNumber: partReadBlock.GetHeader().GetNumber(),
|
|
Events: []*peer.ChaincodeEvent{
|
|
{
|
|
ChaincodeId: testChaincode,
|
|
TxId: matchEvent.GetTxId(),
|
|
EventName: matchEvent.GetEventName(),
|
|
Payload: matchEvent.GetPayload(),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "passes channel name to ledger provider",
|
|
postTest: func(t *testing.T, test *preparedTest) {
|
|
require.Equal(t, 1, test.ledgerProvider.LedgerCallCount())
|
|
require.Equal(t, testChannel, test.ledgerProvider.LedgerArgsForCall(0))
|
|
},
|
|
},
|
|
{
|
|
name: "returns error obtaining ledger",
|
|
blocks: []*cp.Block{
|
|
matchingEventBlock,
|
|
},
|
|
errCode: codes.NotFound,
|
|
errString: "LEDGER_PROVIDER_ERROR",
|
|
postSetup: func(t *testing.T, test *preparedTest) {
|
|
test.ledgerProvider.LedgerReturns(nil, errors.New("LEDGER_PROVIDER_ERROR"))
|
|
},
|
|
},
|
|
{
|
|
name: "returns error obtaining ledger height",
|
|
blocks: []*cp.Block{
|
|
matchingEventBlock,
|
|
},
|
|
errCode: codes.Aborted,
|
|
errString: "LEDGER_INFO_ERROR",
|
|
postSetup: func(t *testing.T, test *preparedTest) {
|
|
test.ledger.GetBlockchainInfoReturns(nil, errors.New("LEDGER_INFO_ERROR"))
|
|
},
|
|
},
|
|
{
|
|
name: "uses block height as start block if next commit is specified as start position",
|
|
blocks: []*cp.Block{
|
|
matchingEventBlock,
|
|
},
|
|
localLedgerHeight: 101,
|
|
startPosition: &ab.SeekPosition{
|
|
Type: &ab.SeekPosition_NextCommit{
|
|
NextCommit: &ab.SeekNextCommit{},
|
|
},
|
|
},
|
|
postTest: func(t *testing.T, test *preparedTest) {
|
|
require.Equal(t, 1, test.ledger.GetBlocksIteratorCallCount())
|
|
require.EqualValues(t, 101, test.ledger.GetBlocksIteratorArgsForCall(0))
|
|
},
|
|
},
|
|
{
|
|
name: "uses specified start block",
|
|
blocks: []*cp.Block{
|
|
matchingEventBlock,
|
|
},
|
|
localLedgerHeight: 101,
|
|
startPosition: &ab.SeekPosition{
|
|
Type: &ab.SeekPosition_Specified{
|
|
Specified: &ab.SeekSpecified{
|
|
Number: 99,
|
|
},
|
|
},
|
|
},
|
|
postTest: func(t *testing.T, test *preparedTest) {
|
|
require.Equal(t, 1, test.ledger.GetBlocksIteratorCallCount())
|
|
require.EqualValues(t, 99, test.ledger.GetBlocksIteratorArgsForCall(0))
|
|
},
|
|
},
|
|
{
|
|
name: "defaults to next commit if start position not specified",
|
|
blocks: []*cp.Block{
|
|
matchingEventBlock,
|
|
},
|
|
localLedgerHeight: 101,
|
|
postTest: func(t *testing.T, test *preparedTest) {
|
|
require.Equal(t, 1, test.ledger.GetBlocksIteratorCallCount())
|
|
require.EqualValues(t, 101, test.ledger.GetBlocksIteratorArgsForCall(0))
|
|
},
|
|
},
|
|
{
|
|
name: "uses block containing specified transaction instead of start block",
|
|
blocks: []*cp.Block{
|
|
matchingEventBlock,
|
|
},
|
|
localLedgerHeight: 101,
|
|
postSetup: func(t *testing.T, test *preparedTest) {
|
|
block := &cp.Block{
|
|
Header: &cp.BlockHeader{
|
|
Number: 99,
|
|
},
|
|
}
|
|
test.ledger.GetBlockByTxIDReturns(block, nil)
|
|
},
|
|
afterTxID: "TX_ID",
|
|
startPosition: &ab.SeekPosition{
|
|
Type: &ab.SeekPosition_Specified{
|
|
Specified: &ab.SeekSpecified{
|
|
Number: 1,
|
|
},
|
|
},
|
|
},
|
|
postTest: func(t *testing.T, test *preparedTest) {
|
|
require.Equal(t, 1, test.ledger.GetBlocksIteratorCallCount())
|
|
require.EqualValues(t, 99, test.ledger.GetBlocksIteratorArgsForCall(0))
|
|
require.Equal(t, 1, test.ledger.GetBlockByTxIDCallCount())
|
|
require.Equal(t, "TX_ID", test.ledger.GetBlockByTxIDArgsForCall(0))
|
|
},
|
|
},
|
|
{
|
|
name: "uses start block if specified transaction not found",
|
|
blocks: []*cp.Block{
|
|
matchingEventBlock,
|
|
},
|
|
localLedgerHeight: 101,
|
|
postSetup: func(t *testing.T, test *preparedTest) {
|
|
test.ledger.GetBlockByTxIDReturns(nil, errors.New("NOT_FOUND"))
|
|
},
|
|
afterTxID: "TX_ID",
|
|
startPosition: &ab.SeekPosition{
|
|
Type: &ab.SeekPosition_Specified{
|
|
Specified: &ab.SeekSpecified{
|
|
Number: 1,
|
|
},
|
|
},
|
|
},
|
|
postTest: func(t *testing.T, test *preparedTest) {
|
|
require.Equal(t, 1, test.ledger.GetBlocksIteratorCallCount())
|
|
require.EqualValues(t, 1, test.ledger.GetBlocksIteratorArgsForCall(0))
|
|
},
|
|
},
|
|
{
|
|
name: "returns error for unsupported start position type",
|
|
blocks: []*cp.Block{
|
|
matchingEventBlock,
|
|
},
|
|
startPosition: &ab.SeekPosition{
|
|
Type: &ab.SeekPosition_Oldest{
|
|
Oldest: &ab.SeekOldest{},
|
|
},
|
|
},
|
|
errCode: codes.InvalidArgument,
|
|
errString: "invalid start position type: *orderer.SeekPosition_Oldest",
|
|
},
|
|
{
|
|
name: "returns error obtaining ledger iterator",
|
|
blocks: []*cp.Block{
|
|
matchingEventBlock,
|
|
},
|
|
errCode: codes.Aborted,
|
|
errString: "LEDGER_ITERATOR_ERROR",
|
|
postSetup: func(t *testing.T, test *preparedTest) {
|
|
test.ledger.GetBlocksIteratorReturns(nil, errors.New("LEDGER_ITERATOR_ERROR"))
|
|
},
|
|
},
|
|
{
|
|
name: "returns canceled status error when client closes stream",
|
|
blocks: []*cp.Block{
|
|
matchingEventBlock,
|
|
},
|
|
errCode: codes.Canceled,
|
|
postSetup: func(t *testing.T, test *preparedTest) {
|
|
test.eventsServer.SendReturns(io.EOF)
|
|
},
|
|
},
|
|
{
|
|
name: "returns status error from send to client",
|
|
blocks: []*cp.Block{
|
|
matchingEventBlock,
|
|
},
|
|
errCode: codes.Aborted,
|
|
errString: "SEND_ERROR",
|
|
postSetup: func(t *testing.T, test *preparedTest) {
|
|
test.eventsServer.SendReturns(status.Error(codes.Aborted, "SEND_ERROR"))
|
|
},
|
|
},
|
|
{
|
|
name: "failed policy or signature check",
|
|
policyErr: errors.New("POLICY_ERROR"),
|
|
errCode: codes.PermissionDenied,
|
|
errString: "POLICY_ERROR",
|
|
},
|
|
{
|
|
name: "passes channel name to policy checker",
|
|
postTest: func(t *testing.T, test *preparedTest) {
|
|
require.Equal(t, 1, test.policy.CheckACLCallCount())
|
|
_, channelName, _ := test.policy.CheckACLArgsForCall(0)
|
|
require.Equal(t, testChannel, channelName)
|
|
},
|
|
},
|
|
{
|
|
name: "passes identity to policy checker",
|
|
identity: []byte("IDENTITY"),
|
|
postTest: func(t *testing.T, test *preparedTest) {
|
|
require.Equal(t, 1, test.policy.CheckACLCallCount())
|
|
_, _, data := test.policy.CheckACLArgsForCall(0)
|
|
require.IsType(t, &protoutil.SignedData{}, data)
|
|
signedData := data.(*protoutil.SignedData)
|
|
require.Equal(t, []byte("IDENTITY"), signedData.Identity)
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
test := prepareTest(t, &tt)
|
|
|
|
request := &pb.ChaincodeEventsRequest{
|
|
ChannelId: testChannel,
|
|
Identity: tt.identity,
|
|
ChaincodeId: testChaincode,
|
|
}
|
|
if tt.startPosition != nil {
|
|
request.StartPosition = tt.startPosition
|
|
}
|
|
if len(tt.afterTxID) > 0 {
|
|
request.AfterTransactionId = tt.afterTxID
|
|
}
|
|
requestBytes, err := proto.Marshal(request)
|
|
require.NoError(t, err)
|
|
|
|
signedRequest := &pb.SignedChaincodeEventsRequest{
|
|
Request: requestBytes,
|
|
Signature: []byte{},
|
|
}
|
|
|
|
err = test.server.ChaincodeEvents(signedRequest, test.eventsServer)
|
|
|
|
if checkError(t, &tt, err) {
|
|
return
|
|
}
|
|
|
|
for i, expectedResponse := range tt.expectedResponses {
|
|
actualResponse := test.eventsServer.SendArgsForCall(i)
|
|
require.True(t, proto.Equal(expectedResponse, actualResponse), "response[%d] mismatch: %v", i, actualResponse)
|
|
}
|
|
|
|
if tt.postTest != nil {
|
|
tt.postTest(t, test)
|
|
}
|
|
})
|
|
}
|
|
}
|