3109 lines
103 KiB
Go
3109 lines
103 KiB
Go
/*
|
|
Copyright IBM Corp. All Rights Reserved.
|
|
|
|
SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
package chaincode_test
|
|
|
|
import (
|
|
"io"
|
|
"time"
|
|
|
|
"github.com/golang/protobuf/proto"
|
|
pb "github.com/hyperledger/fabric-protos-go/peer"
|
|
"github.com/hyperledger/fabric/common/metrics/metricsfakes"
|
|
"github.com/hyperledger/fabric/common/util"
|
|
ar "github.com/hyperledger/fabric/core/aclmgmt/resources"
|
|
"github.com/hyperledger/fabric/core/chaincode"
|
|
"github.com/hyperledger/fabric/core/chaincode/fake"
|
|
"github.com/hyperledger/fabric/core/chaincode/mock"
|
|
"github.com/hyperledger/fabric/core/common/ccprovider"
|
|
"github.com/hyperledger/fabric/core/common/sysccprovider"
|
|
"github.com/hyperledger/fabric/core/scc"
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
var _ = Describe("Handler", func() {
|
|
var (
|
|
fakeTransactionRegistry *mock.TransactionRegistry
|
|
fakeContextRegistry *fake.ContextRegistry
|
|
fakeChatStream *mock.ChaincodeStream
|
|
builtinSCCs scc.BuiltinSCCs
|
|
fakeTxSimulator *mock.TxSimulator
|
|
fakeHistoryQueryExecutor *mock.HistoryQueryExecutor
|
|
fakeQueryResponseBuilder *fake.QueryResponseBuilder
|
|
fakeACLProvider *mock.ACLProvider
|
|
fakeInvoker *mock.Invoker
|
|
fakeLedgerGetter *mock.LedgerGetter
|
|
fakeHandlerRegistry *fake.Registry
|
|
fakeApplicationConfigRetriever *fake.ApplicationConfigRetriever
|
|
fakeCollectionStore *mock.CollectionStore
|
|
fakeShimRequestsReceived *metricsfakes.Counter
|
|
fakeShimRequestsCompleted *metricsfakes.Counter
|
|
fakeShimRequestDuration *metricsfakes.Histogram
|
|
fakeExecuteTimeouts *metricsfakes.Counter
|
|
fakeCapabilites *mock.ApplicationCapabilities
|
|
|
|
responseNotifier chan *pb.ChaincodeMessage
|
|
txContext *chaincode.TransactionContext
|
|
|
|
handler *chaincode.Handler
|
|
)
|
|
|
|
BeforeEach(func() {
|
|
fakeTransactionRegistry = &mock.TransactionRegistry{}
|
|
fakeTransactionRegistry.AddReturns(true)
|
|
|
|
fakeChatStream = &mock.ChaincodeStream{}
|
|
|
|
fakeTxSimulator = &mock.TxSimulator{}
|
|
fakeHistoryQueryExecutor = &mock.HistoryQueryExecutor{}
|
|
fakeCollectionStore = &mock.CollectionStore{}
|
|
|
|
responseNotifier = make(chan *pb.ChaincodeMessage, 1)
|
|
txContext = &chaincode.TransactionContext{
|
|
ChannelID: "channel-id",
|
|
NamespaceID: "cc-instance-name",
|
|
TXSimulator: fakeTxSimulator,
|
|
HistoryQueryExecutor: fakeHistoryQueryExecutor,
|
|
ResponseNotifier: responseNotifier,
|
|
CollectionStore: fakeCollectionStore,
|
|
}
|
|
txContext.InitializeCollectionACLCache()
|
|
|
|
fakeACLProvider = &mock.ACLProvider{}
|
|
fakeInvoker = &mock.Invoker{}
|
|
fakeLedgerGetter = &mock.LedgerGetter{}
|
|
fakeQueryResponseBuilder = &fake.QueryResponseBuilder{}
|
|
fakeHandlerRegistry = &fake.Registry{}
|
|
|
|
fakeContextRegistry = &fake.ContextRegistry{}
|
|
fakeContextRegistry.GetReturns(txContext)
|
|
fakeContextRegistry.CreateReturns(txContext, nil)
|
|
|
|
fakeApplicationConfig := &mock.ApplicationConfig{}
|
|
fakeCapabilites = &mock.ApplicationCapabilities{}
|
|
fakeCapabilites.KeyLevelEndorsementReturns(true)
|
|
fakeCapabilites.PurgePvtDataReturns(true)
|
|
fakeApplicationConfig.CapabilitiesReturns(fakeCapabilites)
|
|
fakeApplicationConfigRetriever = &fake.ApplicationConfigRetriever{}
|
|
fakeApplicationConfigRetriever.GetApplicationConfigReturns(fakeApplicationConfig, true)
|
|
|
|
fakeShimRequestsReceived = &metricsfakes.Counter{}
|
|
fakeShimRequestsReceived.WithReturns(fakeShimRequestsReceived)
|
|
fakeShimRequestsCompleted = &metricsfakes.Counter{}
|
|
fakeShimRequestsCompleted.WithReturns(fakeShimRequestsCompleted)
|
|
fakeShimRequestDuration = &metricsfakes.Histogram{}
|
|
fakeShimRequestDuration.WithReturns(fakeShimRequestDuration)
|
|
fakeExecuteTimeouts = &metricsfakes.Counter{}
|
|
fakeExecuteTimeouts.WithReturns(fakeExecuteTimeouts)
|
|
|
|
builtinSCCs = map[string]struct{}{}
|
|
|
|
chaincodeMetrics := &chaincode.HandlerMetrics{
|
|
ShimRequestsReceived: fakeShimRequestsReceived,
|
|
ShimRequestsCompleted: fakeShimRequestsCompleted,
|
|
ShimRequestDuration: fakeShimRequestDuration,
|
|
ExecuteTimeouts: fakeExecuteTimeouts,
|
|
}
|
|
|
|
handler = &chaincode.Handler{
|
|
ACLProvider: fakeACLProvider,
|
|
ActiveTransactions: fakeTransactionRegistry,
|
|
Invoker: fakeInvoker,
|
|
LedgerGetter: fakeLedgerGetter,
|
|
QueryResponseBuilder: fakeQueryResponseBuilder,
|
|
Registry: fakeHandlerRegistry,
|
|
BuiltinSCCs: builtinSCCs,
|
|
TXContexts: fakeContextRegistry,
|
|
UUIDGenerator: chaincode.UUIDGeneratorFunc(func() string {
|
|
return "generated-query-id"
|
|
}),
|
|
AppConfig: fakeApplicationConfigRetriever,
|
|
Metrics: chaincodeMetrics,
|
|
}
|
|
chaincode.SetHandlerChatStream(handler, fakeChatStream)
|
|
chaincode.SetHandlerChaincodeID(handler, "test-handler-name:1.0")
|
|
})
|
|
|
|
Describe("HandleTransaction", func() {
|
|
var (
|
|
incomingMessage *pb.ChaincodeMessage
|
|
fakeMessageHandler *fake.MessageHandler
|
|
expectedResponse *pb.ChaincodeMessage
|
|
)
|
|
|
|
BeforeEach(func() {
|
|
incomingMessage = &pb.ChaincodeMessage{
|
|
Type: pb.ChaincodeMessage_GET_STATE,
|
|
Txid: "tx-id",
|
|
ChannelId: "channel-id",
|
|
}
|
|
|
|
expectedResponse = &pb.ChaincodeMessage{
|
|
Type: pb.ChaincodeMessage_UNDEFINED,
|
|
Payload: []byte("handler-response-payload"),
|
|
Txid: "response-tx-id",
|
|
ChannelId: "response-channel-id",
|
|
}
|
|
fakeMessageHandler = &fake.MessageHandler{}
|
|
fakeMessageHandler.HandleReturns(expectedResponse, nil)
|
|
})
|
|
|
|
It("registers the transaction ID from the message", func() {
|
|
handler.HandleTransaction(incomingMessage, fakeMessageHandler.Handle)
|
|
|
|
Expect(fakeTransactionRegistry.AddCallCount()).To(Equal(1))
|
|
channelID, transactionID := fakeTransactionRegistry.AddArgsForCall(0)
|
|
Expect(channelID).To(Equal("channel-id"))
|
|
Expect(transactionID).To(Equal("tx-id"))
|
|
})
|
|
|
|
It("ensures there is a valid transaction simulator available in the context", func() {
|
|
handler.HandleTransaction(incomingMessage, fakeMessageHandler.Handle)
|
|
|
|
Expect(fakeContextRegistry.GetCallCount()).To(Equal(1))
|
|
channelID, txID := fakeContextRegistry.GetArgsForCall(0)
|
|
Expect(channelID).To(Equal("channel-id"))
|
|
Expect(txID).To(Equal("tx-id"))
|
|
})
|
|
|
|
It("calls the delegate with the correct arguments", func() {
|
|
handler.HandleTransaction(incomingMessage, fakeMessageHandler.Handle)
|
|
|
|
Expect(fakeMessageHandler.HandleCallCount()).To(Equal(1))
|
|
msg, ctx := fakeMessageHandler.HandleArgsForCall(0)
|
|
Expect(msg).To(Equal(incomingMessage))
|
|
Expect(ctx).To(Equal(txContext))
|
|
})
|
|
|
|
It("sends the response message returned by the delegate", func() {
|
|
handler.HandleTransaction(incomingMessage, fakeMessageHandler.Handle)
|
|
|
|
Eventually(fakeChatStream.SendCallCount).Should(Equal(1))
|
|
msg := fakeChatStream.SendArgsForCall(0)
|
|
Expect(msg).To(Equal(expectedResponse))
|
|
})
|
|
|
|
It("deregisters the transaction ID before sending the response", func() {
|
|
fakeTransactionRegistry.RemoveStub = func(channelID, txID string) {
|
|
Consistently(fakeChatStream.SendCallCount).Should(Equal(0))
|
|
}
|
|
|
|
handler.HandleTransaction(incomingMessage, fakeMessageHandler.Handle)
|
|
|
|
Expect(fakeTransactionRegistry.RemoveCallCount()).To(Equal(1))
|
|
channelID, transactionID := fakeTransactionRegistry.RemoveArgsForCall(0)
|
|
Expect(channelID).To(Equal("channel-id"))
|
|
Expect(transactionID).To(Equal("tx-id"))
|
|
})
|
|
|
|
It("records shim requests received before requests completed", func() {
|
|
fakeShimRequestsReceived.AddStub = func(delta float64) {
|
|
defer GinkgoRecover()
|
|
Expect(fakeShimRequestsCompleted.AddCallCount()).To(Equal(0))
|
|
}
|
|
|
|
handler.HandleTransaction(incomingMessage, fakeMessageHandler.Handle)
|
|
Eventually(fakeChatStream.SendCallCount).Should(Equal(1))
|
|
msg := fakeChatStream.SendArgsForCall(0)
|
|
Expect(msg).To(Equal(expectedResponse))
|
|
|
|
Expect(fakeShimRequestsReceived.WithCallCount()).To(Equal(1))
|
|
labelValues := fakeShimRequestsReceived.WithArgsForCall(0)
|
|
Expect(labelValues).To(Equal([]string{
|
|
"type", "GET_STATE",
|
|
"channel", "channel-id",
|
|
"chaincode", "test-handler-name:1.0",
|
|
}))
|
|
Expect(fakeShimRequestsReceived.AddCallCount()).To(Equal(1))
|
|
Expect(fakeShimRequestsReceived.AddArgsForCall(0)).To(BeNumerically("~", 1.0))
|
|
})
|
|
|
|
It("records transactions completed after transactions received", func() {
|
|
fakeShimRequestsCompleted.AddStub = func(delta float64) {
|
|
defer GinkgoRecover()
|
|
Expect(fakeShimRequestsReceived.AddCallCount()).To(Equal(1))
|
|
}
|
|
|
|
handler.HandleTransaction(incomingMessage, fakeMessageHandler.Handle)
|
|
Eventually(fakeChatStream.SendCallCount).Should(Equal(1))
|
|
|
|
Expect(fakeShimRequestsCompleted.WithCallCount()).To(Equal(1))
|
|
labelValues := fakeShimRequestsCompleted.WithArgsForCall(0)
|
|
Expect(labelValues).To(Equal([]string{
|
|
"type", "GET_STATE",
|
|
"channel", "channel-id",
|
|
"chaincode", "test-handler-name:1.0",
|
|
"success", "true",
|
|
}))
|
|
Expect(fakeShimRequestsCompleted.AddCallCount()).To(Equal(1))
|
|
Expect(fakeShimRequestsCompleted.AddArgsForCall(0)).To(BeNumerically("~", 1.0))
|
|
})
|
|
|
|
It("records transactions duration", func() {
|
|
handler.HandleTransaction(incomingMessage, fakeMessageHandler.Handle)
|
|
Eventually(fakeChatStream.SendCallCount).Should(Equal(1))
|
|
|
|
Expect(fakeShimRequestDuration.WithCallCount()).To(Equal(1))
|
|
labelValues := fakeShimRequestDuration.WithArgsForCall(0)
|
|
Expect(labelValues).To(Equal([]string{
|
|
"type", "GET_STATE",
|
|
"channel", "channel-id",
|
|
"chaincode", "test-handler-name:1.0",
|
|
"success", "true",
|
|
}))
|
|
Expect(fakeShimRequestDuration.ObserveArgsForCall(0)).NotTo(BeZero())
|
|
Expect(fakeShimRequestDuration.ObserveArgsForCall(0)).To(BeNumerically("<", 1.0))
|
|
})
|
|
|
|
Context("when the transaction returns an error", func() {
|
|
BeforeEach(func() {
|
|
fakeMessageHandler.HandleReturns(nil, errors.New("I am a total failure"))
|
|
})
|
|
|
|
It("records metrics with success=false", func() {
|
|
handler.HandleTransaction(incomingMessage, fakeMessageHandler.Handle)
|
|
Eventually(fakeChatStream.SendCallCount).Should(Equal(1))
|
|
|
|
Expect(fakeShimRequestsCompleted.WithCallCount()).To(Equal(1))
|
|
labelValues := fakeShimRequestsCompleted.WithArgsForCall(0)
|
|
Expect(labelValues).To(Equal([]string{
|
|
"type", "GET_STATE",
|
|
"channel", "channel-id",
|
|
"chaincode", "test-handler-name:1.0",
|
|
"success", "false",
|
|
}))
|
|
Expect(fakeShimRequestsCompleted.AddCallCount()).To(Equal(1))
|
|
Expect(fakeShimRequestsCompleted.AddArgsForCall(0)).To(BeNumerically("~", 1.0))
|
|
|
|
Expect(fakeShimRequestDuration.WithCallCount()).To(Equal(1))
|
|
labelValues = fakeShimRequestDuration.WithArgsForCall(0)
|
|
Expect(labelValues).To(Equal([]string{
|
|
"type", "GET_STATE",
|
|
"channel", "channel-id",
|
|
"chaincode", "test-handler-name:1.0",
|
|
"success", "false",
|
|
}))
|
|
Expect(fakeShimRequestDuration.ObserveArgsForCall(0)).NotTo(BeZero())
|
|
Expect(fakeShimRequestDuration.ObserveArgsForCall(0)).To(BeNumerically("<", 1.0))
|
|
})
|
|
})
|
|
|
|
Context("when the transaction ID has already been registered", func() {
|
|
BeforeEach(func() {
|
|
fakeTransactionRegistry.AddReturns(false)
|
|
})
|
|
|
|
It("returns without sending a response", func() {
|
|
handler.HandleTransaction(incomingMessage, fakeMessageHandler.Handle)
|
|
Consistently(fakeChatStream.SendCallCount).Should(Equal(0))
|
|
})
|
|
|
|
It("does not call the delegate", func() {
|
|
handler.HandleTransaction(incomingMessage, fakeMessageHandler.Handle)
|
|
Expect(fakeMessageHandler.HandleCallCount()).To(Equal(0))
|
|
})
|
|
|
|
It("does not attempt to deregister the transaction ID", func() {
|
|
handler.HandleTransaction(incomingMessage, fakeMessageHandler.Handle)
|
|
Expect(fakeTransactionRegistry.RemoveCallCount()).To(Equal(0))
|
|
})
|
|
})
|
|
|
|
Context("when the transaction context does not exist", func() {
|
|
BeforeEach(func() {
|
|
fakeContextRegistry.GetReturns(nil)
|
|
})
|
|
|
|
It("sends an error message", func() {
|
|
handler.HandleTransaction(incomingMessage, fakeMessageHandler.Handle)
|
|
|
|
Eventually(fakeChatStream.SendCallCount).Should(Equal(1))
|
|
msg := fakeChatStream.SendArgsForCall(0)
|
|
Expect(msg).To(Equal(&pb.ChaincodeMessage{
|
|
Type: pb.ChaincodeMessage_ERROR,
|
|
Payload: []byte("GET_STATE failed: transaction ID: tx-id: no ledger context"),
|
|
Txid: "tx-id",
|
|
ChannelId: "channel-id",
|
|
}))
|
|
})
|
|
|
|
It("deregisters the message transaction ID", func() {
|
|
handler.HandleTransaction(incomingMessage, fakeMessageHandler.Handle)
|
|
|
|
Expect(fakeTransactionRegistry.RemoveCallCount()).To(Equal(1))
|
|
channelID, transactionID := fakeTransactionRegistry.RemoveArgsForCall(0)
|
|
Expect(channelID).To(Equal("channel-id"))
|
|
Expect(transactionID).To(Equal("tx-id"))
|
|
})
|
|
})
|
|
|
|
Context("when the transaction context is missing a transaction simulator", func() {
|
|
BeforeEach(func() {
|
|
txContext.TXSimulator = nil
|
|
})
|
|
|
|
It("sends an error response", func() {
|
|
handler.HandleTransaction(incomingMessage, fakeMessageHandler.Handle)
|
|
|
|
Eventually(fakeChatStream.SendCallCount).Should(Equal(1))
|
|
msg := fakeChatStream.SendArgsForCall(0)
|
|
Expect(msg).To(Equal(&pb.ChaincodeMessage{
|
|
Type: pb.ChaincodeMessage_ERROR,
|
|
Payload: []byte("GET_STATE failed: transaction ID: tx-id: no ledger context"),
|
|
Txid: "tx-id",
|
|
ChannelId: "channel-id",
|
|
}))
|
|
})
|
|
|
|
It("deregisters the message transaction ID", func() {
|
|
handler.HandleTransaction(incomingMessage, fakeMessageHandler.Handle)
|
|
|
|
Expect(fakeTransactionRegistry.RemoveCallCount()).To(Equal(1))
|
|
channelID, transactionID := fakeTransactionRegistry.RemoveArgsForCall(0)
|
|
Expect(channelID).To(Equal("channel-id"))
|
|
Expect(transactionID).To(Equal("tx-id"))
|
|
})
|
|
})
|
|
|
|
Context("when the incoming message is INVOKE_CHAINCODE", func() {
|
|
var chaincodeSpec *pb.ChaincodeSpec
|
|
|
|
BeforeEach(func() {
|
|
chaincodeSpec = &pb.ChaincodeSpec{
|
|
Type: pb.ChaincodeSpec_GOLANG,
|
|
ChaincodeId: &pb.ChaincodeID{
|
|
Name: "target-chaincode-name",
|
|
Version: "target-chaincode-version",
|
|
},
|
|
Input: &pb.ChaincodeInput{
|
|
Args: util.ToChaincodeArgs("command", "arg"),
|
|
},
|
|
}
|
|
payloadBytes, err := proto.Marshal(chaincodeSpec)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
incomingMessage.Type = pb.ChaincodeMessage_INVOKE_CHAINCODE
|
|
incomingMessage.Payload = payloadBytes
|
|
})
|
|
|
|
It("validates the transaction context", func() {
|
|
txContext.TXSimulator = nil
|
|
handler.HandleTransaction(incomingMessage, fakeMessageHandler.Handle)
|
|
|
|
Eventually(fakeChatStream.SendCallCount).Should(Equal(1))
|
|
msg := fakeChatStream.SendArgsForCall(0)
|
|
Expect(msg).To(Equal(&pb.ChaincodeMessage{
|
|
Type: pb.ChaincodeMessage_ERROR,
|
|
Payload: []byte("INVOKE_CHAINCODE failed: transaction ID: tx-id: could not get valid transaction"),
|
|
Txid: "tx-id",
|
|
ChannelId: "channel-id",
|
|
}))
|
|
})
|
|
|
|
Context("and the channel ID is not set", func() {
|
|
BeforeEach(func() {
|
|
incomingMessage.ChannelId = ""
|
|
})
|
|
|
|
It("validates the transaction context", func() {
|
|
txContext.TXSimulator = nil
|
|
handler.HandleTransaction(incomingMessage, fakeMessageHandler.Handle)
|
|
|
|
Eventually(fakeChatStream.SendCallCount).Should(Equal(1))
|
|
msg := fakeChatStream.SendArgsForCall(0)
|
|
Expect(msg).To(Equal(&pb.ChaincodeMessage{
|
|
Type: pb.ChaincodeMessage_ERROR,
|
|
Payload: []byte("INVOKE_CHAINCODE failed: transaction ID: tx-id: could not get valid transaction"),
|
|
Txid: "tx-id",
|
|
}))
|
|
})
|
|
|
|
Context("when the target is system chaincode", func() {
|
|
BeforeEach(func() {
|
|
builtinSCCs["target-chaincode-name"] = struct{}{}
|
|
})
|
|
|
|
It("gets the transaction context without validation", func() {
|
|
txContext.TXSimulator = nil
|
|
handler.HandleTransaction(incomingMessage, fakeMessageHandler.Handle)
|
|
|
|
Expect(fakeContextRegistry.GetCallCount()).To(Equal(1))
|
|
Eventually(fakeChatStream.SendCallCount).Should(Equal(1))
|
|
msg := fakeChatStream.SendArgsForCall(0)
|
|
Expect(msg).To(Equal(expectedResponse))
|
|
})
|
|
|
|
Context("and the transaction context is missing", func() {
|
|
It("returns an error", func() {
|
|
fakeContextRegistry.GetReturns(nil)
|
|
handler.HandleTransaction(incomingMessage, fakeMessageHandler.Handle)
|
|
|
|
Eventually(fakeChatStream.SendCallCount).Should(Equal(1))
|
|
msg := fakeChatStream.SendArgsForCall(0)
|
|
Expect(msg).To(Equal(&pb.ChaincodeMessage{
|
|
Type: pb.ChaincodeMessage_ERROR,
|
|
Payload: []byte("INVOKE_CHAINCODE failed: transaction ID: tx-id: failed to get transaction context"),
|
|
Txid: "tx-id",
|
|
}))
|
|
})
|
|
})
|
|
})
|
|
|
|
Context("when the payload fails to unmarshal into a chaincode spec", func() {
|
|
BeforeEach(func() {
|
|
incomingMessage.Payload = []byte("this-is-a-bogus-payload")
|
|
})
|
|
|
|
It("sends an error response", func() {
|
|
handler.HandleTransaction(incomingMessage, fakeMessageHandler.Handle)
|
|
|
|
Eventually(fakeChatStream.SendCallCount).Should(Equal(1))
|
|
msg := fakeChatStream.SendArgsForCall(0)
|
|
Expect(msg.Type).To(Equal(pb.ChaincodeMessage_ERROR))
|
|
Expect(msg.Txid).To(Equal("tx-id"))
|
|
Expect(string(msg.Payload)).To(HavePrefix("INVOKE_CHAINCODE failed: transaction ID: tx-id"))
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
Context("when the delegate returns an error", func() {
|
|
BeforeEach(func() {
|
|
fakeMessageHandler.HandleReturns(nil, errors.New("watermelon-swirl"))
|
|
})
|
|
|
|
It("sends an error response", func() {
|
|
handler.HandleTransaction(incomingMessage, fakeMessageHandler.Handle)
|
|
|
|
Eventually(fakeChatStream.SendCallCount).Should(Equal(1))
|
|
msg := fakeChatStream.SendArgsForCall(0)
|
|
Expect(msg).To(Equal(&pb.ChaincodeMessage{
|
|
Type: pb.ChaincodeMessage_ERROR,
|
|
Payload: []byte("GET_STATE failed: transaction ID: tx-id: watermelon-swirl"),
|
|
Txid: "tx-id",
|
|
ChannelId: "channel-id",
|
|
}))
|
|
})
|
|
})
|
|
})
|
|
|
|
Describe("HandlePutState", func() {
|
|
var incomingMessage *pb.ChaincodeMessage
|
|
var request *pb.PutState
|
|
|
|
BeforeEach(func() {
|
|
request = &pb.PutState{
|
|
Key: "put-state-key",
|
|
Value: []byte("put-state-value"),
|
|
}
|
|
payload, err := proto.Marshal(request)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
incomingMessage = &pb.ChaincodeMessage{
|
|
Type: pb.ChaincodeMessage_PUT_STATE,
|
|
Payload: payload,
|
|
Txid: "tx-id",
|
|
ChannelId: "channel-id",
|
|
}
|
|
})
|
|
|
|
It("returns a response message", func() {
|
|
resp, err := handler.HandlePutState(incomingMessage, txContext)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(resp).To(Equal(&pb.ChaincodeMessage{
|
|
Type: pb.ChaincodeMessage_RESPONSE,
|
|
Txid: "tx-id",
|
|
ChannelId: "channel-id",
|
|
}))
|
|
})
|
|
|
|
Context("when unmarshalling the request fails", func() {
|
|
BeforeEach(func() {
|
|
incomingMessage.Payload = []byte("this-is-a-bogus-payload")
|
|
})
|
|
|
|
It("returns an error", func() {
|
|
_, err := handler.HandlePutState(incomingMessage, txContext)
|
|
Expect(err).To(Not(BeNil()))
|
|
Expect(err.Error()).To(HavePrefix("unmarshal failed:"))
|
|
})
|
|
})
|
|
|
|
Context("when the collection is not provided", func() {
|
|
It("calls SetState on the transaction simulator", func() {
|
|
_, err := handler.HandlePutState(incomingMessage, txContext)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(fakeTxSimulator.SetStateCallCount()).To(Equal(1))
|
|
ccname, key, value := fakeTxSimulator.SetStateArgsForCall(0)
|
|
Expect(ccname).To(Equal("cc-instance-name"))
|
|
Expect(key).To(Equal("put-state-key"))
|
|
Expect(value).To(Equal([]byte("put-state-value")))
|
|
})
|
|
|
|
Context("when SeteState fails", func() {
|
|
BeforeEach(func() {
|
|
fakeTxSimulator.SetStateReturns(errors.New("king-kong"))
|
|
})
|
|
|
|
It("returns an error", func() {
|
|
_, err := handler.HandlePutState(incomingMessage, txContext)
|
|
Expect(err).To(MatchError("king-kong"))
|
|
})
|
|
})
|
|
})
|
|
|
|
Context("when the collection is provided", func() {
|
|
BeforeEach(func() {
|
|
request.Collection = "collection-name"
|
|
payload, err := proto.Marshal(request)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
incomingMessage.Payload = payload
|
|
fakeCollectionStore.RetrieveReadWritePermissionReturns(false, true, nil) // to
|
|
})
|
|
|
|
It("calls SetPrivateData on the transaction simulator", func() {
|
|
_, err := handler.HandlePutState(incomingMessage, txContext)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(fakeTxSimulator.SetPrivateDataCallCount()).To(Equal(1))
|
|
ccname, collection, key, value := fakeTxSimulator.SetPrivateDataArgsForCall(0)
|
|
Expect(ccname).To(Equal("cc-instance-name"))
|
|
Expect(collection).To(Equal("collection-name"))
|
|
Expect(key).To(Equal("put-state-key"))
|
|
Expect(value).To(Equal([]byte("put-state-value")))
|
|
})
|
|
|
|
Context("when SetPrivateData fails due to ledger error", func() {
|
|
BeforeEach(func() {
|
|
fakeTxSimulator.SetPrivateDataReturns(errors.New("godzilla"))
|
|
fakeCollectionStore.RetrieveReadWritePermissionReturns(false, true, nil) // to
|
|
})
|
|
|
|
It("returns an error", func() {
|
|
_, err := handler.HandlePutState(incomingMessage, txContext)
|
|
Expect(err).To(MatchError("godzilla"))
|
|
})
|
|
})
|
|
|
|
Context("when SetPrivateData fails due to Init transaction", func() {
|
|
BeforeEach(func() {
|
|
txContext.IsInitTransaction = true
|
|
})
|
|
|
|
It("returns the error from errorIfInitTransaction", func() {
|
|
_, err := handler.HandlePutState(incomingMessage, txContext)
|
|
Expect(err).To(MatchError("private data APIs are not allowed in chaincode Init()"))
|
|
})
|
|
})
|
|
|
|
Context("when SetPrivateData fails due to no write access permission", func() {
|
|
BeforeEach(func() {
|
|
fakeCollectionStore.RetrieveReadWritePermissionReturns(false, false, nil)
|
|
})
|
|
|
|
It("returns the error from errorIfCreatorHasNoWriteAccess", func() {
|
|
_, err := handler.HandlePutState(incomingMessage, txContext)
|
|
Expect(err).To(MatchError("tx creator does not have write access" +
|
|
" permission on privatedata in chaincodeName:cc-instance-name" +
|
|
" collectionName: collection-name"))
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
Describe("HandlePutStateMetadata", func() {
|
|
var incomingMessage *pb.ChaincodeMessage
|
|
var request *pb.PutStateMetadata
|
|
|
|
BeforeEach(func() {
|
|
request = &pb.PutStateMetadata{
|
|
Key: "put-state-key",
|
|
Metadata: &pb.StateMetadata{
|
|
Metakey: "put-state-metakey",
|
|
Value: []byte("put-state-metadata-value"),
|
|
},
|
|
}
|
|
payload, err := proto.Marshal(request)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
incomingMessage = &pb.ChaincodeMessage{
|
|
Type: pb.ChaincodeMessage_PUT_STATE_METADATA,
|
|
Payload: payload,
|
|
Txid: "tx-id",
|
|
ChannelId: "channel-id",
|
|
}
|
|
})
|
|
|
|
It("returns a response message", func() {
|
|
resp, err := handler.HandlePutStateMetadata(incomingMessage, txContext)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(resp).To(Equal(&pb.ChaincodeMessage{
|
|
Type: pb.ChaincodeMessage_RESPONSE,
|
|
Txid: "tx-id",
|
|
ChannelId: "channel-id",
|
|
}))
|
|
})
|
|
|
|
It("acquires application config for the channel", func() {
|
|
_, err := handler.HandlePutStateMetadata(incomingMessage, txContext)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(fakeApplicationConfigRetriever.GetApplicationConfigCallCount()).To(Equal(1))
|
|
cid := fakeApplicationConfigRetriever.GetApplicationConfigArgsForCall(0)
|
|
Expect(cid).To(Equal("channel-id"))
|
|
})
|
|
|
|
Context("when getting the app config metadata fails", func() {
|
|
BeforeEach(func() {
|
|
fakeApplicationConfigRetriever.GetApplicationConfigReturns(nil, false)
|
|
})
|
|
|
|
It("returns an error", func() {
|
|
_, err := handler.HandlePutStateMetadata(incomingMessage, txContext)
|
|
Expect(err).To(MatchError("application config does not exist for channel-id"))
|
|
})
|
|
})
|
|
|
|
Context("when key level endorsement is not supported", func() {
|
|
BeforeEach(func() {
|
|
fakeCapabilites.KeyLevelEndorsementReturns(false)
|
|
})
|
|
|
|
It("returns an error", func() {
|
|
_, err := handler.HandlePutStateMetadata(incomingMessage, txContext)
|
|
Expect(err).To(MatchError("key level endorsement is not enabled, channel application capability of V1_3 or later is required"))
|
|
})
|
|
})
|
|
|
|
Context("when purge private data is not supported", func() {
|
|
BeforeEach(func() {
|
|
fakeCapabilites.PurgePvtDataReturns(false)
|
|
})
|
|
|
|
It("returns an error", func() {
|
|
_, err := handler.HandlePurgePrivateData(incomingMessage, txContext)
|
|
Expect(err).To(MatchError("purge private data is not enabled, channel application capability of V2_5 or later is required"))
|
|
})
|
|
})
|
|
|
|
Context("when unmarshalling the request fails", func() {
|
|
BeforeEach(func() {
|
|
incomingMessage.Payload = []byte("this-is-a-bogus-payload")
|
|
})
|
|
|
|
It("returns an error", func() {
|
|
_, err := handler.HandlePutStateMetadata(incomingMessage, txContext)
|
|
Expect(err).To(Not(BeNil()))
|
|
Expect(err.Error()).To(HavePrefix("unmarshal failed:"))
|
|
})
|
|
})
|
|
|
|
Context("when the collection is not provided", func() {
|
|
It("calls SetStateMetadata on the transaction simulator", func() {
|
|
_, err := handler.HandlePutStateMetadata(incomingMessage, txContext)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(fakeTxSimulator.SetStateMetadataCallCount()).To(Equal(1))
|
|
ccname, key, value := fakeTxSimulator.SetStateMetadataArgsForCall(0)
|
|
Expect(ccname).To(Equal("cc-instance-name"))
|
|
Expect(key).To(Equal("put-state-key"))
|
|
Expect(value).To(Equal(map[string][]byte{
|
|
"put-state-metakey": []byte("put-state-metadata-value"),
|
|
}))
|
|
})
|
|
|
|
Context("when SetStateMetadata fails", func() {
|
|
BeforeEach(func() {
|
|
fakeTxSimulator.SetStateMetadataReturns(errors.New("king-kong"))
|
|
})
|
|
|
|
It("returns an error", func() {
|
|
_, err := handler.HandlePutStateMetadata(incomingMessage, txContext)
|
|
Expect(err).To(MatchError("king-kong"))
|
|
})
|
|
})
|
|
})
|
|
|
|
Context("when the collection is provided", func() {
|
|
BeforeEach(func() {
|
|
request.Collection = "collection-name"
|
|
payload, err := proto.Marshal(request)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
incomingMessage.Payload = payload
|
|
fakeCollectionStore.RetrieveReadWritePermissionReturns(false, true, nil) // to
|
|
})
|
|
|
|
It("calls SetPrivateDataMetadata on the transaction simulator", func() {
|
|
_, err := handler.HandlePutStateMetadata(incomingMessage, txContext)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(fakeTxSimulator.SetPrivateDataMetadataCallCount()).To(Equal(1))
|
|
ccname, collection, key, value := fakeTxSimulator.SetPrivateDataMetadataArgsForCall(0)
|
|
Expect(ccname).To(Equal("cc-instance-name"))
|
|
Expect(collection).To(Equal("collection-name"))
|
|
Expect(key).To(Equal("put-state-key"))
|
|
Expect(value).To(Equal(map[string][]byte{
|
|
"put-state-metakey": []byte("put-state-metadata-value"),
|
|
}))
|
|
})
|
|
|
|
Context("when SetPrivateDataMetadata fails due to ledger error", func() {
|
|
BeforeEach(func() {
|
|
fakeTxSimulator.SetPrivateDataMetadataReturns(errors.New("godzilla"))
|
|
})
|
|
|
|
It("returns an error", func() {
|
|
_, err := handler.HandlePutStateMetadata(incomingMessage, txContext)
|
|
Expect(err).To(MatchError("godzilla"))
|
|
})
|
|
})
|
|
|
|
Context("when SetPrivateDataMetadata fails due to Init transaction", func() {
|
|
BeforeEach(func() {
|
|
txContext.IsInitTransaction = true
|
|
})
|
|
|
|
It("returns the error from errorIfInitTransaction", func() {
|
|
_, err := handler.HandlePutStateMetadata(incomingMessage, txContext)
|
|
Expect(err).To(MatchError("private data APIs are not allowed in chaincode Init()"))
|
|
})
|
|
})
|
|
|
|
Context("when SetPrivateDataMetadata fails due to no write access permission", func() {
|
|
BeforeEach(func() {
|
|
fakeCollectionStore.RetrieveReadWritePermissionReturns(false, false, nil)
|
|
})
|
|
|
|
It("returns the error from errorIfCreatorHasNoWriteAccess", func() {
|
|
_, err := handler.HandlePutStateMetadata(incomingMessage, txContext)
|
|
Expect(err).To(MatchError("tx creator does not have write access" +
|
|
" permission on privatedata in chaincodeName:cc-instance-name" +
|
|
" collectionName: collection-name"))
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
Describe("HandleDelState", func() {
|
|
var incomingMessage *pb.ChaincodeMessage
|
|
var request *pb.DelState
|
|
|
|
BeforeEach(func() {
|
|
request = &pb.DelState{
|
|
Key: "del-state-key",
|
|
}
|
|
payload, err := proto.Marshal(request)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
incomingMessage = &pb.ChaincodeMessage{
|
|
Type: pb.ChaincodeMessage_DEL_STATE,
|
|
Payload: payload,
|
|
Txid: "tx-id",
|
|
ChannelId: "channel-id",
|
|
}
|
|
})
|
|
|
|
It("returns a response message", func() {
|
|
resp, err := handler.HandleDelState(incomingMessage, txContext)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(resp).To(Equal(&pb.ChaincodeMessage{
|
|
Type: pb.ChaincodeMessage_RESPONSE,
|
|
Txid: "tx-id",
|
|
ChannelId: "channel-id",
|
|
}))
|
|
})
|
|
|
|
Context("when unmarshalling the request fails", func() {
|
|
BeforeEach(func() {
|
|
incomingMessage.Payload = []byte("this-is-a-bogus-payload")
|
|
})
|
|
|
|
It("returns an error", func() {
|
|
_, err := handler.HandleDelState(incomingMessage, txContext)
|
|
Expect(err).To(Not(BeNil()))
|
|
Expect(err.Error()).To(HavePrefix("unmarshal failed:"))
|
|
})
|
|
})
|
|
|
|
Context("when collection is not set", func() {
|
|
It("calls DeleteState on the transaction simulator", func() {
|
|
_, err := handler.HandleDelState(incomingMessage, txContext)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(fakeTxSimulator.DeleteStateCallCount()).To(Equal(1))
|
|
ccname, key := fakeTxSimulator.DeleteStateArgsForCall(0)
|
|
Expect(ccname).To(Equal("cc-instance-name"))
|
|
Expect(key).To(Equal("del-state-key"))
|
|
})
|
|
|
|
Context("when DeleteState returns an error", func() {
|
|
BeforeEach(func() {
|
|
fakeTxSimulator.DeleteStateReturns(errors.New("orange"))
|
|
})
|
|
|
|
It("return an error", func() {
|
|
_, err := handler.HandleDelState(incomingMessage, txContext)
|
|
Expect(err).To(MatchError("orange"))
|
|
})
|
|
})
|
|
})
|
|
|
|
Context("when collection is set", func() {
|
|
BeforeEach(func() {
|
|
request.Collection = "collection-name"
|
|
payload, err := proto.Marshal(request)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
incomingMessage.Payload = payload
|
|
fakeCollectionStore.RetrieveReadWritePermissionReturns(false, true, nil) // to
|
|
})
|
|
|
|
It("calls DeletePrivateData on the transaction simulator", func() {
|
|
_, err := handler.HandleDelState(incomingMessage, txContext)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(fakeTxSimulator.DeletePrivateDataCallCount()).To(Equal(1))
|
|
ccname, collection, key := fakeTxSimulator.DeletePrivateDataArgsForCall(0)
|
|
Expect(ccname).To(Equal("cc-instance-name"))
|
|
Expect(collection).To(Equal("collection-name"))
|
|
Expect(key).To(Equal("del-state-key"))
|
|
})
|
|
|
|
Context("when DeletePrivateData fails due to ledger error", func() {
|
|
BeforeEach(func() {
|
|
fakeTxSimulator.DeletePrivateDataReturns(errors.New("mango"))
|
|
})
|
|
|
|
It("returns an error", func() {
|
|
_, err := handler.HandleDelState(incomingMessage, txContext)
|
|
Expect(err).To(MatchError("mango"))
|
|
})
|
|
})
|
|
|
|
Context("when DeletePrivateData fails due to Init transaction", func() {
|
|
BeforeEach(func() {
|
|
txContext.IsInitTransaction = true
|
|
})
|
|
|
|
It("returns the error from errorIfInitTransaction", func() {
|
|
_, err := handler.HandleDelState(incomingMessage, txContext)
|
|
Expect(err).To(MatchError("private data APIs are not allowed in chaincode Init()"))
|
|
})
|
|
})
|
|
|
|
Context("when DeletePrivateData fails due to no write access permission", func() {
|
|
BeforeEach(func() {
|
|
fakeCollectionStore.RetrieveReadWritePermissionReturns(false, false, nil)
|
|
})
|
|
|
|
It("returns the error from errorIfCreatorHasNoWriteAccess", func() {
|
|
_, err := handler.HandleDelState(incomingMessage, txContext)
|
|
Expect(err).To(MatchError("tx creator does not have write access" +
|
|
" permission on privatedata in chaincodeName:cc-instance-name" +
|
|
" collectionName: collection-name"))
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
Describe("HandleGetState", func() {
|
|
var (
|
|
incomingMessage *pb.ChaincodeMessage
|
|
request *pb.GetState
|
|
expectedResponse *pb.ChaincodeMessage
|
|
)
|
|
|
|
BeforeEach(func() {
|
|
request = &pb.GetState{
|
|
Key: "get-state-key",
|
|
}
|
|
payload, err := proto.Marshal(request)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
incomingMessage = &pb.ChaincodeMessage{
|
|
Type: pb.ChaincodeMessage_GET_STATE,
|
|
Payload: payload,
|
|
Txid: "tx-id",
|
|
ChannelId: "channel-id",
|
|
}
|
|
|
|
expectedResponse = &pb.ChaincodeMessage{
|
|
Type: pb.ChaincodeMessage_RESPONSE,
|
|
Txid: "tx-id",
|
|
ChannelId: "channel-id",
|
|
}
|
|
})
|
|
|
|
Context("when unmarshalling the request fails", func() {
|
|
BeforeEach(func() {
|
|
incomingMessage.Payload = []byte("this-is-a-bogus-payload")
|
|
})
|
|
|
|
It("returns an error", func() {
|
|
_, err := handler.HandleGetState(incomingMessage, txContext)
|
|
Expect(err).To(Not(BeNil()))
|
|
Expect(err.Error()).To(HavePrefix("unmarshal failed:"))
|
|
})
|
|
})
|
|
|
|
Context("when collection is set", func() {
|
|
BeforeEach(func() {
|
|
request.Collection = "collection-name"
|
|
payload, err := proto.Marshal(request)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
incomingMessage.Payload = payload
|
|
|
|
fakeCollectionStore.RetrieveReadWritePermissionReturns(true, false, nil)
|
|
fakeTxSimulator.GetPrivateDataReturns([]byte("get-private-data-response"), nil)
|
|
expectedResponse.Payload = []byte("get-private-data-response")
|
|
})
|
|
|
|
It("calls GetPrivateData on the transaction simulator", func() {
|
|
_, err := handler.HandleGetState(incomingMessage, txContext)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(fakeTxSimulator.GetPrivateDataCallCount()).To(Equal(1))
|
|
ccname, collection, key := fakeTxSimulator.GetPrivateDataArgsForCall(0)
|
|
Expect(ccname).To(Equal("cc-instance-name"))
|
|
Expect(collection).To(Equal("collection-name"))
|
|
Expect(key).To(Equal("get-state-key"))
|
|
})
|
|
|
|
Context("and GetPrivateData fails due to ledger error", func() {
|
|
BeforeEach(func() {
|
|
fakeTxSimulator.GetPrivateDataReturns(nil, errors.New("french fries"))
|
|
})
|
|
|
|
It("returns the error from GetPrivateData", func() {
|
|
_, err := handler.HandleGetState(incomingMessage, txContext)
|
|
Expect(err).To(MatchError("french fries"))
|
|
})
|
|
})
|
|
|
|
Context("and GetPrivateData fails due to no read access permission", func() {
|
|
BeforeEach(func() {
|
|
fakeCollectionStore.RetrieveReadWritePermissionReturns(false, false, nil)
|
|
})
|
|
|
|
It("returns the error from errorIfCreatorHasNoReadAccess", func() {
|
|
_, err := handler.HandleGetState(incomingMessage, txContext)
|
|
Expect(err).To(MatchError("tx creator does not have read access" +
|
|
" permission on privatedata in chaincodeName:cc-instance-name" +
|
|
" collectionName: collection-name"))
|
|
})
|
|
})
|
|
|
|
Context("and GetPrivateData fails due to error in checking the read access permission", func() {
|
|
BeforeEach(func() {
|
|
fakeCollectionStore.RetrieveReadWritePermissionReturns(false, false, errors.New("no collection config"))
|
|
})
|
|
|
|
It("returns the error from errorIfCreatorHasNoReadAccess", func() {
|
|
_, err := handler.HandleGetState(incomingMessage, txContext)
|
|
Expect(err).To(MatchError("no collection config"))
|
|
})
|
|
})
|
|
|
|
Context("and GetPrivateData fails due to Init transaction", func() {
|
|
BeforeEach(func() {
|
|
txContext.IsInitTransaction = true
|
|
})
|
|
|
|
It("returns the error from errorIfInitTransaction", func() {
|
|
_, err := handler.HandleGetState(incomingMessage, txContext)
|
|
Expect(err).To(MatchError("private data APIs are not allowed in chaincode Init()"))
|
|
})
|
|
})
|
|
|
|
Context("and GetPrivateData returns the response message", func() {
|
|
BeforeEach(func() {
|
|
// txContext.CollectionACLCache.put("collection-name", true, false)
|
|
// fakeCollectionStore.HasReadAccessReturns(false, nil) // to
|
|
// ensure that the access cache is used
|
|
})
|
|
|
|
It("returns the response message from GetPrivateData", func() {
|
|
fakeCollectionStore.RetrieveReadWritePermissionReturns(true, false, nil) // to
|
|
resp, err := handler.HandleGetState(incomingMessage, txContext)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(resp).To(Equal(&pb.ChaincodeMessage{
|
|
Type: pb.ChaincodeMessage_RESPONSE,
|
|
Payload: []byte("get-private-data-response"),
|
|
Txid: "tx-id",
|
|
ChannelId: "channel-id",
|
|
}))
|
|
// as the cache hit should happen in CollectionACLCache, the following
|
|
// RetrieveReadWritePermissionReturns should not be called
|
|
fakeCollectionStore.RetrieveReadWritePermissionReturns(false, false, nil) // to
|
|
resp, err = handler.HandleGetState(incomingMessage, txContext)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(resp).To(Equal(&pb.ChaincodeMessage{
|
|
Type: pb.ChaincodeMessage_RESPONSE,
|
|
Payload: []byte("get-private-data-response"),
|
|
Txid: "tx-id",
|
|
ChannelId: "channel-id",
|
|
}))
|
|
})
|
|
})
|
|
|
|
It("returns the response message from GetPrivateData", func() {
|
|
resp, err := handler.HandleGetState(incomingMessage, txContext)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(resp).To(Equal(&pb.ChaincodeMessage{
|
|
Type: pb.ChaincodeMessage_RESPONSE,
|
|
Payload: []byte("get-private-data-response"),
|
|
Txid: "tx-id",
|
|
ChannelId: "channel-id",
|
|
}))
|
|
})
|
|
})
|
|
|
|
Context("when collection is not set", func() {
|
|
BeforeEach(func() {
|
|
fakeTxSimulator.GetStateReturns([]byte("get-state-response"), nil)
|
|
expectedResponse.Payload = []byte("get-state-response")
|
|
})
|
|
|
|
It("calls GetState on the transaction simulator", func() {
|
|
_, err := handler.HandleGetState(incomingMessage, txContext)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(fakeTxSimulator.GetStateCallCount()).To(Equal(1))
|
|
ccname, key := fakeTxSimulator.GetStateArgsForCall(0)
|
|
Expect(ccname).To(Equal("cc-instance-name"))
|
|
Expect(key).To(Equal("get-state-key"))
|
|
})
|
|
|
|
Context("and GetState fails", func() {
|
|
BeforeEach(func() {
|
|
fakeTxSimulator.GetStateReturns(nil, errors.New("tomato"))
|
|
})
|
|
|
|
It("returns the error from GetState", func() {
|
|
_, err := handler.HandleGetState(incomingMessage, txContext)
|
|
Expect(err).To(MatchError("tomato"))
|
|
})
|
|
})
|
|
|
|
It("returns the response from GetState", func() {
|
|
resp, err := handler.HandleGetState(incomingMessage, txContext)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(resp).To(Equal(&pb.ChaincodeMessage{
|
|
Type: pb.ChaincodeMessage_RESPONSE,
|
|
Payload: []byte("get-state-response"),
|
|
Txid: "tx-id",
|
|
ChannelId: "channel-id",
|
|
}))
|
|
})
|
|
})
|
|
})
|
|
|
|
Describe("HandleGetPrivateDataHash", func() {
|
|
var (
|
|
incomingMessage *pb.ChaincodeMessage
|
|
request *pb.GetState
|
|
expectedResponse *pb.ChaincodeMessage
|
|
)
|
|
|
|
BeforeEach(func() {
|
|
request = &pb.GetState{
|
|
Collection: "collection-name",
|
|
Key: "get-pvtdata-hash-key",
|
|
}
|
|
payload, err := proto.Marshal(request)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
incomingMessage = &pb.ChaincodeMessage{
|
|
Type: pb.ChaincodeMessage_GET_PRIVATE_DATA_HASH,
|
|
Payload: payload,
|
|
Txid: "tx-id",
|
|
ChannelId: "channel-id",
|
|
}
|
|
|
|
expectedResponse = &pb.ChaincodeMessage{
|
|
Type: pb.ChaincodeMessage_RESPONSE,
|
|
Payload: []byte("get-private-data-hash-response"),
|
|
Txid: "tx-id",
|
|
ChannelId: "channel-id",
|
|
}
|
|
fakeTxSimulator.GetPrivateDataHashReturns([]byte("get-private-data-hash-response"), nil)
|
|
})
|
|
|
|
It("calls GetPrivateDataHash on the transaction simulator and receives expected response", func() {
|
|
response, err := handler.HandleGetPrivateDataHash(incomingMessage, txContext)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(fakeTxSimulator.GetPrivateDataHashCallCount()).To(Equal(1))
|
|
ccname, collection, key := fakeTxSimulator.GetPrivateDataHashArgsForCall(0)
|
|
Expect(ccname).To(Equal("cc-instance-name"))
|
|
Expect(collection).To(Equal("collection-name"))
|
|
Expect(key).To(Equal("get-pvtdata-hash-key"))
|
|
Expect(response).To(Equal(expectedResponse))
|
|
})
|
|
|
|
Context("when unmarshalling the request fails", func() {
|
|
BeforeEach(func() {
|
|
incomingMessage.Payload = []byte("this-is-a-bogus-payload")
|
|
})
|
|
|
|
It("returns an error", func() {
|
|
_, err := handler.HandleGetPrivateDataHash(incomingMessage, txContext)
|
|
Expect(err).To(Not(BeNil()))
|
|
Expect(err.Error()).To(HavePrefix("unmarshal failed:"))
|
|
})
|
|
})
|
|
|
|
Context("and GetPrivateDataHash fails due to ledger error", func() {
|
|
BeforeEach(func() {
|
|
fakeTxSimulator.GetPrivateDataHashReturns(nil, errors.New("french fries"))
|
|
})
|
|
|
|
It("returns the error from GetPrivateData", func() {
|
|
_, err := handler.HandleGetPrivateDataHash(incomingMessage, txContext)
|
|
Expect(err).To(MatchError("french fries"))
|
|
})
|
|
})
|
|
|
|
Context("and GetPrivateData fails due to Init transaction", func() {
|
|
BeforeEach(func() {
|
|
txContext.IsInitTransaction = true
|
|
})
|
|
|
|
It("returns the error from errorIfInitTransaction", func() {
|
|
_, err := handler.HandleGetPrivateDataHash(incomingMessage, txContext)
|
|
Expect(err).To(MatchError("private data APIs are not allowed in chaincode Init()"))
|
|
})
|
|
})
|
|
})
|
|
|
|
Describe("HandleGetStateMetadata", func() {
|
|
var (
|
|
incomingMessage *pb.ChaincodeMessage
|
|
request *pb.GetStateMetadata
|
|
expectedResponse *pb.ChaincodeMessage
|
|
)
|
|
|
|
BeforeEach(func() {
|
|
request = &pb.GetStateMetadata{
|
|
Key: "get-state-key",
|
|
}
|
|
payload, err := proto.Marshal(request)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
incomingMessage = &pb.ChaincodeMessage{
|
|
Type: pb.ChaincodeMessage_GET_STATE_METADATA,
|
|
Payload: payload,
|
|
Txid: "tx-id",
|
|
ChannelId: "channel-id",
|
|
}
|
|
|
|
expectedResponse = &pb.ChaincodeMessage{
|
|
Type: pb.ChaincodeMessage_RESPONSE,
|
|
Txid: "tx-id",
|
|
ChannelId: "channel-id",
|
|
}
|
|
})
|
|
|
|
It("acquires application config for the channel", func() {
|
|
_, err := handler.HandleGetStateMetadata(incomingMessage, txContext)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(fakeApplicationConfigRetriever.GetApplicationConfigCallCount()).To(Equal(1))
|
|
cid := fakeApplicationConfigRetriever.GetApplicationConfigArgsForCall(0)
|
|
Expect(cid).To(Equal("channel-id"))
|
|
})
|
|
|
|
Context("when getting the app config metadata fails", func() {
|
|
BeforeEach(func() {
|
|
fakeApplicationConfigRetriever.GetApplicationConfigReturns(nil, false)
|
|
})
|
|
|
|
It("returns an error", func() {
|
|
_, err := handler.HandleGetStateMetadata(incomingMessage, txContext)
|
|
Expect(err).To(MatchError("application config does not exist for channel-id"))
|
|
})
|
|
})
|
|
|
|
Context("when key level endorsement is not supported", func() {
|
|
BeforeEach(func() {
|
|
fakeCapabilites.KeyLevelEndorsementReturns(false)
|
|
})
|
|
|
|
It("returns an error", func() {
|
|
_, err := handler.HandleGetStateMetadata(incomingMessage, txContext)
|
|
Expect(err).To(MatchError("key level endorsement is not enabled, channel application capability of V1_3 or later is required"))
|
|
})
|
|
})
|
|
|
|
Context("when unmarshalling the request fails", func() {
|
|
BeforeEach(func() {
|
|
incomingMessage.Payload = []byte("this-is-a-bogus-payload")
|
|
})
|
|
|
|
It("returns an error", func() {
|
|
_, err := handler.HandleGetStateMetadata(incomingMessage, txContext)
|
|
Expect(err).To(Not(BeNil()))
|
|
Expect(err.Error()).To(HavePrefix("unmarshal failed:"))
|
|
})
|
|
})
|
|
|
|
Context("when collection is set", func() {
|
|
BeforeEach(func() {
|
|
request.Collection = "collection-name"
|
|
payload, err := proto.Marshal(request)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
incomingMessage.Payload = payload
|
|
|
|
metadata := map[string][]byte{
|
|
"get-state-metakey": []byte("get-private-metadata-response"),
|
|
}
|
|
fakeCollectionStore.RetrieveReadWritePermissionReturns(true, false, nil)
|
|
fakeTxSimulator.GetPrivateDataMetadataReturns(metadata, nil)
|
|
responsePayload, err := proto.Marshal(&pb.StateMetadataResult{
|
|
Entries: []*pb.StateMetadata{{
|
|
Metakey: "get-state-metakey",
|
|
Value: []byte("get-private-metadata-response"),
|
|
}},
|
|
})
|
|
Expect(err).NotTo(HaveOccurred())
|
|
expectedResponse.Payload = responsePayload
|
|
})
|
|
|
|
It("calls GetPrivateDataMetadata on the transaction simulator", func() {
|
|
_, err := handler.HandleGetStateMetadata(incomingMessage, txContext)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(fakeTxSimulator.GetPrivateDataMetadataCallCount()).To(Equal(1))
|
|
ccname, collection, key := fakeTxSimulator.GetPrivateDataMetadataArgsForCall(0)
|
|
Expect(ccname).To(Equal("cc-instance-name"))
|
|
Expect(collection).To(Equal("collection-name"))
|
|
Expect(key).To(Equal("get-state-key"))
|
|
})
|
|
|
|
It("returns the response message from GetPrivateDataMetadata", func() {
|
|
resp, err := handler.HandleGetStateMetadata(incomingMessage, txContext)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(resp).To(Equal(expectedResponse))
|
|
})
|
|
|
|
Context("and GetPrivateDataMetadata fails due to ledger error", func() {
|
|
BeforeEach(func() {
|
|
fakeTxSimulator.GetPrivateDataMetadataReturns(nil, errors.New("french fries"))
|
|
})
|
|
|
|
It("returns the error from GetPrivateDataMetadata", func() {
|
|
_, err := handler.HandleGetStateMetadata(incomingMessage, txContext)
|
|
Expect(err).To(MatchError("french fries"))
|
|
})
|
|
})
|
|
|
|
Context("and GetPrivateDataMetadata fails due to no read access permission", func() {
|
|
BeforeEach(func() {
|
|
fakeCollectionStore.RetrieveReadWritePermissionReturns(false, false, nil)
|
|
})
|
|
|
|
It("returns the error from GetPrivateDataMetadata", func() {
|
|
_, err := handler.HandleGetStateMetadata(incomingMessage, txContext)
|
|
Expect(err).To(MatchError("tx creator does not have read access" +
|
|
" permission on privatedata in chaincodeName:cc-instance-name" +
|
|
" collectionName: collection-name"))
|
|
})
|
|
})
|
|
|
|
Context("and GetPrivateDataMetadata fails due to error in checking the read access permission", func() {
|
|
BeforeEach(func() {
|
|
fakeCollectionStore.RetrieveReadWritePermissionReturns(false, false, errors.New("no collection config"))
|
|
})
|
|
|
|
It("returns the error from GetPrivateDataMetadata", func() {
|
|
_, err := handler.HandleGetStateMetadata(incomingMessage, txContext)
|
|
Expect(err).To(MatchError("no collection config"))
|
|
})
|
|
})
|
|
|
|
Context("and GetPrivateDataMetadata fails due to Init transaction", func() {
|
|
BeforeEach(func() {
|
|
txContext.IsInitTransaction = true
|
|
})
|
|
|
|
It("returns the error from errorIfInitTransaction", func() {
|
|
_, err := handler.HandleGetStateMetadata(incomingMessage, txContext)
|
|
Expect(err).To(MatchError("private data APIs are not allowed in chaincode Init()"))
|
|
})
|
|
})
|
|
})
|
|
|
|
Context("when collection is not set", func() {
|
|
BeforeEach(func() {
|
|
metadata := map[string][]byte{
|
|
"get-state-metakey": []byte("get-state-metadata-response"),
|
|
}
|
|
fakeTxSimulator.GetStateMetadataReturns(metadata, nil)
|
|
responsePayload, err := proto.Marshal(&pb.StateMetadataResult{
|
|
Entries: []*pb.StateMetadata{{
|
|
Metakey: "get-state-metakey",
|
|
Value: []byte("get-state-metadata-response"),
|
|
}},
|
|
})
|
|
Expect(err).NotTo(HaveOccurred())
|
|
expectedResponse.Payload = responsePayload
|
|
})
|
|
|
|
It("calls GetStateMetadata on the transaction simulator", func() {
|
|
_, err := handler.HandleGetStateMetadata(incomingMessage, txContext)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(fakeTxSimulator.GetStateMetadataCallCount()).To(Equal(1))
|
|
ccname, key := fakeTxSimulator.GetStateMetadataArgsForCall(0)
|
|
Expect(ccname).To(Equal("cc-instance-name"))
|
|
Expect(key).To(Equal("get-state-key"))
|
|
})
|
|
|
|
It("returns the response from GetStateMetadata", func() {
|
|
resp, err := handler.HandleGetStateMetadata(incomingMessage, txContext)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(resp).To(Equal(expectedResponse))
|
|
})
|
|
|
|
Context("and GetStateMetadata fails", func() {
|
|
BeforeEach(func() {
|
|
fakeTxSimulator.GetStateMetadataReturns(nil, errors.New("tomato"))
|
|
})
|
|
|
|
It("returns the error from GetStateMetadata", func() {
|
|
_, err := handler.HandleGetStateMetadata(incomingMessage, txContext)
|
|
Expect(err).To(MatchError("tomato"))
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
Describe("HandleGetStateByRange", func() {
|
|
var (
|
|
incomingMessage *pb.ChaincodeMessage
|
|
request *pb.GetStateByRange
|
|
expectedResponse *pb.ChaincodeMessage
|
|
fakeIterator *mock.QueryResultsIterator
|
|
expectedQueryResponse *pb.QueryResponse
|
|
expectedPayload []byte
|
|
)
|
|
|
|
BeforeEach(func() {
|
|
request = &pb.GetStateByRange{
|
|
StartKey: "get-state-start-key",
|
|
EndKey: "get-state-end-key",
|
|
}
|
|
payload, err := proto.Marshal(request)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
incomingMessage = &pb.ChaincodeMessage{
|
|
Type: pb.ChaincodeMessage_GET_STATE,
|
|
Payload: payload,
|
|
Txid: "tx-id",
|
|
ChannelId: "channel-id",
|
|
}
|
|
|
|
fakeIterator = &mock.QueryResultsIterator{}
|
|
fakeTxSimulator.GetStateRangeScanIteratorReturns(fakeIterator, nil)
|
|
|
|
expectedQueryResponse = &pb.QueryResponse{
|
|
Results: nil,
|
|
HasMore: true,
|
|
Id: "query-response-id",
|
|
}
|
|
fakeQueryResponseBuilder.BuildQueryResponseReturns(expectedQueryResponse, nil)
|
|
|
|
expectedPayload, err = proto.Marshal(expectedQueryResponse)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
expectedResponse = &pb.ChaincodeMessage{
|
|
Type: pb.ChaincodeMessage_RESPONSE,
|
|
Txid: "tx-id",
|
|
Payload: expectedPayload,
|
|
ChannelId: "channel-id",
|
|
}
|
|
})
|
|
|
|
It("initializes a query context", func() {
|
|
_, err := handler.HandleGetStateByRange(incomingMessage, txContext)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
pqr := txContext.GetPendingQueryResult("generated-query-id")
|
|
Expect(pqr).To(Equal(&chaincode.PendingQueryResult{}))
|
|
iter := txContext.GetQueryIterator("generated-query-id")
|
|
Expect(iter).To(Equal(fakeIterator))
|
|
retCount := txContext.GetTotalReturnCount("generated-query-id")
|
|
Expect(*retCount).To(Equal(int32(0)))
|
|
})
|
|
|
|
It("returns the response message", func() {
|
|
resp, err := handler.HandleGetStateByRange(incomingMessage, txContext)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(resp).To(Equal(expectedResponse))
|
|
})
|
|
|
|
Context("when collection is not set", func() {
|
|
It("calls GetStateRangeScanIterator on the transaction simulator", func() {
|
|
_, err := handler.HandleGetStateByRange(incomingMessage, txContext)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(fakeTxSimulator.GetStateRangeScanIteratorCallCount()).To(Equal(1))
|
|
ccname, startKey, endKey := fakeTxSimulator.GetStateRangeScanIteratorArgsForCall(0)
|
|
Expect(ccname).To(Equal("cc-instance-name"))
|
|
Expect(startKey).To(Equal("get-state-start-key"))
|
|
Expect(endKey).To(Equal("get-state-end-key"))
|
|
})
|
|
|
|
Context("and GetStateRangeScanIterator fails", func() {
|
|
BeforeEach(func() {
|
|
fakeTxSimulator.GetStateRangeScanIteratorReturns(nil, errors.New("tomato"))
|
|
})
|
|
|
|
It("returns the error from GetStateRangeScanIterator", func() {
|
|
_, err := handler.HandleGetStateByRange(incomingMessage, txContext)
|
|
Expect(err).To(MatchError("tomato"))
|
|
})
|
|
})
|
|
|
|
It("returns the response from GetStateRangeScanIterator", func() {
|
|
resp, err := handler.HandleGetStateByRange(incomingMessage, txContext)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(resp).To(Equal(expectedResponse))
|
|
})
|
|
})
|
|
|
|
Context("when collection is set", func() {
|
|
BeforeEach(func() {
|
|
request.Collection = "collection-name"
|
|
payload, err := proto.Marshal(request)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
incomingMessage.Payload = payload
|
|
|
|
fakeCollectionStore.RetrieveReadWritePermissionReturns(true, false, nil)
|
|
fakeTxSimulator.GetPrivateDataRangeScanIteratorReturns(fakeIterator, nil)
|
|
})
|
|
|
|
It("calls GetPrivateDataRangeScanIterator on the transaction simulator", func() {
|
|
_, err := handler.HandleGetStateByRange(incomingMessage, txContext)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(fakeTxSimulator.GetPrivateDataRangeScanIteratorCallCount()).To(Equal(1))
|
|
ccname, collection, startKey, endKey := fakeTxSimulator.GetPrivateDataRangeScanIteratorArgsForCall(0)
|
|
Expect(ccname).To(Equal("cc-instance-name"))
|
|
Expect(collection).To(Equal("collection-name"))
|
|
Expect(startKey).To(Equal("get-state-start-key"))
|
|
Expect(endKey).To(Equal("get-state-end-key"))
|
|
})
|
|
|
|
Context("and GetPrivateDataRangeScanIterator fails due to ledger error", func() {
|
|
BeforeEach(func() {
|
|
fakeTxSimulator.GetPrivateDataRangeScanIteratorReturns(nil, errors.New("french fries"))
|
|
})
|
|
|
|
It("returns the error from GetPrivateDataRangeScanIterator", func() {
|
|
_, err := handler.HandleGetStateByRange(incomingMessage, txContext)
|
|
Expect(err).To(MatchError("french fries"))
|
|
})
|
|
})
|
|
|
|
Context("and GetPrivateDataRangeScanIterator fails due to no read access permission", func() {
|
|
BeforeEach(func() {
|
|
fakeCollectionStore.RetrieveReadWritePermissionReturns(false, false, nil)
|
|
})
|
|
|
|
It("returns the error from GetPrivateDataRangeScanIterator", func() {
|
|
_, err := handler.HandleGetStateByRange(incomingMessage, txContext)
|
|
Expect(err).To(MatchError("tx creator does not have read access" +
|
|
" permission on privatedata in chaincodeName:cc-instance-name" +
|
|
" collectionName: collection-name"))
|
|
})
|
|
})
|
|
|
|
Context("and GetPrivateDataRangeScanIterator fails due to error in checking the read access permission", func() {
|
|
BeforeEach(func() {
|
|
fakeCollectionStore.RetrieveReadWritePermissionReturns(false, false, errors.New("no collection config"))
|
|
})
|
|
|
|
It("returns the error from GetPrivateDataRangeScanIterator", func() {
|
|
_, err := handler.HandleGetStateByRange(incomingMessage, txContext)
|
|
Expect(err).To(MatchError("no collection config"))
|
|
})
|
|
})
|
|
|
|
Context("and GetPrivateDataRangeScanIterator fails due to Init transaction", func() {
|
|
BeforeEach(func() {
|
|
txContext.IsInitTransaction = true
|
|
})
|
|
|
|
It("returns the error from errorIfInitTransaction", func() {
|
|
_, err := handler.HandleGetStateByRange(incomingMessage, txContext)
|
|
Expect(err).To(MatchError("private data APIs are not allowed in chaincode Init()"))
|
|
})
|
|
})
|
|
})
|
|
|
|
Context("when unmarshalling the request fails", func() {
|
|
BeforeEach(func() {
|
|
incomingMessage.Payload = []byte("this-is-a-bogus-payload")
|
|
})
|
|
|
|
It("returns an error", func() {
|
|
_, err := handler.HandleGetStateByRange(incomingMessage, txContext)
|
|
Expect(err).To(Not(BeNil()))
|
|
Expect(err.Error()).To(HavePrefix("unmarshal failed:"))
|
|
})
|
|
})
|
|
|
|
Context("when building the query response fails", func() {
|
|
BeforeEach(func() {
|
|
fakeQueryResponseBuilder.BuildQueryResponseReturns(nil, errors.New("garbanzo"))
|
|
})
|
|
|
|
It("returns the error", func() {
|
|
_, err := handler.HandleGetStateByRange(incomingMessage, txContext)
|
|
Expect(err).To(MatchError("garbanzo"))
|
|
})
|
|
|
|
It("cleans up the query context", func() {
|
|
handler.HandleGetStateByRange(incomingMessage, txContext)
|
|
|
|
pqr := txContext.GetPendingQueryResult("generated-query-id")
|
|
Expect(pqr).To(BeNil())
|
|
iter := txContext.GetQueryIterator("generated-query-id")
|
|
Expect(iter).To(BeNil())
|
|
})
|
|
})
|
|
|
|
Context("when marshling the response fails", func() {
|
|
BeforeEach(func() {
|
|
fakeQueryResponseBuilder.BuildQueryResponseReturns(nil, nil)
|
|
})
|
|
|
|
It("returns the error", func() {
|
|
_, err := handler.HandleGetStateByRange(incomingMessage, txContext)
|
|
Expect(err).To(MatchError("marshal failed: proto: Marshal called with nil"))
|
|
})
|
|
|
|
It("cleans up the query context", func() {
|
|
handler.HandleGetStateByRange(incomingMessage, txContext)
|
|
|
|
pqr := txContext.GetPendingQueryResult("generated-query-id")
|
|
Expect(pqr).To(BeNil())
|
|
iter := txContext.GetQueryIterator("generated-query-id")
|
|
Expect(iter).To(BeNil())
|
|
retCount := txContext.GetTotalReturnCount("generated-query-id")
|
|
Expect(retCount).To(BeNil())
|
|
})
|
|
})
|
|
})
|
|
|
|
Describe("HandleQueryStateNext", func() {
|
|
var (
|
|
fakeIterator *mock.QueryResultsIterator
|
|
expectedQueryResponse *pb.QueryResponse
|
|
request *pb.QueryStateNext
|
|
incomingMessage *pb.ChaincodeMessage
|
|
)
|
|
|
|
BeforeEach(func() {
|
|
request = &pb.QueryStateNext{
|
|
Id: "query-state-next-id",
|
|
}
|
|
payload, err := proto.Marshal(request)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
fakeIterator = &mock.QueryResultsIterator{}
|
|
txContext.InitializeQueryContext("query-state-next-id", fakeIterator)
|
|
|
|
incomingMessage = &pb.ChaincodeMessage{
|
|
Type: pb.ChaincodeMessage_GET_STATE,
|
|
Payload: payload,
|
|
Txid: "tx-id",
|
|
ChannelId: "channel-id",
|
|
}
|
|
expectedQueryResponse = &pb.QueryResponse{
|
|
Results: nil,
|
|
HasMore: true,
|
|
Id: "query-response-id",
|
|
}
|
|
fakeQueryResponseBuilder.BuildQueryResponseReturns(expectedQueryResponse, nil)
|
|
})
|
|
|
|
It("builds a query response", func() {
|
|
_, err := handler.HandleQueryStateNext(incomingMessage, txContext)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(fakeQueryResponseBuilder.BuildQueryResponseCallCount()).To(Equal(1))
|
|
tctx, iter, id, _, _ := fakeQueryResponseBuilder.BuildQueryResponseArgsForCall(0)
|
|
Expect(tctx).To(Equal(txContext))
|
|
Expect(iter).To(Equal(fakeIterator))
|
|
Expect(id).To(Equal("query-state-next-id"))
|
|
})
|
|
|
|
It("returns a chaincode message with the query response", func() {
|
|
resp, err := handler.HandleQueryStateNext(incomingMessage, txContext)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
expectedPayload, err := proto.Marshal(expectedQueryResponse)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(resp).To(Equal(&pb.ChaincodeMessage{
|
|
Type: pb.ChaincodeMessage_RESPONSE,
|
|
Payload: expectedPayload,
|
|
ChannelId: "channel-id",
|
|
Txid: "tx-id",
|
|
}))
|
|
})
|
|
|
|
Context("when unmarshalling the request fails", func() {
|
|
BeforeEach(func() {
|
|
incomingMessage.Payload = []byte("this-is-a-bogus-payload")
|
|
})
|
|
|
|
It("returns an error", func() {
|
|
_, err := handler.HandleQueryStateNext(incomingMessage, txContext)
|
|
Expect(err).To(Not(BeNil()))
|
|
Expect(err.Error()).To(HavePrefix("unmarshal failed:"))
|
|
})
|
|
})
|
|
|
|
Context("when the query iterator is missing", func() {
|
|
BeforeEach(func() {
|
|
txContext.CleanupQueryContext("query-state-next-id")
|
|
})
|
|
|
|
It("returns an error", func() {
|
|
_, err := handler.HandleQueryStateNext(incomingMessage, txContext)
|
|
Expect(err).To(MatchError("query iterator not found"))
|
|
})
|
|
})
|
|
|
|
Context("when building the query response fails", func() {
|
|
BeforeEach(func() {
|
|
fakeQueryResponseBuilder.BuildQueryResponseReturns(nil, errors.New("potato"))
|
|
})
|
|
|
|
It("returns an error", func() {
|
|
_, err := handler.HandleQueryStateNext(incomingMessage, txContext)
|
|
Expect(err).To(MatchError("potato"))
|
|
})
|
|
|
|
It("cleans up the query context", func() {
|
|
handler.HandleQueryStateNext(incomingMessage, txContext)
|
|
|
|
pqr := txContext.GetPendingQueryResult("generated-query-id")
|
|
Expect(pqr).To(BeNil())
|
|
iter := txContext.GetQueryIterator("generated-query-id")
|
|
Expect(iter).To(BeNil())
|
|
retCount := txContext.GetTotalReturnCount("generated-query-id")
|
|
Expect(retCount).To(BeNil())
|
|
})
|
|
})
|
|
|
|
Context("when marshaling the query response fails", func() {
|
|
BeforeEach(func() {
|
|
fakeQueryResponseBuilder.BuildQueryResponseReturns(nil, nil)
|
|
})
|
|
|
|
It("returns an error", func() {
|
|
_, err := handler.HandleQueryStateNext(incomingMessage, txContext)
|
|
Expect(err).To(MatchError("marshal failed: proto: Marshal called with nil"))
|
|
})
|
|
|
|
It("cleans up the query context", func() {
|
|
handler.HandleQueryStateNext(incomingMessage, txContext)
|
|
|
|
pqr := txContext.GetPendingQueryResult("generated-query-id")
|
|
Expect(pqr).To(BeNil())
|
|
iter := txContext.GetQueryIterator("generated-query-id")
|
|
Expect(iter).To(BeNil())
|
|
retCount := txContext.GetTotalReturnCount("generated-query-id")
|
|
Expect(retCount).To(BeNil())
|
|
})
|
|
})
|
|
})
|
|
|
|
Describe("HandleQueryStateClose", func() {
|
|
var (
|
|
fakeIterator *mock.QueryResultsIterator
|
|
expectedQueryResponse *pb.QueryResponse
|
|
request *pb.QueryStateClose
|
|
incomingMessage *pb.ChaincodeMessage
|
|
)
|
|
|
|
BeforeEach(func() {
|
|
request = &pb.QueryStateClose{
|
|
Id: "query-state-close-id",
|
|
}
|
|
payload, err := proto.Marshal(request)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
fakeIterator = &mock.QueryResultsIterator{}
|
|
txContext.InitializeQueryContext("query-state-close-id", fakeIterator)
|
|
|
|
incomingMessage = &pb.ChaincodeMessage{
|
|
Type: pb.ChaincodeMessage_GET_STATE,
|
|
Payload: payload,
|
|
Txid: "tx-id",
|
|
ChannelId: "channel-id",
|
|
}
|
|
expectedQueryResponse = &pb.QueryResponse{
|
|
Id: "query-state-close-id",
|
|
}
|
|
})
|
|
|
|
It("returns a chaincode message with the query response", func() {
|
|
resp, err := handler.HandleQueryStateClose(incomingMessage, txContext)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
expectedPayload, err := proto.Marshal(expectedQueryResponse)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(resp).To(Equal(&pb.ChaincodeMessage{
|
|
Type: pb.ChaincodeMessage_RESPONSE,
|
|
Payload: expectedPayload,
|
|
ChannelId: "channel-id",
|
|
Txid: "tx-id",
|
|
}))
|
|
})
|
|
|
|
Context("when unmarshalling the request fails", func() {
|
|
BeforeEach(func() {
|
|
incomingMessage.Payload = []byte("this-is-a-bogus-payload")
|
|
})
|
|
|
|
It("returns an error", func() {
|
|
_, err := handler.HandleQueryStateClose(incomingMessage, txContext)
|
|
Expect(err).To(Not(BeNil()))
|
|
Expect(err.Error()).To(HavePrefix("unmarshal failed:"))
|
|
})
|
|
})
|
|
|
|
Context("when the query iterator is missing", func() {
|
|
BeforeEach(func() {
|
|
txContext.CleanupQueryContext("query-state-close-id")
|
|
})
|
|
|
|
It("keeps calm and carries on", func() {
|
|
_, err := handler.HandleQueryStateClose(incomingMessage, txContext)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
})
|
|
})
|
|
})
|
|
|
|
Describe("HandleGetQueryResult", func() {
|
|
var (
|
|
request *pb.GetQueryResult
|
|
incomingMessage *pb.ChaincodeMessage
|
|
expectedQueryResponse *pb.QueryResponse
|
|
fakeIterator *mock.QueryResultsIterator
|
|
)
|
|
|
|
BeforeEach(func() {
|
|
request = &pb.GetQueryResult{
|
|
Query: "query-result",
|
|
}
|
|
payload, err := proto.Marshal(request)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
incomingMessage = &pb.ChaincodeMessage{
|
|
Type: pb.ChaincodeMessage_GET_STATE,
|
|
Payload: payload,
|
|
Txid: "tx-id",
|
|
ChannelId: "channel-id",
|
|
}
|
|
|
|
expectedQueryResponse = &pb.QueryResponse{
|
|
Id: "query-response-id",
|
|
}
|
|
fakeQueryResponseBuilder.BuildQueryResponseReturns(expectedQueryResponse, nil)
|
|
|
|
fakeIterator = &mock.QueryResultsIterator{}
|
|
fakeTxSimulator.ExecuteQueryReturns(fakeIterator, nil)
|
|
})
|
|
|
|
Context("when collection is not set", func() {
|
|
It("calls ExecuteQuery on the transaction simulator", func() {
|
|
_, err := handler.HandleGetQueryResult(incomingMessage, txContext)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(fakeTxSimulator.ExecuteQueryCallCount()).To(Equal(1))
|
|
ccname, query := fakeTxSimulator.ExecuteQueryArgsForCall(0)
|
|
Expect(ccname).To(Equal("cc-instance-name"))
|
|
Expect(query).To(Equal("query-result"))
|
|
})
|
|
|
|
Context("and ExecuteQuery fails", func() {
|
|
BeforeEach(func() {
|
|
fakeTxSimulator.ExecuteQueryReturns(nil, errors.New("mushrooms"))
|
|
})
|
|
|
|
It("returns the error", func() {
|
|
_, err := handler.HandleGetQueryResult(incomingMessage, txContext)
|
|
Expect(err).To(MatchError("mushrooms"))
|
|
})
|
|
})
|
|
})
|
|
|
|
Context("when collection is set", func() {
|
|
BeforeEach(func() {
|
|
request.Collection = "collection-name"
|
|
payload, err := proto.Marshal(request)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
incomingMessage.Payload = payload
|
|
|
|
fakeCollectionStore.RetrieveReadWritePermissionReturns(true, false, nil)
|
|
fakeTxSimulator.ExecuteQueryOnPrivateDataReturns(fakeIterator, nil)
|
|
})
|
|
|
|
It("calls ExecuteQueryOnPrivateDataon the transaction simulator", func() {
|
|
_, err := handler.HandleGetQueryResult(incomingMessage, txContext)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(fakeTxSimulator.ExecuteQueryOnPrivateDataCallCount()).To(Equal(1))
|
|
ccname, collection, query := fakeTxSimulator.ExecuteQueryOnPrivateDataArgsForCall(0)
|
|
Expect(ccname).To(Equal("cc-instance-name"))
|
|
Expect(collection).To(Equal("collection-name"))
|
|
Expect(query).To(Equal("query-result"))
|
|
})
|
|
|
|
It("initializes a query context", func() {
|
|
_, err := handler.HandleGetQueryResult(incomingMessage, txContext)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
pqr := txContext.GetPendingQueryResult("generated-query-id")
|
|
Expect(pqr).To(Equal(&chaincode.PendingQueryResult{}))
|
|
iter := txContext.GetQueryIterator("generated-query-id")
|
|
Expect(iter).To(Equal(fakeIterator))
|
|
retCount := txContext.GetTotalReturnCount("generated-query-id")
|
|
Expect(*retCount).To(Equal(int32(0)))
|
|
})
|
|
|
|
Context("and ExecuteQueryOnPrivateData fails due to ledger error", func() {
|
|
BeforeEach(func() {
|
|
fakeTxSimulator.ExecuteQueryOnPrivateDataReturns(nil, errors.New("pizza"))
|
|
})
|
|
|
|
It("returns the error", func() {
|
|
_, err := handler.HandleGetQueryResult(incomingMessage, txContext)
|
|
Expect(err).To(MatchError("pizza"))
|
|
})
|
|
})
|
|
|
|
Context("and ExecuteQueryOnPrivateData fails due to no read access permission", func() {
|
|
BeforeEach(func() {
|
|
fakeCollectionStore.RetrieveReadWritePermissionReturns(false, false, nil)
|
|
})
|
|
|
|
It("returns the error", func() {
|
|
_, err := handler.HandleGetQueryResult(incomingMessage, txContext)
|
|
Expect(err).To(MatchError("tx creator does not have read access" +
|
|
" permission on privatedata in chaincodeName:cc-instance-name" +
|
|
" collectionName: collection-name"))
|
|
})
|
|
})
|
|
|
|
Context("and ExecuteQueryOnPrivateData fails due to error in checking the read access permission", func() {
|
|
BeforeEach(func() {
|
|
fakeCollectionStore.RetrieveReadWritePermissionReturns(false, false, errors.New("no collection config"))
|
|
})
|
|
|
|
It("returns the error", func() {
|
|
_, err := handler.HandleGetQueryResult(incomingMessage, txContext)
|
|
Expect(err).To(MatchError("no collection config"))
|
|
})
|
|
})
|
|
|
|
Context("and ExecuteQueryOnPrivateData fails due to Init transaction", func() {
|
|
BeforeEach(func() {
|
|
txContext.IsInitTransaction = true
|
|
})
|
|
|
|
It("returns the error from errorIfInitTransaction", func() {
|
|
_, err := handler.HandleGetQueryResult(incomingMessage, txContext)
|
|
Expect(err).To(MatchError("private data APIs are not allowed in chaincode Init()"))
|
|
})
|
|
})
|
|
})
|
|
|
|
It("builds the query response", func() {
|
|
_, err := handler.HandleGetQueryResult(incomingMessage, txContext)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(fakeQueryResponseBuilder.BuildQueryResponseCallCount()).To(Equal(1))
|
|
tctx, iter, iterID, _, _ := fakeQueryResponseBuilder.BuildQueryResponseArgsForCall(0)
|
|
Expect(tctx).To(Equal(txContext))
|
|
Expect(iter).To(Equal(fakeIterator))
|
|
Expect(iterID).To(Equal("generated-query-id"))
|
|
})
|
|
|
|
Context("when building the query response fails", func() {
|
|
BeforeEach(func() {
|
|
fakeQueryResponseBuilder.BuildQueryResponseReturns(nil, errors.New("peppers"))
|
|
})
|
|
|
|
It("returns the error", func() {
|
|
_, err := handler.HandleGetQueryResult(incomingMessage, txContext)
|
|
Expect(err).To(MatchError("peppers"))
|
|
})
|
|
|
|
It("cleans up the query context", func() {
|
|
handler.HandleGetQueryResult(incomingMessage, txContext)
|
|
|
|
pqr := txContext.GetPendingQueryResult("generated-query-id")
|
|
Expect(pqr).To(BeNil())
|
|
iter := txContext.GetQueryIterator("generated-query-id")
|
|
Expect(iter).To(BeNil())
|
|
retCount := txContext.GetTotalReturnCount("generated-query-id")
|
|
Expect(retCount).To(BeNil())
|
|
})
|
|
})
|
|
|
|
Context("when unmarshalling the request fails", func() {
|
|
BeforeEach(func() {
|
|
incomingMessage.Payload = []byte("this-is-a-bogus-payload")
|
|
})
|
|
|
|
It("returns an error", func() {
|
|
_, err := handler.HandleGetQueryResult(incomingMessage, txContext)
|
|
Expect(err).To(Not(BeNil()))
|
|
Expect(err.Error()).To(HavePrefix("unmarshal failed:"))
|
|
})
|
|
})
|
|
|
|
Context("when marshaling the response fails", func() {
|
|
BeforeEach(func() {
|
|
fakeQueryResponseBuilder.BuildQueryResponseReturns(nil, nil)
|
|
})
|
|
|
|
It("returns an error", func() {
|
|
_, err := handler.HandleGetQueryResult(incomingMessage, txContext)
|
|
Expect(err).To(Not(BeNil()))
|
|
Expect(err.Error()).To(HavePrefix("marshal failed:"))
|
|
})
|
|
|
|
It("cleans up the query context", func() {
|
|
handler.HandleGetQueryResult(incomingMessage, txContext)
|
|
|
|
pqr := txContext.GetPendingQueryResult("generated-query-id")
|
|
Expect(pqr).To(BeNil())
|
|
iter := txContext.GetQueryIterator("generated-query-id")
|
|
Expect(iter).To(BeNil())
|
|
retCount := txContext.GetTotalReturnCount("generated-query-id")
|
|
Expect(retCount).To(BeNil())
|
|
})
|
|
})
|
|
})
|
|
|
|
Describe("HandlePurgePrivateData", func() {
|
|
var incomingMessage *pb.ChaincodeMessage
|
|
var request *pb.PurgePrivateState
|
|
|
|
BeforeEach(func() {
|
|
request = &pb.PurgePrivateState{
|
|
Key: "purge-state-key",
|
|
}
|
|
payload, err := proto.Marshal(request)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
incomingMessage = &pb.ChaincodeMessage{
|
|
Type: pb.ChaincodeMessage_PURGE_PRIVATE_DATA,
|
|
Payload: payload,
|
|
Txid: "tx-id",
|
|
ChannelId: "channel-id",
|
|
}
|
|
})
|
|
|
|
Context("when unmarshalling the request fails", func() {
|
|
BeforeEach(func() {
|
|
incomingMessage.Payload = []byte("this-is-a-bogus-payload")
|
|
})
|
|
|
|
It("returns an error", func() {
|
|
_, err := handler.HandlePurgePrivateData(incomingMessage, txContext)
|
|
Expect(err).To(Not(BeNil()))
|
|
Expect(err.Error()).To(HavePrefix("unmarshal failed"))
|
|
})
|
|
})
|
|
|
|
Context("when collection is not set", func() {
|
|
It("calls PurgePrivateState on the transaction simulator", func() {
|
|
_, err := handler.HandlePurgePrivateData(incomingMessage, txContext)
|
|
Expect(err).To(MatchError("only applicable for private data"))
|
|
})
|
|
|
|
Context("when PurgePrivateState returns an error", func() {
|
|
BeforeEach(func() {
|
|
fakeTxSimulator.PurgePrivateDataReturns(errors.New("orange"))
|
|
request.Collection = "collection-name"
|
|
payload, err := proto.Marshal(request)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
incomingMessage.Payload = payload
|
|
fakeCollectionStore.RetrieveReadWritePermissionReturns(false, true, nil) // to
|
|
})
|
|
|
|
It("return an error", func() {
|
|
_, err := handler.HandlePurgePrivateData(incomingMessage, txContext)
|
|
Expect(err).To(MatchError("orange"))
|
|
})
|
|
})
|
|
})
|
|
|
|
Context("when collection is set", func() {
|
|
BeforeEach(func() {
|
|
request.Collection = "collection-name"
|
|
payload, err := proto.Marshal(request)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
incomingMessage.Payload = payload
|
|
fakeCollectionStore.RetrieveReadWritePermissionReturns(false, true, nil) // to
|
|
})
|
|
|
|
It("calls PurgePrivateData on the transaction simulator", func() {
|
|
_, err := handler.HandlePurgePrivateData(incomingMessage, txContext)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(fakeTxSimulator.PurgePrivateDataCallCount()).To(Equal(1))
|
|
ccname, collection, key := fakeTxSimulator.PurgePrivateDataArgsForCall(0)
|
|
Expect(ccname).To(Equal("cc-instance-name"))
|
|
Expect(collection).To(Equal("collection-name"))
|
|
Expect(key).To(Equal("purge-state-key"))
|
|
})
|
|
|
|
Context("when PurgePrivateData fails due to ledger error", func() {
|
|
BeforeEach(func() {
|
|
fakeTxSimulator.PurgePrivateDataReturns(errors.New("mango"))
|
|
})
|
|
|
|
It("returns an error", func() {
|
|
_, err := handler.HandlePurgePrivateData(incomingMessage, txContext)
|
|
Expect(err).To(MatchError("mango"))
|
|
})
|
|
})
|
|
|
|
Context("when PurgePrivateData fails due to Init transaction", func() {
|
|
BeforeEach(func() {
|
|
txContext.IsInitTransaction = true
|
|
})
|
|
|
|
It("returns the error from errorIfInitTransaction", func() {
|
|
_, err := handler.HandlePurgePrivateData(incomingMessage, txContext)
|
|
Expect(err).To(MatchError("private data APIs are not allowed in chaincode Init()"))
|
|
})
|
|
})
|
|
|
|
Context("when PurgePrivateData fails due to no write access permission", func() {
|
|
BeforeEach(func() {
|
|
fakeCollectionStore.RetrieveReadWritePermissionReturns(false, false, nil)
|
|
})
|
|
|
|
It("returns the error from errorIfCreatorHasNoWriteAccess", func() {
|
|
_, err := handler.HandlePurgePrivateData(incomingMessage, txContext)
|
|
Expect(err).To(MatchError("tx creator does not have write access" +
|
|
" permission on privatedata in chaincodeName:cc-instance-name" +
|
|
" collectionName: collection-name"))
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
Describe("HandleGetHistoryForKey", func() {
|
|
var (
|
|
request *pb.GetHistoryForKey
|
|
incomingMessage *pb.ChaincodeMessage
|
|
expectedQueryResponse *pb.QueryResponse
|
|
fakeIterator *mock.QueryResultsIterator
|
|
)
|
|
|
|
BeforeEach(func() {
|
|
request = &pb.GetHistoryForKey{
|
|
Key: "history-key",
|
|
}
|
|
payload, err := proto.Marshal(request)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
incomingMessage = &pb.ChaincodeMessage{
|
|
Type: pb.ChaincodeMessage_GET_HISTORY_FOR_KEY,
|
|
Payload: payload,
|
|
Txid: "tx-id",
|
|
ChannelId: "channel-id",
|
|
}
|
|
|
|
expectedQueryResponse = &pb.QueryResponse{
|
|
Id: "query-response-id",
|
|
}
|
|
fakeQueryResponseBuilder.BuildQueryResponseReturns(expectedQueryResponse, nil)
|
|
|
|
fakeIterator = &mock.QueryResultsIterator{}
|
|
fakeHistoryQueryExecutor.GetHistoryForKeyReturns(fakeIterator, nil)
|
|
})
|
|
|
|
It("calls GetHistoryForKey on the history query executor", func() {
|
|
_, err := handler.HandleGetHistoryForKey(incomingMessage, txContext)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(fakeHistoryQueryExecutor.GetHistoryForKeyCallCount()).To(Equal(1))
|
|
ccname, key := fakeHistoryQueryExecutor.GetHistoryForKeyArgsForCall(0)
|
|
Expect(ccname).To(Equal("cc-instance-name"))
|
|
Expect(key).To(Equal("history-key"))
|
|
})
|
|
|
|
It("initializes a query context", func() {
|
|
_, err := handler.HandleGetHistoryForKey(incomingMessage, txContext)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
pqr := txContext.GetPendingQueryResult("generated-query-id")
|
|
Expect(pqr).To(Equal(&chaincode.PendingQueryResult{}))
|
|
iter := txContext.GetQueryIterator("generated-query-id")
|
|
Expect(iter).To(Equal(fakeIterator))
|
|
retCount := txContext.GetTotalReturnCount("generated-query-id")
|
|
Expect(*retCount).To(Equal(int32(0)))
|
|
})
|
|
|
|
It("builds a query response", func() {
|
|
_, err := handler.HandleGetHistoryForKey(incomingMessage, txContext)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(fakeQueryResponseBuilder.BuildQueryResponseCallCount()).To(Equal(1))
|
|
tctx, iter, iterID, _, _ := fakeQueryResponseBuilder.BuildQueryResponseArgsForCall(0)
|
|
Expect(tctx).To(Equal(txContext))
|
|
Expect(iter).To(Equal(fakeIterator))
|
|
Expect(iterID).To(Equal("generated-query-id"))
|
|
})
|
|
|
|
Context("when unmarshalling the request fails", func() {
|
|
BeforeEach(func() {
|
|
incomingMessage.Payload = []byte("this-is-a-bogus-payload")
|
|
})
|
|
|
|
It("returns an error", func() {
|
|
_, err := handler.HandleGetHistoryForKey(incomingMessage, txContext)
|
|
Expect(err).To(Not(BeNil()))
|
|
Expect(err.Error()).To(HavePrefix("unmarshal failed:"))
|
|
})
|
|
})
|
|
|
|
Context("when the history query executor fails", func() {
|
|
BeforeEach(func() {
|
|
fakeHistoryQueryExecutor.GetHistoryForKeyReturns(nil, errors.New("pepperoni"))
|
|
})
|
|
|
|
It("returns an error", func() {
|
|
_, err := handler.HandleGetHistoryForKey(incomingMessage, txContext)
|
|
Expect(err).To(MatchError("pepperoni"))
|
|
})
|
|
})
|
|
|
|
Context("when building the query response fails", func() {
|
|
BeforeEach(func() {
|
|
fakeQueryResponseBuilder.BuildQueryResponseReturns(nil, errors.New("mushrooms"))
|
|
})
|
|
|
|
It("returns an error", func() {
|
|
_, err := handler.HandleGetHistoryForKey(incomingMessage, txContext)
|
|
Expect(err).To(MatchError("mushrooms"))
|
|
})
|
|
|
|
It("cleans up the query context", func() {
|
|
handler.HandleGetHistoryForKey(incomingMessage, txContext)
|
|
|
|
pqr := txContext.GetPendingQueryResult("generated-query-id")
|
|
Expect(pqr).To(BeNil())
|
|
iter := txContext.GetQueryIterator("generated-query-id")
|
|
Expect(iter).To(BeNil())
|
|
retCount := txContext.GetTotalReturnCount("generated-query-id")
|
|
Expect(retCount).To(BeNil())
|
|
})
|
|
})
|
|
|
|
Context("when marshaling the query response fails", func() {
|
|
BeforeEach(func() {
|
|
fakeQueryResponseBuilder.BuildQueryResponseReturns(nil, nil)
|
|
})
|
|
|
|
It("returns an error", func() {
|
|
_, err := handler.HandleGetHistoryForKey(incomingMessage, txContext)
|
|
Expect(err).To(Not(BeNil()))
|
|
Expect(err.Error()).To(HavePrefix("marshal failed:"))
|
|
})
|
|
|
|
It("cleans up the query context", func() {
|
|
handler.HandleGetHistoryForKey(incomingMessage, txContext)
|
|
|
|
pqr := txContext.GetPendingQueryResult("generated-query-id")
|
|
Expect(pqr).To(BeNil())
|
|
iter := txContext.GetQueryIterator("generated-query-id")
|
|
Expect(iter).To(BeNil())
|
|
retCount := txContext.GetTotalReturnCount("generated-query-id")
|
|
Expect(retCount).To(BeNil())
|
|
})
|
|
})
|
|
|
|
Context("when HistoryQueryExecutor is nil", func() {
|
|
BeforeEach(func() {
|
|
txContext.HistoryQueryExecutor = nil
|
|
})
|
|
|
|
It("returns an error", func() {
|
|
_, err := handler.HandleGetHistoryForKey(incomingMessage, txContext)
|
|
Expect(err).To(MatchError("history database is not enabled"))
|
|
})
|
|
})
|
|
})
|
|
|
|
Describe("HandleInvokeChaincode", func() {
|
|
var (
|
|
expectedSignedProp *pb.SignedProposal
|
|
expectedProposal *pb.Proposal
|
|
fakePeerLedger *mock.PeerLedger
|
|
newTxSimulator *mock.TxSimulator
|
|
newHistoryQueryExecutor *mock.HistoryQueryExecutor
|
|
request *pb.ChaincodeSpec
|
|
incomingMessage *pb.ChaincodeMessage
|
|
responseMessage *pb.ChaincodeMessage
|
|
)
|
|
|
|
BeforeEach(func() {
|
|
expectedProposal = &pb.Proposal{
|
|
Header: []byte("proposal-header"),
|
|
Payload: []byte("proposal-payload"),
|
|
}
|
|
expectedSignedProp = &pb.SignedProposal{
|
|
ProposalBytes: []byte("signed-proposal-bytes"),
|
|
Signature: []byte("signature"),
|
|
}
|
|
txContext.Proposal = expectedProposal
|
|
txContext.SignedProp = expectedSignedProp
|
|
|
|
newTxSimulator = &mock.TxSimulator{}
|
|
newHistoryQueryExecutor = &mock.HistoryQueryExecutor{}
|
|
fakePeerLedger = &mock.PeerLedger{}
|
|
fakePeerLedger.NewTxSimulatorReturns(newTxSimulator, nil)
|
|
fakeLedgerGetter.GetLedgerReturns(fakePeerLedger)
|
|
fakePeerLedger.NewHistoryQueryExecutorReturns(newHistoryQueryExecutor, nil)
|
|
|
|
request = &pb.ChaincodeSpec{
|
|
ChaincodeId: &pb.ChaincodeID{
|
|
Name: "target-chaincode-name:target-version",
|
|
},
|
|
}
|
|
payload, err := proto.Marshal(request)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
incomingMessage = &pb.ChaincodeMessage{
|
|
Type: pb.ChaincodeMessage_INVOKE_CHAINCODE,
|
|
Payload: payload,
|
|
Txid: "tx-id",
|
|
ChannelId: "channel-id",
|
|
}
|
|
|
|
responseMessage = &pb.ChaincodeMessage{}
|
|
fakeInvoker.InvokeReturns(responseMessage, nil)
|
|
})
|
|
|
|
It("evaluates the access control policy", func() {
|
|
_, err := handler.HandleInvokeChaincode(incomingMessage, txContext)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(fakeACLProvider.CheckACLCallCount()).To(Equal(1))
|
|
resource, chainID, proposal := fakeACLProvider.CheckACLArgsForCall(0)
|
|
Expect(resource).To(Equal(ar.Peer_ChaincodeToChaincode))
|
|
Expect(chainID).To(Equal("channel-id"))
|
|
Expect(proposal).To(Equal(expectedSignedProp))
|
|
})
|
|
|
|
Context("when the target channel is different from the context", func() {
|
|
BeforeEach(func() {
|
|
request = &pb.ChaincodeSpec{
|
|
ChaincodeId: &pb.ChaincodeID{
|
|
Name: "target-chaincode-name:target-version/target-channel-id",
|
|
},
|
|
}
|
|
payload, err := proto.Marshal(request)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
incomingMessage.Payload = payload
|
|
})
|
|
|
|
It("uses the channel form the target for access checks", func() {
|
|
_, err := handler.HandleInvokeChaincode(incomingMessage, txContext)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(fakeACLProvider.CheckACLCallCount()).To(Equal(1))
|
|
resource, chainID, proposal := fakeACLProvider.CheckACLArgsForCall(0)
|
|
Expect(resource).To(Equal(ar.Peer_ChaincodeToChaincode))
|
|
Expect(chainID).To(Equal("target-channel-id"))
|
|
Expect(proposal).To(Equal(expectedSignedProp))
|
|
})
|
|
|
|
It("gets the ledger for the target channel", func() {
|
|
_, err := handler.HandleInvokeChaincode(incomingMessage, txContext)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(fakeLedgerGetter.GetLedgerCallCount()).To(Equal(1))
|
|
chainID := fakeLedgerGetter.GetLedgerArgsForCall(0)
|
|
Expect(chainID).To(Equal("target-channel-id"))
|
|
})
|
|
|
|
It("creates a new tx simulator for target execution", func() {
|
|
_, err := handler.HandleInvokeChaincode(incomingMessage, txContext)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(fakePeerLedger.NewTxSimulatorCallCount()).To(Equal(1))
|
|
txid := fakePeerLedger.NewTxSimulatorArgsForCall(0)
|
|
Expect(txid).To(Equal("tx-id"))
|
|
})
|
|
|
|
It("provides the new simulator in the context used for execution", func() {
|
|
_, err := handler.HandleInvokeChaincode(incomingMessage, txContext)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(fakeInvoker.InvokeCallCount()).To(Equal(1))
|
|
txParams, _, _ := fakeInvoker.InvokeArgsForCall(0)
|
|
Expect(txParams.TXSimulator).To(BeIdenticalTo(newTxSimulator)) // same instance, not just equal
|
|
})
|
|
|
|
It("creates a new history query executor for target execution", func() {
|
|
_, err := handler.HandleInvokeChaincode(incomingMessage, txContext)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(fakePeerLedger.NewHistoryQueryExecutorCallCount()).To(Equal(1))
|
|
})
|
|
|
|
It("provides the new history query executor in the context used for execution", func() {
|
|
_, err := handler.HandleInvokeChaincode(incomingMessage, txContext)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(fakeInvoker.InvokeCallCount()).To(Equal(1))
|
|
txParams, _, _ := fakeInvoker.InvokeArgsForCall(0)
|
|
Expect(txParams.HistoryQueryExecutor).To(BeIdenticalTo(newHistoryQueryExecutor)) // same instance, not just equal
|
|
})
|
|
|
|
It("marks the new transaction simulator as done after execute", func() {
|
|
fakeInvoker.InvokeStub = func(*ccprovider.TransactionParams, string, *pb.ChaincodeInput) (*pb.ChaincodeMessage, error) {
|
|
Expect(newTxSimulator.DoneCallCount()).To(Equal(0))
|
|
return responseMessage, nil
|
|
}
|
|
_, err := handler.HandleInvokeChaincode(incomingMessage, txContext)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(fakeInvoker.InvokeCallCount()).To(Equal(1))
|
|
Expect(newTxSimulator.DoneCallCount()).To(Equal(1))
|
|
})
|
|
|
|
Context("when getting the ledger for the target channel fails", func() {
|
|
BeforeEach(func() {
|
|
fakeLedgerGetter.GetLedgerReturns(nil)
|
|
})
|
|
|
|
It("returns an error", func() {
|
|
_, err := handler.HandleInvokeChaincode(incomingMessage, txContext)
|
|
Expect(err).To(MatchError("failed to find ledger for channel: target-channel-id"))
|
|
})
|
|
})
|
|
|
|
Context("when creating the new tx simulator fails", func() {
|
|
BeforeEach(func() {
|
|
fakePeerLedger.NewTxSimulatorReturns(nil, errors.New("bonkers"))
|
|
})
|
|
|
|
It("returns an error", func() {
|
|
_, err := handler.HandleInvokeChaincode(incomingMessage, txContext)
|
|
Expect(err).To(MatchError("bonkers"))
|
|
})
|
|
})
|
|
|
|
Context("when creating the new history query executor fails", func() {
|
|
BeforeEach(func() {
|
|
fakePeerLedger.NewHistoryQueryExecutorReturns(nil, errors.New("razzies"))
|
|
})
|
|
|
|
It("returns an error", func() {
|
|
_, err := handler.HandleInvokeChaincode(incomingMessage, txContext)
|
|
Expect(err).To(MatchError("razzies"))
|
|
})
|
|
})
|
|
})
|
|
|
|
Context("when the target is a system chaincode", func() {
|
|
BeforeEach(func() {
|
|
builtinSCCs["target-chaincode-name"] = struct{}{}
|
|
})
|
|
|
|
It("does not perform acl checks", func() {
|
|
_, err := handler.HandleInvokeChaincode(incomingMessage, txContext)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Expect(fakeACLProvider.CheckACLCallCount()).To(Equal(0))
|
|
})
|
|
})
|
|
|
|
Context("when the target is not a system chaincode", func() {
|
|
Context("when the signed proposal is nil", func() {
|
|
BeforeEach(func() {
|
|
txContext.SignedProp = nil
|
|
})
|
|
|
|
It("returns an error", func() {
|
|
_, err := handler.HandleInvokeChaincode(incomingMessage, txContext)
|
|
Expect(err).To(MatchError("signed proposal must not be nil from caller [channel-id.target-chaincode-name#target-version]"))
|
|
})
|
|
})
|
|
})
|
|
|
|
Context("when the access control check fails", func() {
|
|
BeforeEach(func() {
|
|
fakeACLProvider.CheckACLReturns(errors.New("no-soup-for-you"))
|
|
})
|
|
|
|
It("returns the error", func() {
|
|
_, err := handler.HandleInvokeChaincode(incomingMessage, txContext)
|
|
Expect(err).To(MatchError("no-soup-for-you"))
|
|
})
|
|
})
|
|
|
|
Context("when execute fails", func() {
|
|
BeforeEach(func() {
|
|
fakeInvoker.InvokeReturns(nil, errors.New("lemons"))
|
|
})
|
|
|
|
It("returns an error", func() {
|
|
_, err := handler.HandleInvokeChaincode(incomingMessage, txContext)
|
|
Expect(err).To(MatchError("execute failed: lemons"))
|
|
})
|
|
})
|
|
|
|
Context("when unmarshalling the request fails", func() {
|
|
BeforeEach(func() {
|
|
incomingMessage.Payload = []byte("this-is-a-bogus-payload")
|
|
})
|
|
|
|
It("returns an error", func() {
|
|
_, err := handler.HandleInvokeChaincode(incomingMessage, txContext)
|
|
Expect(err).To(Not(BeNil()))
|
|
Expect(err.Error()).To(HavePrefix("unmarshal failed"))
|
|
})
|
|
})
|
|
|
|
Context("when marshaling the response fails", func() {
|
|
BeforeEach(func() {
|
|
fakeInvoker.InvokeReturns(nil, nil)
|
|
})
|
|
|
|
It("returns an error", func() {
|
|
_, err := handler.HandleInvokeChaincode(incomingMessage, txContext)
|
|
Expect(err).To(Not(BeNil()))
|
|
Expect(err.Error()).To(HavePrefix("marshal failed:"))
|
|
})
|
|
})
|
|
})
|
|
|
|
Describe("Execute", func() {
|
|
var (
|
|
incomingMessage *pb.ChaincodeMessage
|
|
expectedProposal *pb.Proposal
|
|
expectedSignedProp *pb.SignedProposal
|
|
txParams *ccprovider.TransactionParams
|
|
)
|
|
|
|
BeforeEach(func() {
|
|
request := &pb.ChaincodeInput{
|
|
Args: util.ToChaincodeArgs("arg1", "arg2"),
|
|
}
|
|
payload, err := proto.Marshal(request)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
expectedProposal = &pb.Proposal{
|
|
Header: []byte("proposal-header"),
|
|
Payload: []byte("proposal-payload"),
|
|
}
|
|
|
|
expectedSignedProp = &pb.SignedProposal{
|
|
ProposalBytes: []byte("signed-proposal-bytes"),
|
|
Signature: []byte("signature"),
|
|
}
|
|
|
|
incomingMessage = &pb.ChaincodeMessage{
|
|
Type: pb.ChaincodeMessage_TRANSACTION,
|
|
Txid: "tx-id",
|
|
Payload: payload,
|
|
ChannelId: "channel-id",
|
|
}
|
|
|
|
txParams = &ccprovider.TransactionParams{
|
|
TxID: "tx-id",
|
|
ChannelID: "channel-id",
|
|
SignedProp: expectedSignedProp,
|
|
Proposal: expectedProposal,
|
|
}
|
|
})
|
|
|
|
It("creates transaction context", func() {
|
|
close(responseNotifier)
|
|
handler.Execute(txParams, "chaincode-name", incomingMessage, time.Second)
|
|
|
|
Expect(fakeContextRegistry.CreateCallCount()).To(Equal(1))
|
|
Expect(fakeContextRegistry.CreateArgsForCall(0)).To(Equal(txParams))
|
|
})
|
|
|
|
It("sends an execute message to the chaincode with the correct proposal", func() {
|
|
expectedMessage := *incomingMessage
|
|
expectedMessage.Proposal = expectedSignedProp
|
|
|
|
close(responseNotifier)
|
|
handler.Execute(txParams, "chaincode-name", incomingMessage, time.Second)
|
|
|
|
Eventually(fakeChatStream.SendCallCount).Should(Equal(1))
|
|
Consistently(fakeChatStream.SendCallCount).Should(Equal(1))
|
|
msg := fakeChatStream.SendArgsForCall(0)
|
|
Expect(msg).To(Equal(&expectedMessage))
|
|
Expect(msg.Proposal).To(Equal(expectedSignedProp))
|
|
})
|
|
|
|
It("waits for the chaincode to respond", func() {
|
|
doneCh := make(chan struct{})
|
|
go func() {
|
|
handler.Execute(txParams, "chaincode-name", incomingMessage, time.Second)
|
|
close(doneCh)
|
|
}()
|
|
|
|
Eventually(fakeChatStream.SendCallCount).Should(Equal(1))
|
|
Consistently(fakeChatStream.SendCallCount).Should(Equal(1))
|
|
|
|
Consistently(doneCh).ShouldNot(Receive())
|
|
Eventually(responseNotifier).Should(BeSent(&pb.ChaincodeMessage{}))
|
|
Eventually(doneCh).Should(BeClosed())
|
|
})
|
|
|
|
It("returns the chaincode response", func() {
|
|
Eventually(responseNotifier).Should(BeSent(&pb.ChaincodeMessage{Txid: "a-transaction-id"}))
|
|
|
|
resp, err := handler.Execute(txParams, "chaincode-name", incomingMessage, time.Second)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(resp).To(Equal(&pb.ChaincodeMessage{Txid: "a-transaction-id"}))
|
|
})
|
|
|
|
It("deletes the transaction context", func() {
|
|
close(responseNotifier)
|
|
handler.Execute(txParams, "chaincode-name", incomingMessage, time.Second)
|
|
|
|
Expect(fakeContextRegistry.DeleteCallCount()).Should(Equal(1))
|
|
channelID, txid := fakeContextRegistry.DeleteArgsForCall(0)
|
|
Expect(channelID).To(Equal("channel-id"))
|
|
Expect(txid).To(Equal("tx-id"))
|
|
})
|
|
|
|
Context("when the serial send fails", func() {
|
|
BeforeEach(func() {
|
|
fakeChatStream.SendReturns(errors.New("where-is-waldo?"))
|
|
})
|
|
|
|
It("returns an error before timing out", func() {
|
|
respCh := make(chan *pb.ChaincodeMessage, 1)
|
|
go func() {
|
|
defer GinkgoRecover()
|
|
resp, err := handler.Execute(txParams, "chaincode-name", incomingMessage, time.Second)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Eventually(respCh).Should(BeSent(resp))
|
|
}()
|
|
|
|
Eventually(respCh).Should(Receive(Equal(&pb.ChaincodeMessage{
|
|
Type: pb.ChaincodeMessage_ERROR,
|
|
Payload: []byte("[tx-id] error sending TRANSACTION: where-is-waldo?"),
|
|
Txid: "tx-id",
|
|
ChannelId: "channel-id",
|
|
})))
|
|
})
|
|
})
|
|
|
|
Context("when the proposal is missing", func() {
|
|
BeforeEach(func() {
|
|
txParams.Proposal = nil
|
|
})
|
|
|
|
It("sends a nil proposal", func() {
|
|
close(responseNotifier)
|
|
_, err := handler.Execute(txParams, "chaincode-name", incomingMessage, time.Second)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
Eventually(fakeChatStream.SendCallCount).Should(Equal(1))
|
|
msg := fakeChatStream.SendArgsForCall(0)
|
|
Expect(msg).NotTo(BeNil())
|
|
Expect(msg.Proposal).To(BeNil())
|
|
})
|
|
})
|
|
|
|
Context("when the signed proposal is missing", func() {
|
|
BeforeEach(func() {
|
|
txParams.SignedProp = nil
|
|
})
|
|
|
|
It("returns an error", func() {
|
|
close(responseNotifier)
|
|
_, err := handler.Execute(txParams, "chaincode-name", incomingMessage, time.Second)
|
|
|
|
Expect(err).To(MatchError("failed getting proposal context. Signed proposal is nil"))
|
|
})
|
|
|
|
It("deletes the transaction context", func() {
|
|
close(responseNotifier)
|
|
handler.Execute(txParams, "chaincode-name", incomingMessage, time.Second)
|
|
|
|
Expect(fakeContextRegistry.DeleteCallCount()).Should(Equal(1))
|
|
channelID, txid := fakeContextRegistry.DeleteArgsForCall(0)
|
|
Expect(channelID).To(Equal("channel-id"))
|
|
Expect(txid).To(Equal("tx-id"))
|
|
})
|
|
})
|
|
|
|
Context("when creating the transaction context fails", func() {
|
|
BeforeEach(func() {
|
|
fakeContextRegistry.CreateReturns(nil, errors.New("burger"))
|
|
})
|
|
|
|
It("returns an error", func() {
|
|
_, err := handler.Execute(txParams, "chaincode-name", incomingMessage, time.Second)
|
|
Expect(err).To(MatchError("burger"))
|
|
})
|
|
|
|
It("does not try to delete the tranasction context", func() {
|
|
handler.Execute(txParams, "chaincode-name", incomingMessage, time.Second)
|
|
Expect(fakeContextRegistry.CreateCallCount()).To(Equal(1))
|
|
Expect(fakeContextRegistry.DeleteCallCount()).To(Equal(0))
|
|
})
|
|
})
|
|
|
|
Context("when the chaincode stream terminates", func() {
|
|
It("returns an error", func() {
|
|
streamDoneChan := make(chan struct{})
|
|
chaincode.SetStreamDoneChan(handler, streamDoneChan)
|
|
|
|
errCh := make(chan error, 1)
|
|
go func() {
|
|
_, err := handler.Execute(txParams, "chaincode-name", incomingMessage, time.Hour)
|
|
errCh <- err
|
|
}()
|
|
Consistently(errCh).ShouldNot(Receive())
|
|
|
|
close(streamDoneChan)
|
|
Eventually(errCh).Should(Receive(MatchError("chaincode stream terminated")))
|
|
})
|
|
})
|
|
|
|
Context("when execute times out", func() {
|
|
It("returns an error", func() {
|
|
errCh := make(chan error, 1)
|
|
go func() {
|
|
_, err := handler.Execute(txParams, "chaincode-name", incomingMessage, time.Millisecond)
|
|
errCh <- err
|
|
}()
|
|
Eventually(errCh).Should(Receive(MatchError("timeout expired while executing transaction")))
|
|
})
|
|
|
|
It("records execute timeouts", func() {
|
|
errCh := make(chan error, 1)
|
|
go func() {
|
|
_, err := handler.Execute(txParams, "chaincode-name", incomingMessage, time.Millisecond)
|
|
errCh <- err
|
|
}()
|
|
Eventually(errCh).Should(Receive(MatchError("timeout expired while executing transaction")))
|
|
Expect(fakeExecuteTimeouts.WithCallCount()).To(Equal(1))
|
|
labelValues := fakeExecuteTimeouts.WithArgsForCall(0)
|
|
Expect(labelValues).To(Equal([]string{
|
|
"chaincode", "test-handler-name:1.0",
|
|
}))
|
|
Expect(fakeExecuteTimeouts.AddCallCount()).To(Equal(1))
|
|
Expect(fakeExecuteTimeouts.AddArgsForCall(0)).To(BeNumerically("~", 1.0))
|
|
})
|
|
|
|
It("deletes the transaction context", func() {
|
|
handler.Execute(txParams, "chaincode-name", incomingMessage, time.Millisecond)
|
|
|
|
Expect(fakeContextRegistry.DeleteCallCount()).Should(Equal(1))
|
|
channelID, txid := fakeContextRegistry.DeleteArgsForCall(0)
|
|
Expect(channelID).To(Equal("channel-id"))
|
|
Expect(txid).To(Equal("tx-id"))
|
|
})
|
|
})
|
|
})
|
|
|
|
Describe("HandleRegister", func() {
|
|
var incomingMessage *pb.ChaincodeMessage
|
|
|
|
BeforeEach(func() {
|
|
request := &pb.ChaincodeID{
|
|
Name: "chaincode-id-name",
|
|
Version: "chaincode-id-version",
|
|
}
|
|
payload, err := proto.Marshal(request)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
incomingMessage = &pb.ChaincodeMessage{
|
|
Type: pb.ChaincodeMessage_REGISTER,
|
|
Txid: "tx-id",
|
|
ChannelId: "channel-id",
|
|
Payload: payload,
|
|
}
|
|
})
|
|
|
|
It("registers the handler with the registry", func() {
|
|
handler.HandleRegister(incomingMessage)
|
|
|
|
Expect(fakeHandlerRegistry.RegisterCallCount()).To(Equal(1))
|
|
h := fakeHandlerRegistry.RegisterArgsForCall(0)
|
|
Expect(h).To(Equal(handler))
|
|
})
|
|
|
|
It("transitions the handler into ready state", func() {
|
|
handler.HandleRegister(incomingMessage)
|
|
Eventually(handler.State).Should(Equal(chaincode.Ready))
|
|
})
|
|
|
|
It("notifies the registry that the handler is ready", func() {
|
|
handler.HandleRegister(incomingMessage)
|
|
Expect(fakeHandlerRegistry.FailedCallCount()).To(Equal(0))
|
|
Expect(fakeHandlerRegistry.ReadyCallCount()).To(Equal(1))
|
|
name := fakeHandlerRegistry.ReadyArgsForCall(0)
|
|
Expect(name).To(Equal("chaincode-id-name"))
|
|
})
|
|
|
|
It("sends registered and ready messsages", func() {
|
|
handler.HandleRegister(incomingMessage)
|
|
|
|
Eventually(fakeChatStream.SendCallCount).Should(Equal(2))
|
|
Consistently(fakeChatStream.SendCallCount).Should(Equal(2))
|
|
registeredMessage := fakeChatStream.SendArgsForCall(0)
|
|
readyMessage := fakeChatStream.SendArgsForCall(1)
|
|
|
|
Expect(registeredMessage).To(Equal(&pb.ChaincodeMessage{
|
|
Type: pb.ChaincodeMessage_REGISTERED,
|
|
}))
|
|
|
|
Expect(readyMessage).To(Equal(&pb.ChaincodeMessage{
|
|
Type: pb.ChaincodeMessage_READY,
|
|
}))
|
|
})
|
|
|
|
Context("when sending the ready message fails", func() {
|
|
BeforeEach(func() {
|
|
fakeChatStream.SendReturnsOnCall(1, errors.New("carrot"))
|
|
})
|
|
|
|
It("state remains in established", func() {
|
|
Expect(handler.State()).To(Equal(chaincode.Created))
|
|
handler.HandleRegister(incomingMessage)
|
|
Expect(handler.State()).To(Equal(chaincode.Established))
|
|
})
|
|
|
|
It("notifies the registry of the failure", func() {
|
|
handler.HandleRegister(incomingMessage)
|
|
Expect(fakeHandlerRegistry.ReadyCallCount()).To(Equal(0))
|
|
Expect(fakeHandlerRegistry.FailedCallCount()).To(Equal(1))
|
|
name, err := fakeHandlerRegistry.FailedArgsForCall(0)
|
|
Expect(name).To(Equal("chaincode-id-name"))
|
|
Expect(err).To(MatchError("[] error sending READY: carrot"))
|
|
})
|
|
})
|
|
|
|
Context("when registering the handler with registry fails", func() {
|
|
BeforeEach(func() {
|
|
fakeHandlerRegistry.RegisterReturns(errors.New("cake"))
|
|
})
|
|
|
|
It("remains in created state", func() {
|
|
Expect(handler.State()).To(Equal(chaincode.Created))
|
|
handler.HandleRegister(incomingMessage)
|
|
Expect(handler.State()).To(Equal(chaincode.Created))
|
|
})
|
|
})
|
|
|
|
Context("when sending a registered message failed", func() {
|
|
BeforeEach(func() {
|
|
fakeChatStream.SendReturns(errors.New("potato"))
|
|
})
|
|
|
|
It("ready is not sent", func() {
|
|
handler.HandleRegister(incomingMessage)
|
|
|
|
Eventually(fakeChatStream.SendCallCount).Should(Equal(1))
|
|
Consistently(fakeChatStream.SendCallCount).Should(Equal(1))
|
|
msg := fakeChatStream.SendArgsForCall(0)
|
|
Expect(msg.Type).To(Equal(pb.ChaincodeMessage_REGISTERED))
|
|
})
|
|
|
|
It("remains in created state", func() {
|
|
Expect(handler.State()).To(Equal(chaincode.Created))
|
|
handler.HandleRegister(incomingMessage)
|
|
Expect(handler.State()).To(Equal(chaincode.Created))
|
|
})
|
|
})
|
|
|
|
Context("when unmarshalling the request fails", func() {
|
|
BeforeEach(func() {
|
|
incomingMessage.Payload = []byte("this-is-a-bogus-payload")
|
|
})
|
|
|
|
It("sends no messages", func() {
|
|
handler.HandleRegister(incomingMessage)
|
|
Consistently(fakeChatStream.SendCallCount).Should(Equal(0))
|
|
})
|
|
})
|
|
})
|
|
|
|
Describe("ProcessStream", func() {
|
|
BeforeEach(func() {
|
|
incomingMessage := &pb.ChaincodeMessage{
|
|
Type: pb.ChaincodeMessage_KEEPALIVE,
|
|
Txid: "tx-id",
|
|
ChannelId: "channel-id",
|
|
}
|
|
fakeChatStream.RecvReturns(incomingMessage, nil)
|
|
})
|
|
|
|
It("receives messages until an error is received", func() {
|
|
fakeChatStream.RecvReturnsOnCall(99, nil, errors.New("done-for-now"))
|
|
handler.ProcessStream(fakeChatStream)
|
|
|
|
Eventually(fakeChatStream.RecvCallCount).Should(Equal(100))
|
|
})
|
|
|
|
It("manages the stream done channel", func() {
|
|
releaseChan := make(chan struct{})
|
|
fakeChatStream.RecvStub = func() (*pb.ChaincodeMessage, error) {
|
|
<-releaseChan
|
|
return nil, errors.New("cc-went-away")
|
|
}
|
|
go handler.ProcessStream(fakeChatStream)
|
|
Eventually(fakeChatStream.RecvCallCount).Should(Equal(1))
|
|
|
|
streamDoneChan := chaincode.StreamDone(handler)
|
|
Consistently(streamDoneChan).ShouldNot(Receive())
|
|
|
|
close(releaseChan)
|
|
Eventually(streamDoneChan).Should(BeClosed())
|
|
})
|
|
|
|
Context("when receive fails with an io.EOF", func() {
|
|
BeforeEach(func() {
|
|
fakeChatStream.RecvReturns(nil, io.EOF)
|
|
})
|
|
|
|
It("returns the error", func() {
|
|
err := handler.ProcessStream(fakeChatStream)
|
|
Expect(err).To(Equal(io.EOF))
|
|
})
|
|
})
|
|
|
|
Context("when receive fails", func() {
|
|
BeforeEach(func() {
|
|
fakeChatStream.RecvReturns(nil, errors.New("chocolate"))
|
|
})
|
|
|
|
It("returns an error", func() {
|
|
err := handler.ProcessStream(fakeChatStream)
|
|
Expect(err).To(MatchError("receive from chaincode support stream failed: chocolate"))
|
|
})
|
|
})
|
|
|
|
Context("when a nil message is received", func() {
|
|
BeforeEach(func() {
|
|
fakeChatStream.RecvReturns(nil, nil)
|
|
})
|
|
|
|
It("returns an error", func() {
|
|
err := handler.ProcessStream(fakeChatStream)
|
|
Expect(err).To(MatchError("received nil message, ending chaincode support stream"))
|
|
})
|
|
})
|
|
|
|
Describe("keepalive messages", func() {
|
|
var recvChan chan *pb.ChaincodeMessage
|
|
|
|
BeforeEach(func() {
|
|
recvChan = make(chan *pb.ChaincodeMessage, 1)
|
|
fakeChatStream.RecvStub = func() (*pb.ChaincodeMessage, error) {
|
|
msg := <-recvChan
|
|
return msg, nil
|
|
}
|
|
|
|
handler.Keepalive = 50 * time.Millisecond
|
|
})
|
|
|
|
It("sends a keep alive messages until the stream ends", func() {
|
|
errChan := make(chan error, 1)
|
|
go func() { errChan <- handler.ProcessStream(fakeChatStream) }()
|
|
|
|
Eventually(fakeChatStream.SendCallCount).Should(Equal(5))
|
|
recvChan <- nil
|
|
Eventually(errChan).Should(Receive())
|
|
|
|
for i := 0; i < 5; i++ {
|
|
m := fakeChatStream.SendArgsForCall(i)
|
|
Expect(m.Type).To(Equal(pb.ChaincodeMessage_KEEPALIVE))
|
|
}
|
|
})
|
|
|
|
Context("when keepalive is disabled", func() {
|
|
BeforeEach(func() {
|
|
handler.Keepalive = 0
|
|
})
|
|
|
|
It("does not send keep alive messages", func() {
|
|
errChan := make(chan error, 1)
|
|
go func() { errChan <- handler.ProcessStream(fakeChatStream) }()
|
|
|
|
Consistently(fakeChatStream.SendCallCount).Should(Equal(0))
|
|
recvChan <- nil
|
|
Eventually(errChan).Should(Receive())
|
|
})
|
|
})
|
|
})
|
|
|
|
Context("when handling a received message fails", func() {
|
|
var recvChan chan *pb.ChaincodeMessage
|
|
|
|
BeforeEach(func() {
|
|
recvChan = make(chan *pb.ChaincodeMessage, 1)
|
|
fakeChatStream.RecvStub = func() (*pb.ChaincodeMessage, error) {
|
|
msg := <-recvChan
|
|
return msg, nil
|
|
}
|
|
|
|
recvChan <- &pb.ChaincodeMessage{
|
|
Txid: "tx-id",
|
|
Type: pb.ChaincodeMessage_Type(9999),
|
|
}
|
|
})
|
|
|
|
It("returns an error", func() {
|
|
err := handler.ProcessStream(fakeChatStream)
|
|
Expect(err).To(MatchError("error handling message, ending stream: [tx-id] Fabric side handler cannot handle message (9999) while in created state"))
|
|
})
|
|
})
|
|
|
|
Context("when an async error is sent", func() {
|
|
var (
|
|
incomingMessage *pb.ChaincodeMessage
|
|
recvChan chan *pb.ChaincodeMessage
|
|
)
|
|
|
|
BeforeEach(func() {
|
|
request := &pb.ChaincodeInput{}
|
|
payload, err := proto.Marshal(request)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
|
|
incomingMessage = &pb.ChaincodeMessage{
|
|
Type: pb.ChaincodeMessage_TRANSACTION,
|
|
Txid: "tx-id",
|
|
Payload: payload,
|
|
ChannelId: "channel-id",
|
|
}
|
|
|
|
recvChan = make(chan *pb.ChaincodeMessage, 1)
|
|
shadowRecvChan := recvChan
|
|
fakeChatStream.RecvStub = func() (*pb.ChaincodeMessage, error) {
|
|
msg := <-shadowRecvChan
|
|
return msg, nil
|
|
}
|
|
fakeChatStream.SendReturns(errors.New("candy"))
|
|
})
|
|
|
|
AfterEach(func() {
|
|
close(recvChan)
|
|
})
|
|
|
|
It("returns an error", func() {
|
|
errChan := make(chan error, 1)
|
|
go func() { errChan <- handler.ProcessStream(fakeChatStream) }()
|
|
Eventually(fakeChatStream.RecvCallCount).ShouldNot(Equal(0)) // wait for loop to start
|
|
handler.Execute(&ccprovider.TransactionParams{}, "chaincode-name", incomingMessage, time.Millisecond) // force async error
|
|
|
|
Eventually(errChan).Should(Receive(MatchError("received error while sending message, ending chaincode support stream: [tx-id] error sending TRANSACTION: candy")))
|
|
})
|
|
|
|
It("stops receiving messages", func() {
|
|
errChan := make(chan error, 1)
|
|
go func() { errChan <- handler.ProcessStream(fakeChatStream) }()
|
|
Eventually(fakeChatStream.RecvCallCount).ShouldNot(Equal(0)) // wait for loop to start
|
|
handler.Execute(&ccprovider.TransactionParams{}, "chaincode-name", incomingMessage, time.Millisecond) // force async error
|
|
|
|
Eventually(fakeChatStream.RecvCallCount).Should(Equal(1))
|
|
Consistently(fakeChatStream.RecvCallCount).Should(Equal(1))
|
|
})
|
|
})
|
|
})
|
|
|
|
Describe("Notify", func() {
|
|
var fakeIterator *mock.QueryResultsIterator
|
|
var incomingMessage *pb.ChaincodeMessage
|
|
|
|
BeforeEach(func() {
|
|
fakeIterator = &mock.QueryResultsIterator{}
|
|
incomingMessage = &pb.ChaincodeMessage{
|
|
Txid: "tx-id",
|
|
ChannelId: "channel-id",
|
|
}
|
|
})
|
|
|
|
It("gets the transaction context", func() {
|
|
handler.Notify(incomingMessage)
|
|
|
|
Expect(fakeContextRegistry.GetCallCount()).To(Equal(1))
|
|
channelID, txID := fakeContextRegistry.GetArgsForCall(0)
|
|
Expect(channelID).To(Equal("channel-id"))
|
|
Expect(txID).To(Equal("tx-id"))
|
|
})
|
|
|
|
It("sends the message on response notifier", func() {
|
|
handler.Notify(incomingMessage)
|
|
|
|
Eventually(responseNotifier).Should(Receive(Equal(incomingMessage)))
|
|
})
|
|
|
|
It("should close query iterators on the transaction context", func() {
|
|
txContext.InitializeQueryContext("query-id", fakeIterator)
|
|
Expect(fakeIterator.CloseCallCount()).To(Equal(0))
|
|
|
|
handler.Notify(incomingMessage)
|
|
Eventually(fakeIterator.CloseCallCount).Should(Equal(1))
|
|
})
|
|
|
|
Context("when the transaction context cannot be found", func() {
|
|
BeforeEach(func() {
|
|
fakeContextRegistry.GetReturns(nil)
|
|
})
|
|
|
|
It("keeps calm and carries on", func() {
|
|
handler.Notify(incomingMessage)
|
|
Expect(fakeContextRegistry.GetCallCount()).To(Equal(1))
|
|
})
|
|
})
|
|
})
|
|
|
|
Describe("ParseName", func() {
|
|
It("parses the chaincode name", func() {
|
|
ci := chaincode.ParseName("name")
|
|
Expect(ci).To(Equal(&sysccprovider.ChaincodeInstance{ChaincodeName: "name"}))
|
|
ci = chaincode.ParseName("name:version")
|
|
Expect(ci).To(Equal(&sysccprovider.ChaincodeInstance{ChaincodeName: "name", ChaincodeVersion: "version"}))
|
|
ci = chaincode.ParseName("name/channel-id")
|
|
Expect(ci).To(Equal(&sysccprovider.ChaincodeInstance{ChaincodeName: "name", ChannelID: "channel-id"}))
|
|
ci = chaincode.ParseName("name:version/channel-id")
|
|
Expect(ci).To(Equal(&sysccprovider.ChaincodeInstance{ChaincodeName: "name", ChaincodeVersion: "version", ChannelID: "channel-id"}))
|
|
})
|
|
})
|
|
|
|
DescribeTable("Handler State",
|
|
func(state chaincode.State, strval string) {
|
|
Expect(state.String()).To(Equal(strval))
|
|
},
|
|
Entry("created", chaincode.Created, "created"),
|
|
Entry("ready", chaincode.Ready, "ready"),
|
|
Entry("established", chaincode.Established, "established"),
|
|
Entry("unknown", chaincode.State(999), "UNKNOWN"),
|
|
)
|
|
})
|