Skip to content

Commit

Permalink
Add integration tests for transaction return types and commit flag (c…
Browse files Browse the repository at this point in the history
…ontributes to #4165, #4224) (hyperledger-archives#4228)

Signed-off-by: Simon Stone <[email protected]>
  • Loading branch information
Simon Stone authored Jul 4, 2018
1 parent 4d73e16 commit b9edd1f
Show file tree
Hide file tree
Showing 13 changed files with 687 additions and 145 deletions.
69 changes: 41 additions & 28 deletions packages/composer-cli/lib/cmds/transaction/lib/submit.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
'use strict';

const cmdUtil = require('../../utils/cmdutils');
const { Typed } = require('composer-common');
const Pretty = require('prettyjson');

/**
* <p>
Expand All @@ -28,41 +30,52 @@ class Submit {
/**
* Command process for deploy command
* @param {string} argv argument list from composer command
* @return {Promise} promise when command complete
*/
static handler(argv) {
let businessNetworkConnection;
let cardName = argv.card;
static async handler(argv) {
const cardName = argv.card;
const businessNetworkConnection = cmdUtil.createBusinessNetworkConnection();
await businessNetworkConnection.connect(cardName);

businessNetworkConnection = cmdUtil.createBusinessNetworkConnection();
return businessNetworkConnection.connect(cardName)
let data = argv.data;
if (typeof data === 'string') {
try {
data = JSON.parse(data);
} catch(e) {
throw new Error('JSON error. Have you quoted the JSON string?', e);
}
} else {
throw new Error('Data must be a string');
}

if (!data.$class) {
throw new Error('$class attribute not supplied');
}

.then(() => {
let data = argv.data;
const businessNetwork = businessNetworkConnection.getBusinessNetwork();
const serializer = businessNetwork.getSerializer();
const resource = serializer.fromJSON(data);

if (typeof data === 'string') {
try {
data = JSON.parse(data);
} catch(e) {
throw new Error('JSON error. Have you quoted the JSON string?', e);
const result = await businessNetworkConnection.submitTransaction(resource);
if (result) {
const prettify = (result) => {
if (result instanceof Typed) {
return serializer.toJSON(result);
}
return result;
};
let prettyResult;
if (Array.isArray(result)) {
prettyResult = result.map(item => prettify(item));
} else {
throw new Error('Data must be a string');
}

if (!data.$class) {
throw new Error('$class attribute not supplied');
prettyResult = prettify(result);
}

let businessNetwork = businessNetworkConnection.getBusinessNetwork();
let serializer = businessNetwork.getSerializer();
let resource = serializer.fromJSON(data);

return businessNetworkConnection.submitTransaction(resource);
})
.then((submitted) => {
cmdUtil.log('Transaction Submitted.');
});
cmdUtil.log(Pretty.render(prettyResult, {
keysColor: 'blue',
dashColor: 'blue',
stringColor: 'white'
}));
}
cmdUtil.log('Transaction Submitted.');
}

}
Expand Down
161 changes: 107 additions & 54 deletions packages/composer-cli/test/transaction/submit.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,50 +14,50 @@

'use strict';

const Client = require('composer-client');
const Admin = require('composer-admin');
const Common = require('composer-common');
const BusinessNetworkConnection = Client.BusinessNetworkConnection;
const BusinessNetworkDefinition = Admin.BusinessNetworkDefinition;
const Serializer = Common.Serializer;
const Resource = Common.Resource;

const Submit = require('../../lib/cmds/transaction/submitCommand.js');
const { BusinessNetworkConnection } = require('composer-client');
const { BusinessNetworkDefinition } = require('composer-common');
const CmdUtil = require('../../lib/cmds/utils/cmdutils.js');
const Pretty = require('prettyjson');
const Submit = require('../../lib/cmds/transaction/submitCommand.js');

const chai = require('chai');
const sinon = require('sinon');

chai.should();
chai.use(require('chai-things'));
chai.use(require('chai-as-promised'));
const sinon = require('sinon');

const NAMESPACE = 'net.biz.TestNetwork';
const ENROLL_SECRET = 'SuccessKidWin';



describe('composer transaction submit CLI unit tests', () => {
let sandbox;
let mockBusinessNetworkConnection;
let mockBusinessNetwork;
let mockSerializer;
let mockResource;
let businessNetworkDefinition;
let modelManager;
let factory;
let spyPretty;

beforeEach(() => {
sandbox = sinon.sandbox.create();

businessNetworkDefinition = new BusinessNetworkDefinition('[email protected]');
modelManager = businessNetworkDefinition.getModelManager();
modelManager.addModelFile(`
namespace org.acme
concept MyConcept {
o String value
}
transaction MyTransaction {
o Boolean success
}`);
factory = businessNetworkDefinition.getFactory();

mockBusinessNetworkConnection = sinon.createStubInstance(BusinessNetworkConnection);
mockBusinessNetwork = sinon.createStubInstance(BusinessNetworkDefinition);
mockSerializer = sinon.createStubInstance(Serializer);
mockResource = sinon.createStubInstance(Resource);
mockBusinessNetworkConnection.getBusinessNetwork.returns(mockBusinessNetwork);
mockBusinessNetworkConnection.getBusinessNetwork.returns(businessNetworkDefinition);
mockBusinessNetworkConnection.connect.resolves();
mockBusinessNetwork.getSerializer.returns(mockSerializer);
mockSerializer.fromJSON.returns(mockResource);
mockResource.getIdentifier.returns('SuccessKid');

sandbox.stub(CmdUtil, 'createBusinessNetworkConnection').returns(mockBusinessNetworkConnection);
sandbox.stub(process, 'exit');
spyPretty = sandbox.spy(Pretty, 'render');
});

afterEach(() => {
Expand All @@ -66,71 +66,124 @@ describe('composer transaction submit CLI unit tests', () => {

describe('#hander', () => {

it('should not error when all requred params (card based) are specified', () => {
it('should not error when all requred params (card based) are specified', async () => {
sandbox.stub(CmdUtil, 'prompt').resolves(ENROLL_SECRET);

let argv = {
card: 'cardname',
data: '{"$class": "org.acme.MyTransaction", "success": true}'
};

await Submit.handler(argv);
sinon.assert.calledWith(mockBusinessNetworkConnection.connect,'cardname');
});

it('should not error when the transaction returns a primitive value', async () => {
mockBusinessNetworkConnection.submitTransaction.resolves('foobar');
sandbox.stub(CmdUtil, 'prompt').resolves(ENROLL_SECRET);

let argv = {
card: 'cardname',
data: '{"$class": "org.acme.MyTransaction", "success": true}'
};

await Submit.handler(argv);
sinon.assert.calledWith(mockBusinessNetworkConnection.connect,'cardname');
sinon.assert.calledOnce(spyPretty);
sinon.assert.calledWith(spyPretty, 'foobar');
});

it('should not error when the transaction returns an array of primitive values', async () => {
mockBusinessNetworkConnection.submitTransaction.resolves(['foobar', 'doge', 'cat']);
sandbox.stub(CmdUtil, 'prompt').resolves(ENROLL_SECRET);

let argv = {
card: 'cardname',
data: '{"$class": "'+NAMESPACE+'", "success": true}'
data: '{"$class": "org.acme.MyTransaction", "success": true}'
};

return Submit.handler(argv)
.then((res) => {
sinon.assert.calledWith(mockBusinessNetworkConnection.connect,'cardname');
await Submit.handler(argv);
sinon.assert.calledWith(mockBusinessNetworkConnection.connect,'cardname');
sinon.assert.calledOnce(spyPretty);
sinon.assert.calledWith(spyPretty, ['foobar', 'doge', 'cat']);
});

it('should not error when the transaction returns a concept value', async () => {
const concept = factory.newConcept('org.acme', 'MyConcept');
concept.value = 'foobar';
mockBusinessNetworkConnection.submitTransaction.resolves(concept);
sandbox.stub(CmdUtil, 'prompt').resolves(ENROLL_SECRET);

let argv = {
card: 'cardname',
data: '{"$class": "org.acme.MyTransaction", "success": true}'
};

});
await Submit.handler(argv);
sinon.assert.calledWith(mockBusinessNetworkConnection.connect,'cardname');
sinon.assert.calledOnce(spyPretty);
sinon.assert.calledWith(spyPretty, { $class: 'org.acme.MyConcept', value: 'foobar' });
});

it('should error when can not parse the json (card based)', () => {
sandbox.stub(JSON, 'parse').throws(new Error('failure'));
it('should not error when the transaction returns an array of concept values', async () => {
const concept1 = factory.newConcept('org.acme', 'MyConcept');
concept1.value = 'foobar';
const concept2 = factory.newConcept('org.acme', 'MyConcept');
concept2.value = 'doge';
const concept3 = factory.newConcept('org.acme', 'MyConcept');
concept3.value = 'cat';
mockBusinessNetworkConnection.submitTransaction.resolves([concept1, concept2, concept3]);
sandbox.stub(CmdUtil, 'prompt').resolves(ENROLL_SECRET);

let argv = {
card: 'cardname',
data: '{"$class": "org.acme.MyTransaction", "success": true}'
};

await Submit.handler(argv);
sinon.assert.calledWith(mockBusinessNetworkConnection.connect,'cardname');
sinon.assert.calledOnce(spyPretty);
sinon.assert.calledWith(spyPretty, [{ $class: 'org.acme.MyConcept', value: 'foobar' }, { $class: 'org.acme.MyConcept', value: 'doge' }, { $class: 'org.acme.MyConcept', value: 'cat' }]);
});

it('should error when can not parse the json (card based)', async () => {
let argv = {
card: 'cardname',
data: '{"$class": "'+NAMESPACE+'", "success": true}'
data: '{"$class": "org.acme.MyTransaction", "success": true'
};

return Submit.handler(argv).should.be.rejectedWith(/JSON error/);
await Submit.handler(argv).should.be.rejectedWith(/JSON error/);
});

it('should error when the transaction fails to submit', () => {
it('should error when the transaction fails to submit', async () => {
let argv = {
card: 'cardname',
data: '{"$class": "'+NAMESPACE+'", "success": true}'
data: '{"$class": "org.acme.MyTransaction", "success": true}'
};

mockBusinessNetworkConnection.submitTransaction.rejects(new Error('some error'));
return Submit.handler(argv)
.then((res) => {
// sinon.assert.calledWith(process.exit, 1);
}).catch((error) => {
error.toString().should.equal('Error: some error');
});
await Submit.handler(argv)
.should.be.rejectedWith(/some error/);
});

it('should error if data is not a string', () => {
it('should error if data is not a string', async () => {
let argv = {
card: 'cardname',
data: {}
};

return Submit.handler(argv)
.then((res) => {
}).catch((error) => {
error.toString().should.equal('Error: Data must be a string');
});
await Submit.handler(argv)
.should.be.rejectedWith(/Data must be a string/);
});

it('should error if data class is not supplied', () => {
it('should error if data class is not supplied', async () => {
let argv = {
card: 'cardname',
data: '{"success": true}'
};

return Submit.handler(argv)
.then((res) => {
}).catch((error) => {
error.toString().should.equal('Error: $class attribute not supplied');
});
await Submit.handler(argv)
.should.be.rejectedWith(/\$class attribute not supplied/);
});
});
});
15 changes: 8 additions & 7 deletions packages/composer-connector-hlfv1/lib/hlfconnection.js
Original file line number Diff line number Diff line change
Expand Up @@ -900,6 +900,14 @@ class HLFConnection extends Connection {
const method = 'invokeChainCode';
LOG.entry(method, securityContext, functionName, args, options);

// If commit has been set to false, we do not want to order the transaction or wait for any events.
if (options.commit === false) {
LOG.debug(method, 'Commit has been set to false, deferring to queryChainCode instead');
const result = await this.queryChainCode(securityContext, functionName, args, options);
LOG.exit(method, result);
return result;
}

if (!this.businessNetworkIdentifier) {
throw new Error('No business network has been specified for this connection');
}
Expand Down Expand Up @@ -961,13 +969,6 @@ class HLFConnection extends Connection {
LOG.debug(method, 'Response does not include payload data');
}

// If commit has been set to false, do not order the transaction or wait for any events.
if (options.commit === false) {
LOG.debug(method, 'Commit has been set to false, not ordering transaction or waiting for any events');
LOG.exit(method, result);
return result;
}

// Submit the endorsed transaction to the primary orderers.
const proposal = results[1];
const header = results[2];
Expand Down
30 changes: 3 additions & 27 deletions packages/composer-connector-hlfv1/test/hlfconnection.js
Original file line number Diff line number Diff line change
Expand Up @@ -2052,36 +2052,12 @@ describe('HLFConnection', () => {
});

it('should submit an invoke request to the chaincode and not order it if commit specified as false', () => {
const proposalResponses = [{
response: {
status: 200,
payload: 'hello world'
}
}];
const proposal = { proposal: 'i do' };
const header = { header: 'gooooal' };
mockChannel.sendTransactionProposal.resolves([ proposalResponses, proposal, header ]);
connection._validatePeerResponses.returns({ignoredErrors: 0, validResponses: proposalResponses});
// This is the commit proposal and response (from the orderer).
const response = {
status: 'SUCCESS'
};
mockChannel.sendTransaction.withArgs({ proposalResponses: proposalResponses, proposal: proposal, header: header }).resolves(response);
// This is the event hub response.
mockEventHub1.registerTxEvent.yields('00000000-0000-0000-0000-000000000000', 'VALID');
sandbox.stub(connection, 'queryChainCode').resolves('hello world');
return connection.invokeChainCode(mockSecurityContext, 'myfunc', ['arg1', 'arg2'], { commit: false })
.then((result) => {
result.should.equal('hello world');
sinon.assert.calledOnce(mockChannel.sendTransactionProposal);
sinon.assert.calledWith(mockChannel.sendTransactionProposal, {
chaincodeId: mockBusinessNetwork.getName(),
txId: mockTransactionID,
fcn: 'myfunc',
args: ['arg1', 'arg2']
});
sinon.assert.notCalled(mockChannel.sendTransaction);
sinon.assert.notCalled(connection._checkCCListener);
sinon.assert.calledOnce(connection._checkEventhubs);
sinon.assert.calledOnce(connection.queryChainCode);
sinon.assert.calledWith(connection.queryChainCode, mockSecurityContext, 'myfunc', ['arg1', 'arg2'], { commit: false });
});
});

Expand Down
2 changes: 1 addition & 1 deletion packages/composer-runtime/lib/engine.transactions.js
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ class EngineTransactions {
LOG.error(method, error);
throw error;
}
return context.getSerializer().toJSON(actualReturnValue);
return context.getSerializer().toJSON(actualReturnValue, { convertResourcesToRelationships: true, permitResourcesForRelationships: false });
};

// Handle the non-array case - a single return value.
Expand Down
Loading

0 comments on commit b9edd1f

Please sign in to comment.