375 lines
14 KiB
Go
375 lines
14 KiB
Go
/*
|
|
Copyright IBM Corp All Rights Reserved.
|
|
|
|
SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
package ledger
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"path/filepath"
|
|
"syscall"
|
|
|
|
"github.com/hyperledger/fabric/integration/nwo"
|
|
"github.com/hyperledger/fabric/integration/nwo/commands"
|
|
"github.com/hyperledger/fabric/integration/nwo/runner"
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
"github.com/onsi/gomega/gexec"
|
|
"github.com/tedsuo/ifrit"
|
|
)
|
|
|
|
var _ = Describe("all shim APIs for non-private data", func() {
|
|
var (
|
|
setup *setup
|
|
helper *marblesTestHelper
|
|
chaincode nwo.Chaincode
|
|
)
|
|
|
|
BeforeEach(func() {
|
|
setup = initThreeOrgsSetup()
|
|
helper = &marblesTestHelper{
|
|
networkHelper: &networkHelper{
|
|
Network: setup.network,
|
|
orderer: setup.orderer,
|
|
peers: setup.peers,
|
|
testDir: setup.testDir,
|
|
channelID: setup.channelID,
|
|
},
|
|
}
|
|
|
|
chaincode = nwo.Chaincode{
|
|
Name: "marbles",
|
|
Version: "0.0",
|
|
Path: "github.com/hyperledger/fabric/integration/chaincode/marbles/cmdwithindexspecs",
|
|
Lang: "golang",
|
|
PackageFile: filepath.Join(setup.testDir, "marbles.tar.gz"),
|
|
Label: "marbles",
|
|
SignaturePolicy: `OR ('Org1MSP.member','Org2MSP.member', 'Org3MSP.member')`,
|
|
Sequence: "1",
|
|
}
|
|
})
|
|
|
|
AfterEach(func() {
|
|
setup.cleanup()
|
|
})
|
|
|
|
// assertMarbleAPIs invoke marble APIs to add/transfer/delete marbles and get marbles without rich queries.
|
|
// These APIs are applicable to both levelDB and CouchDB.
|
|
assertMarbleAPIs := func(ccName string, peer *nwo.Peer) {
|
|
height := helper.getLedgerHeight(peer)
|
|
|
|
By("adding six marbles, marble-0 to marble-5")
|
|
for i := 0; i <= 5; i++ {
|
|
helper.invokeMarblesChaincode(ccName, peer, "initMarble", fmt.Sprintf("marble-%d", i), "blue", "35", "tom")
|
|
helper.waitUntilAllPeersEqualLedgerHeight(height + i + 1)
|
|
}
|
|
|
|
By("getting marbles by range")
|
|
expectedQueryResult := newMarbleQueryResult(1, 4, "blue", 35, "tom")
|
|
helper.assertQueryMarbles(ccName, peer, expectedQueryResult, "getMarblesByRange", "marble-1", "marble-5")
|
|
|
|
By("transferring marble-0 to jerry")
|
|
helper.invokeMarblesChaincode(ccName, peer, "transferMarble", "marble-0", "jerry")
|
|
|
|
By("verifying new owner of marble-0 after transfer")
|
|
expectedResult := newMarble("marble-0", "blue", 35, "jerry")
|
|
helper.assertMarbleExists(ccName, peer, expectedResult, "marble-0")
|
|
|
|
By("deleting marble-0")
|
|
helper.invokeMarblesChaincode(ccName, peer, "delete", "marble-0")
|
|
|
|
By("verifying deletion of marble-0")
|
|
helper.assertMarbleDoesNotExist(peer, ccName, "marble-0")
|
|
|
|
By("transferring marbles by color")
|
|
helper.invokeMarblesChaincode(ccName, peer, "transferMarblesBasedOnColor", "blue", "jerry")
|
|
|
|
By("verifying new owner after transfer by color")
|
|
for i := 1; i <= 5; i++ {
|
|
name := fmt.Sprintf("marble-%d", i)
|
|
expectedResult = newMarble(name, "blue", 35, "jerry")
|
|
helper.assertMarbleExists(ccName, peer, expectedResult, name)
|
|
}
|
|
|
|
By("getting history for marble-0")
|
|
expectedHistoryResult := []*marbleHistoryResult{
|
|
{IsDelete: "true"},
|
|
{IsDelete: "false", Value: newMarble("marble-0", "blue", 35, "jerry")},
|
|
{IsDelete: "false", Value: newMarble("marble-0", "blue", 35, "tom")},
|
|
}
|
|
helper.assertGetHistoryForMarble(ccName, peer, expectedHistoryResult, "marble-0")
|
|
}
|
|
|
|
// assertMarbleAPIs verifies marbles APIs using rich queries and pagination that are only applicable to CouchDB.
|
|
assertMarbleAPIsRichQueries := func(ccName string, peer *nwo.Peer) {
|
|
By("querying marbles by owner")
|
|
expectedQueryResult := newMarbleQueryResult(1, 5, "blue", 35, "jerry")
|
|
helper.assertQueryMarbles(ccName, peer, expectedQueryResult, "queryMarblesByOwner", "jerry")
|
|
|
|
By("quering marbles by search criteria")
|
|
helper.assertQueryMarbles(ccName, peer, expectedQueryResult, "queryMarbles", `{"selector":{"color":"blue"}}`)
|
|
|
|
By("quering marbles by range with pagination size 3, 1st call")
|
|
bookmark := ""
|
|
expectedQueryResult = newMarbleQueryResult(1, 3, "blue", 35, "jerry")
|
|
bookmark = helper.assertQueryMarblesWithPagination(ccName, peer, expectedQueryResult,
|
|
"getMarblesByRangeWithPagination", "marble-1", "marble-6", "3", bookmark)
|
|
|
|
By("quering marbles by range with pagination size 3, 2nd call")
|
|
expectedQueryResult = newMarbleQueryResult(4, 5, "blue", 35, "jerry")
|
|
bookmark = helper.assertQueryMarblesWithPagination(ccName, peer, expectedQueryResult,
|
|
"getMarblesByRangeWithPagination", "marble-1", "marble-6", "3", bookmark)
|
|
|
|
By("quering marbles by range with pagination size 3, 3rd call should return no marble")
|
|
expectedQueryResult = make([]*marbleQueryResult, 0)
|
|
helper.assertQueryMarblesWithPagination(ccName, peer, expectedQueryResult,
|
|
"getMarblesByRangeWithPagination", "marble-1", "marble-6", "3", bookmark)
|
|
|
|
By("quering marbles by search criteria with pagination size 10, 1st call")
|
|
bookmark = ""
|
|
expectedQueryResult = newMarbleQueryResult(1, 5, "blue", 35, "jerry")
|
|
bookmark = helper.assertQueryMarblesWithPagination(ccName, peer, expectedQueryResult,
|
|
"queryMarblesWithPagination", `{"selector":{"owner":"jerry"}}`, "10", bookmark)
|
|
|
|
By("quering marbles by search criteria with pagination size 10, 2nd call should return no marble")
|
|
expectedQueryResult = make([]*marbleQueryResult, 0)
|
|
helper.assertQueryMarblesWithPagination(ccName, peer, expectedQueryResult,
|
|
"queryMarblesWithPagination", `{"selector":{"owner":"jerry"}}`, "10", bookmark)
|
|
}
|
|
|
|
When("levelDB is used as stateDB", func() {
|
|
It("calls marbles APIs", func() {
|
|
peer := setup.network.Peer("Org2", "peer0")
|
|
|
|
By("deploying new lifecycle chaincode")
|
|
nwo.EnableCapabilities(setup.network, setup.channelID, "Application", "V2_0", setup.orderer, setup.peers...)
|
|
helper.deployChaincode(chaincode)
|
|
|
|
By("verifying marbles chaincode APIs")
|
|
assertMarbleAPIs(chaincode.Name, peer)
|
|
})
|
|
})
|
|
|
|
When("CouchDB is used as stateDB", func() {
|
|
var couchProcess ifrit.Process
|
|
|
|
BeforeEach(func() {
|
|
By("stopping peers")
|
|
setup.stopPeers()
|
|
|
|
By("configuring a peer with couchdb")
|
|
// configure only one of the peers (Org2, peer0) to use couchdb.
|
|
// Note that we do not support a channel with mixed DBs.
|
|
// However, for testing, it would be fine to use couchdb for one
|
|
// peer and sending all the couchdb related test queries to this peer
|
|
couchDB := &runner.CouchDB{}
|
|
couchProcess = ifrit.Invoke(couchDB)
|
|
Eventually(couchProcess.Ready(), runner.DefaultStartTimeout).Should(BeClosed())
|
|
Consistently(couchProcess.Wait()).ShouldNot(Receive())
|
|
couchAddr := couchDB.Address()
|
|
peer := setup.network.Peer("Org2", "peer0")
|
|
core := setup.network.ReadPeerConfig(peer)
|
|
core.Ledger.State.StateDatabase = "CouchDB"
|
|
core.Ledger.State.CouchDBConfig.CouchDBAddress = couchAddr
|
|
setup.network.WritePeerConfig(peer, core)
|
|
|
|
By("restarting peers with couchDB")
|
|
setup.startPeers()
|
|
})
|
|
|
|
AfterEach(func() {
|
|
couchProcess.Signal(syscall.SIGTERM)
|
|
Eventually(couchProcess.Wait(), setup.network.EventuallyTimeout).Should(Receive())
|
|
})
|
|
|
|
It("calls marbles APIs", func() {
|
|
peer := setup.network.Peer("Org2", "peer0")
|
|
|
|
By("deploying new lifecycle chaincode")
|
|
nwo.EnableCapabilities(setup.network, setup.channelID, "Application", "V2_0", setup.orderer, setup.peers...)
|
|
helper.deployChaincode(chaincode)
|
|
|
|
By("verifying marbles chaincode APIs")
|
|
assertMarbleAPIs(chaincode.Name, peer)
|
|
|
|
By("verifying marbles rich queries")
|
|
assertMarbleAPIsRichQueries(chaincode.Name, peer)
|
|
})
|
|
})
|
|
})
|
|
|
|
// marble is the struct to unmarshal the response bytes returned from getMarble API
|
|
type marble struct {
|
|
ObjectType string `json:"docType"` // docType is "marble"
|
|
Name string `json:"name"`
|
|
Color string `json:"color"`
|
|
Size int `json:"size"`
|
|
Owner string `json:"owner"`
|
|
}
|
|
|
|
// marbleQueryResult is the struct to unmarshal the response bytes returned from marbles query APIs
|
|
type marbleQueryResult struct {
|
|
Key string `json:"Key"`
|
|
Record *marble `json:"Record"`
|
|
}
|
|
|
|
type metadata struct {
|
|
RecordsCount string `json:"RecordsCount"`
|
|
Bookmark string `json:"Bookmark"`
|
|
}
|
|
|
|
// marbleQueryResult is the struct to unmarshal the metadata bytes returned from marbles pagination APIs
|
|
type paginationMetadata struct {
|
|
ResponseMetadata *metadata `json:"ResponseMetadata"`
|
|
}
|
|
|
|
// marbleHistoryResult is the struct to unmarshal the response bytes returned from marbles history API
|
|
type marbleHistoryResult struct {
|
|
TxId string `json:"TxId"`
|
|
Value *marble `json:"Value"`
|
|
Timestamp string `json:"Timestamp"`
|
|
IsDelete string `json:"IsDelete"`
|
|
}
|
|
|
|
// newMarble creates a marble object for the given parameters
|
|
func newMarble(name, color string, size int, owner string) *marble {
|
|
return &marble{"marble", name, color, size, owner}
|
|
}
|
|
|
|
// newMarbleQueryResult creates a slice of marbleQueryResult for the marbles based on startIndex and endIndex.
|
|
// Both startIndex and endIndex are inclusive
|
|
func newMarbleQueryResult(startIndex, endIndex int, color string, size int, owner string) []*marbleQueryResult {
|
|
expectedResult := make([]*marbleQueryResult, 0)
|
|
for i := startIndex; i <= endIndex; i++ {
|
|
name := fmt.Sprintf("marble-%d", i)
|
|
item := marbleQueryResult{Key: name, Record: newMarble(name, color, size, owner)}
|
|
expectedResult = append(expectedResult, &item)
|
|
}
|
|
return expectedResult
|
|
}
|
|
|
|
// marblesTestHelper implements helper methods to call marbles chaincode APIs and verify results
|
|
type marblesTestHelper struct {
|
|
*networkHelper
|
|
}
|
|
|
|
// invokeMarblesChaincode invokes marbles APIs such as initMarble, transfer and delete.
|
|
func (th *marblesTestHelper) invokeMarblesChaincode(chaincodeName string, peer *nwo.Peer, funcAndArgs ...string) {
|
|
command := commands.ChaincodeInvoke{
|
|
ChannelID: th.channelID,
|
|
Orderer: th.OrdererAddress(th.orderer, nwo.ListenPort),
|
|
Name: chaincodeName,
|
|
Ctor: prepareChaincodeInvokeArgs(funcAndArgs...),
|
|
PeerAddresses: []string{
|
|
th.PeerAddress(peer, nwo.ListenPort),
|
|
},
|
|
WaitForEvent: true,
|
|
}
|
|
th.invokeChaincode(peer, command)
|
|
nwo.WaitUntilEqualLedgerHeight(th.Network, th.channelID, nwo.GetLedgerHeight(th.Network, peer, th.channelID), th.peers...)
|
|
}
|
|
|
|
// assertMarbleExists asserts that the marble exists and matches the expected result
|
|
func (th *marblesTestHelper) assertMarbleExists(chaincodeName string, peer *nwo.Peer, expectedResult *marble, marbleName string) {
|
|
command := commands.ChaincodeQuery{
|
|
ChannelID: th.channelID,
|
|
Name: chaincodeName,
|
|
Ctor: fmt.Sprintf(`{"Args":["readMarble","%s"]}`, marbleName),
|
|
}
|
|
sess, err := th.PeerUserSession(peer, "User1", command)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Eventually(sess, th.EventuallyTimeout).Should(gexec.Exit(0))
|
|
result := &marble{}
|
|
err = json.Unmarshal(sess.Out.Contents(), result)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(result).To(Equal(expectedResult))
|
|
}
|
|
|
|
// assertMarbleDoesNotExist asserts that the marble does not exist
|
|
func (th *marblesTestHelper) assertMarbleDoesNotExist(peer *nwo.Peer, chaincodeName, marbleName string) {
|
|
command := commands.ChaincodeQuery{
|
|
ChannelID: th.channelID,
|
|
Name: chaincodeName,
|
|
Ctor: fmt.Sprintf(`{"Args":["readMarble","%s"]}`, marbleName),
|
|
}
|
|
th.queryChaincode(peer, command, "Marble does not exist", false)
|
|
}
|
|
|
|
// assertQueryMarbles queries the chaincode and verifies the result based on the function and arguments,
|
|
// including range queries and rich queries.
|
|
func (th *marblesTestHelper) assertQueryMarbles(chaincodeName string, peer *nwo.Peer, expectedResult []*marbleQueryResult, funcAndArgs ...string) {
|
|
command := commands.ChaincodeQuery{
|
|
ChannelID: th.channelID,
|
|
Name: chaincodeName,
|
|
Ctor: prepareChaincodeInvokeArgs(funcAndArgs...),
|
|
}
|
|
sess, err := th.PeerUserSession(peer, "User1", command)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Eventually(sess, th.EventuallyTimeout).Should(gexec.Exit(0))
|
|
results := make([]*marbleQueryResult, 0)
|
|
err = json.Unmarshal(sess.Out.Contents(), &results)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(results).To(Equal(expectedResult))
|
|
}
|
|
|
|
// assertQueryMarbles queries the chaincode with pagination and verifies the result based on the function and arguments,
|
|
// including range queries and rich queries.
|
|
func (th *marblesTestHelper) assertQueryMarblesWithPagination(chaincodeName string, peer *nwo.Peer, expectedResult []*marbleQueryResult, funcAndArgs ...string) string {
|
|
command := commands.ChaincodeQuery{
|
|
ChannelID: th.channelID,
|
|
Name: chaincodeName,
|
|
Ctor: prepareChaincodeInvokeArgs(funcAndArgs...),
|
|
}
|
|
sess, err := th.PeerUserSession(peer, "User1", command)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Eventually(sess, th.EventuallyTimeout).Should(gexec.Exit(0))
|
|
|
|
// response bytes contains 2 json arrays: [{"Key":...}][{"ResponseMetadata": ...}]
|
|
responseBytes := sess.Out.Contents()
|
|
index := bytes.LastIndex(responseBytes, []byte("]["))
|
|
|
|
// unmarshal and verify response result
|
|
results := make([]*marbleQueryResult, 0)
|
|
err = json.Unmarshal(responseBytes[:index+1], &results)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(results).To(Equal(expectedResult))
|
|
|
|
// unmarshal ResponseMetadata and return bookmark to the caller for next call
|
|
respMetadata := make([]*paginationMetadata, 0)
|
|
err = json.Unmarshal(responseBytes[index+1:], &respMetadata)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(respMetadata).To(HaveLen(1))
|
|
|
|
return respMetadata[0].ResponseMetadata.Bookmark
|
|
}
|
|
|
|
// assertGetHistoryForMarble queries the history for a specific marble and verifies the result
|
|
func (th *marblesTestHelper) assertGetHistoryForMarble(chaincodeName string, peer *nwo.Peer, expectedResult []*marbleHistoryResult, marbleName string) {
|
|
command := commands.ChaincodeQuery{
|
|
ChannelID: th.channelID,
|
|
Name: chaincodeName,
|
|
Ctor: fmt.Sprintf(`{"Args":["getHistoryForMarble","%s"]}`, marbleName),
|
|
}
|
|
sess, err := th.PeerUserSession(peer, "User1", command)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Eventually(sess, th.EventuallyTimeout).Should(gexec.Exit(0))
|
|
|
|
// unmarshal bytes and verify history
|
|
results := make([]*marbleHistoryResult, 0)
|
|
err = json.Unmarshal(sess.Out.Contents(), &results)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(results).To(HaveLen(len(expectedResult)))
|
|
for i, result := range results {
|
|
Expect(result.IsDelete).To(Equal(expectedResult[i].IsDelete))
|
|
if result.IsDelete == "true" {
|
|
Expect(result.Value).To(BeNil())
|
|
continue
|
|
}
|
|
Expect(result.Value).To(Equal(expectedResult[i].Value))
|
|
}
|
|
}
|