From 1b1402908a60744e3116beb33e7404294bf5aeb2 Mon Sep 17 00:00:00 2001 From: "Mark S. Lewis" Date: Wed, 31 Oct 2018 14:50:46 +0000 Subject: [PATCH] FABN-929: Transient data with evaluateTransaction() - Add Transaction.evaluate() - Refactor Contract.evaluateTransaction() to use Transaction.evaluate() Change-Id: I7aca331351aec8201b3bba2bb0f2ddef141b6f89 Signed-off-by: Mark S. Lewis --- fabric-client/lib/Channel.js | 29 +-- fabric-network/lib/contract.js | 29 ++- .../lib/impl/query/defaultqueryhandler.js | 13 +- fabric-network/lib/transaction.js | 56 +++-- fabric-network/test/contract.js | 61 ++---- .../test/impl/query/defaultqueryhandler.js | 194 ++++++++---------- fabric-network/test/transaction.js | 70 ++++++- test/integration/network-e2e/invoke.js | 2 +- test/integration/network-e2e/query.js | 136 +++++++++--- 9 files changed, 349 insertions(+), 241 deletions(-) diff --git a/fabric-client/lib/Channel.js b/fabric-client/lib/Channel.js index 9ec3444fda..bffebd0d57 100755 --- a/fabric-client/lib/Channel.js +++ b/fabric-client/lib/Channel.js @@ -2359,10 +2359,10 @@ const Channel = class { * and nonce * @property {string} collections-config - Optional. The path to the collections * config. More details can be found at this [tutorial]{@link https://fabric-sdk-node.github.io/tutorial-private-data.html} - * @property {map} transientMap - Optional. map that can be - * used by the chaincode during initialization, but not saved in the - * ledger. Data such as cryptographic information for encryption can - * be passed to the chaincode using this technique. + * @property {object} [transientMap] - Optional. Object with String property names + * and Buffer property values that can be used by the chaincode but not + * saved in the ledger. Data such as cryptographic information for + * encryption can be passed to the chaincode using this technique. * @property {string} fcn - Optional. The function name to be returned when * calling stub.GetFunctionAndParameters() in the target * chaincode. Default is 'init'. @@ -2563,9 +2563,10 @@ const Channel = class { * @property {TransactionID} txId - Optional. TransactionID object with the * transaction id and nonce. txId is required for [sendTransactionProposal]{@link Channel#sendTransactionProposal} * and optional for [generateUnsignedProposal]{@link Channel#generateUnsignedProposal} - * @property {map} transientMap - Optional. map that can be - * used by the chaincode but not - * saved in the ledger, such as cryptographic information for encryption + * @property {object} [transientMap] - Optional. Object with String property names + * and Buffer property values that can be used by the chaincode but not + * saved in the ledger. Data such as cryptographic information for + * encryption can be passed to the chaincode using this technique. * @property {string} fcn - Optional. The function name to be returned when * calling stub.GetFunctionAndParameters() * in the target chaincode. Default is 'invoke' @@ -2832,9 +2833,10 @@ const Channel = class { * @property {string[]} args - Required. Arguments to send to chaincode. * @property {string} chaincodeId - Required. ChaincodeId. * @property {Buffer} argbytes - Optional. Include when an argument must be included as bytes. - * @property {map} transientMap - Optional. The Map that can be - * used by the chaincode but not saved in the ledger, such as - * cryptographic information for encryption. + * @property {object} [transientMap] - Optional. Object with String property names + * and Buffer property values that can be used by the chaincode but not + * saved in the ledger. Data such as cryptographic information for + * encryption can be passed to the chaincode using this technique. */ @@ -3067,9 +3069,10 @@ const Channel = class { * object will be used. * @property {string} chaincodeId - Required. The id of the chaincode to process * the transaction proposal - * @property {map} transientMap - Optional. map that can be - * used by the chaincode but not saved in the ledger, such as cryptographic - * information for encryption + * @property {object} [transientMap] - Optional. Object with String property names + * and Buffer property values that can be used by the chaincode but not + * saved in the ledger. Data such as cryptographic information for + * encryption can be passed to the chaincode using this technique. * @property {string} fcn - Optional. The function name to be returned when * calling stub.GetFunctionAndParameters() * in the target chaincode. Default is 'invoke' diff --git a/fabric-network/lib/contract.js b/fabric-network/lib/contract.js index 2fdada3af9..7680c4cb4c 100644 --- a/fabric-network/lib/contract.js +++ b/fabric-network/lib/contract.js @@ -75,6 +75,10 @@ class Contract { return this.gateway.getOptions().eventHandlerOptions; } + getQueryHandler() { + return this.queryHandler; + } + /** * Submit a transaction to the ledger. The transaction function transactionName * will be evaluated on the endorsing peers and then submitted to the ordering service @@ -105,33 +109,26 @@ class Contract { * for committing to the ledger. * @async * @param {string} name Transaction function name. - * @param {...string} args Transaction function arguments. + * @param {...string} [args] Transaction function arguments. * @returns {Buffer} Payload response from the transaction function. */ async submitTransaction(name, ...args) { - const transaction = this.createTransaction(name); - return transaction.submit(...args); + return this.createTransaction(name).submit(...args); } /** * Evaluate a transaction function and return its results. * The transaction function transactionName - * will be evaluated on the endorsing peers but the responses will not be sent to to + * will be evaluated on the endorsing peers but the responses will not be sent to * the ordering service and hence will not be committed to the ledger. * This is used for querying the world state. - * @param {string} name Transaction function name - * @param {...string} parameters Transaction function parameters - * @returns {Buffer} Payload response from the transaction function + * @async + * @param {string} name Transaction function name. + * @param {...string} [args] Transaction function arguments. + * @returns {Buffer} Payload response from the transaction function. */ - async evaluateTransaction(name, ...parameters) { - verifyTransactionName(name); - Transaction.verifyArguments(parameters); - - // form the transaction name with the namespace - const qualifiedName = this._getQualifiedName(name); - const txId = this.gateway.getClient().newTransactionID(); - const result = await this.queryHandler.queryChaincode(this.chaincodeId, txId, qualifiedName, parameters); - return result ? result : null; + async evaluateTransaction(name, ...args) { + return this.createTransaction(name).evaluate(...args); } } diff --git a/fabric-network/lib/impl/query/defaultqueryhandler.js b/fabric-network/lib/impl/query/defaultqueryhandler.js index ceb493f384..5b28b70911 100644 --- a/fabric-network/lib/impl/query/defaultqueryhandler.js +++ b/fabric-network/lib/impl/query/defaultqueryhandler.js @@ -49,9 +49,10 @@ class DefaultQueryHandler extends QueryHandler { * @param {string} functionName the function name to invoke * @param {string[]} args the arguments * @param {TransactionID} txId the transaction id to use + * @param {object} [transientMap] transient data * @returns {object} asynchronous response or async error. */ - async queryChaincode(chaincodeId, txId, functionName, args) { + async queryChaincode(chaincodeId, txId, functionName, args, transientMap) { const method = 'queryChaincode'; let success = false; let payload; @@ -67,7 +68,7 @@ class DefaultQueryHandler extends QueryHandler { const peer = this.allQueryablePeers[this.queryPeerIndex]; try { logger.debug('%s - querying previously successful peer: %s', method, peer.getName()); - payload = await this._querySinglePeer(peer, chaincodeId, txId, functionName, args); + payload = await this._querySinglePeer(peer, chaincodeId, txId, functionName, args, transientMap); success = true; } catch (error) { logger.warn('%s - error response trying previously successful peer: %s. Error: %s', method, peer.getName(), error); @@ -88,7 +89,7 @@ class DefaultQueryHandler extends QueryHandler { const peer = this.allQueryablePeers[i]; try { logger.debug('%s - querying new peer: %s', method, peer.getName()); - payload = await this._querySinglePeer(peer, chaincodeId, txId, functionName, args); + payload = await this._querySinglePeer(peer, chaincodeId, txId, functionName, args, transientMap); this.queryPeerIndex = i; success = true; break; @@ -120,9 +121,10 @@ class DefaultQueryHandler extends QueryHandler { * @param {string} functionName the function name of the query * @param {array} args the arguments to ass * @param {TransactionID} txId the transaction id to use + * @param {object} [transientMap] transient data * @returns {Buffer} asynchronous response to query */ - async _querySinglePeer(peer, chaincodeId, txId, functionName, args) { + async _querySinglePeer(peer, chaincodeId, txId, functionName, args, transientMap) { const method = '_querySinglePeer'; const request = { targets: [peer], @@ -131,6 +133,9 @@ class DefaultQueryHandler extends QueryHandler { fcn: functionName, args: args }; + if (transientMap) { + request.transientMap = transientMap; + } const payloads = await this.channel.queryByChaincode(request); if (!payloads.length) { diff --git a/fabric-network/lib/transaction.js b/fabric-network/lib/transaction.js index bd9682ea93..0ce49bc91e 100644 --- a/fabric-network/lib/transaction.js +++ b/fabric-network/lib/transaction.js @@ -14,6 +14,23 @@ function getResponsePayload(peerResponse) { return (payload && payload.length > 0) ? payload : null; } +/** + * Ensure supplied transaction arguments are not strings. + * @private + * @static + * @param {Array} args transaction arguments. + * @throws {Error} if any arguments are invalid. + */ +function verifyArguments(args) { + const isInvalid = args.some((arg) => typeof arg !== 'string'); + if (isInvalid) { + const argsString = args.map((arg) => util.format('%j', arg)).join(', '); + const msg = util.format('Transaction arguments must be strings: %s', argsString); + logger.error('verifyArguments:', msg); + throw new Error(msg); + } +} + class Transaction { /** * Constructor. @@ -34,23 +51,6 @@ class Transaction { }; } - /** - * Ensure supplied transaction arguments are not strings. - * @private - * @static - * @param {Array} args transaction argument. - * @throws {Error} if any arguments are invalid. - */ - static verifyArguments(args) { - const isInvalid = args.some((arg) => typeof arg !== 'string'); - if (isInvalid) { - const argsString = args.map((arg) => util.format('%j', arg)).join(', '); - const msg = util.format('Transaction arguments must be strings: %s', argsString); - logger.error('_verifyTransactionArguments:', msg); - throw new Error(msg); - } - } - getName() { return this._name; } @@ -72,11 +72,11 @@ class Transaction { * will be evaluated on the endorsing peers and then submitted to the ordering service * for committing to the ledger. * @async - * @param {...string} args Transaction function arguments. + * @param {...string} [args] Transaction function arguments. * @returns {Buffer} Payload response from the transaction function. */ async submit(...args) { - Transaction.verifyArguments(args); + verifyArguments(args); const network = this._contract.getNetwork(); const channel = network.getChannel(); @@ -166,6 +166,24 @@ class Transaction { return { validResponses, invalidResponses }; } + + /** + * Evaluate a transaction function and return its results. + * The transaction function will be evaluated on the endorsing peers but + * the responses will not be sent to the ordering service and hence will + * not be committed to the ledger. + * This is used for querying the world state. + * @async + * @param {...string} [args] Transaction function arguments. + * @returns {Buffer} Payload response from the transaction function. + */ + async evaluate(...args) { + verifyArguments(args); + + const queryHandler = this._contract.getQueryHandler(); + const chaincodeId = this._contract.getChaincodeId(); + return queryHandler.queryChaincode(chaincodeId, this._transactionId, this._name, args, this._transientMap); + } } module.exports = Transaction; diff --git a/fabric-network/test/contract.js b/fabric-network/test/contract.js index 43873965e3..f3b68999aa 100644 --- a/fabric-network/test/contract.js +++ b/fabric-network/test/contract.js @@ -96,6 +96,13 @@ describe('Contract', () => { }); }); + describe('#getQueryhandler', () => { + it('returns the query handler', () => { + const result = contract.getQueryHandler(); + result.should.equal(mockQueryHandler); + }); + }); + describe('#createTransaction', () => { it('returns a transaction with only a name', () => { const name = 'name'; @@ -150,54 +157,16 @@ describe('Contract', () => { }); describe('#evaluateTransaction', () => { - it('should query chaincode and handle a good response without return data', async () => { - mockQueryHandler.queryChaincode.withArgs(chaincodeId, mockTransactionID, 'myfunc', ['arg1', 'arg2']).resolves(); - - const result = await contract.evaluateTransaction('myfunc', 'arg1', 'arg2'); - sinon.assert.calledOnce(mockQueryHandler.queryChaincode); - should.equal(result, null); - }); - - it('should query chaincode and handle a good response with return data', async () => { - const response = Buffer.from('hello world'); - mockQueryHandler.queryChaincode.withArgs(chaincodeId, mockTransactionID, 'myfunc', ['arg1', 'arg2']).resolves(response); - - const result = await contract.evaluateTransaction('myfunc', 'arg1', 'arg2'); - sinon.assert.calledOnce(mockQueryHandler.queryChaincode); - result.equals(response).should.be.true; - }); - - it('should query chaincode and handle an error response', () => { - const response = new Error('such error'); - mockQueryHandler.queryChaincode.withArgs(chaincodeId, mockTransactionID, 'myfunc', ['arg1', 'arg2']).rejects(response); - return contract.evaluateTransaction('myfunc', 'arg1', 'arg2') - .should.be.rejectedWith(/such error/); - - }); - - it('should query chaincode with namespace added to the function', async () => { - const nscontract = new Contract(network, chaincodeId, mockGateway, mockQueryHandler, 'my.name.space'); - - mockQueryHandler.queryChaincode.withArgs(chaincodeId, mockTransactionID, 'myfunc', ['arg1', 'arg2']).resolves(); - - await nscontract.evaluateTransaction('myfunc', 'arg1', 'arg2'); - sinon.assert.calledOnce(mockQueryHandler.queryChaincode); - sinon.assert.calledWith(mockQueryHandler.queryChaincode, - sinon.match.any, - sinon.match.any, - 'my.name.space:myfunc', - sinon.match.any); - }); + it('evaluates a transaction with supplied arguments', async () => { + const args = [ 'a', 'b', 'c' ]; + const expected = Buffer.from('result'); + const stubTransaction = sinon.createStubInstance(Transaction); + stubTransaction.evaluate.withArgs(...args).resolves(expected); + sinon.stub(contract, 'createTransaction').returns(stubTransaction); - it('throws if name is an empty string', () => { - const promise = contract.evaluateTransaction(''); - return promise.should.be.rejectedWith('name'); - }); + const result = await contract.evaluateTransaction('name', ...args); - it('throws is name is not a string', () => { - const promise = contract.evaluateTransaction(123); - return promise.should.be.rejectedWith('name'); + result.should.equal(expected); }); - }); }); diff --git a/fabric-network/test/impl/query/defaultqueryhandler.js b/fabric-network/test/impl/query/defaultqueryhandler.js index 9423211a38..829edacedd 100644 --- a/fabric-network/test/impl/query/defaultqueryhandler.js +++ b/fabric-network/test/impl/query/defaultqueryhandler.js @@ -19,8 +19,6 @@ const should = chai.should(); chai.use(require('chai-as-promised')); describe('DefaultQueryHandler', () => { - - const sandbox = sinon.createSandbox(); let mockPeer1, mockPeer2, mockPeer3, mockPeer4; let mockPeerMap, mockTransactionID, mockChannel; let queryHandler; @@ -48,10 +46,9 @@ describe('DefaultQueryHandler', () => { mockTransactionID = sinon.createStubInstance(TransactionID); mockChannel = sinon.createStubInstance(Channel); queryHandler = new DefaultQueryHandler(mockChannel, 'mspid', mockPeerMap); - }); afterEach(() => { - sandbox.restore(); + sinon.restore(); }); describe('#constructor', () => { @@ -69,98 +66,117 @@ describe('DefaultQueryHandler', () => { }); describe('#queryChaincode', () => { + let errorResponse; + let validResponse; + let failResponse; + + beforeEach(() => { + errorResponse = new Error('Chaincode error response'); + errorResponse.status = 500; + errorResponse.isProposalResponse = true; + + validResponse = Buffer.from('hello world'); + + failResponse = new Error('Failed to contact peer'); + + mockChannel.queryByChaincode.resolves([ validResponse ]); + }); + it('should not switch to another peer if peer returns a payload which is an error', async () => { - const response = new Error('my chaincode error'); - response.status = 500; - response.isProposalResponse = true; - mockChannel.queryByChaincode.resolves([response]); - const qspSpy = sinon.spy(queryHandler, '_querySinglePeer'); + mockChannel.queryByChaincode.resolves([ errorResponse ]); try { await queryHandler.queryChaincode('chaincodeId', mockTransactionID, 'myfunc', ['arg1', 'arg2']); should.fail('expected error to be thrown'); } catch(error) { - error.message.should.equal('my chaincode error'); - sinon.assert.calledOnce(qspSpy); - sinon.assert.calledWith(qspSpy, mockPeer1, 'chaincodeId', mockTransactionID, 'myfunc', ['arg1', 'arg2']); + error.message.should.equal(errorResponse.message); + sinon.assert.calledWith(mockChannel.queryByChaincode, { + targets: [ mockPeer1 ], + chaincodeId: 'chaincodeId', + txId: mockTransactionID, + fcn: 'myfunc', + args: [ 'arg1', 'arg2' ] + }); queryHandler.queryPeerIndex.should.equal(0); } - }); it('should choose a valid peer', async () => { - const response = Buffer.from('hello world'); - sandbox.stub(queryHandler, '_querySinglePeer').resolves(response); - const result = await queryHandler.queryChaincode('chaincodeId', mockTransactionID, 'myfunc', ['arg1', 'arg2']); - sinon.assert.calledOnce(queryHandler._querySinglePeer); - sinon.assert.calledWith(queryHandler._querySinglePeer, mockPeer1, 'chaincodeId', mockTransactionID, 'myfunc', ['arg1', 'arg2']); + + sinon.assert.calledWith(mockChannel.queryByChaincode, { + targets: [ mockPeer1 ], + chaincodeId: 'chaincodeId', + txId: mockTransactionID, + fcn: 'myfunc', + args: [ 'arg1', 'arg2' ] + }); queryHandler.queryPeerIndex.should.equal(0); - result.equals(response).should.be.true; + result.equals(validResponse).should.be.true; }); it('should cache a valid peer and reuse', async () => { - const response = Buffer.from('hello world'); - sandbox.stub(queryHandler, '_querySinglePeer').resolves(response); - await queryHandler.queryChaincode('chaincodeId', mockTransactionID, 'myfunc', ['arg1', 'arg2']); const result = await queryHandler.queryChaincode('chaincodeId', mockTransactionID, 'myfunc', ['arg1', 'arg2']); - sinon.assert.calledTwice(queryHandler._querySinglePeer); - sinon.assert.alwaysCalledWith(queryHandler._querySinglePeer, mockPeer1, 'chaincodeId', mockTransactionID, 'myfunc', ['arg1', 'arg2']); + + sinon.assert.calledTwice(mockChannel.queryByChaincode); + sinon.assert.alwaysCalledWith(mockChannel.queryByChaincode, { + targets: [ mockPeer1 ], + chaincodeId: 'chaincodeId', + txId: mockTransactionID, + fcn: 'myfunc', + args: [ 'arg1', 'arg2' ] + }); queryHandler.queryPeerIndex.should.equal(0); - result.equals(response).should.be.true; + result.equals(validResponse).should.be.true; }); it('should choose a valid peer if any respond with an error', async () => { - const response = Buffer.from('hello world'); - const qsp = sandbox.stub(queryHandler, '_querySinglePeer'); - - /* this didn't work as the mockPeers look the same - qsp.withArgs(mockPeer2, 'aTxID', 'myfunc', ['arg1', 'arg2']).rejects(new Error('I failed')); - qsp.withArgs(mockPeer1, 'aTxID', 'myfunc', ['arg1', 'arg2']).rejects(new Error('I failed')); - qsp.withArgs(mockPeer3, 'aTxID', 'myfunc', ['arg1', 'arg2']).resolves(response); - */ - qsp.onFirstCall().rejects(new Error('I failed')); - qsp.onSecondCall().rejects(new Error('I failed')); - qsp.onThirdCall().resolves(response); + mockChannel.queryByChaincode.onFirstCall().resolves([failResponse]); + mockChannel.queryByChaincode.onSecondCall().resolves([failResponse]); + mockChannel.queryByChaincode.onThirdCall().resolves([validResponse]); const result = await queryHandler.queryChaincode('chaincodeId', mockTransactionID, 'myfunc', ['arg1', 'arg2']); - sinon.assert.calledThrice(qsp); - sinon.assert.calledWith(qsp.thirdCall, mockPeer4, 'chaincodeId', mockTransactionID, 'myfunc', ['arg1', 'arg2']); + + sinon.assert.calledThrice(mockChannel.queryByChaincode); + sinon.assert.calledWith(mockChannel.queryByChaincode, { + targets: [ mockPeer4 ], + chaincodeId: 'chaincodeId', + txId: mockTransactionID, + fcn: 'myfunc', + args: [ 'arg1', 'arg2' ] + }); queryHandler.queryPeerIndex.should.equal(2); - result.equals(response).should.be.true; + result.equals(validResponse).should.be.true; }); it('should handle when the last successful peer fails', async () => { - const response = Buffer.from('hello world'); - const qsp = sandbox.stub(queryHandler, '_querySinglePeer'); - qsp.onFirstCall().resolves(response); - qsp.onSecondCall().rejects(new Error('I failed')); - qsp.onThirdCall().resolves(response); + mockChannel.queryByChaincode.onFirstCall().resolves([validResponse]); + mockChannel.queryByChaincode.onSecondCall().resolves([failResponse]); + mockChannel.queryByChaincode.onThirdCall().resolves([validResponse]); let result = await queryHandler.queryChaincode('chaincodeId', mockTransactionID, 'myfunc', ['arg1', 'arg2']); - result.equals(response).should.be.true; + result.equals(validResponse).should.be.true; result = await queryHandler.queryChaincode('chaincodeId', mockTransactionID, 'myfunc', ['arg1', 'arg2']); - result.equals(response).should.be.true; - sinon.assert.calledThrice(queryHandler._querySinglePeer); - sinon.assert.calledWith(qsp.firstCall, mockPeer1, 'chaincodeId', mockTransactionID, 'myfunc', ['arg1', 'arg2']); - sinon.assert.calledWith(qsp.secondCall, mockPeer1, 'chaincodeId', mockTransactionID, 'myfunc', ['arg1', 'arg2']); - sinon.assert.calledWith(qsp.thirdCall, mockPeer3, 'chaincodeId', mockTransactionID, 'myfunc', ['arg1', 'arg2']); + + result.equals(validResponse).should.be.true; + sinon.assert.calledThrice(mockChannel.queryByChaincode); + sinon.assert.calledWith(mockChannel.queryByChaincode.firstCall, sinon.match({ targets: [ mockPeer1 ] })); + sinon.assert.calledWith(mockChannel.queryByChaincode.secondCall, sinon.match({ targets: [ mockPeer1 ] })); + sinon.assert.calledWith(mockChannel.queryByChaincode.thirdCall, sinon.match({ targets: [ mockPeer3 ] })); queryHandler.queryPeerIndex.should.equal(1); - result.equals(response).should.be.true; + result.equals(validResponse).should.be.true; }); it('should throw if all peers respond with errors', () => { - const qsp = sandbox.stub(queryHandler, '_querySinglePeer'); - qsp.onFirstCall().rejects(new Error('I failed 1')); - qsp.onSecondCall().rejects(new Error('I failed 2')); - qsp.onThirdCall().rejects(new Error('I failed 3')); + mockChannel.queryByChaincode.onFirstCall().resolves([ new Error('FAIL_1') ]); + mockChannel.queryByChaincode.onSecondCall().resolves([ new Error('FAIL_2') ]); + mockChannel.queryByChaincode.onThirdCall().resolves([ new Error('FAIL_3') ]); return queryHandler.queryChaincode('chaincodeId', mockTransactionID, 'myfunc', ['arg1', 'arg2']) - .should.be.rejectedWith(/No peers available.+failed 3/); + .should.be.rejectedWith(/No peers available.+FAIL_3/); }); - it('should throw if no peers are suitable to query', () => { mockPeer1 = sinon.createStubInstance(Peer); mockPeer1.getName.returns('Peer1'); @@ -174,62 +190,30 @@ describe('DefaultQueryHandler', () => { .should.be.rejectedWith(/No peers have been provided/); }); - }); - - describe('#_querySinglePeer', () => { - - it('should query a single peer', async () => { - const response = Buffer.from('hello world'); - mockChannel.queryByChaincode.resolves([response]); - const result = await queryHandler._querySinglePeer(mockPeer2, 'org-acme-biznet', mockTransactionID, 'myfunc', ['arg1', 'arg2']); - sinon.assert.calledOnce(mockChannel.queryByChaincode); - sinon.assert.calledWith(mockChannel.queryByChaincode, { - chaincodeId: 'org-acme-biznet', - txId: mockTransactionID, - fcn: 'myfunc', - args: ['arg1', 'arg2'], - targets: [mockPeer2] - }); - result.equals(response).should.be.true; - + it('throws if peers return no responses', () => { + mockChannel.queryByChaincode.resolves([]); + return queryHandler.queryChaincode('chaincodeId', mockTransactionID, 'myfunc', ['arg1', 'arg2']) + .should.be.rejectedWith(/No payloads were returned/); }); - it('should throw if no responses are returned', () => { - mockChannel.queryByChaincode.resolves([]); - return queryHandler._querySinglePeer(mockPeer2, 'org-acme-biznet', 'txid', 'myfunc', ['arg1', 'arg2']) - .should.be.rejectedWith(/No payloads were returned from request:myfunc/); + it('throws if queryByChaincode throws', () => { + mockChannel.queryByChaincode.rejects(new Error('queryByChaincode failed')); + return queryHandler.queryChaincode('chaincodeId', mockTransactionID, 'myfunc', ['arg1', 'arg2']) + .should.be.rejectedWith(/No peers available.+queryByChaincode failed/); }); - it('should return any responses that are chaincode errors', async () => { - const response = new Error('such error'); - response.status = 500; - response.isProposalResponse = true; - mockChannel.queryByChaincode.resolves([response]); - const result = await queryHandler._querySinglePeer(mockPeer2, 'org-acme-biznet', mockTransactionID, 'myfunc', ['arg1', 'arg2']); - sinon.assert.calledOnce(mockChannel.queryByChaincode); + it('passes transient data to queryByChaincode', async () => { + const transientMap = { transientKey: Buffer.from('value') }; + await queryHandler.queryChaincode('chaincodeId', mockTransactionID, 'myfunc', ['arg1', 'arg2'], transientMap); + sinon.assert.calledWith(mockChannel.queryByChaincode, { - chaincodeId: 'org-acme-biznet', + targets: [ mockPeer1 ], + chaincodeId: 'chaincodeId', txId: mockTransactionID, fcn: 'myfunc', - args: ['arg1', 'arg2'], - targets: [mockPeer2] + args: [ 'arg1', 'arg2' ], + transientMap: transientMap }); - result.should.be.instanceOf(Error); - result.message.should.equal('such error'); - }); - - it('should throw any responses that are errors and code 14 being unavailable.', () => { - const response = new Error('14 UNAVAILABLE: Connect Failed'); - response.code = 14; - mockChannel.queryByChaincode.resolves([response]); - return queryHandler._querySinglePeer(mockPeer2, 'org-acme-biznet', 'txid', 'myfunc', ['arg1', 'arg2']) - .should.be.rejectedWith(/Connect Failed/); - }); - - it('should throw if query request fails', () => { - mockChannel.queryByChaincode.rejects(new Error('Query Failed')); - return queryHandler._querySinglePeer(mockPeer2, 'org-acme-biznet', 'txid', 'myfunc', ['arg1', 'arg2']) - .should.be.rejectedWith(/Query Failed/); }); }); diff --git a/fabric-network/test/transaction.js b/fabric-network/test/transaction.js index 022dcadd11..05c12e7d1d 100644 --- a/fabric-network/test/transaction.js +++ b/fabric-network/test/transaction.js @@ -16,6 +16,7 @@ const util = require('util'); const Channel = require('fabric-client/lib/Channel'); const Contract = require('fabric-network/lib/contract'); const Network = require('fabric-network/lib/network'); +const QueryHandler = require('fabric-network/lib/api/queryhandler'); const Transaction = require('fabric-network/lib/transaction'); const TransactionEventHandler = require('fabric-network/lib/impl/event/transactioneventhandler'); const TransactionID = require('fabric-client/lib/TransactionID'); @@ -184,10 +185,7 @@ describe('Transaction', () => { }); it('sends a proposal with transient data', async () => { - const transientMap = new Map([ - [ 'key1', 'value1' ], - [ 'key2', 'value2' ] - ]); + const transientMap = { key1: 'value1', key2: 'value2' }; expectedProposal.transientMap = transientMap; transaction.setTransient(transientMap); @@ -196,4 +194,68 @@ describe('Transaction', () => { sinon.assert.calledWith(channel.sendTransactionProposal, sinon.match(expectedProposal)); }); }); + + describe('#evaluate', () => { + const transactionName = 'TRANSACTION_NAME'; + const expectedResult = Buffer.from('42'); + + let stubQueryHandler; + let transaction; + + beforeEach(() => { + stubQueryHandler = sinon.createStubInstance(QueryHandler); + stubQueryHandler.queryChaincode.resolves(expectedResult); + stubContract.getQueryHandler.returns(stubQueryHandler); + + transaction = new Transaction(stubContract, transactionName); + }); + + it('returns the result from the query handler', async () => { + const result = await transaction.evaluate(); + expect(result).to.equal(expectedResult); + }); + + it('passes required parameters to query handler for no-args invocation', async () => { + await transaction.evaluate(); + sinon.assert.calledWith(stubQueryHandler.queryChaincode, + stubContract.getChaincodeId(), + transaction.getTransactionID(), + transactionName, + [] + ); + }); + + it('passes required parameters to query handler for with-args invocation', async () => { + const args = [ 'a', 'b', 'c' ]; + + await transaction.evaluate(...args); + + sinon.assert.calledWith(stubQueryHandler.queryChaincode, + stubContract.getChaincodeId(), + transaction.getTransactionID(), + transactionName, + args + ); + }); + + it('passes transient data to query handler', async () => { + const transientMap = { key1: 'value1', key2: 'value2' }; + transaction.setTransient(transientMap); + + await transaction.evaluate(); + + sinon.assert.calledWith(stubQueryHandler.queryChaincode, + sinon.match.any, + sinon.match.any, + sinon.match.any, + sinon.match.any, + transientMap + ); + }); + + it('rejects for non-string arguments', () => { + const promise = transaction.evaluate('arg1', 3.142, null); + return expect(promise).to.be.rejectedWith('"arg1", 3.142, null'); + }); + }); }); diff --git a/test/integration/network-e2e/invoke.js b/test/integration/network-e2e/invoke.js index 565125a26f..2ffa4ffef4 100644 --- a/test/integration/network-e2e/invoke.js +++ b/test/integration/network-e2e/invoke.js @@ -473,7 +473,7 @@ test('\n\n***** Network End-to-end flow: invoke transaction to move money using t.end(); }); -test('\n\n***** Network End-to-end flow: transient data *****\n\n', async (t) => { +test('\n\n***** Network End-to-end flow: invoke transaction with transient data *****\n\n', async (t) => { const gateway = new Gateway(); try { diff --git a/test/integration/network-e2e/query.js b/test/integration/network-e2e/query.js index 0bd312004c..5f64853a3b 100644 --- a/test/integration/network-e2e/query.js +++ b/test/integration/network-e2e/query.js @@ -22,45 +22,63 @@ const testUtils = require('../../unit/util.js'); const channelName = testUtils.NETWORK_END2END.channel; const chaincodeId = testUtils.NETWORK_END2END.chaincodeId; +const fixtures = process.cwd() + '/test/fixtures'; +const identityLabel = 'User1@org1.example.com'; +const tlsLabel = 'tlsId'; + +async function createWallet(t, path) { + // define the identity to use + const credPath = fixtures + '/channel/crypto-config/peerOrganizations/org1.example.com/users/User1@org1.example.com'; + const cert = fs.readFileSync(credPath + '/signcerts/User1@org1.example.com-cert.pem').toString(); + const key = fs.readFileSync(credPath + '/keystore/e4af7f90fa89b3e63116da5d278855cfb11e048397261844db89244549918731_sk').toString(); + + const fileSystemWallet = new FileSystemWallet(path); + + // prep wallet and test it at the same time + await fileSystemWallet.import(identityLabel, X509WalletMixin.createIdentity('Org1MSP', cert, key)); + const exists = await fileSystemWallet.exists(identityLabel); + t.ok(exists, 'Successfully imported User1@org1.example.com into wallet'); + const tlsInfo = await e2eUtils.tlsEnroll('org1'); + + await fileSystemWallet.import(tlsLabel, X509WalletMixin.createIdentity('org1', tlsInfo.certificate, tlsInfo.key)); + + return fileSystemWallet; +} + +async function deleteWallet(path) { + const rimRafPromise = new Promise((resolve) => { + rimraf(path, (err) => { + if (err) { + //eslint-disable-next-line no-console + console.log(`failed to delete ${path}, error was ${err}`); + resolve(); + } + resolve(); + }); + }); + await rimRafPromise; +} + test('\n\n***** Network End-to-end flow: evaluate transaction to get information *****\n\n', async (t) => { const tmpdir = path.join(os.tmpdir(), 'integration-network-test988'); const gateway = new Gateway(); try { - // define the identity to use - const fixtures = process.cwd() + '/test/fixtures'; - const credPath = fixtures + '/channel/crypto-config/peerOrganizations/org1.example.com/users/User1@org1.example.com'; - const cert = fs.readFileSync(credPath + '/signcerts/User1@org1.example.com-cert.pem').toString(); - const key = fs.readFileSync(credPath + '/keystore/e4af7f90fa89b3e63116da5d278855cfb11e048397261844db89244549918731_sk').toString(); - const identityLabel = 'User1@org1.example.com'; - - const fileSystemWallet = new FileSystemWallet(tmpdir); - - // prep wallet and test it at the same time - await fileSystemWallet.import(identityLabel, X509WalletMixin.createIdentity('Org1MSP', cert, key)); - const exists = await fileSystemWallet.exists(identityLabel); - t.ok(exists, 'Successfully imported User1@org1.example.com into wallet'); - const tlsInfo = await e2eUtils.tlsEnroll('org1'); - - await fileSystemWallet.import('tlsId', X509WalletMixin.createIdentity('org1', tlsInfo.certificate, tlsInfo.key)); - + const wallet = await createWallet(t, tmpdir); const ccp = fs.readFileSync(fixtures + '/network.json'); const ccpObject = JSON.parse(ccp.toString()); await gateway.connect(ccpObject, { - wallet: fileSystemWallet, + wallet: wallet, identity: identityLabel, - clientTlsIdentity: 'tlsId' + clientTlsIdentity: tlsLabel }); - t.pass('Connected to the gateway'); const channel = await gateway.getNetwork(channelName); - t.pass('Initialized the channel, ' + channelName); const contract = await channel.getContract(chaincodeId); - t.pass('Got the contract, about to evaluate (query) transaction'); @@ -89,18 +107,70 @@ test('\n\n***** Network End-to-end flow: evaluate transaction to get information } catch(err) { t.fail('Failed to invoke transaction chaincode on channel. ' + err.stack ? err.stack : err); } finally { - // delete the file system wallet. - const rimRafPromise = new Promise((resolve) => { - rimraf(tmpdir, (err) => { - if (err) { - //eslint-disable-next-line no-console - console.log(`failed to delete ${tmpdir}, error was ${err}`); - resolve(); - } - resolve(); - }); + await deleteWallet(tmpdir); + gateway.disconnect(); + } + + t.end(); +}); + +test('\n\n***** Network End-to-end flow: evaluate transaction with transient data *****\n\n', async (t) => { + const tmpdir = path.join(os.tmpdir(), 'integration-network-test988'); + const gateway = new Gateway(); + + try { + const wallet = await createWallet(t, tmpdir); + const ccp = fs.readFileSync(fixtures + '/network.json'); + const ccpObject = JSON.parse(ccp.toString()); + + await gateway.connect(ccpObject, { + wallet: wallet, + identity: identityLabel, + clientTlsIdentity: tlsLabel }); - await rimRafPromise; + t.pass('Connected to the gateway'); + + const channel = await gateway.getNetwork(channelName); + t.pass('Initialized the channel, ' + channelName); + + const contract = await channel.getContract(chaincodeId); + t.pass('Got the contract, about to evaluate (query) transaction'); + + const transaction = contract.createTransaction('getTransient'); + const transientMap = { + key1: Buffer.from('value1'), + key2: Buffer.from('value2') + }; + transaction.setTransient(transientMap); + const response = await transaction.evaluate(); + + t.pass('Got response: ' + response.toString('utf8')); + const result = JSON.parse(response.toString('utf8')); + + let success = true; + + if (Object.keys(transientMap).length !== Object.keys(result).length) { + success = false; + } + + Object.entries(transientMap).forEach((entry) => { + const key = entry[0]; + const value = entry[1].toString(); + if (value !== result[key]) { + t.fail(`Expected ${key} to be ${value} but was ${result[key]}`); + success = false; + } + }); + + if (success) { + t.pass('Got expected transaction response'); + } else { + t.fail('Unexpected transaction response: ' + response); + } + } catch(err) { + t.fail('Failed to invoke transaction chaincode on channel. ' + err.stack ? err.stack : err); + } finally { + await deleteWallet(tmpdir); gateway.disconnect(); }