/* 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"), ) })