/* Copyright IBM Corp. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */ package chaincode_test import ( "encoding/json" "os" "testing" "github.com/hyperledger/fabric-chaincode-go/pkg/cid" "github.com/hyperledger/fabric-chaincode-go/shim" "github.com/hyperledger/fabric-contract-api-go/contractapi" "github.com/hyperledger/fabric-samples/asset-transfer-private-data/chaincode-go/chaincode" "github.com/hyperledger/fabric-samples/asset-transfer-private-data/chaincode-go/chaincode/mocks" "github.com/stretchr/testify/require" ) /* These unit tests use mocks to simulate chaincode-api & fabric interactions The mocks are generated using counterfeiter directives in the comments (starting with "go:generate counterfeiter") All files in mocks/* are generated by running following, in the directory with your directive: `go generate` */ //go:generate counterfeiter -o mocks/transaction.go -fake-name TransactionContext . transactionContext type transactionContext interface { contractapi.TransactionContextInterface } //go:generate counterfeiter -o mocks/chaincodestub.go -fake-name ChaincodeStub . chaincodeStub type chaincodeStub interface { shim.ChaincodeStubInterface } //go:generate counterfeiter -o mocks/statequeryiterator.go -fake-name StateQueryIterator . stateQueryIterator type stateQueryIterator interface { shim.StateQueryIteratorInterface } //go:generate counterfeiter -o mocks/clientIdentity.go -fake-name ClientIdentity . clientIdentity type clientIdentity interface { cid.ClientIdentity } const assetCollectionName = "assetCollection" const transferAgreementObjectType = "transferAgreement" const myOrg1Msp = "Org1Testmsp" const myOrg1Clientid = "myOrg1Userid" const myOrg1PrivCollection = "Org1TestmspPrivateCollection" const myOrg2Msp = "Org2Testmsp" const myOrg2Clientid = "myOrg2Userid" const myOrg2PrivCollection = "Org2TestmspPrivateCollection" type assetTransientInput struct { Type string `json:"objectType"` ID string `json:"assetID"` Color string `json:"color"` Size int `json:"size"` AppraisedValue int `json:"appraisedValue"` } type assetTransferTransientInput struct { ID string `json:"assetID"` BuyerMSP string `json:"buyerMSP"` } func TestCreateAssetBadInput(t *testing.T) { transactionContext, chaincodeStub := prepMocksAsOrg1() assetTransferCC := chaincode.SmartContract{} // No transient map err := assetTransferCC.CreateAsset(transactionContext) require.EqualError(t, err, "asset not found in the transient map input") // transient map with incomplete asset data assetPropMap := map[string][]byte{ "asset_properties": []byte("ill formatted property"), } chaincodeStub.GetTransientReturns(assetPropMap, nil) err = assetTransferCC.CreateAsset(transactionContext) require.Error(t, err, "Expected error: transient map with incomplete asset data") require.Contains(t, err.Error(), "failed to unmarshal JSON") testAsset := &assetTransientInput{ Type: "testfulasset", } setReturnAssetPropsInTransientMap(t, chaincodeStub, testAsset) err = assetTransferCC.CreateAsset(transactionContext) require.EqualError(t, err, "assetID field must be a non-empty string") testAsset = &assetTransientInput{ ID: "id1", Color: "gray", } setReturnAssetPropsInTransientMap(t, chaincodeStub, testAsset) err = assetTransferCC.CreateAsset(transactionContext) require.EqualError(t, err, "objectType field must be a non-empty string") // case when asset exists, GetPrivateData returns a valid data from ledger testAsset = &assetTransientInput{ ID: "id1", Type: "testfulasset", Color: "gray", Size: 7, AppraisedValue: 500, } setReturnAssetPropsInTransientMap(t, chaincodeStub, testAsset) chaincodeStub.GetPrivateDataReturns([]byte{}, nil) err = assetTransferCC.CreateAsset(transactionContext) require.EqualError(t, err, "this asset already exists: id1") } func TestCreateAssetSuccessful(t *testing.T) { transactionContext, chaincodeStub := prepMocksAsOrg1() assetTransferCC := chaincode.SmartContract{} testAsset := &assetTransientInput{ ID: "id1", Type: "testfulasset", Color: "gray", Size: 7, AppraisedValue: 500, } setReturnAssetPropsInTransientMap(t, chaincodeStub, testAsset) err := assetTransferCC.CreateAsset(transactionContext) require.NoError(t, err) //Validate PutPrivateData calls calledCollection, calledId, _ := chaincodeStub.PutPrivateDataArgsForCall(0) require.Equal(t, assetCollectionName, calledCollection) require.Equal(t, "id1", calledId) expectedPrivateDetails := &chaincode.AssetPrivateDetails{ ID: "id1", AppraisedValue: 500, } assetBytes, err := json.Marshal(expectedPrivateDetails) calledCollection, calledId, calledAssetBytes := chaincodeStub.PutPrivateDataArgsForCall(1) require.Equal(t, myOrg1PrivCollection, calledCollection) require.Equal(t, "id1", calledId) require.Equal(t, assetBytes, calledAssetBytes) } func TestAgreeToTransferBadInput(t *testing.T) { transactionContext, chaincodeStub := prepMocksAsOrg1() assetTransferCC := chaincode.SmartContract{} assetPrivDetail := &chaincode.AssetPrivateDetails{ ID: "id1", //no AppraisedValue } setReturnAssetPrivateDetailsInTransientMap(t, chaincodeStub, assetPrivDetail) origAsset := chaincode.Asset{ ID: "id1", Type: "testfulasset", Color: "gray", Size: 7, Owner: myOrg1Clientid, } setReturnPrivateDataInStub(t, chaincodeStub, &origAsset) err := assetTransferCC.AgreeToTransfer(transactionContext) require.EqualError(t, err, "appraisedValue field must be a positive integer") assetPrivDetail = &chaincode.AssetPrivateDetails{ //no ID AppraisedValue: 500, } setReturnAssetPrivateDetailsInTransientMap(t, chaincodeStub, assetPrivDetail) err = assetTransferCC.AgreeToTransfer(transactionContext) require.EqualError(t, err, "assetID field must be a non-empty string") assetPrivDetail = &chaincode.AssetPrivateDetails{ ID: "id1", AppraisedValue: 500, } setReturnAssetPrivateDetailsInTransientMap(t, chaincodeStub, assetPrivDetail) //asset does not exist setReturnPrivateDataInStub(t, chaincodeStub, nil) err = assetTransferCC.AgreeToTransfer(transactionContext) require.EqualError(t, err, "id1 does not exist") } func TestAgreeToTransferSuccessful(t *testing.T) { transactionContext, chaincodeStub := prepMocksAsOrg1() assetTransferCC := chaincode.SmartContract{} assetPrivDetail := &chaincode.AssetPrivateDetails{ ID: "id1", AppraisedValue: 500, } setReturnAssetPrivateDetailsInTransientMap(t, chaincodeStub, assetPrivDetail) origAsset := chaincode.Asset{ ID: "id1", Type: "testfulasset", Color: "gray", Size: 7, Owner: myOrg1Clientid, } setReturnPrivateDataInStub(t, chaincodeStub, &origAsset) chaincodeStub.CreateCompositeKeyReturns(transferAgreementObjectType+"id1", nil) err := assetTransferCC.AgreeToTransfer(transactionContext) require.NoError(t, err) expectedDataBytes, err := json.Marshal(assetPrivDetail) calledCollection, calledId, calledWithDataBytes := chaincodeStub.PutPrivateDataArgsForCall(0) require.Equal(t, myOrg1PrivCollection, calledCollection) require.Equal(t, "id1", calledId) require.Equal(t, expectedDataBytes, calledWithDataBytes) calledCollection, calledId, calledWithDataBytes = chaincodeStub.PutPrivateDataArgsForCall(1) require.Equal(t, assetCollectionName, calledCollection) require.Equal(t, transferAgreementObjectType+"id1", calledId) require.Equal(t, []byte(myOrg1Clientid), calledWithDataBytes) } func TestTransferAssetBadInput(t *testing.T) { transactionContext, chaincodeStub := prepMocksAsOrg1() assetTransferCC := chaincode.SmartContract{} assetNewOwner := &assetTransferTransientInput{ ID: "id1", BuyerMSP: "", } setReturnAssetOwnerInTransientMap(t, chaincodeStub, assetNewOwner) setReturnPrivateDataInStub(t, chaincodeStub, &chaincode.Asset{}) err := assetTransferCC.TransferAsset(transactionContext) require.EqualError(t, err, "buyerMSP field must be a non-empty string") assetNewOwner = &assetTransferTransientInput{ ID: "id1", BuyerMSP: myOrg2Msp, } setReturnAssetOwnerInTransientMap(t, chaincodeStub, assetNewOwner) //asset does not exist setReturnPrivateDataInStub(t, chaincodeStub, nil) err = assetTransferCC.TransferAsset(transactionContext) require.EqualError(t, err, "id1 does not exist") } func TestTransferAssetSuccessful(t *testing.T) { transactionContext, chaincodeStub := prepMocksAsOrg1() assetTransferCC := chaincode.SmartContract{} assetNewOwner := &assetTransferTransientInput{ ID: "id1", BuyerMSP: myOrg2Msp, } setReturnAssetOwnerInTransientMap(t, chaincodeStub, assetNewOwner) origAsset := chaincode.Asset{ ID: "id1", Type: "testfulasset", Color: "gray", Size: 7, Owner: myOrg1Clientid, } setReturnPrivateDataInStub(t, chaincodeStub, &origAsset) //to ensure we pass data hash verification chaincodeStub.GetPrivateDataHashReturns([]byte("datahash"), nil) //to ensure that ReadTransferAgreement call returns org2 client ID chaincodeStub.GetPrivateDataReturnsOnCall(1, []byte(myOrg2Clientid), nil) chaincodeStub.CreateCompositeKeyReturns(transferAgreementObjectType+"id1", nil) err := assetTransferCC.TransferAsset(transactionContext) require.NoError(t, err) //Validate PutPrivateData calls expectedNewAsset := origAsset expectedNewAsset.Owner = myOrg2Clientid expectedNewAssetBytes, err := json.Marshal(expectedNewAsset) require.NoError(t, err) calledCollection, calledId, calledWithAssetBytes := chaincodeStub.PutPrivateDataArgsForCall(0) require.Equal(t, assetCollectionName, calledCollection) require.Equal(t, "id1", calledId) require.Equal(t, expectedNewAssetBytes, calledWithAssetBytes) calledCollection, calledId = chaincodeStub.DelPrivateDataArgsForCall(0) require.Equal(t, myOrg1PrivCollection, calledCollection) require.Equal(t, "id1", calledId) calledCollection, calledId = chaincodeStub.DelPrivateDataArgsForCall(1) require.Equal(t, assetCollectionName, calledCollection) require.Equal(t, transferAgreementObjectType+"id1", calledId) } func TestTransferAssetByNonOwner(t *testing.T) { transactionContext, chaincodeStub := prepMocksAsOrg1() assetTransferCC := chaincode.SmartContract{} assetNewOwner := &assetTransferTransientInput{ ID: "id1", BuyerMSP: myOrg1Msp, } setReturnAssetOwnerInTransientMap(t, chaincodeStub, assetNewOwner) //Try to transfer asset owned by Org2 org2Asset := chaincode.Asset{ ID: "id1", Type: "testfulasset", Color: "gray", Size: 7, Owner: myOrg2Clientid, } setReturnPrivateDataInStub(t, chaincodeStub, &org2Asset) err := assetTransferCC.TransferAsset(transactionContext) require.EqualError(t, err, "failed transfer verification: error: submitting client identity does not own asset") } func TestTransferAssetWithoutAnAgreement(t *testing.T) { transactionContext, chaincodeStub := prepMocksAsOrg1() assetTransferCC := chaincode.SmartContract{} assetNewOwner := &assetTransferTransientInput{ ID: "id1", BuyerMSP: myOrg1Msp, } setReturnAssetOwnerInTransientMap(t, chaincodeStub, assetNewOwner) orgAsset := chaincode.Asset{ ID: "id1", Type: "testfulasset", Color: "gray", Size: 7, Owner: myOrg1Clientid, } setReturnPrivateDataInStub(t, chaincodeStub, &orgAsset) //to ensure we pass data hash verification chaincodeStub.GetPrivateDataHashReturns([]byte("datahash"), nil) chaincodeStub.CreateCompositeKeyReturns(transferAgreementObjectType+"id1", nil) //ReadTransferAgreement call returns no buyer client ID chaincodeStub.GetPrivateDataReturnsOnCall(1, []byte{}, nil) err := assetTransferCC.TransferAsset(transactionContext) require.EqualError(t, err, "BuyerID not found in TransferAgreement for id1") } func TestTransferAssetNonMatchingAppraisalValue(t *testing.T) { transactionContext, chaincodeStub := prepMocksAsOrg1() assetTransferCC := chaincode.SmartContract{} assetNewOwner := &assetTransferTransientInput{ ID: "id1", BuyerMSP: myOrg2Msp, } setReturnAssetOwnerInTransientMap(t, chaincodeStub, assetNewOwner) orgAsset := chaincode.Asset{ ID: "id1", Type: "testfulasset", Color: "gray", Size: 7, Owner: myOrg1Clientid, } setReturnPrivateDataInStub(t, chaincodeStub, &orgAsset) chaincodeStub.CreateCompositeKeyReturns(transferAgreementObjectType+"id1", nil) //data hash different in each collection chaincodeStub.GetPrivateDataHashReturnsOnCall(0, []byte("datahash1"), nil) chaincodeStub.GetPrivateDataHashReturnsOnCall(1, []byte("datahash2"), nil) err := assetTransferCC.TransferAsset(transactionContext) require.Error(t, err, "Expected failed hash verification") require.Contains(t, err.Error(), "failed transfer verification: hash for appraised value") } func prepMocksAsOrg1() (*mocks.TransactionContext, *mocks.ChaincodeStub) { return prepMocks(myOrg1Msp, myOrg1Clientid) } func prepMocksAsOrg2() (*mocks.TransactionContext, *mocks.ChaincodeStub) { return prepMocks(myOrg2Msp, myOrg2Clientid) } func prepMocks(orgMSP, clientId string) (*mocks.TransactionContext, *mocks.ChaincodeStub) { chaincodeStub := &mocks.ChaincodeStub{} transactionContext := &mocks.TransactionContext{} transactionContext.GetStubReturns(chaincodeStub) clientIdentity := &mocks.ClientIdentity{} clientIdentity.GetMSPIDReturns(orgMSP, nil) clientIdentity.GetIDReturns(clientId, nil) //set matching msp ID using peer shim env variable os.Setenv("CORE_PEER_LOCALMSPID", orgMSP) transactionContext.GetClientIdentityReturns(clientIdentity) return transactionContext, chaincodeStub } func setReturnAssetPrivateDetailsInTransientMap(t *testing.T, chaincodeStub *mocks.ChaincodeStub, assetPrivDetail *chaincode.AssetPrivateDetails) []byte { assetOwnerBytes := []byte{} if assetPrivDetail != nil { var err error assetOwnerBytes, err = json.Marshal(assetPrivDetail) require.NoError(t, err) } assetPropMap := map[string][]byte{ "asset_value": assetOwnerBytes, } chaincodeStub.GetTransientReturns(assetPropMap, nil) return assetOwnerBytes } func setReturnAssetOwnerInTransientMap(t *testing.T, chaincodeStub *mocks.ChaincodeStub, assetOwner *assetTransferTransientInput) []byte { assetOwnerBytes := []byte{} if assetOwner != nil { var err error assetOwnerBytes, err = json.Marshal(assetOwner) require.NoError(t, err) } assetPropMap := map[string][]byte{ "asset_owner": assetOwnerBytes, } chaincodeStub.GetTransientReturns(assetPropMap, nil) return assetOwnerBytes } func setReturnAssetPropsInTransientMap(t *testing.T, chaincodeStub *mocks.ChaincodeStub, testAsset *assetTransientInput) []byte { assetBytes := []byte{} if testAsset != nil { var err error assetBytes, err = json.Marshal(testAsset) require.NoError(t, err) } assetPropMap := map[string][]byte{ "asset_properties": assetBytes, } chaincodeStub.GetTransientReturns(assetPropMap, nil) return assetBytes } func setReturnPrivateDataInStub(t *testing.T, chaincodeStub *mocks.ChaincodeStub, testAsset *chaincode.Asset) []byte { if testAsset == nil { chaincodeStub.GetPrivateDataReturns(nil, nil) return nil } else { var err error assetBytes, err := json.Marshal(testAsset) require.NoError(t, err) chaincodeStub.GetPrivateDataReturns(assetBytes, nil) return assetBytes } } func setReturnAssetPrivateDetailsInStub(t *testing.T, chaincodeStub *mocks.ChaincodeStub, testAsset *chaincode.AssetPrivateDetails) []byte { if testAsset == nil { chaincodeStub.GetPrivateDataReturns(nil, nil) return nil } else { var err error assetBytes, err := json.Marshal(testAsset) require.NoError(t, err) chaincodeStub.GetPrivateDataReturns(assetBytes, nil) return assetBytes } }