diff --git a/.gitignore b/.gitignore index 0978f5ee5b..3766ee86f8 100644 --- a/.gitignore +++ b/.gitignore @@ -46,6 +46,9 @@ jspm_packages .DS_Store *.swp +# vscode folder +.vscode + #generated jsdoc packages/composer-website/jekylldocs/jsdoc/ diff --git a/.travis.yml b/.travis.yml index b01f71965a..fea9575657 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,7 @@ matrix: - env: DOCS=full FC_TASK=docs - env: SYSTEST=embedded FC_TASK=systest - env: SYSTEST=hlf FC_TASK=systest - - env: SYSTEST=hlfv1 FC_TASK=systest + - env: SYSTEST=hlfv1_tls FC_TASK=systest - env: SYSTEST=proxy FC_TASK=systest - env: SYSTEST=web FC_TASK=systest dist: trusty diff --git a/.travis/before-install.sh b/.travis/before-install.sh index 579b642391..29e40a7e71 100755 --- a/.travis/before-install.sh +++ b/.travis/before-install.sh @@ -4,6 +4,27 @@ set -ev set -o pipefail +# Download specific version of docker-compose +export DOCKER_COMPOSE_VERSION=1.11.2 +sudo rm /usr/local/bin/docker-compose +curl -L https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > docker-compose +chmod +x docker-compose +sudo mv docker-compose /usr/local/bin +echo "Docker-compose version: " +docker-compose --version + +# Update docker +sudo apt-get update +sudo apt-get remove docker docker-engine +sudo apt-get install linux-image-extra-$(uname -r) linux-image-extra-virtual +sudo apt-get install apt-transport-https ca-certificates curl software-properties-common +curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - +sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" +sudo apt-get update +sudo apt-get install docker-ce +echo "Docker version: " +docker --version + # Grab the parent (root) directory. DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )/.." && pwd )" diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index a0d83fe2e5..0000000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - // Use IntelliSense to learn about possible Node.js debug attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "type": "node", - "request": "launch", - "name": "Launch Program", - "program": "${file}" - }, - { - "type": "node", - "request": "launch", - "name": "Launch Program", - "program": "${workspaceRoot}/node_modules/.bin/_mocha", - "args": [ "-t", "0", "systest" ], - "cwd": "${workspaceRoot}/packages/composer-systests", - "env": { "npm_lifecycle_event": "systest:embedded"} - } - ] -} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index b806dfdc1b..0000000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "eslint.enable": true, - "eslint.autoFixOnSave": true, - "vsicons.presets.angular": true -} \ No newline at end of file diff --git a/packages/composer-admin/api.txt b/packages/composer-admin/api.txt index a749d6189f..202fedd050 100644 --- a/packages/composer-admin/api.txt +++ b/packages/composer-admin/api.txt @@ -11,4 +11,5 @@ class AdminConnection { + Promise update(BusinessNetworkDefinition) + Promise ping() + Promise list() + + Promise importIdentity(string,string,string,string) } diff --git a/packages/composer-admin/changelog.txt b/packages/composer-admin/changelog.txt index d062c4d2f6..b83ec2c03b 100644 --- a/packages/composer-admin/changelog.txt +++ b/packages/composer-admin/changelog.txt @@ -11,6 +11,8 @@ # # Note that the latest public API is documented using JSDocs and is available in api.txt. # +Version 0.7.5 {3981adc1fb192a8e53d2250ea8feec9a} 2017-05-22 +- added importIdentity method. Version 0.3.7 {af35a7b1a5872beed588f70d3ee0f345} 2017-01-24 - Move to single version diff --git a/packages/composer-admin/lib/adminconnection.js b/packages/composer-admin/lib/adminconnection.js index a1a40dd459..141bb4e93f 100644 --- a/packages/composer-admin/lib/adminconnection.js +++ b/packages/composer-admin/lib/adminconnection.js @@ -318,6 +318,32 @@ class AdminConnection { return this.connection.list(this.securityContext); } + /** + * import an identity into a profiles' wallet + * + * @param {string} connectionProfile Name of the connection profile + * @param {string} id The id to associate with this identity + * @param {string} publicKey The signer cert in PEM format + * @param {string} privateKey The private key in PEM format + * @returns {Promise} A promise which is resolved when the identity is imported + * + * @memberOf AdminConnection + */ + importIdentity(connectionProfile, id, publicKey, privateKey) { + let savedConnectionManager; + return this.connectionProfileManager.getConnectionManager(connectionProfile) + .then((connectionManager) => { + savedConnectionManager = connectionManager; + return this.getProfile(connectionProfile); + }) + .then((profileData) => { + return savedConnectionManager.importIdentity(profileData, id, publicKey, privateKey); + }) + .catch((error) => { + throw new Error('failed to import identity. ' + error.message); + }); + } + } module.exports = AdminConnection; diff --git a/packages/composer-admin/test/adminconnection.js b/packages/composer-admin/test/adminconnection.js index dec9ff8ed2..ee4544f21b 100644 --- a/packages/composer-admin/test/adminconnection.js +++ b/packages/composer-admin/test/adminconnection.js @@ -68,6 +68,7 @@ describe('AdminConnection', () => { mockConnectionManager.connect.resolves(mockConnection); adminConnection = new AdminConnection(); sinon.stub(adminConnection.connectionProfileManager, 'connect').resolves(mockConnection); + sinon.stub(adminConnection.connectionProfileManager, 'getConnectionManager').resolves(mockConnectionManager); sinon.stub(adminConnection.connectionProfileStore, 'save').withArgs('testprofile', sinon.match.any).resolves(); sinon.stub(adminConnection.connectionProfileStore, 'load').withArgs('testprofile').resolves(config); sinon.stub(adminConnection.connectionProfileStore, 'loadAll').resolves({ profile1: config, profile2: config2 }); @@ -253,4 +254,28 @@ describe('AdminConnection', () => { }); + describe('#importIdentity', () => { + it('should be able to import an identity', () => { + mockConnectionManager.importIdentity = sinon.stub(); + adminConnection.connection = mockConnection; + adminConnection.securityContext = mockSecurityContext; + return adminConnection.importIdentity('testprofile', 'anid', 'acerttosign', 'akey') + .then(() => { + sinon.assert.calledOnce(mockConnectionManager.importIdentity); + sinon.assert.calledWith(mockConnectionManager.importIdentity, config, 'anid', 'acerttosign', 'akey'); + }); + }); + + it('should throw an error if import fails', () => { + mockConnectionManager.importIdentity = sinon.stub(); + mockConnectionManager.importIdentity.rejects(new Error('no identity imported')); + adminConnection.connection = mockConnection; + adminConnection.securityContext = mockSecurityContext; + return adminConnection.importIdentity('testprofile', 'anid', 'acerttosign', 'akey') + .should.be.rejectedWith(/no identity imported/); + }); + + + }); + }); diff --git a/packages/composer-cli/cli.js b/packages/composer-cli/cli.js index a86ace550c..7173f63ea3 100755 --- a/packages/composer-cli/cli.js +++ b/packages/composer-cli/cli.js @@ -37,8 +37,9 @@ let results = yargs .version(function() { return getInfo('composer-cli')+ getInfo('composer-admin')+getInfo('composer-client')+ - getInfo('composer-common')+getInfo('composer-runtime')+ - getInfo('composer-connector-hlfv1')+getInfo('composer-runtime-hlfv1'); + getInfo('composer-common')+getInfo('composer-runtime-hlf')+ + getInfo('composer-connector-hlf')+getInfo('composer-runtime-hlfv1')+ + getInfo('composer-connector-hlfv1'); }) .describe('v', 'show version information') .command( diff --git a/packages/composer-cli/lib/cmds/identity/importCommand.js b/packages/composer-cli/lib/cmds/identity/importCommand.js new file mode 100644 index 0000000000..8313432b56 --- /dev/null +++ b/packages/composer-cli/lib/cmds/identity/importCommand.js @@ -0,0 +1,34 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +const Import = require ('./lib/import.js'); + +module.exports.command = 'import [options]'; +module.exports.describe = 'Import an identity to wallet defined by the connection profile'; +module.exports.builder = { + connectionProfileName: {alias: 'p', required: true, describe: 'The connection profile name', type: 'string' }, + userId: { alias: 'u', required: true, describe: 'The user ID for the new identity', type: 'string' }, + publicKeyFile: { alias: 'c', required: true, describe: 'File containing the public key', type: 'string' }, + privateKeyFile: { alias: 'k', required: true, describe: 'File containing the private key', type: 'string' } +}; + +module.exports.handler = (argv) => { + argv.thePromise = Import.handler(argv) + .then(() => { + console.log ('Command completed successfully.'); + }); + return argv.thePromise; +}; diff --git a/packages/composer-cli/lib/cmds/identity/lib/import.js b/packages/composer-cli/lib/cmds/identity/lib/import.js new file mode 100644 index 0000000000..90798be94a --- /dev/null +++ b/packages/composer-cli/lib/cmds/identity/lib/import.js @@ -0,0 +1,65 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +const fs = require('fs'); + +const cmdUtil = require('../../utils/cmdutils'); + +/** + *

+ * Composer "identity issue" command + *

+ *

+ * @private + */ +class Import { + + /** + * Command process for deploy command + * @param {string} argv argument list from composer command + * @return {Promise} promise when command complete + */ + static handler(argv) { + let userId; + let publicKeyFile; + let privateKeyFile; + let connectionProfileName; + + userId = argv.userId; + publicKeyFile = argv.publicKeyFile; + privateKeyFile = argv.privateKeyFile; + connectionProfileName = argv.connectionProfileName; + let adminConnection = cmdUtil.createAdminConnection(); + let signerCert; + let key; + try { + signerCert = fs.readFileSync(publicKeyFile).toString(); + } catch(error) { + return Promise.reject(new Error('Unable to read public key file ' + publicKeyFile + '. ' + error.message)); + } + try { + key = fs.readFileSync(privateKeyFile).toString(); + } catch(error) { + return Promise.reject(new Error('Unable to read private key file ' + privateKeyFile + '. ' + error.message)); + } + return adminConnection.importIdentity(connectionProfileName, userId, signerCert, key) + .then((result) => { + console.log(`An identity was imported with name '${userId}' successfully`); + }); + } +} + +module.exports = Import; diff --git a/packages/composer-cli/test/identity/import.js b/packages/composer-cli/test/identity/import.js new file mode 100644 index 0000000000..81745d59fc --- /dev/null +++ b/packages/composer-cli/test/identity/import.js @@ -0,0 +1,115 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +const Client = require('composer-admin'); + +const Import = require('../../lib/cmds/identity/importCommand.js'); +const CmdUtil = require('../../lib/cmds/utils/cmdutils.js'); +const fs = require('fs'); + +const sinon = require('sinon'); +require('sinon-as-promised'); +const chai = require('chai'); +chai.should(); +chai.use(require('chai-as-promised')); + +const PROFILE_NAME = 'myprofile'; +const USER_ID = 'SuccessKid'; +const CERT_PATH = 'someCertPath'; +const KEY_PATH = 'someKeyPath'; + +describe('composer identity import CLI unit tests', () => { + + let sandbox; + let mockAdminConnection; + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + mockAdminConnection = sinon.createStubInstance(Client.AdminConnection); + sandbox.stub(CmdUtil, 'createAdminConnection').returns(mockAdminConnection); + sandbox.stub(process, 'exit'); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should import a new identity using the specified profile', () => { + let argv = { + connectionProfileName: PROFILE_NAME, + userId: USER_ID, + publicKeyFile: CERT_PATH, + privateKeyFile: KEY_PATH + }; + + let fsStub = sandbox.stub(fs, 'readFileSync').withArgs(CERT_PATH).returns('acert'); + fsStub.withArgs(KEY_PATH).returns('akey'); + mockAdminConnection.importIdentity.withArgs(PROFILE_NAME, USER_ID, 'acert', 'akey').resolves(); + return Import.handler(argv) + .then(() => { + sinon.assert.calledOnce(mockAdminConnection.importIdentity); + sinon.assert.calledWith(mockAdminConnection.importIdentity, PROFILE_NAME, USER_ID, 'acert', 'akey'); + }); + }); + + it('should fail gracefully if importIdentity fails', () => { + let argv = { + connectionProfileName: PROFILE_NAME, + userId: USER_ID, + publicKeyFile: CERT_PATH, + privateKeyFile: KEY_PATH + }; + + let fsStub = sandbox.stub(fs, 'readFileSync').withArgs(CERT_PATH).returns('acert'); + fsStub.withArgs(KEY_PATH).returns('akey'); + mockAdminConnection.importIdentity.withArgs(PROFILE_NAME, USER_ID, 'acert', 'akey').rejects('some error'); + return Import.handler(argv) + .should.be.rejectedWith(/some error/); + }); + + it('should fail gracefully if cert file cannot be found', () => { + let argv = { + connectionProfileName: PROFILE_NAME, + userId: USER_ID, + publicKeyFile: CERT_PATH, + privateKeyFile: KEY_PATH + }; + + let fsStub = sandbox.stub(fs, 'readFileSync').withArgs(CERT_PATH).throws(new Error('no file found')); + fsStub.withArgs(KEY_PATH).returns('akey'); + mockAdminConnection.importIdentity.withArgs(PROFILE_NAME, USER_ID, 'acert', 'akey').resolves(); + return Import.handler(argv) + .should.be.rejectedWith(/no file found/); + + }); + + it('should fail gracefully if key file cannot be found', () => { + let argv = { + connectionProfileName: PROFILE_NAME, + userId: USER_ID, + publicKeyFile: CERT_PATH, + privateKeyFile: KEY_PATH + }; + + let fsStub = sandbox.stub(fs, 'readFileSync').withArgs(CERT_PATH).returns('acert'); + fsStub.withArgs(KEY_PATH).throws(new Error('no key file found')); + mockAdminConnection.importIdentity.withArgs(PROFILE_NAME, USER_ID, 'acert', 'akey').resolves(); + return Import.handler(argv) + .should.be.rejectedWith(/no key file found/); + + }); + +}); diff --git a/packages/composer-common/lib/connectionmanager.js b/packages/composer-common/lib/connectionmanager.js index 59921a749a..a446a2ac6b 100644 --- a/packages/composer-common/lib/connectionmanager.js +++ b/packages/composer-common/lib/connectionmanager.js @@ -65,6 +65,20 @@ class ConnectionManager { return Promise.reject(new Error('abstract function called')); } + /** + * Import an identity into a profile wallet or keystore + * + * @param {object} profileDefinition the profile definition + * @param {string} id the id to associate with the identity + * @param {string} publicKey the public key + * @param {string} privateKey the private key + * @returns {Promise} a promise which resolves when the identity is imported + * + * @memberOf HLFConnectionManager + */ + importIdentity(profileDefinition, id, publicKey, privateKey) { + return Promise.reject(new Error('Import identity not supported by this connection profile')); + } /** * Stop serialization of this object. * @return {Object} An empty object. diff --git a/packages/composer-common/lib/connectionprofilemanager.js b/packages/composer-common/lib/connectionprofilemanager.js index 2f78fc356a..a4de6a14e4 100644 --- a/packages/composer-common/lib/connectionprofilemanager.js +++ b/packages/composer-common/lib/connectionprofilemanager.js @@ -166,7 +166,6 @@ class ConnectionProfileManager { * @abstract */ connect(connectionProfile, businessNetworkIdentifier, additionalConnectOptions) { - LOG.info('connect','Connecting using ' + connectionProfile, businessNetworkIdentifier); let connectOptions; diff --git a/packages/composer-connector-hlfv1/lib/hlfconnection.js b/packages/composer-connector-hlfv1/lib/hlfconnection.js index 3f06813b4c..8c2ca4acd9 100644 --- a/packages/composer-connector-hlfv1/lib/hlfconnection.js +++ b/packages/composer-connector-hlfv1/lib/hlfconnection.js @@ -23,7 +23,7 @@ const semver = require('semver'); const temp = require('temp').track(); const thenifyAll = require('thenify-all'); const User = require('fabric-client/lib/User.js'); -const utils = require('fabric-client/lib/utils.js'); +const EventHub = require('fabric-client/lib/EventHub'); const LOG = Logger.getLog('HLFConnection'); @@ -49,7 +49,19 @@ class HLFConnection extends Connection { * @return {User} A new user. */ static createUser(enrollmentID, client) { - return new User(enrollmentID, client); + let user = new User(enrollmentID); + user.setCryptoSuite(client.getCryptoSuite()); + return user; + } + + /** + * Create a new event hub. + * + * @param {hfc} clientContext client context + * @return {EventHub} A new event hub. + */ + static createEventHub(clientContext) { + return new EventHub(clientContext); } /** @@ -62,8 +74,7 @@ class HLFConnection extends Connection { * @memberOf HLFConnection */ static generateCcid(input) { - //return input.replace(/\./g, '-').replace(/[|&;$%@"<>()+,]/g, ''); New Required for alpha2 - return input; + return input.replace(/\./g, '-').replace(/[|&;$%@"<>()+,]/g, '').toLowerCase(); } /** @@ -74,23 +85,24 @@ class HLFConnection extends Connection { * or null if this connection if an admin connection * @param {object} connectOptions The connection options in use by this connection. * @param {Client} client A configured and connected {@link Client} object. - * @param {Chain} chain A configured and connected {@link Chain} object. - * @param {array} eventHubs A configured and connected {@link EventHub} object. + * @param {Chain} channel A configured and connected {@link Chain} object. + * @param {array} eventHubDefs An array of event hub definitions * @param {FabricCAClientImpl} caClient A configured and connected {@link FabricCAClientImpl} object. */ - constructor(connectionManager, connectionProfile, businessNetworkIdentifier, connectOptions, client, chain, eventHubs, caClient) { + constructor(connectionManager, connectionProfile, businessNetworkIdentifier, connectOptions, client, channel, eventHubDefs, caClient) { super(connectionManager, connectionProfile, businessNetworkIdentifier); const method = 'constructor'; - LOG.entry(method, connectionManager, connectionProfile, businessNetworkIdentifier, connectOptions, client, chain, eventHubs, caClient); + LOG.entry(method, connectionManager, connectionProfile, businessNetworkIdentifier, connectOptions, client, channel, eventHubDefs, caClient); + // Validate all the arguments. if (!connectOptions) { throw new Error('connectOptions not specified'); } else if (!client) { throw new Error('client not specified'); - } else if (!chain) { - throw new Error('chain not specified'); - } else if (!eventHubs || !Array.isArray(eventHubs)) { - throw new Error('eventHubs not specified or not an array'); + } else if (!channel) { + throw new Error('channel not specified'); + } else if (!eventHubDefs || !Array.isArray(eventHubDefs)) { + throw new Error('eventHubDefs not specified or not an array'); } else if (!caClient) { throw new Error('caClient not specified'); } @@ -98,30 +110,16 @@ class HLFConnection extends Connection { // Save all the arguments away for later. this.connectOptions = connectOptions; this.client = client; - this.chain = chain; - this.businessNetworkIdentifier = businessNetworkIdentifier; - - this.eventHubs = eventHubs; - - if (businessNetworkIdentifier) { - LOG.entry(method, 'registerChaincodeEvent', businessNetworkIdentifier, 'composer'); - eventHubs[0].registerChaincodeEvent(businessNetworkIdentifier, 'composer', (event) => { - - // Remove the first set of "" around the event so it can be parsed first time - let evt = event.payload.toString('utf8'); - evt = evt.replace(/^"(.*)"$/, '$1'); // Remove end quotes - evt = evt.replace(/\\/g, ''); - evt = JSON.parse(evt); - this.emit('events', evt); - }); - } - + this.channel = channel; + this.eventHubDefs = eventHubDefs; + this.eventHubs = []; + this.ccEvents = []; this.caClient = caClient; + this.initialized = false; // We create promisified versions of these APIs. this.fs = thenifyAll(fs); this.temp = thenifyAll(temp); - LOG.exit(method); } @@ -145,17 +143,23 @@ class HLFConnection extends Connection { // Disconnect from the business network. return Promise.resolve() .then(() => { - this.eventHubs.forEach((eventHub) => { + this.eventHubs.forEach((eventHub, index) => { if (eventHub.isconnected()) { eventHub.disconnect(); } - this.eventHubs[0].unregisterChaincodeEvent(this.businessNetworkIdentifier); + + // unregister any eventhub chaincode event registrations + if (this.ccEvents[index]) { + this.eventHubs[index].unregisterChaincodeEvent(this.ccEvents[index]); + } + }); LOG.exit(method); }) .catch((error) => { - LOG.error(method, error); - throw error; + const newError = new Error('error trying disconnect. ' + error); + LOG.error(method, newError); + throw newError; }); } @@ -183,7 +187,6 @@ class HLFConnection extends Connection { let user; return this.caClient.enroll(options) .then((enrollment) => { - // Store the certificate data in a new user object. LOG.debug(method, 'Successfully enrolled, creating user object'); user = HLFConnection.createUser(enrollmentID, this.client); @@ -197,18 +200,66 @@ class HLFConnection extends Connection { }) .then(() => { - return this.chain.initialize(); + return this._initializeChannel(); }) .then(() => { LOG.exit(method, user); return user; }) .catch((error) => { - LOG.error(method, error); - throw error; + const newError = new Error('error trying to enroll user. ' + error); + LOG.error(method, newError); + throw newError; }); } + /** + * process the event hub defs to create event hubs and connect + * to them + */ + _connectToEventHubs() { + const method = '_connectToEventHubs'; + LOG.entry(method); + this.eventHubDefs.forEach((eventHubDef) => { + const eventHub = HLFConnection.createEventHub(this.client); //TODO: Change this. + eventHub.setPeerAddr(eventHubDef.eventURL, eventHubDef.opts); + eventHub.connect(); + this.eventHubs.push(eventHub); + }); + + if (this.businessNetworkIdentifier) { + + // register a chaincode event listener on the first peer only. + let ccid = HLFConnection.generateCcid(this.businessNetworkIdentifier); + LOG.debug(method, 'registerChaincodeEvent', ccid, 'composer'); + let ccEvent = this.eventHubs[0].registerChaincodeEvent(ccid, 'composer', (event) => { + //let evt = Buffer.from(event.payload, 'hex').toString('utf8'); + // Remove the first set of "" around the event so it can be parsed first time + let evt = event.payload.toString('utf8'); + evt = evt.replace(/^"(.*)"$/, '$1'); // Remove end quotes + evt = evt.replace(/\\/g, ''); + evt = JSON.parse(evt); + this.emit('events', evt); + }); + this.ccEvents[0] = ccEvent; + } + + process.on('exit', () => { + this.eventHubs.forEach((eventHub, index) => { + if (eventHub.isconnected()) { + eventHub.disconnect(); + } + + // unregister any eventhub chaincode event registrations + if (this.ccEvents[index]) { + this.eventHubs[index].unregisterChaincodeEvent(this.ccEvents[index]); + } + }); + }); + + LOG.exit(method); + } + /** * Login as a participant on the business network. * @param {string} enrollmentID The enrollment ID of the participant. @@ -228,7 +279,7 @@ class HLFConnection extends Connection { } // Get the user context (certificate) from the state store. - return this.client.getUserContext(enrollmentID) + return this.client.getUserContext(enrollmentID, true) .then((user) => { // If the user exists and is enrolled, we use the data from the state store. @@ -248,13 +299,17 @@ class HLFConnection extends Connection { let result = new HLFSecurityContext(this); result.setUser(enrollmentID); this.user = user; + + // now we can connect to the eventhubs + this._connectToEventHubs(); LOG.exit(method, result); return result; }) .catch((error) => { - LOG.error(method, error); - throw error; + const newError = new Error('error trying login and get user Context. ' + error); + LOG.error(method, newError); + throw newError; }); } @@ -272,21 +327,11 @@ class HLFConnection extends Connection { const method = '_install'; LOG.entry(method, securityContext, businessNetwork); - // Check that a valid security context has been specified. - HLFUtil.securityCheck(securityContext); - - // Validate all the arguments. - if (!businessNetwork) { - throw new Error('businessNetwork not specified'); - } - - // Because hfc needs to write a Dockerfile to the chaincode directory, we // must copy the chaincode to a temporary directory. We need to do this // to handle the case where Composer is installed into the global directory // (npm install -g) and is therefore owned by the root user. let tempDirectoryPath; - let nonce = utils.getNonce(); return this.temp.mkdir('composer') .then((tempDirectoryPath_) => { @@ -309,89 +354,105 @@ class HLFConnection extends Connection { }) .then(() => { - //let txId = Hfc.buildTransactionID(nonce, this._getLoggedInUser()); New version coming - let txId = this.chain.buildTransactionID(nonce, this._getLoggedInUser()); - + let txId = this.client.newTransactionID(); // This is evil! I shouldn't need to set GOPATH in a node.js program. process.env.GOPATH = tempDirectoryPath; // Submit the install request to the peer const request = { chaincodePath: chaincodePath, - chaincodeVersion: connectorPackageJSON.version, + chaincodeVersion: runtimePackageJSON.version, chaincodeId: HLFConnection.generateCcid(businessNetwork.getName()), - chainId: this.connectOptions.channel, // alpha2 will remove this line txId: txId, - nonce: nonce//, - //targets: this.chain.getPeers() alpha2 will add this line + targets: this.channel.getPeers() }; - return this.chain.sendInstallProposal(request); - //return this.client.installChaincode(request); New version coming + + return this.client.installChaincode(request); + }) + .then((results) => { + LOG.debug(method, `Received ${results.length} results(s) from installing the chaincode`, results); + + // Validate the proposal results, ignore chaincode exists messages + this._validateResponses(results[0], false, /chaincode .+ exists/); + + LOG.debug(method, 'chaincode installed, or already installed'); + }) + .then(() => { + LOG.exit(method); }) .catch((error) => { - LOG.error(method, error); - throw error; + const newError = new Error('error trying install chaincode. ' + error); + LOG.error(method, newError); + throw newError; }); } - //TODO: Do we want to separate out instantiate ? + /** + * initialize the channel if it hasn't been done + * + * @returns {Promise} a promise that the channel is initialized + * + * @memberOf HLFConnection + */ + _initializeChannel() { + if (!this.initialized) { + return this.channel.initialize() + .then(() => { + this.initialized = true; + }); + } + return Promise.resolve(); + } /** - * Deploy all business network artifacts. - * @param {HFCSecurityContext} securityContext The participant's security context. - * @param {boolean} [force] Force the deployment of the business network artifacts. Not used by this connector. - * @param {BusinessNetwork} businessNetwork The BusinessNetwork to deploy - * @return {Promise} A promise that is resolved once the business network - * artifacts have been deployed, or rejected with an error. + * instantiate the chaincode + * + * + * @param {any} securityContext the security context + * @param {any} businessNetwork the business network + * @private + * @returns {Promise} a promise for instantiation completion + * + * @memberOf HLFConnection */ - deploy(securityContext, force, businessNetwork) { - const method = 'deploy'; - LOG.entry(method, securityContext, force, businessNetwork); + _instantiate(securityContext, businessNetwork) { + const method = '_instantiate'; + LOG.entry(method, securityContext, businessNetwork); + let businessNetworkArchive; - let nonce; let finalTxId; - return this._install(securityContext, businessNetwork) - .then((results) => { - LOG.debug(method, `Received ${results.length} results(s) from installing the chaincode`, results); - - // Validate the proposal results, ignore chaincode exists messages - this._validateResponses(results[0], /chaincode .+ exists/); - LOG.debug(method, 'chaincode installed, or already installed'); - // initialize the chain ready for instantiation - return this.chain.initialize(); - }) + // initialize the channel ready for instantiation + return this._initializeChannel() .then(() => { // serialise the business network return businessNetwork.toArchive(); }) .then((bna) => { businessNetworkArchive = bna; - nonce = utils.getNonce(); // prepare and send the instantiate proposal - finalTxId = this.chain.buildTransactionID(nonce, this._getLoggedInUser()); + finalTxId = this.client.newTransactionID(); + const request = { chaincodePath: chaincodePath, - chaincodeVersion: connectorPackageJSON.version, + chaincodeVersion: runtimePackageJSON.version, chaincodeId: HLFConnection.generateCcid(businessNetwork.getName()), - chainId: this.connectOptions.channel, txId: finalTxId, - nonce: nonce, fcn: 'init', args: [businessNetworkArchive.toString('base64')] }; - return this.chain.sendInstantiateProposal(request); + return this.channel.sendInstantiateProposal(request); }) .then((results) => { // Validate the instantiate proposal results LOG.debug(method, `Received ${results.length} results(s) from deploying the chaincode`, results); let proposalResponses = results[0]; - this._validateResponses(proposalResponses); + this._validateResponses(proposalResponses, true); - // Submit the endorsed transaction to the orderers. + // Submit the endorsed transaction to the primary orderer. const proposal = results[1]; const header = results[2]; - return this.chain.sendTransaction({ + return this.channel.sendTransaction({ proposalResponses: proposalResponses, proposal: proposal, header: header @@ -405,41 +466,107 @@ class HLFConnection extends Connection { if (response.status !== 'SUCCESS') { throw new Error(`Failed to commit transaction '${finalTxId}' with response status '${response.status}'`); } - return this._waitForEvents(finalTxId, this.connectOptions.deployWaitTime); + return this._waitForEvents(finalTxId, this.connectOptions.timeout); }) .then(() => { LOG.exit(method); }) .catch((error) => { - LOG.error(method, error); - throw error; + const newError = new Error('error trying instantiate chaincode. ' + error); + LOG.error(method, newError); + throw newError; }); } + /** + * Deploy all business network artifacts. + * @param {HFCSecurityContext} securityContext The participant's security context. + * @param {boolean} [force] Force the deployment of the business network artifacts. Not used by this connector. + * @param {BusinessNetwork} businessNetwork The BusinessNetwork to deploy + * @return {Promise} A promise that is resolved once the business network + * artifacts have been deployed, or rejected with an error. + */ + deploy(securityContext, force, businessNetwork) { + const method = 'deploy'; + LOG.entry(method, securityContext, force, businessNetwork); + + // Check that a valid security context has been specified. + HLFUtil.securityCheck(securityContext); + + // Validate all the arguments. + if (!businessNetwork) { + throw new Error('businessNetwork not specified'); + } + + return this._install(securityContext, businessNetwork) + .then(() => { + // check to see if the chaincode is already instantiated + return this.channel.queryInstantiatedChaincodes(); + }) + .then((queryResults) => { + LOG.debug(method, 'Queried instantiated chaincodes', queryResults); + let alreadyInstantiated = queryResults.chaincodes.some((chaincode) => { + return chaincode.path === 'composer' && chaincode.name === HLFConnection.generateCcid(businessNetwork.getName()); + }); + if (alreadyInstantiated) { + LOG.debug(method, 'chaincode already instantiated'); + return Promise.resolve(); + } + return this._instantiate(securityContext, businessNetwork); + }) + .then(() => { + LOG.exit(method); + }) + .catch((error) => { + const newError = new Error('error trying deploy. ' + error); + LOG.error(method, newError); + throw newError; + }); + + } + /** * Check for proposal response errors. * @private - * @param {any} proposalResponses the proposal responses - * @param {regexp} pattern regular expression for message which isn't an error + * @param {any} responses the responses from the install, instantiate or invoke + * @param {boolean} isProposal true is the responses are from a proposal + * @param {regexp} pattern optional regular expression for message which isn't an error * @throws if not valid */ - _validateResponses(proposalResponses, pattern) { - if (!proposalResponses.length) { + _validateResponses(responses, isProposal, pattern) { + const method = '_validateResponses'; + LOG.entry(method, responses, pattern, isProposal); + + if (!responses.length) { throw new Error('No results were returned from the request'); } - proposalResponses.forEach((proposalResponse) => { - if (proposalResponse instanceof Error) { - if (pattern && pattern.test(proposalResponse.message)) { - return true; + responses.forEach((responseContent) => { + if (responseContent instanceof Error) { + // check to see if we should ignore the error, this also means we cannot verify the proposal + // or check the proposals across peers + if (!pattern || !pattern.test(responseContent.message)) { + throw responseContent; + } + } else { + + // not an error, if it is from a proposal, verify the response + if (isProposal && !this.channel.verifyProposalResponse(responseContent)) { + throw new Error('Response from peer was not valid'); + } + if (responseContent.response.status !== 200) { + throw new Error('Unexpected response of ' + responseContent.response.status + '. payload was :' +responseContent.response.payload); } - throw proposalResponse; - } else if (proposalResponse.response.status === 200) { - return true; } - throw new Error(proposalResponse.response.payload); + }); + + // if it was a proposal and all the responses were good, check that they compare + if (isProposal && !this.channel.compareProposalResponseResults(responses)) { + throw new Error('Peers do not agree, RW sets differ'); + } + LOG.exit(method); } /** @@ -471,8 +598,9 @@ class HLFConnection extends Connection { LOG.exit(method); }) .catch((error) => { - LOG.error(method, error); - throw error; + const newError = new Error('error trying undeploy. ' + error); + LOG.error(method, newError); + throw newError; }); } @@ -508,8 +636,9 @@ class HLFConnection extends Connection { LOG.exit(method); }) .catch((error) => { - LOG.error(method, error); - throw error; + const newError = new Error('error trying to update business network. ' + error); + LOG.error(method, newError); + throw newError; }); } @@ -551,8 +680,9 @@ class HLFConnection extends Connection { }) .catch((error) => { - LOG.error(method, error); - throw error; + const newError = new Error('error trying to ping. ' + error); + LOG.error(method, newError); + throw newError; }); } @@ -583,20 +713,18 @@ class HLFConnection extends Connection { } }); - let nonce = utils.getNonce(); - let txId = this.chain.buildTransactionID(nonce, this._getLoggedInUser()); + let txId = this.client.newTransactionID(); + // Submit the query request. const request = { chaincodeId: HLFConnection.generateCcid(this.businessNetworkIdentifier), - chainId: this.connectOptions.channel, + chaincodeVersion: runtimePackageJSON.version, txId: txId, - nonce: nonce, fcn: functionName, - args: args, - attrs: ['userID'] + args: args }; - return this.chain.queryByChaincode(request) + return this.channel.queryByChaincode(request) .then((payloads) => { LOG.debug(method, `Received ${payloads.length} payloads(s) from querying the chaincode`, payloads); if (!payloads.length) { @@ -611,8 +739,9 @@ class HLFConnection extends Connection { return payload; }) .catch((error) => { - LOG.error(method, error); - throw error; + const newError = new Error('error trying to query chaincode. ' + error); + LOG.error(method, newError); + throw newError; }); } @@ -644,31 +773,34 @@ class HLFConnection extends Connection { } }); - let nonce = utils.getNonce(); - let txId = this.chain.buildTransactionID(nonce, this._getLoggedInUser()); - // Submit the transaction to the endorsers. - const request = { - chaincodeId: HLFConnection.generateCcid(this.businessNetworkIdentifier), - chainId: this.connectOptions.channel, - txId: txId, - nonce: nonce, - fcn: functionName, - args: args, - attrs: ['userID'] - }; - return this.chain.sendTransactionProposal(request) - .then((results) => { + let txId = this.client.newTransactionID(); + + // initialize the channel if it hasn't been initialized already otherwise verification will fail. + return this._initializeChannel() + .then(() => { + + // Submit the transaction to the endorsers. + const request = { + chaincodeId: HLFConnection.generateCcid(this.businessNetworkIdentifier), + chaincodeVersion: runtimePackageJSON.version, + txId: txId, + fcn: functionName, + args: args + }; + return this.channel.sendTransactionProposal(request); + }) + .then((results) => { // Validate the endorsement results. LOG.debug(method, `Received ${results.length} results(s) from invoking the chaincode`, results); const proposalResponses = results[0]; - this._validateResponses(proposalResponses); + this._validateResponses(proposalResponses, true); - // Submit the endorsed transaction to the orderers. + // Submit the endorsed transaction to the primary orderers. const proposal = results[1]; const header = results[2]; - return this.chain.sendTransaction({ + return this.channel.sendTransaction({ proposalResponses: proposalResponses, proposal: proposal, header: header @@ -682,14 +814,15 @@ class HLFConnection extends Connection { if (response.status !== 'SUCCESS') { throw new Error(`Failed to commit transaction '${txId}' with response status '${response.status}'`); } - return this._waitForEvents(txId, this.connectOptions.invokeWaitTime); + return this._waitForEvents(txId, this.connectOptions.timeout); }) .then(() => { LOG.exit(method); }) .catch((error) => { - LOG.error(method, error); - throw error; + const newError = new Error('error trying invoke chaincode. ' + error); + LOG.error(method, newError); + throw newError; }); } @@ -757,7 +890,7 @@ class HLFConnection extends Connection { }); }) .catch((error) => { - LOG.error(method, 'Register request failed', error); + LOG.error(method, 'Register request failed trying to create identity', error); return reject(error); }); }); @@ -778,7 +911,7 @@ class HLFConnection extends Connection { HLFUtil.securityCheck(securityContext); // Query all instantiated chaincodes. - return this.chain.queryInstantiatedChaincodes() + return this.channel.queryInstantiatedChaincodes() .then((queryResults) => { LOG.debug(method, 'Queried instantiated chaincodes', queryResults); const result = queryResults.chaincodes.filter((chaincode) => { @@ -790,28 +923,32 @@ class HLFConnection extends Connection { return result; }) .catch((error) => { - LOG.error(method, error); - throw error; + const newError = new Error('error trying to list instantiated chaincodes. ' + error); + LOG.error(method, newError); + throw newError; }); } - /** + /** * wait for events from the peers associated with the provided transaction id. - * @param {string} txId the transaction id to listen for events on + * @param {string} txObj the transaction id to listen for events on * @param {number} waitTime the time to wait in seconds for an event response * @returns {Promise} A promise which resolves when all the events are received or rejected * if an event is not received within the given timeout period * @memberOf HLFConnection */ - _waitForEvents(txId, waitTime) { + _waitForEvents(txObj, waitTime) { + const txId = txObj.getTransactionID().toString(); + const method = '_waitForEvents'; + LOG.entry(method, txId, waitTime); let eventPromises = []; this.eventHubs.forEach((eh) => { let txPromise = new Promise((resolve, reject) => { const handle = setTimeout(() => { reject(new Error(`Failed to receive commit notification for transaction '${txId}' within the timeout period`)); }, waitTime * 1000); - eh.registerTxEvent(txId.toString(), (tx, code) => { + eh.registerTxEvent(txId, (tx, code) => { clearTimeout(handle); eh.unregisterTxEvent(txId); if (code !== 'VALID') { @@ -821,10 +958,12 @@ class HLFConnection extends Connection { } }); }); - eventPromises.push(txPromise); }); - return Promise.all(eventPromises); + return Promise.all(eventPromises) + .then(() => { + LOG.exit(method); + }); } /** diff --git a/packages/composer-connector-hlfv1/lib/hlfconnectionmanager.js b/packages/composer-connector-hlfv1/lib/hlfconnectionmanager.js index c2e1e5f213..46c2717149 100644 --- a/packages/composer-connector-hlfv1/lib/hlfconnectionmanager.js +++ b/packages/composer-connector-hlfv1/lib/hlfconnectionmanager.js @@ -69,16 +69,16 @@ global.hfc = { } }; + const Client = require('fabric-client'); -const ConnectionManager = require('composer-common').ConnectionManager; -const EventHub = require('fabric-client/lib/EventHub'); const FabricCAClientImpl = require('fabric-ca-client'); -const HLFConnection = require('./hlfconnection'); -const HLFWalletProxy = require('./hlfwalletproxy'); const Orderer = require('fabric-client/lib/Orderer'); const Peer = require('fabric-client/lib/Peer'); -const Wallet = require('composer-common').Wallet; +const ConnectionManager = require('composer-common').ConnectionManager; +const HLFConnection = require('./hlfconnection'); +const HLFWalletProxy = require('./hlfwalletproxy'); +const Wallet = require('composer-common').Wallet; /** * Class representing a connection manager that establishes and manages @@ -99,53 +99,61 @@ class HLFConnectionManager extends ConnectionManager { /** * Create a new orderer. * @param {string} ordererURL The orderer URL string or the orderer object definition - * @param {object} tlsOpts optional tls options + * @param {object} opts optional tls options * @return {Orderer} A new orderer. */ - static createOrderer(ordererURL, tlsOpts) { - return new Orderer(ordererURL, tlsOpts); + static createOrderer(ordererURL, opts) { + return new Orderer(ordererURL, opts); //TODO: Change this } /** * parse the orderer definition * @param {string|object} orderer The orderer definition + * @param {number} timeout the request + * @param {string} globalCert if provided use this unless cert is provided * @return {Orderer} A new orderer. */ - static parseOrderer(orderer) { + static parseOrderer(orderer, timeout, globalCert) { if (typeof orderer === 'object') { - const opts = HLFConnectionManager._createOpts(orderer.cert, orderer.hostnameOverride); + const opts = HLFConnectionManager._createOpts(timeout, orderer.cert, orderer.hostnameOverride, globalCert); return HLFConnectionManager.createOrderer(orderer.url, opts); } - return HLFConnectionManager.createOrderer(orderer); + return HLFConnectionManager.createOrderer(orderer, HLFConnectionManager._createOpts(timeout, null, null, globalCert)); } /** - * create tls options for fabric-client + * create options for fabric-client * @static * @private + * @param {number} timeout timeout for requests * @param {string} cert the certificate in PEM format * @param {string} override hostname override required for tests - * @returns {object} tls options + * @param {string} globalCert if provided use this unless cert is provided + * @returns {object} options */ - static _createOpts(cert, override) { - if (!cert) { - return undefined; - } - let embeddedCert; - if (cert.match('^-----BEGIN CERTIFICATE-----')) { - embeddedCert = cert; - } else { - // assume a file path for now but should support a url mechanism - let data = fs.readFileSync(cert); - embeddedCert = Buffer.from(data).toString(); - } - + static _createOpts(timeout, cert, override, globalCert) { let opts = { - pem: embeddedCert + 'request-timeout': timeout * 1000 }; if (override) { opts['ssl-target-name-override'] = override; } + + if (!cert && !globalCert) { + return opts; + } + let finalCert = globalCert; + if (cert) { + finalCert = cert; + } + + if (finalCert.match('^-----BEGIN CERTIFICATE-----')) { + opts.pem = finalCert; + } else { + // assume a file path for now but should support a url mechanism + let data = fs.readFileSync(finalCert); + opts.pem = Buffer.from(data).toString(); + } return opts; } @@ -153,24 +161,26 @@ class HLFConnectionManager extends ConnectionManager { * create a new peer * @static * @param {string} peerURL The peer URL. - * @param {object} tlsOpts the tls options + * @param {object} opts the tls and other options * @returns {Peer} A new Peer * @memberOf HLFConnectionManager */ - static createPeer(peerURL, tlsOpts) { - return new Peer(peerURL, tlsOpts); + static createPeer(peerURL, opts) { + return new Peer(peerURL, opts); } /** * parse the peer definition in a connection * @param {string} peer The peer URL. - * @param {array} eventHubs Array to store any created event hubs + * @param {number} timeout the request timeout + * @param {string} globalCert if provided use this unless cert is provided + * @param {array} eventHubDefs Array to store any created event hubs * @return {Peer} A new peer. */ - static parsePeer(peer, eventHubs) { + static parsePeer(peer, timeout, globalCert, eventHubDefs) { const method = 'parsePeer'; - const tlsOpts = HLFConnectionManager._createOpts(peer.cert, peer.hostnameOverride); + const opts = HLFConnectionManager._createOpts(timeout, peer.cert, peer.hostnameOverride, globalCert); if (!peer.requestURL && !peer.eventURL) { throw new Error('peer incorrectly defined'); } @@ -181,42 +191,48 @@ class HLFConnectionManager extends ConnectionManager { throw new Error(`The peer at eventURL ${peer.eventURL} has no requestURL defined`); } - const hfc_peer = HLFConnectionManager.createPeer(peer.requestURL, tlsOpts); - // load and connect to the event hub - const eventHub = HLFConnectionManager.createEventHub(); + const hfc_peer = HLFConnectionManager.createPeer(peer.requestURL, opts); + // extract and save event hub definitions for later. + const eventHub = HLFConnectionManager.createEventHubDefinition(peer.eventURL, opts); LOG.debug(method, 'Setting event hub URL', peer.eventURL); - eventHub.setPeerAddr(peer.eventURL, tlsOpts); - eventHub.connect(); - eventHubs.push(eventHub); + eventHubDefs.push(eventHub); return hfc_peer; } /** - * Create a new event hub. - * @return {EventHub} A new event hub. + * create an eventhub definition which can be used to instantiate an event hub later. + * @static + * @param {any} eventURL the event hub url + * @param {any} opts options for the event hub + * @returns {object} event hub definition + * @memberOf HLFConnectionManager */ - static createEventHub() { - return new EventHub(); + static createEventHubDefinition(eventURL, opts) { + return { + 'eventURL': eventURL, + 'opts': opts + }; } /** * Create a new CA client. * @param {string} caURL The CA URL. * @param {object} tlsOpts the tls options - * @param {KeyValStore} keyValStore The key value store. + * @param {string} caName The name of the CA + * @param {any} cryptosuite The cryptosuite to use * @return {FabricCAClientImpl} A new CA client. */ - static createCAClient(caURL, tlsOpts, keyValStore) { - return new FabricCAClientImpl(caURL, tlsOpts, {'path': keyValStore}); + static createCAClient(caURL, tlsOpts, caName, cryptosuite) { + return new FabricCAClientImpl(caURL, tlsOpts, caName, cryptosuite); } /** * Create a new CA client from a ca definition * @param {string|object} ca The CA object or string - * @param {KeyValStore} keyValStore The key value store. + * @param {any} cryptosuite The cryptosuite to assign to the fabric-ca * @return {FabricCAClientImpl} A new CA client. */ - static parseCA(ca, keyValStore) { + static parseCA(ca, cryptosuite) { let tlsOpts = null; if (typeof ca === 'object') { if (ca.trustedRoots) { @@ -225,9 +241,9 @@ class HLFConnectionManager extends ConnectionManager { verify: ca.verify // undefined gets set to true by client }; } - return HLFConnectionManager.createCAClient(ca.url, tlsOpts, keyValStore); + return HLFConnectionManager.createCAClient(ca.url, tlsOpts, (ca.name || null), cryptosuite); } - return HLFConnectionManager.createCAClient(ca, null, keyValStore); + return HLFConnectionManager.createCAClient(ca, null, null, cryptosuite); } /** @@ -242,6 +258,144 @@ class HLFConnectionManager extends ConnectionManager { LOG.exit(method); } + /** + * Validate the profile + * + * @param {object} profileDefinition profile definition + * @param {wallet} wallet an optional wallet + * + * @memberOf HLFConnectionManager + */ + validateProfileDefinition(profileDefinition, wallet) { + if (!Array.isArray(profileDefinition.orderers)) { + throw new Error('The orderers array has not been specified in the connection profile'); + } else if (!profileDefinition.orderers.length) { + throw new Error('No orderer URLs have been specified in the connection profile'); + } else if (!Array.isArray(profileDefinition.peers)) { + throw new Error('The peers array has not been specified in the connection profile'); + } else if (!profileDefinition.peers.length) { + throw new Error('No peer URLs have been specified in the connection profile'); + } else if (!wallet && !profileDefinition.keyValStore) { + throw new Error('No key value store directory or wallet has been specified'); + } else if (!profileDefinition.ca) { + throw new Error('The certificate authority URL has not been specified in the connection profile'); + } else if (!profileDefinition.channel) { + throw new Error('No channel has been specified in the connection profile'); + } else if (!profileDefinition.mspID) { + throw new Error('No msp id defined'); + } + } + + /** + * link a wallet to the fabric-client store and cryptostore + * or use the fabric-clients default FileKeyValStore if no + * wallet specified. + * + * @param {Client} client the fabric-client + * @param {Wallet} wallet the wallet implementation or null/undefined + * @param {string} keyValStorePath a path for the fileKeyValStore to use or null/undefined if a wallet specified. + * @returns {Promise} resolves to a client configured with the required stores + * + * @memberOf HLFConnectionManager + */ + _setupWallet(client, wallet, keyValStorePath) { + const method = '_setupWallet'; + // If a wallet has been specified, then we want to use that. + //let result; + LOG.entry(method, client, wallet, keyValStorePath); + + if (wallet) { + LOG.debug(method, 'A wallet has been specified, using wallet proxy'); + return new HLFWalletProxy(wallet) + .then((store) => { + let cryptostore = Client.newCryptoKeyStore(HLFWalletProxy, wallet); + client.setStateStore(store); + let cryptoSuite = Client.newCryptoSuite(); + cryptoSuite.setCryptoKeyStore(cryptostore); + client.setCryptoSuite(cryptoSuite); + return store; + }) + .catch((error) => { + LOG.error(method, error); + let newError = new Error('error trying to setup a wallet. ' + error); + throw newError; + }); + + } else { + // No wallet specified, so create a file based key value store. + LOG.debug(method, 'Using key value store', keyValStorePath); + return Client.newDefaultKeyValueStore({path: keyValStorePath}) + .then((store) => { + client.setStateStore(store); + + let cryptoSuite = Client.newCryptoSuite(); + cryptoSuite.setCryptoKeyStore(Client.newCryptoKeyStore({path: keyValStorePath})); + client.setCryptoSuite(cryptoSuite); + + return store; + }) + .catch((error) => { + LOG.error(method, error); + let newError = new Error('error trying to setup a keystore path. ' + error); + throw newError; + }); + } + } + + /** + * Import an identity into a profile wallet or keystore + * + * @param {object} profileDefinition the profile definition + * @param {string} id the id to associate with the identity + * @param {string} publicKey the public key + * @param {string} privateKey the private key + * @returns {Promise} a promise + * + * @memberOf HLFConnectionManager + */ + importIdentity(profileDefinition, id, publicKey, privateKey) { + const method = 'importIdentity'; + LOG.entry(method, profileDefinition, id, publicKey, privateKey); + + // validate arguments + if (!profileDefinition || typeof profileDefinition !== 'object') { + throw new Error('profileDefinition not specified or not an object'); + } else if (!id || typeof id !== 'string') { + throw new Error('id not specified or not a string'); + } else if (!publicKey || typeof publicKey !== 'string') { + throw new Error('publicKey not specified or not a string'); + } else if (!privateKey || typeof privateKey !== 'string') { + throw new Error('privateKey not specified or not a string'); + } + + //default the optional wallet + let wallet = profileDefinition.wallet || Wallet.getWallet(); + + // validate the profile + this.validateProfileDefinition(profileDefinition, wallet); + + let mspID = profileDefinition.mspID; + const client = HLFConnectionManager.createClient(); + return this._setupWallet(client, wallet, profileDefinition.keyValStore) + .then(() => { + return client.createUser({ + username: id, + mspid: mspID, + cryptoContent: { + privateKeyPEM: privateKey, + signedCertPEM: publicKey + } + }); + }) + .then(() => { + LOG.exit(method); + }) + .catch((error) => { + LOG.error(method, error); + throw error; + }); + } + /** * Establish a connection to the business network. * @param {string} connectionProfile The name of the connection profile @@ -261,103 +415,63 @@ class HLFConnectionManager extends ConnectionManager { throw new Error('connectOptions not specified'); } - // Validate the connection profile. + //default the optional wallet let wallet = connectOptions.wallet || Wallet.getWallet(); - if (!Array.isArray(connectOptions.orderers)) { - throw new Error('The orderers array has not been specified in the connection profile'); - } else if (!connectOptions.orderers.length) { - throw new Error('No orderer URLs have been specified in the connection profile'); - } else if (!Array.isArray(connectOptions.peers)) { - throw new Error('The peers array has not been specified in the connection profile'); - } else if (!connectOptions.peers.length) { - throw new Error('No peer URLs have been specified in the connection profile'); - } else if (!wallet && !connectOptions.keyValStore) { - throw new Error('No key value store directory has been specified'); - } else if (!connectOptions.ca) { - throw new Error('The certificate authority URL has not been specified in the connection profile'); - } else if (!connectOptions.channel) { - throw new Error('No channel has been specified in the connection profile'); - } else if (!connectOptions.mspID) { - throw new Error('No msp id defined'); - } + + // validate the profile + this.validateProfileDefinition(connectOptions, wallet); + // Default the optional connection options. - if (!connectOptions.deployWaitTime) { - connectOptions.deployWaitTime = 60; + if (!connectOptions.timeout) { + connectOptions.timeout = 180; + } + + // set the message limits if required + if (connectOptions.maxSendSize && typeof connectOptions.maxSendSize === 'number' && connectOptions.maxSendSize !== 0) { + Client.setConfigSetting('grpc-max-send-message-length', connectOptions.maxSendSize < 0 ? -1 : 1024 * 1024 * connectOptions.maxSendSize); } - if (!connectOptions.invokeWaitTime) { - connectOptions.invokeWaitTime = 60; + + // set the message limits if required + if (connectOptions.maxRecvSize && typeof connectOptions.maxRecvSize === 'number' && connectOptions.maxRecvSize !== 0) { + Client.setConfigSetting('grpc-max-receive-message-length', connectOptions.maxRecvSize < 0 ? -1 : 1024 * 1024 * connectOptions.maxRecvSize); } // Create a new client instance. const client = HLFConnectionManager.createClient(); - // Create a new chain instance. - const chain = client.newChain(connectOptions.channel); + // Create a new channel instance. + const channel = client.newChannel(connectOptions.channel); // Load all of the orderers into the client. connectOptions.orderers.forEach((orderer) => { LOG.debug(method, 'Adding orderer URL', orderer); - chain.addOrderer(HLFConnectionManager.parseOrderer(orderer)); + channel.addOrderer(HLFConnectionManager.parseOrderer(orderer, connectOptions.timeout, connectOptions.globalCert)); }); - let eventHubs = []; + let eventHubDefs = []; // Load all of the peers into the client. connectOptions.peers.forEach((peer) => { LOG.debug(method, 'Adding peer URL', peer); - chain.addPeer(HLFConnectionManager.parsePeer(peer, eventHubs)); - }); - - // register to disconnect on exit for all event hubs - process.on('exit', () => { - eventHubs.forEach((eventHub) => { - if (eventHub.isconnected()) { - eventHub.disconnect(); - } - }); + channel.addPeer(HLFConnectionManager.parsePeer(peer, connectOptions.timeout, connectOptions.globalCert, eventHubDefs)); }); + return this._setupWallet(client, wallet, connectOptions.keyValStore) + .then(() => { + // Create a CA client. + const caClient = HLFConnectionManager.parseCA(connectOptions.ca, client.getCryptoSuite()); - // If a wallet has been specified, then we want to use that. - let result; - if (wallet) { - LOG.debug(method, 'A wallet has been specified, using wallet proxy'); - let store = new HLFWalletProxy(wallet); - client.setStateStore(store); - result = Promise.resolve(store); - } else { + // Now we can create the connection. + let connection = new HLFConnection(this, connectionProfile, businessNetworkIdentifier, connectOptions, client, channel, eventHubDefs, caClient); + LOG.exit(method, connection); + return connection; - // No wallet specified, so create a file based key value store. - LOG.debug(method, 'Using key value store', connectOptions.keyValStore); - result = Client.newDefaultKeyValueStore({ - path: connectOptions.keyValStore - }) - .then((store) => { - client.setStateStore(store); - return store; }) .catch((error) => { LOG.error(method, error); throw error; }); - } - - return result.then((store) => { - - // Create a CA client. - const caClient = HLFConnectionManager.parseCA(connectOptions.ca, connectOptions.keyValStore); - - // Now we can create the connection. - let connection = new HLFConnection(this, connectionProfile, businessNetworkIdentifier, connectOptions, client, chain, eventHubs, caClient); - LOG.exit(method, connection); - return connection; - - }).catch((error) => { - LOG.error(method, error); - throw error; - }); - } } diff --git a/packages/composer-connector-hlfv1/lib/hlfwalletproxy.js b/packages/composer-connector-hlfv1/lib/hlfwalletproxy.js index 61893972eb..ff32b132aa 100644 --- a/packages/composer-connector-hlfv1/lib/hlfwalletproxy.js +++ b/packages/composer-connector-hlfv1/lib/hlfwalletproxy.js @@ -36,6 +36,7 @@ class HLFWalletProxy extends KeyValueStore { LOG.entry(method, wallet); this.wallet = wallet; LOG.exit(method); + return Promise.resolve(this); } /** diff --git a/packages/composer-connector-hlfv1/package.json b/packages/composer-connector-hlfv1/package.json index 1e05ed3113..6ea9a1b8bb 100644 --- a/packages/composer-connector-hlfv1/package.json +++ b/packages/composer-connector-hlfv1/package.json @@ -42,8 +42,8 @@ "dependencies": { "composer-common": "^0.7.6", "composer-runtime-hlfv1": "^0.7.6", - "fabric-ca-client": "1.0.0-alpha.1", - "fabric-client": "1.0.0-alpha.1", + "fabric-ca-client": "1.0.0-beta", + "fabric-client": "1.0.0-beta", "fs-extra": "^1.0.0", "semver": "^5.3.0", "temp": "^0.8.3", diff --git a/packages/composer-connector-hlfv1/scripts/copysdk.js b/packages/composer-connector-hlfv1/scripts/copysdk.js deleted file mode 100644 index 956d2d66ab..0000000000 --- a/packages/composer-connector-hlfv1/scripts/copysdk.js +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict'; - -const fs = require('fs-extra'); - -// copy the node sdk to the expected node module directories -// and also patch the default.json file to locate the ECDSA_AES.js file -fs.copy('./node_modules/fabric-sdk-node/fabric-ca-client', './node_modules/fabric-ca-client', (err) => { - if (err) { - return console.error(err); - } -}); -fs.copy('./node_modules/fabric-sdk-node/fabric-client', './node_modules/fabric-client', (err) => { - if (err) { - return console.error(err); - } - fs.readJson('./node_modules/fabric-client/config/default.json', (err, config) => { - config['crypto-suite-software'].EC = './impl/CryptoSuite_ECDSA_AES.js'; - fs.writeJson('./node_modules/fabric-client/config/default.json', config, (err) => { - if (err) { - return console.error(err); - } - }); - }); -}); - - diff --git a/packages/composer-connector-hlfv1/test/hlfconnection.js b/packages/composer-connector-hlfv1/test/hlfconnection.js index fc0fc6b5ca..2a30b17e3c 100644 --- a/packages/composer-connector-hlfv1/test/hlfconnection.js +++ b/packages/composer-connector-hlfv1/test/hlfconnection.js @@ -15,17 +15,22 @@ 'use strict'; const BusinessNetworkDefinition = require('composer-common').BusinessNetworkDefinition; -const Chain = require('fabric-client/lib/Chain'); + +const Channel = require('fabric-client/lib/Channel'); +const Peer = require('fabric-client/lib/Peer'); const Client = require('fabric-client'); const EventHub = require('fabric-client/lib/EventHub'); +const TransactionID = require('fabric-client/lib/TransactionID.js'); +const User = require('fabric-client/lib/User.js'); + const FabricCAClientImpl = require('fabric-ca-client'); + const HLFConnection = require('../lib/hlfconnection'); const HLFConnectionManager = require('../lib/hlfconnectionmanager'); const HLFSecurityContext = require('../lib/hlfsecuritycontext'); + const path = require('path'); const semver = require('semver'); -const User = require('fabric-client/lib/User.js'); -const utils = require('fabric-client/lib/utils.js'); const connectorPackageJSON = require('../package.json'); const originalVersion = connectorPackageJSON.version; @@ -41,39 +46,38 @@ const runtimeModulePath = path.dirname(require.resolve('composer-runtime-hlfv1') describe('HLFConnection', () => { let sandbox; - let mockConnectionManager, mockChain, mockClient, mockEventHub, mockCAClient, mockUser, mockSecurityContext, mockBusinessNetwork; + let mockConnectionManager, mockChannel, mockClient, mockEventHub, mockCAClient, mockUser, mockSecurityContext, mockBusinessNetwork, mockPeer; let connectOptions; let connection; + let mockEventHubDef, mockTransactionID; beforeEach(() => { sandbox = sinon.sandbox.create(); mockConnectionManager = sinon.createStubInstance(HLFConnectionManager); - mockChain = sinon.createStubInstance(Chain); + mockPeer = sinon.createStubInstance(Peer); + mockChannel = sinon.createStubInstance(Channel); mockClient = sinon.createStubInstance(Client); mockEventHub = sinon.createStubInstance(EventHub); mockCAClient = sinon.createStubInstance(FabricCAClientImpl); mockUser = sinon.createStubInstance(User); - + mockChannel.getPeers.returns([mockPeer]); + mockTransactionID = sinon.createStubInstance(TransactionID); + mockTransactionID.getTransactionID.returns('00000000-0000-0000-0000-000000000000'); + mockClient.newTransactionID.returns(mockTransactionID); mockSecurityContext = sinon.createStubInstance(HLFSecurityContext); mockBusinessNetwork = sinon.createStubInstance(BusinessNetworkDefinition); mockBusinessNetwork.getName.returns('org.acme.biznet'); mockBusinessNetwork.toArchive.resolves(Buffer.from('hello world')); connectOptions = { - orderers: [ - 'grpc://localhost:7050' - ], - peers: [ - 'grpc://localhost:7051' - ], - events: [ - 'grpc://localhost:7053' - ], - ca: 'http://localhost:7054', - keyValStore: '/tmp/hlfabric1', channel: 'testchainid', - mspID: 'suchmsp' + mspID: 'suchmsp', + deployWaitTime: 30, + invokeWaitTime: 30 + }; + mockEventHubDef = { + 'eventURL': 'http://localhost:7053' }; - connection = new HLFConnection(mockConnectionManager, 'hlfabric1', 'org.acme.biznet', connectOptions, mockClient, mockChain, [mockEventHub], mockCAClient); + connection = new HLFConnection(mockConnectionManager, 'hlfabric1', 'org.acme.biznet', connectOptions, mockClient, mockChannel, [mockEventHubDef], mockCAClient); }); afterEach(() => { @@ -81,6 +85,14 @@ describe('HLFConnection', () => { connectorPackageJSON.version = originalVersion; }); + describe('#generateCcid', () => { + it('should create a valid ccid', () => { + HLFConnection.generateCcid('my.domain.biznet').should.equal('my-domain-biznet'); + HLFConnection.generateCcid('My.Domain.BIZnet').should.equal('my-domain-biznet'); + }); + + }); + describe('#createUser', () => { it('should create a new user', () => { @@ -90,57 +102,61 @@ describe('HLFConnection', () => { }); - describe('#constructor', () => { + describe('#_getLoggedInUser', () => { - it('should subscribe to the eventHub and emit events', () => { - const events = { - payload: { - toString: () => { - return '"{"event":"event"}"'; - } - } - }; - connection.emit = sandbox.stub(); - mockEventHub.registerChaincodeEvent.withArgs('org.acme.biznet', 'composer', sinon.match.func).yield(events); - sinon.assert.calledOnce(mockEventHub.registerChaincodeEvent); - sinon.assert.calledWith(mockEventHub.registerChaincodeEvent, 'org.acme.biznet', 'composer', sinon.match.func); - sinon.assert.calledOnce(connection.emit); + it('should return the current user', () => { + connection.user = 'CurrentUser'; + connection._getLoggedInUser().should.equal('CurrentUser'); + }); + + }); + + describe('#createEventHub', () => { + + it('should call new event hub', () => { + (() => { + HLFConnection.createEventHub(mockClient); + }).should.throw(/The clientContext has not been properly initialized, missing userContext/); }); + }); + + describe('#constructor', () => { + it('should throw if connectOptions not specified', () => { (() => { - new HLFConnection(mockConnectionManager, 'hlfabric1', 'org.acme.biznet', null, mockClient, mockChain, mockEventHub, mockCAClient); + new HLFConnection(mockConnectionManager, 'hlfabric1', 'org.acme.biznet', null, mockClient, mockChannel, [mockEventHubDef], mockCAClient); }).should.throw(/connectOptions not specified/); }); it('should throw if client not specified', () => { (() => { - new HLFConnection(mockConnectionManager, 'hlfabric1', 'org.acme.biznet', { type: 'hlfv1' }, null, mockChain, mockEventHub, mockCAClient); + new HLFConnection(mockConnectionManager, 'hlfabric1', 'org.acme.biznet', { type: 'hlfv1' }, null, mockChannel, [mockEventHubDef], mockCAClient); }).should.throw(/client not specified/); }); - it('should throw if chain not specified', () => { + it('should throw if channel not specified', () => { (() => { - new HLFConnection(mockConnectionManager, 'hlfabric1', 'org.acme.biznet', { type: 'hlfv1' }, mockClient, null, mockEventHub, mockCAClient); - }).should.throw(/chain not specified/); + new HLFConnection(mockConnectionManager, 'hlfabric1', 'org.acme.biznet', { type: 'hlfv1' }, mockClient, null, [mockEventHubDef], mockCAClient); + }).should.throw(/channel not specified/); }); - it('should throw if eventHubs not specified', () => { + it('should throw if eventHubDefs not specified', () => { (() => { - new HLFConnection(mockConnectionManager, 'hlfabric1', 'org.acme.biznet', { type: 'hlfv1' }, mockClient, mockChain, null, mockCAClient); - }).should.throw(/eventHubs not specified or not an array/); + new HLFConnection(mockConnectionManager, 'hlfabric1', 'org.acme.biznet', { type: 'hlfv1' }, mockClient, mockChannel, null, mockCAClient); + }).should.throw(/eventHubDefs not specified or not an array/); }); - it('should throw if eventHubs not an array', () => { + it('should throw if eventHubDefs not an array', () => { (() => { - new HLFConnection(mockConnectionManager, 'hlfabric1', 'org.acme.biznet', { type: 'hlfv1' }, mockClient, mockChain, mockEventHub, mockCAClient); - }).should.throw(/eventHubs not specified or not an array/); + new HLFConnection(mockConnectionManager, 'hlfabric1', 'org.acme.biznet', { type: 'hlfv1' }, mockClient, mockChannel, mockEventHubDef, mockCAClient); + }).should.throw(/eventHubDefs not specified or not an array/); }); it('should throw if caClient not specified', () => { (() => { - new HLFConnection(mockConnectionManager, 'hlfabric1', 'org.acme.biznet', { type: 'hlfv1' }, mockClient, mockChain, [mockEventHub], null); + new HLFConnection(mockConnectionManager, 'hlfabric1', 'org.acme.biznet', { type: 'hlfv1' }, mockClient, mockChannel, [mockEventHubDef], null); }).should.throw(/caClient not specified/); }); }); @@ -153,10 +169,84 @@ describe('HLFConnection', () => { }); + describe('#_connectToEventHubs', () => { + beforeEach(() => { + sandbox.stub(HLFConnection, 'createEventHub').returns(mockEventHub); + }); + + it('should ignore a disconnected event hub on process exit', () => { + sandbox.stub(process, 'on').withArgs('exit').yields(); + mockEventHub.isconnected.returns(false); + connection._connectToEventHubs(); + sinon.assert.calledOnce(process.on); + sinon.assert.calledWith(process.on, 'exit'); + sinon.assert.notCalled(mockEventHub.disconnect); + }); + + it('should disconnect a connected event hub on process exit', () => { + sandbox.stub(process, 'on').withArgs('exit').yields(); + mockEventHub.isconnected.returns(true); + connection._connectToEventHubs(); + sinon.assert.calledOnce(process.on); + sinon.assert.calledWith(process.on, 'exit'); + sinon.assert.calledOnce(mockEventHub.disconnect); + }); + + it('should subscribe to the eventHub and emit events', () => { + //sandbox.stub(Buffer, 'from').returns('"{"event":"event"}"'); + connection._connectToEventHubs(); + const events = { + payload: { + toString: () => { + return '"{"event":"event"}"'; + } + } + }; + connection.emit = sandbox.stub(); + mockEventHub.registerChaincodeEvent.withArgs('org-acme-biznet', 'composer', sinon.match.func).yield(events); + sinon.assert.calledOnce(mockEventHub.registerChaincodeEvent); + sinon.assert.calledWith(mockEventHub.registerChaincodeEvent, 'org-acme-biznet', 'composer', sinon.match.func); + sinon.assert.calledOnce(connection.emit); + }); + + it('should not register any listeners for chaincode events if no business network is specified', () => { + connection = new HLFConnection(mockConnectionManager, 'hlfabric1', null, connectOptions, mockClient, mockChannel, [mockEventHub], mockCAClient); + connection._connectToEventHubs(); + sinon.assert.notCalled(mockEventHub.registerChaincodeEvent); + connection.ccEvents.length.should.equal(0); + + }); + + }); + describe('#disconnect', () => { + beforeEach(() => { + sandbox.stub(HLFConnection, 'createEventHub').returns(mockEventHub); + mockEventHub.registerChaincodeEvent.withArgs('org-acme-biznet', 'composer', sinon.match.func).returns('events'); + }); + + it('should not unregister any chaincode listeners if non were setup', () => { + connection = new HLFConnection(mockConnectionManager, 'hlfabric1', null, connectOptions, mockClient, mockChannel, [mockEventHub], mockCAClient); + connection._connectToEventHubs(); + return connection.disconnect() + .then(() => { + sinon.assert.notCalled(mockEventHub.unregisterChaincodeEvent); + }); + }); + + it('should unregister a registered chaincode listener', () => { + mockEventHub.unregisterChaincodeEvent.returns(true); + connection._connectToEventHubs(); + return connection.disconnect() + .then(() => { + sinon.assert.calledOnce(mockEventHub.unregisterChaincodeEvent); + sinon.assert.calledWith(mockEventHub.unregisterChaincodeEvent, 'events'); + }); + }); it('should disconnect from the event hub if connected', () => { mockEventHub.isconnected.returns(true); + connection._connectToEventHubs(); return connection.disconnect() .then(() => { sinon.assert.calledOnce(mockEventHub.disconnect); @@ -165,6 +255,7 @@ describe('HLFConnection', () => { it('should not disconnect from the event hub if not connected', () => { mockEventHub.isconnected.returns(false); + connection._connectToEventHubs(); return connection.disconnect() .then(() => { sinon.assert.notCalled(mockEventHub.disconnect); @@ -172,6 +263,8 @@ describe('HLFConnection', () => { }); it('should handle an error disconnecting from the event hub', () => { + sandbox.stub(process, 'on').withArgs('exit').yields(); + connection._connectToEventHubs(); mockEventHub.isconnected.throws(new Error('such error')); return connection.disconnect() .should.be.rejectedWith(/such error/); @@ -183,6 +276,7 @@ describe('HLFConnection', () => { beforeEach(() => { sandbox.stub(HLFConnection, 'createUser').returns(mockUser); + sandbox.stub(connection, '_initializeChannel').resolves(); }); it('should throw if enrollmentID not specified', () => { @@ -221,6 +315,11 @@ describe('HLFConnection', () => { describe('#login', () => { + beforeEach(() => { + sandbox.stub(process, 'on').withArgs('exit').yields(); + sandbox.stub(HLFConnection, 'createEventHub').returns(mockEventHub); + }); + it('should throw if enrollmentID not specified', () => { (() => { connection.login(null, 'adminpw'); @@ -262,9 +361,13 @@ describe('HLFConnection', () => { }); describe('#_install', () => { + beforeEach(() => { + sandbox.stub(process, 'on').withArgs('exit').yields(); + sandbox.stub(HLFConnection, 'createEventHub').returns(mockEventHub); + connection._connectToEventHubs(); + }); + const tempDirectoryPath = path.resolve('tmp', 'composer1234567890'); - //const targetDirectoryPath = path.resolve(tempDirectoryPath, 'src', 'composer'); - //const versionFilePath = path.resolve(targetDirectoryPath, 'version.go'); it('should rethrow error if unable to create temp dir', () => { sandbox.stub(connection.temp, 'mkdir').withArgs('composer').rejects(new Error('some error 1')); connection._install(mockSecurityContext, mockBusinessNetwork) @@ -288,6 +391,167 @@ describe('HLFConnection', () => { }); + describe('#_validateResponses', () => { + it('should not throw if all is ok', () => { + const responses = [ + { + response: { + status: 200, + payload: 'no error' + } + }, + + { + response: { + status: 200, + payload: 'good here' + } + } + ]; + + mockChannel.verifyProposalResponse.returns(true); + mockChannel.compareProposalResponseResults.returns(true); + + (function() { + connection._validateResponses(responses, true); + }).should.not.throw(); + }); + + it('should not throw if pattern is found in message of Error object', () => { + const err = new Error('the chaincode exists somewhere'); + const responses = [ + err + ]; + + mockChannel.verifyProposalResponse.returns(true); + mockChannel.compareProposalResponseResults.returns(true); + + (function() { + connection._validateResponses(responses, false, /chaincode exists/); + }).should.not.throw(); + }); + + + it('should throw if no responses', () => { + (function() { + connection._validateResponses([], false); + }).should.throw(/No results were returned/); + }); + + it('should throw if no proposal responses', () => { + (function() { + connection._validateResponses([], true); + }).should.throw(/No results were returned/); + }); + + it('should throw if any responses that have a non-200 status code', () => { + const responses = [ + { + response: { + status: 200, + payload: 'no error' + } + }, + + { + response: { + status: 500, + payload: 'such error' + } + } + ]; + + mockChannel.verifyProposalResponse.returns(true); + mockChannel.compareProposalResponseResults.returns(true); + + (function() { + connection._validateResponses(responses, true); + }).should.throw(/such error/); + }); + + it('should throw the error if any of the responses contains an error', () => { + const responses = [ + { + response: { + status: 200, + payload: 'no error' + } + }, + new Error('had a problem'), + { + response: { + status: 500, + payload: 'such error' + } + } + ]; + + mockChannel.verifyProposalResponse.returns(true); + mockChannel.compareProposalResponseResults.returns(true); + + (function() { + connection._validateResponses(responses, true); + }).should.throw(/had a problem/); + + }); + + it('should throw if verifyProposal returns false', () => { + const responses = [ + { + response: { + status: 200, + payload: 'no error' + } + } + ]; + + mockChannel.verifyProposalResponse.returns(false); + mockChannel.compareProposalResponseResults.returns(true); + + (function() { + connection._validateResponses(responses, true); + }).should.throw(/Response from peer was not valid/); + }); + + it('should throw if compareProposals returns false', () => { + const responses = [ + { + response: { + status: 200, + payload: 'no error' + } + } + ]; + + mockChannel.verifyProposalResponse.returns(true); + mockChannel.compareProposalResponseResults.returns(false); + + (function() { + connection._validateResponses(responses, true); + }).should.throw(/Peers do not agree/); + }); + + it('should not try to check proposal responses if not a response from a proposal', () => { + const responses = [ + { + response: { + status: 200, + payload: 'no error' + } + } + ]; + + mockChannel.verifyProposalResponse.returns(false); + mockChannel.compareProposalResponseResults.returns(false); + + (function() { + connection._validateResponses(responses, false); + }).should.not.throw(); + }); + + + }); + describe('#deploy', () => { const tempDirectoryPath = path.resolve('tmp', 'composer1234567890'); @@ -298,6 +562,11 @@ describe('HLFConnection', () => { sandbox.stub(connection.temp, 'mkdir').withArgs('composer').resolves(tempDirectoryPath); sandbox.stub(connection.fs, 'copy').resolves(); sandbox.stub(connection.fs, 'outputFile').resolves(); + sandbox.stub(process, 'on').withArgs('exit').yields(); + sandbox.stub(HLFConnection, 'createEventHub').returns(mockEventHub); + sandbox.stub(connection, '_validateResponses').returns(); + sandbox.stub(connection, '_initializeChannel').resolves(); + connection._connectToEventHubs(); }); it('should throw if businessNetwork not specified', () => { @@ -306,27 +575,26 @@ describe('HLFConnection', () => { }).should.throw(/businessNetwork not specified/); }); + // TODO: should extract out _waitForEvents it('should request an event timeout based on connection settings', () => { connectOptions = { orderers: [ 'grpc://localhost:7050' ], - peers: [ - 'grpc://localhost:7051' - ], - events: [ - 'grpc://localhost:7053' - ], + peers: [ { + requestURL: 'grpc://localhost:7051', + eventURL: 'grpc://localhost:7053' + }], ca: 'http://localhost:7054', keyValStore: '/tmp/hlfabric1', channel: 'testchainid', mspID: 'suchmsp', - deployWaitTime: 39, - invokeWaitTime: 63, + timeout: 22 }; - connection = new HLFConnection(mockConnectionManager, 'hlfabric1', 'org.acme.biznet', connectOptions, mockClient, mockChain, [mockEventHub], mockCAClient); - // This is the generated nonce. - sandbox.stub(utils, 'getNonce').returns('11111111-1111-1111-1111-111111111111'); + connection = new HLFConnection(mockConnectionManager, 'hlfabric1', 'org.acme.biznet', connectOptions, mockClient, mockChannel, [mockEventHubDef], mockCAClient); + sandbox.stub(connection, '_validateResponses').returns(); + sandbox.stub(connection, '_initializeChannel').resolves(); + connection._connectToEventHubs(); // This is the deployment proposal and response (from the peers). const proposalResponses = [{ response: { @@ -338,26 +606,22 @@ describe('HLFConnection', () => { const response = { status: 'SUCCESS' }; - // This is the generated transaction - mockChain.buildTransactionID.returns('00000000-0000-0000-0000-000000000000'); - // mock out getUserContext version in case we need to return to using this one - mockChain.buildTransactionID_getUserContext.resolves('00000000-0000-0000-0000-000000000000'); - mockChain.sendInstallProposal.resolves([ proposalResponses, proposal, header ]); - mockChain.sendInstantiateProposal.resolves([ proposalResponses, proposal, header ]); - mockChain.sendTransaction.withArgs({ proposalResponses: proposalResponses, proposal: proposal, header: header }).resolves(response); + mockClient.installChaincode.resolves([ proposalResponses, proposal, header ]); + mockChannel.queryInstantiatedChaincodes.resolves({chaincodes: []}); + mockChannel.sendInstantiateProposal.resolves([ proposalResponses, proposal, header ]); + mockChannel.sendTransaction.withArgs({ proposalResponses: proposalResponses, proposal: proposal, header: header }).resolves(response); // This is the event hub response. sandbox.stub(global, 'setTimeout').yields(); return connection.deploy(mockSecurityContext, false, mockBusinessNetwork) - .catch(() => { + .should.be.rejectedWith(/Failed to receive commit notification/) + .then(() => { sinon.assert.calledWith(global.setTimeout, sinon.match.func, sinon.match.number); - sinon.assert.calledWith(global.setTimeout, sinon.match.func, connectOptions.deployWaitTime * 1000); + sinon.assert.calledWith(global.setTimeout, sinon.match.func, 22 * 1000); }); }); it('should deploy the business network', () => { sandbox.stub(global, 'setTimeout'); - // This is the generated nonce. - sandbox.stub(utils, 'getNonce').returns('11111111-1111-1111-1111-111111111111'); // This is the deployment proposal and response (from the peers). const proposalResponses = [{ response: { @@ -366,19 +630,16 @@ describe('HLFConnection', () => { }]; const proposal = { proposal: 'i do' }; const header = { header: 'gooooal' }; - // This is the generated transaction - mockChain.buildTransactionID.returns('00000000-0000-0000-0000-000000000000'); - // mock out getUserContext version in case we need to return to using this one - mockChain.buildTransactionID_getUserContext.resolves('00000000-0000-0000-0000-000000000000'); - mockChain.sendInstallProposal.resolves([ proposalResponses, proposal, header ]); - mockChain.sendInstantiateProposal.resolves([ proposalResponses, proposal, header ]); + mockClient.installChaincode.resolves([ proposalResponses, proposal, header ]); + mockChannel.queryInstantiatedChaincodes.resolves({chaincodes: []}); + mockChannel.sendInstantiateProposal.resolves([ proposalResponses, proposal, header ]); // This is the commit proposal and response (from the orderer). const response = { status: 'SUCCESS' }; - mockChain.sendTransaction.withArgs({ proposalResponses: proposalResponses, proposal: proposal, header: header }).resolves(response); + mockChannel.sendTransaction.withArgs({ proposalResponses: proposalResponses, proposal: proposal, header: header }).resolves(response); // This is the event hub response. - mockEventHub.registerTxEvent.yields('00000000-0000-0000-0000-000000000000', 'VALID'); + mockEventHub.registerTxEvent.yields(mockTransactionID.getTransactionID().toString(), 'VALID'); return connection.deploy(mockSecurityContext, false, mockBusinessNetwork) .then(() => { sinon.assert.calledOnce(connection.fs.copy); @@ -389,39 +650,31 @@ describe('HLFConnection', () => { connection.fs.copy.firstCall.args[2].filter('composer-runtime-hlfv1/node_modules/here').should.be.false; sinon.assert.calledOnce(connection.fs.outputFile); sinon.assert.calledWith(connection.fs.outputFile, versionFilePath, sinon.match(/const version = /)); - sinon.assert.calledOnce(mockChain.sendInstallProposal); - sinon.assert.calledOnce(mockChain.initialize); - sinon.assert.calledOnce(mockChain.sendInstantiateProposal); - sinon.assert.calledWith(mockChain.sendInstallProposal, { + sinon.assert.calledOnce(mockClient.installChaincode); + sinon.assert.calledOnce(connection._initializeChannel); + sinon.assert.calledOnce(mockChannel.sendInstantiateProposal); + sinon.assert.calledWith(mockClient.installChaincode, { chaincodePath: 'composer', chaincodeVersion: connectorPackageJSON.version, - //chaincodeId: 'org-acme-biznet', required for alpha2 - chaincodeId: 'org.acme.biznet', - chainId: connectOptions.channel, - txId: '00000000-0000-0000-0000-000000000000', - nonce: '11111111-1111-1111-1111-111111111111', + chaincodeId: 'org-acme-biznet', + txId: mockTransactionID, + targets: [mockPeer] }); - sinon.assert.calledWith(mockChain.sendInstantiateProposal, { + sinon.assert.calledWith(mockChannel.sendInstantiateProposal, { chaincodePath: 'composer', chaincodeVersion: connectorPackageJSON.version, - //chaincodeId: 'org-acme-biznet', required for alpha2 - chaincodeId: 'org.acme.biznet', - chainId: connectOptions.channel, - txId: '00000000-0000-0000-0000-000000000000', - nonce: '11111111-1111-1111-1111-111111111111', + chaincodeId: 'org-acme-biznet', + txId: mockTransactionID, fcn: 'init', args: ['aGVsbG8gd29ybGQ='] }); - sinon.assert.calledOnce(mockChain.sendTransaction); + sinon.assert.calledOnce(mockChannel.sendTransaction); }); }); it('should instantiate the business network if it responds already installed', () => { sandbox.stub(global, 'setTimeout'); - // This is the generated nonce. - sandbox.stub(utils, 'getNonce').returns('11111111-1111-1111-1111-111111111111'); - // This is the deployment proposal and response (from the peers). const errorResp = new Error('Error installing chaincode code systest-participants:0.5.11(chaincode /var/hyperledger/production/chaincodes/systest-participants.0.5.11 exists)'); const installResponses = [errorResp]; const instantiateResponses = [{ @@ -432,19 +685,16 @@ describe('HLFConnection', () => { const proposal = { proposal: 'i do' }; const header = { header: 'gooooal' }; - // This is the generated transaction - mockChain.buildTransactionID.returns('00000000-0000-0000-0000-000000000000'); - // mock out getUserContext version in case we need to return to using this one - mockChain.buildTransactionID_getUserContext.resolves('00000000-0000-0000-0000-000000000000'); - mockChain.sendInstallProposal.resolves([ installResponses, proposal, header ]); - mockChain.sendInstantiateProposal.resolves([ instantiateResponses, proposal, header ]); + mockClient.installChaincode.resolves([ installResponses, proposal, header ]); + mockChannel.queryInstantiatedChaincodes.resolves({chaincodes: []}); + mockChannel.sendInstantiateProposal.resolves([ instantiateResponses, proposal, header ]); // This is the commit proposal and response (from the orderer). const response = { status: 'SUCCESS' }; - mockChain.sendTransaction.withArgs({ proposalResponses: instantiateResponses, proposal: proposal, header: header }).resolves(response); + mockChannel.sendTransaction.withArgs({ proposalResponses: instantiateResponses, proposal: proposal, header: header }).resolves(response); // This is the event hub response. - mockEventHub.registerTxEvent.yields('00000000-0000-0000-0000-000000000000', 'VALID'); + mockEventHub.registerTxEvent.yields(mockTransactionID.getTransactionID.toString(), 'VALID'); return connection.deploy(mockSecurityContext, false, mockBusinessNetwork) .then(() => { sinon.assert.calledOnce(connection.fs.copy); @@ -455,51 +705,65 @@ describe('HLFConnection', () => { connection.fs.copy.firstCall.args[2].filter('composer-runtime-hlfv1/node_modules/here').should.be.false; sinon.assert.calledOnce(connection.fs.outputFile); sinon.assert.calledWith(connection.fs.outputFile, versionFilePath, sinon.match(/const version = /)); - sinon.assert.calledOnce(mockChain.sendInstallProposal); - sinon.assert.calledOnce(mockChain.initialize); - sinon.assert.calledOnce(mockChain.sendInstantiateProposal); - sinon.assert.calledWith(mockChain.sendInstallProposal, { + sinon.assert.calledOnce(mockClient.installChaincode); + sinon.assert.calledOnce(connection._initializeChannel); + sinon.assert.calledOnce(mockChannel.sendInstantiateProposal); + sinon.assert.calledWith(mockClient.installChaincode, { chaincodePath: 'composer', chaincodeVersion: connectorPackageJSON.version, - //chaincodeId: 'org-acme-biznet', required for alpha2 - chaincodeId: 'org.acme.biznet', - chainId: connectOptions.channel, - txId: '00000000-0000-0000-0000-000000000000', - nonce: '11111111-1111-1111-1111-111111111111', + chaincodeId: 'org-acme-biznet', + txId: mockTransactionID, + targets: [mockPeer] }); - sinon.assert.calledWith(mockChain.sendInstantiateProposal, { + sinon.assert.calledWith(mockChannel.sendInstantiateProposal, { chaincodePath: 'composer', chaincodeVersion: connectorPackageJSON.version, - //chaincodeId: 'org-acme-biznet', required for alpha2 - chaincodeId: 'org.acme.biznet', - chainId: connectOptions.channel, - txId: '00000000-0000-0000-0000-000000000000', - nonce: '11111111-1111-1111-1111-111111111111', + chaincodeId: 'org-acme-biznet', + txId: mockTransactionID, fcn: 'init', args: ['aGVsbG8gd29ybGQ='] }); - sinon.assert.calledOnce(mockChain.sendTransaction); + sinon.assert.calledOnce(mockChannel.sendTransaction); }); }); - it('should throw if install fails for unexpected reason', () => { + it('should install and exit cleanly if business network already instantiated', () => { sandbox.stub(global, 'setTimeout'); - // This is the generated nonce. - sandbox.stub(utils, 'getNonce').returns('11111111-1111-1111-1111-111111111111'); - // This is the deployment proposal and response (from the peers). - const errorResp = new Error('Error something went completely wrong'); - const installResponses = [errorResp]; + // This is reponse from an install and instantiate + const installInstantiateResponses = [{ + response: { + status: 200 + } + }]; const proposal = { proposal: 'i do' }; const header = { header: 'gooooal' }; - // This is the generated transaction - mockChain.buildTransactionID.returns('00000000-0000-0000-0000-000000000000'); - // mock out getUserContext version in case we need to return to using this one - mockChain.buildTransactionID_getUserContext.resolves('00000000-0000-0000-0000-000000000000'); - mockChain.sendInstallProposal.resolves([ installResponses, proposal, header ]); + mockClient.installChaincode.resolves([ installInstantiateResponses ]); + + // query instantiate response shows chaincode already instantiated + const queryInstantiatedResponse = { + chaincodes: [ + { + path: 'composer', + name: 'org-acme-biznet' + } + ] + }; + mockChannel.queryInstantiatedChaincodes.resolves(queryInstantiatedResponse); + + // What would be a valid instantiate proposal response, should not be used. + mockChannel.sendInstantiateProposal.resolves([ installInstantiateResponses, proposal, header ]); + // This is the commit proposal and response (from the orderer). + const response = { + status: 'SUCCESS' + }; + mockChannel.sendTransaction.withArgs({ proposalResponses: installInstantiateResponses, proposal: proposal, header: header }).resolves(response); + + // This is the event hub response. + mockEventHub.registerTxEvent.yields(mockTransactionID.getTransactionID().toString(), 'VALID'); + return connection.deploy(mockSecurityContext, false, mockBusinessNetwork) - .catch((error) => { - error.message.should.equal(errorResp.message); + .then(() => { sinon.assert.calledOnce(connection.fs.copy); sinon.assert.calledWith(connection.fs.copy, runtimeModulePath, targetDirectoryPath, sinon.match.object); // Check the filter ignores any relevant node modules files. @@ -508,122 +772,79 @@ describe('HLFConnection', () => { connection.fs.copy.firstCall.args[2].filter('composer-runtime-hlfv1/node_modules/here').should.be.false; sinon.assert.calledOnce(connection.fs.outputFile); sinon.assert.calledWith(connection.fs.outputFile, versionFilePath, sinon.match(/const version = /)); - sinon.assert.calledOnce(mockChain.sendInstallProposal); - sinon.assert.notCalled(mockChain.initialize); - sinon.assert.notCalled(mockChain.sendInstantiateProposal); - sinon.assert.calledWith(mockChain.sendInstallProposal, { + sinon.assert.calledOnce(mockClient.installChaincode); + sinon.assert.calledWith(mockClient.installChaincode, { chaincodePath: 'composer', chaincodeVersion: connectorPackageJSON.version, - //chaincodeId: 'org-acme-biznet', required for alpha2 - chaincodeId: 'org.acme.biznet', - chainId: connectOptions.channel, - txId: '00000000-0000-0000-0000-000000000000', - nonce: '11111111-1111-1111-1111-111111111111', + chaincodeId: 'org-acme-biznet', + txId: mockTransactionID, + targets: [mockPeer] }); + sinon.assert.notCalled(connection._initializeChannel); + sinon.assert.notCalled(mockChannel.sendInstantiateProposal); + sinon.assert.notCalled(mockChannel.sendTransaction); }); }); - it('should throw if no install responses are returned', () => { - // This is the generated nonce. - sandbox.stub(utils, 'getNonce').returns('11111111-1111-1111-1111-111111111111'); - // This is the deployment proposal and response (from the peers). - const installResponses = []; - const proposal = { proposal: 'i do' }; - const header = { header: 'gooooal' }; - // This is the generated transaction - mockChain.buildTransactionID.returns('00000000-0000-0000-0000-000000000000'); - // mock out getUserContext version in case we need to return to using this one - mockChain.buildTransactionID_getUserContext.resolves('00000000-0000-0000-0000-000000000000'); - mockChain.sendInstallProposal.resolves([ installResponses, proposal, header ]); - return connection.deploy(mockSecurityContext, false, mockBusinessNetwork) - .should.be.rejectedWith(/No results were returned/); - }); - - it('should throw if no instantiate responses are returned', () => { - // This is the generated nonce. - sandbox.stub(utils, 'getNonce').returns('11111111-1111-1111-1111-111111111111'); + it('should throw if install fails to validate', () => { + sandbox.stub(global, 'setTimeout'); // This is the deployment proposal and response (from the peers). - const installResponses = [{ - response: { - status: 200 - } - }]; - const proposalResponses = []; + const errorResp = new Error('Error something went completely wrong'); + const installResponses = [errorResp]; const proposal = { proposal: 'i do' }; const header = { header: 'gooooal' }; - // This is the generated transaction - mockChain.buildTransactionID.returns('00000000-0000-0000-0000-000000000000'); - // mock out getUserContext version in case we need to return to using this one - mockChain.buildTransactionID_getUserContext.resolves('00000000-0000-0000-0000-000000000000'); - mockChain.sendInstallProposal.resolves([ installResponses, proposal, header ]); - mockChain.sendInstantiateProposal.resolves([ proposalResponses, proposal, header ]); - // This is the commit proposal and response (from the orderer). - const response = { - status: 'SUCCESS' - }; - mockChain.sendTransaction.withArgs({ proposalResponses: proposalResponses, proposal: proposal, header: header }).resolves(response); - // This is the event hub response. - mockEventHub.registerTxEvent.yields('00000000-0000-0000-0000-000000000000', 'VALID'); + mockClient.installChaincode.resolves([ installResponses, proposal, header ]); + connection._validateResponses.withArgs(installResponses).throws(errorResp); return connection.deploy(mockSecurityContext, false, mockBusinessNetwork) - .should.be.rejectedWith(/No results were returned/); + .should.be.rejectedWith(/Error something went completely wrong/); }); - - it('should throw any instantiate responses that are errors', () => { - // This is the generated nonce. - sandbox.stub(utils, 'getNonce').returns('11111111-1111-1111-1111-111111111111'); + it('should throw any instantiate fails to validate', () => { const installResponses = [{ response: { status: 200 } }]; - const instantiateResponses = [ new Error('such error') ]; + const errorResp = new Error('such error'); + const instantiateResponses = [ errorResp ]; const proposal = { proposal: 'i do' }; const header = { header: 'gooooal' }; - // This is the generated transaction - mockChain.buildTransactionID.returns('00000000-0000-0000-0000-000000000000'); - // mock out getUserContext version in case we need to return to using this one - mockChain.buildTransactionID_getUserContext.resolves('00000000-0000-0000-0000-000000000000'); - mockChain.sendInstallProposal.resolves([ installResponses, proposal, header ]); - mockChain.sendInstantiateProposal.resolves([ instantiateResponses, proposal, header ]); + mockClient.installChaincode.resolves([ installResponses, proposal, header ]); + connection._validateResponses.withArgs(installResponses).returns(); + mockChannel.queryInstantiatedChaincodes.resolves({chaincodes: []}); + mockChannel.sendInstantiateProposal.resolves([ instantiateResponses, proposal, header ]); + connection._validateResponses.withArgs(instantiateResponses).throws(errorResp); // This is the event hub response. - mockEventHub.registerTxEvent.yields('00000000-0000-0000-0000-000000000000', 'VALID'); + mockEventHub.registerTxEvent.yields(mockTransactionID.getTransactionID().toString(), 'VALID'); return connection.deploy(mockSecurityContext, false, mockBusinessNetwork) .should.be.rejectedWith(/such error/); }); - it('should throw any endorsement responses that have a non-200 status code', () => { - // This is the generated nonce. - sandbox.stub(utils, 'getNonce').returns('11111111-1111-1111-1111-111111111111'); + // TODO: should extract out _waitForEvents + it('should throw an error if the commit throws an error', () => { // This is the deployment proposal and response (from the peers). const proposalResponses = [{ response: { - status: 500, - payload: 'such error' + status: 200 } }]; const proposal = { proposal: 'i do' }; const header = { header: 'gooooal' }; - // This is the generated transaction - mockChain.buildTransactionID.returns('00000000-0000-0000-0000-000000000000'); - // mock out getUserContext version in case we need to return to using this one - mockChain.buildTransactionID_getUserContext.resolves('00000000-0000-0000-0000-000000000000'); - mockChain.sendInstallProposal.resolves([ proposalResponses, proposal, header ]); - mockChain.sendInstantiateProposal.resolves([ proposalResponses, proposal, header ]); + mockClient.installChaincode.resolves([ proposalResponses, proposal, header ]); + mockChannel.queryInstantiatedChaincodes.resolves({chaincodes: []}); + mockChannel.sendInstantiateProposal.resolves([ proposalResponses, proposal, header ]); // This is the commit proposal and response (from the orderer). const response = { - status: 'SUCCESS' + status: 'FAILURE' }; - mockChain.sendTransaction.withArgs({ proposalResponses: proposalResponses, proposal: proposal, header: header }).resolves(response); + mockChannel.sendTransaction.withArgs({ proposalResponses: proposalResponses, proposal: proposal, header: header }).resolves(response); // This is the event hub response. - mockEventHub.registerTxEvent.yields('00000000-0000-0000-0000-000000000000', 'VALID'); + mockEventHub.registerTxEvent.yields(mockTransactionID.getTransactionID().toString(), 'INVALID'); return connection.deploy(mockSecurityContext, false, mockBusinessNetwork) - .should.be.rejectedWith(/such error/); + .should.be.rejectedWith(/Failed to commit transaction/); }); it('should throw an error if peer says transaction not valid', () => { - // This is the generated nonce. - sandbox.stub(utils, 'getNonce').returns('11111111-1111-1111-1111-111111111111'); // This is the deployment proposal and response (from the peers). const proposalResponses = [{ response: { @@ -632,83 +853,28 @@ describe('HLFConnection', () => { }]; const proposal = { proposal: 'i do' }; const header = { header: 'gooooal' }; - // This is the generated transaction - mockChain.buildTransactionID.returns('00000000-0000-0000-0000-000000000000'); - // mock out getUserContext version in case we need to return to using this one - mockChain.buildTransactionID_getUserContext.resolves('00000000-0000-0000-0000-000000000000'); - mockChain.sendInstallProposal.resolves([ proposalResponses, proposal, header ]); - mockChain.sendInstantiateProposal.resolves([ proposalResponses, proposal, header ]); + mockClient.installChaincode.resolves([ proposalResponses, proposal, header ]); + mockChannel.queryInstantiatedChaincodes.resolves({chaincodes: []}); + mockChannel.sendInstantiateProposal.resolves([ proposalResponses, proposal, header ]); // This is the commit proposal and response (from the orderer). const response = { status: 'SUCCESS' }; - mockChain.sendTransaction.withArgs({ proposalResponses: proposalResponses, proposal: proposal, header: header }).resolves(response); + mockChannel.sendTransaction.withArgs({ proposalResponses: proposalResponses, proposal: proposal, header: header }).resolves(response); // This is the event hub response. - mockEventHub.registerTxEvent.yields('00000000-0000-0000-0000-000000000000', 'INVALID'); + mockEventHub.registerTxEvent.yields(mockTransactionID.getTransactionID().toString(), 'INVALID'); return connection.deploy(mockSecurityContext, false, mockBusinessNetwork) .should.be.rejectedWith(/Peer has rejected transaction '00000000-0000-0000-0000-000000000000'/); }); - it('should throw an error if the commit of the transaction times out', () => { - // This is the generated nonce. - sandbox.stub(utils, 'getNonce').returns('11111111-1111-1111-1111-111111111111'); - // This is the deployment proposal and response (from the peers). - const proposalResponses = [{ - response: { - status: 200 - } - }]; - const proposal = { proposal: 'i do' }; - const header = { header: 'gooooal' }; - // This is the generated transaction - mockChain.buildTransactionID.returns('00000000-0000-0000-0000-000000000000'); - // mock out getUserContext version in case we need to return to using this one - mockChain.buildTransactionID_getUserContext.resolves('00000000-0000-0000-0000-000000000000'); - mockChain.sendInstallProposal.resolves([ proposalResponses, proposal, header ]); - mockChain.sendInstantiateProposal.resolves([ proposalResponses, proposal, header ]); - // This is the commit proposal and response (from the orderer). - const response = { - status: 'SUCCESS' - }; - mockChain.sendTransaction.withArgs({ proposalResponses: proposalResponses, proposal: proposal, header: header }).resolves(response); - // This is the event hub response. - sandbox.stub(global, 'setTimeout').yields(); - // mockEventHub.registerTxEvent.yields(); - return connection.deploy(mockSecurityContext, false, mockBusinessNetwork) - .should.be.rejectedWith(/Failed to receive commit notification/); - }); - - it('should throw an error if the commit throws an error', () => { - // This is the generated nonce. - sandbox.stub(utils, 'getNonce').returns('11111111-1111-1111-1111-111111111111'); - // This is the deployment proposal and response (from the peers). - const proposalResponses = [{ - response: { - status: 200 - } - }]; - const proposal = { proposal: 'i do' }; - const header = { header: 'gooooal' }; - // This is the generated transaction - mockChain.buildTransactionID.returns('00000000-0000-0000-0000-000000000000'); - // mock out getUserContext version in case we need to return to using this one - mockChain.buildTransactionID_getUserContext.resolves('00000000-0000-0000-0000-000000000000'); - mockChain.sendInstallProposal.resolves([ proposalResponses, proposal, header ]); - mockChain.sendInstantiateProposal.resolves([ proposalResponses, proposal, header ]); - // This is the commit proposal and response (from the orderer). - const response = { - status: 'FAILURE' - }; - mockChain.sendTransaction.withArgs({ proposalResponses: proposalResponses, proposal: proposal, header: header }).resolves(response); - // This is the event hub response. - mockEventHub.registerTxEvent.yields('00000000-0000-0000-0000-000000000000', 'INVALID'); - return connection.deploy(mockSecurityContext, false, mockBusinessNetwork) - .should.be.rejectedWith(/Failed to commit transaction/); - }); - }); describe('#undeploy', () => { + beforeEach(() => { + sandbox.stub(process, 'on').withArgs('exit').yields(); + sandbox.stub(HLFConnection, 'createEventHub').returns(mockEventHub); + connection._connectToEventHubs(); + }); it('should throw if businessNetworkIdentifier not specified', () => { (() => { @@ -740,6 +906,11 @@ describe('HLFConnection', () => { }); describe('#update', () => { + beforeEach(() => { + sandbox.stub(process, 'on').withArgs('exit').yields(); + sandbox.stub(HLFConnection, 'createEventHub').returns(mockEventHub); + connection._connectToEventHubs(); + }); it('should throw if businessNetworkDefinition not specified', () => { (() => { @@ -765,6 +936,11 @@ describe('HLFConnection', () => { }); describe('#ping', () => { + beforeEach(() => { + sandbox.stub(process, 'on').withArgs('exit').yields(); + sandbox.stub(HLFConnection, 'createEventHub').returns(mockEventHub); + connection._connectToEventHubs(); + }); it('should handle a chaincode with the same version as the connector', () => { const response = { @@ -859,6 +1035,11 @@ describe('HLFConnection', () => { }); describe('#queryChainCode', () => { + beforeEach(() => { + sandbox.stub(process, 'on').withArgs('exit').yields(); + sandbox.stub(HLFConnection, 'createEventHub').returns(mockEventHub); + connection._connectToEventHubs(); + }); it('should throw if functionName not specified', () => { (() => { @@ -879,56 +1060,33 @@ describe('HLFConnection', () => { }); it('should submit a query request to the chaincode', () => { - // This is the generated nonce. - sandbox.stub(utils, 'getNonce').returns('11111111-1111-1111-1111-111111111111'); - // This is the generated transaction - mockChain.buildTransactionID.returns('00000000-0000-0000-0000-000000000000'); - // mock out getUserContext version in case we need to return to using this one - mockChain.buildTransactionID_getUserContext.resolves('00000000-0000-0000-0000-000000000000'); - // This is the response from the chaincode. const response = Buffer.from('hello world'); - mockChain.queryByChaincode.resolves([response]); + mockChannel.queryByChaincode.resolves([response]); return connection.queryChainCode(mockSecurityContext, 'myfunc', ['arg1', 'arg2']) .then((result) => { - sinon.assert.calledOnce(mockChain.queryByChaincode); - sinon.assert.calledWith(mockChain.queryByChaincode, { - //chaincodeId: 'org-acme-biznet', required for alpha2 - chaincodeId: 'org.acme.biznet', - chainId: 'testchainid', - txId: '00000000-0000-0000-0000-000000000000', - nonce: '11111111-1111-1111-1111-111111111111', + sinon.assert.calledOnce(mockChannel.queryByChaincode); + sinon.assert.calledWith(mockChannel.queryByChaincode, { + chaincodeId: 'org-acme-biznet', + chaincodeVersion: connectorPackageJSON.version, + txId: mockTransactionID, fcn: 'myfunc', - args: ['arg1', 'arg2'], - attrs: ['userID'] + args: ['arg1', 'arg2'] }); result.equals(response).should.be.true; }); }); it('should throw if no responses are returned', () => { - // This is the generated nonce. - sandbox.stub(utils, 'getNonce').returns('11111111-1111-1111-1111-111111111111'); - // This is the generated transaction - mockChain.buildTransactionID.returns('00000000-0000-0000-0000-000000000000'); - // mock out getUserContext version in case we need to return to using this one - mockChain.buildTransactionID_getUserContext.resolves('00000000-0000-0000-0000-000000000000'); - // This is the response from the chaincode. - mockChain.queryByChaincode.resolves([]); + mockChannel.queryByChaincode.resolves([]); return connection.queryChainCode(mockSecurityContext, 'myfunc', ['arg1', 'arg2']) .should.be.rejectedWith(/No payloads were returned from the query request/); }); it('should throw any responses that are errors', () => { - // This is the generated nonce. - sandbox.stub(utils, 'getNonce').returns('11111111-1111-1111-1111-111111111111'); - // This is the generated transaction - mockChain.buildTransactionID.returns('00000000-0000-0000-0000-000000000000'); - // mock out getUserContext version in case we need to return to using this one - mockChain.buildTransactionID_getUserContext.resolves('00000000-0000-0000-0000-000000000000'); // This is the transaction proposal and response (from the peers). const response = [ new Error('such error') ]; // This is the response from the chaincode. - mockChain.queryByChaincode.resolves(response); + mockChannel.queryByChaincode.resolves(response); return connection.queryChainCode(mockSecurityContext, 'myfunc', ['arg1', 'arg2']) .should.be.rejectedWith(/such error/); @@ -937,6 +1095,13 @@ describe('HLFConnection', () => { }); describe('#invokeChainCode', () => { + beforeEach(() => { + sandbox.stub(process, 'on').withArgs('exit').yields(); + sandbox.stub(HLFConnection, 'createEventHub').returns(mockEventHub); + sandbox.stub(connection, '_validateResponses').returns(); + sandbox.stub(connection, '_initializeChannel').resolves(); + connection._connectToEventHubs(); + }); it('should throw if functionName not specified', () => { (() => { @@ -957,13 +1122,6 @@ describe('HLFConnection', () => { }); it('should submit an invoke request to the chaincode', () => { - // This is the generated nonce. - sandbox.stub(utils, 'getNonce').returns('11111111-1111-1111-1111-111111111111'); - // This is the generated transaction - mockChain.buildTransactionID.returns('00000000-0000-0000-0000-000000000000'); - // mock out getUserContext version in case we need to return to using this one - mockChain.buildTransactionID_getUserContext.resolves('00000000-0000-0000-0000-000000000000'); - // This is the transaction proposal and response (from the peers). const proposalResponses = [{ response: { status: 200 @@ -971,131 +1129,60 @@ describe('HLFConnection', () => { }]; const proposal = { proposal: 'i do' }; const header = { header: 'gooooal' }; - mockChain.sendTransactionProposal.resolves([ proposalResponses, proposal, header ]); + mockChannel.sendTransactionProposal.resolves([ proposalResponses, proposal, header ]); // This is the commit proposal and response (from the orderer). const response = { status: 'SUCCESS' }; - mockChain.sendTransaction.withArgs({ proposalResponses: proposalResponses, proposal: proposal, header: header }).resolves(response); + mockChannel.sendTransaction.withArgs({ proposalResponses: proposalResponses, proposal: proposal, header: header }).resolves(response); // This is the event hub response. mockEventHub.registerTxEvent.yields('00000000-0000-0000-0000-000000000000', 'VALID'); return connection.invokeChainCode(mockSecurityContext, 'myfunc', ['arg1', 'arg2']) .then((result) => { should.equal(result, undefined); - sinon.assert.calledOnce(mockChain.sendTransactionProposal); - sinon.assert.calledWith(mockChain.sendTransactionProposal, { - //chaincodeId: 'org-acme-biznet', required for alpha2 - chaincodeId: 'org.acme.biznet', - chainId: 'testchainid', - txId: '00000000-0000-0000-0000-000000000000', - nonce: '11111111-1111-1111-1111-111111111111', + sinon.assert.calledOnce(mockChannel.sendTransactionProposal); + sinon.assert.calledWith(mockChannel.sendTransactionProposal, { + chaincodeId: 'org-acme-biznet', + chaincodeVersion: connectorPackageJSON.version, + txId: mockTransactionID, fcn: 'myfunc', - args: ['arg1', 'arg2'], - attrs: ['userID'] + args: ['arg1', 'arg2'] }); - sinon.assert.calledOnce(mockChain.sendTransaction); + sinon.assert.calledOnce(mockChannel.sendTransaction); }); }); - it('should throw if no endorsement responses are returned', () => { - // This is the generated nonce. - sandbox.stub(utils, 'getNonce').returns('11111111-1111-1111-1111-111111111111'); - // This is the generated transaction - mockChain.buildTransactionID.returns('00000000-0000-0000-0000-000000000000'); - // mock out getUserContext version in case we need to return to using this one - mockChain.buildTransactionID_getUserContext.resolves('00000000-0000-0000-0000-000000000000'); - // This is the transaction proposal and response (from the peers). + it('should throw if transaction proposals were not valid', () => { const proposalResponses = []; const proposal = { proposal: 'i do' }; const header = { header: 'gooooal' }; - mockChain.sendTransactionProposal.resolves([ proposalResponses, proposal, header ]); - // This is the commit proposal and response (from the orderer). - const response = { - status: 'SUCCESS' - }; - mockChain.sendTransaction.withArgs({ proposalResponses: proposalResponses, proposal: proposal, header: header }).resolves(response); - // This is the event hub response. - mockEventHub.registerTxEvent.yields('00000000-0000-0000-0000-000000000000', 'VALID'); + const errorResp = new Error('an error'); + mockChannel.sendTransactionProposal.resolves([ proposalResponses, proposal, header ]); + connection._validateResponses.withArgs(proposalResponses).throws(errorResp); return connection.invokeChainCode(mockSecurityContext, 'myfunc', ['arg1', 'arg2']) - .should.be.rejectedWith(/No results were returned/); - }); - - it('should throw any endorsement responses that are errors', () => { - // This is the generated nonce. - sandbox.stub(utils, 'getNonce').returns('11111111-1111-1111-1111-111111111111'); - // This is the generated transaction - mockChain.buildTransactionID.returns('00000000-0000-0000-0000-000000000000'); - // mock out getUserContext version in case we need to return to using this one - mockChain.buildTransactionID_getUserContext.resolves('00000000-0000-0000-0000-000000000000'); - // This is the transaction proposal and response (from the peers). - const proposalResponses = [ new Error('such error') ]; - const proposal = { proposal: 'i do' }; - const header = { header: 'gooooal' }; - mockChain.sendTransactionProposal.resolves([ proposalResponses, proposal, header ]); - // This is the commit proposal and response (from the orderer). - const response = { - status: 'SUCCESS' - }; - mockChain.sendTransaction.withArgs({ proposalResponses: proposalResponses, proposal: proposal, header: header }).resolves(response); - // This is the event hub response. - mockEventHub.registerTxEvent.yields('00000000-0000-0000-0000-000000000000', 'VALID'); - return connection.invokeChainCode(mockSecurityContext, 'myfunc', ['arg1', 'arg2']) - .should.be.rejectedWith(/such error/); - }); - - it('should throw any endorsement responses that have a non-200 status code', () => { - // This is the generated nonce. - sandbox.stub(utils, 'getNonce').returns('11111111-1111-1111-1111-111111111111'); - // This is the generated transaction - mockChain.buildTransactionID.returns('00000000-0000-0000-0000-000000000000'); - // mock out getUserContext version in case we need to return to using this one - mockChain.buildTransactionID_getUserContext.resolves('00000000-0000-0000-0000-000000000000'); - // This is the transaction proposal and response (from the peers). - const proposalResponses = [{ - response: { - status: 500, - payload: 'such error' - } - }]; - const proposal = { proposal: 'i do' }; - const header = { header: 'gooooal' }; - mockChain.sendTransactionProposal.resolves([ proposalResponses, proposal, header ]); - // This is the commit proposal and response (from the orderer). - const response = { - status: 'SUCCESS' - }; - mockChain.sendTransaction.withArgs({ proposalResponses: proposalResponses, proposal: proposal, header: header }).resolves(response); - // This is the event hub response. - mockEventHub.registerTxEvent.yields('00000000-0000-0000-0000-000000000000', 'VALID'); - return connection.invokeChainCode(mockSecurityContext, 'myfunc', ['arg1', 'arg2']) - .should.be.rejectedWith(/such error/); + .should.be.rejectedWith(/an error/); }); + //TODO: Should extract out _waitForEvents it('should set the timeout to value specified in connection profile', () => { connectOptions = { orderers: [ 'grpc://localhost:7050' ], - peers: [ - 'grpc://localhost:7051' - ], - events: [ - 'grpc://localhost:7053' - ], + peers: [ { + requestURL: 'grpc://localhost:7051', + eventURL: 'grpc://localhost:7053' + }], ca: 'http://localhost:7054', keyValStore: '/tmp/hlfabric1', channel: 'testchainid', mspID: 'suchmsp', - deployWaitTime: 39, - invokeWaitTime: 63, + timeout: 38 }; - connection = new HLFConnection(mockConnectionManager, 'hlfabric1', 'org.acme.biznet', connectOptions, mockClient, mockChain, [mockEventHub], mockCAClient); - // This is the generated nonce. - sandbox.stub(utils, 'getNonce').returns('11111111-1111-1111-1111-111111111111'); - // This is the generated transaction - mockChain.buildTransactionID.returns('00000000-0000-0000-0000-000000000000'); - // mock out getUserContext version in case we need to return to using this one - mockChain.buildTransactionID_getUserContext.resolves('00000000-0000-0000-0000-000000000000'); + connection = new HLFConnection(mockConnectionManager, 'hlfabric1', 'org.acme.biznet', connectOptions, mockClient, mockChannel, [mockEventHubDef], mockCAClient); + sandbox.stub(connection, '_validateResponses').returns(); + sandbox.stub(connection, '_initializeChannel').resolves(); + connection._connectToEventHubs(); // This is the transaction proposal and response (from the peers). const proposalResponses = [{ response: { @@ -1104,31 +1191,23 @@ describe('HLFConnection', () => { }]; const proposal = { proposal: 'i do' }; const header = { header: 'gooooal' }; - mockChain.sendTransactionProposal.resolves([ proposalResponses, proposal, header ]); + mockChannel.sendTransactionProposal.resolves([ proposalResponses, proposal, header ]); // This is the commit proposal and response (from the orderer). const response = { status: 'SUCCESS' }; - mockChain.sendTransaction.withArgs({ proposalResponses: proposalResponses, proposal: proposal, header: header }).resolves(response); + mockChannel.sendTransaction.withArgs({ proposalResponses: proposalResponses, proposal: proposal, header: header }).resolves(response); // This is the event hub response. sandbox.stub(global, 'setTimeout').yields(); - // mockEventHub.registerTxEvent.yields(); return connection.invokeChainCode(mockSecurityContext, 'myfunc', ['arg1', 'arg2']) .should.be.rejected .then(() => { sinon.assert.calledWith(global.setTimeout, sinon.match.func, sinon.match.number); - sinon.assert.calledWith(global.setTimeout, sinon.match.func, connectOptions.invokeWaitTime * 1000); + sinon.assert.calledWith(global.setTimeout, sinon.match.func, 38 * 1000); }); }); - it('should throw an error if the commit of the transaction times out', () => { - // This is the generated nonce. - sandbox.stub(utils, 'getNonce').returns('11111111-1111-1111-1111-111111111111'); - // This is the generated transaction - mockChain.buildTransactionID.returns('00000000-0000-0000-0000-000000000000'); - // mock out getUserContext version in case we need to return to using this one - mockChain.buildTransactionID_getUserContext.resolves('00000000-0000-0000-0000-000000000000'); // This is the transaction proposal and response (from the peers). const proposalResponses = [{ response: { @@ -1137,12 +1216,12 @@ describe('HLFConnection', () => { }]; const proposal = { proposal: 'i do' }; const header = { header: 'gooooal' }; - mockChain.sendTransactionProposal.resolves([ proposalResponses, proposal, header ]); + mockChannel.sendTransactionProposal.resolves([ proposalResponses, proposal, header ]); // This is the commit proposal and response (from the orderer). const response = { status: 'SUCCESS' }; - mockChain.sendTransaction.withArgs({ proposalResponses: proposalResponses, proposal: proposal, header: header }).resolves(response); + mockChannel.sendTransaction.withArgs({ proposalResponses: proposalResponses, proposal: proposal, header: header }).resolves(response); // This is the event hub response. sandbox.stub(global, 'setTimeout').yields(); // mockEventHub.registerTxEvent.yields(); @@ -1151,13 +1230,6 @@ describe('HLFConnection', () => { }); it('should throw an error if the commit throws an error', () => { - // This is the generated nonce. - sandbox.stub(utils, 'getNonce').returns('11111111-1111-1111-1111-111111111111'); - // This is the generated transaction - mockChain.buildTransactionID.returns('00000000-0000-0000-0000-000000000000'); - // mock out getUserContext version in case we need to return to using this one - mockChain.buildTransactionID_getUserContext.resolves('00000000-0000-0000-0000-000000000000'); - // This is the transaction proposal and response (from the peers). const proposalResponses = [{ response: { status: 200 @@ -1165,12 +1237,12 @@ describe('HLFConnection', () => { }]; const proposal = { proposal: 'i do' }; const header = { header: 'gooooal' }; - mockChain.sendTransactionProposal.resolves([ proposalResponses, proposal, header ]); + mockChannel.sendTransactionProposal.resolves([ proposalResponses, proposal, header ]); // This is the commit proposal and response (from the orderer). const response = { status: 'FAILURE' }; - mockChain.sendTransaction.withArgs({ proposalResponses: proposalResponses, proposal: proposal, header: header }).resolves(response); + mockChannel.sendTransaction.withArgs({ proposalResponses: proposalResponses, proposal: proposal, header: header }).resolves(response); // This is the event hub response. mockEventHub.registerTxEvent.yields('00000000-0000-0000-0000-000000000000', 'VALID'); return connection.invokeChainCode(mockSecurityContext, 'myfunc', ['arg1', 'arg2']) @@ -1227,8 +1299,7 @@ describe('HLFConnection', () => { enrollmentID: 'auser', affiliation: 'org1', attrs: [ - {name: 'hf.Registrar.Roles', value: 'client'}//, -// {name: 'hf.Registrar.DelegateRoles', value: 'client'}, + {name: 'hf.Registrar.Roles', value: 'client'} ], maxEnrollments: 0, role: 'client' @@ -1323,7 +1394,7 @@ describe('HLFConnection', () => { describe('#list', () => { it('should return an empty array if no instantiated chaincodes', () => { - mockChain.queryInstantiatedChaincodes.resolves({ + mockChannel.queryInstantiatedChaincodes.resolves({ chaincodes: [] }); return connection.list(mockSecurityContext) @@ -1331,7 +1402,7 @@ describe('HLFConnection', () => { }); it('should return an array of chaincode names for all instantiated chaincodes', () => { - mockChain.queryInstantiatedChaincodes.resolves({ + mockChannel.queryInstantiatedChaincodes.resolves({ chaincodes: [{ name: 'org.acme.biznet1', version: '1.0.0', @@ -1347,7 +1418,7 @@ describe('HLFConnection', () => { }); it('should filter out any non-composer instantiated chaincodes', () => { - mockChain.queryInstantiatedChaincodes.resolves({ + mockChannel.queryInstantiatedChaincodes.resolves({ chaincodes: [{ name: 'org.acme.biznet1', version: '1.0.0', @@ -1363,11 +1434,34 @@ describe('HLFConnection', () => { }); it('should handle any errors querying instantiated chaincodes', () => { - mockChain.queryInstantiatedChaincodes.rejects(new Error('such error')); + mockChannel.queryInstantiatedChaincodes.rejects(new Error('such error')); return connection.list(mockSecurityContext) .should.be.rejectedWith(/such error/); }); }); + describe('#_initializeChannel', () => { + beforeEach(() => { + mockChannel.initialize.resolves(); + }); + + it('should initialize the channel if not initialized', () => { + connection.initialized.should.be.false; + return connection._initializeChannel() + .then(() => { + sinon.assert.calledOnce(mockChannel.initialize); + connection.initialized.should.be.true; + }); + }); + + it('should not initialize the channel if initialized', () => { + connection.initialized = true; + return connection._initializeChannel() + .then(() => { + sinon.assert.notCalled(mockChannel.initialize); + }); + }); + }); + }); diff --git a/packages/composer-connector-hlfv1/test/hlfconnectionmanager.js b/packages/composer-connector-hlfv1/test/hlfconnectionmanager.js index dba9b9c00d..01ddefc8d2 100644 --- a/packages/composer-connector-hlfv1/test/hlfconnectionmanager.js +++ b/packages/composer-connector-hlfv1/test/hlfconnectionmanager.js @@ -15,22 +15,19 @@ 'use strict'; const api = require('fabric-client/lib/api'); -const Chain = require('fabric-client/lib/Chain'); +const Channel = require('fabric-client/lib/Channel'); const Client = require('fabric-client'); const ConnectionProfileManager = require('composer-common').ConnectionProfileManager; -const EventHub = require('fabric-client/lib/EventHub'); const FabricCAClientImpl = require('fabric-ca-client'); const HLFConnection = require('../lib/hlfconnection'); const HLFConnectionManager = require('..'); const HLFWalletProxy = require('../lib/hlfwalletproxy'); -const Logger = require('composer-common').Logger; const KeyValueStore = api.KeyValueStore; +const CryptoSuite = api.CryptoSuite; const Orderer = require('fabric-client/lib/Orderer'); const Peer = require('fabric-client/lib/Peer'); const Wallet = require('composer-common').Wallet; -const LOG = Logger.getLog('HLFConnectionManager'); - const chai = require('chai'); chai.should(); chai.use(require('chai-as-promised')); @@ -38,6 +35,9 @@ const sinon = require('sinon'); require('sinon-as-promised'); const fs = require('fs'); +const Logger = require('composer-common').Logger; +const LOG = Logger.getLog('HLFConnectionManager'); + describe('HLFConnectionManager', () => { @@ -54,6 +54,9 @@ describe('HLFConnectionManager', () => { 'A1UdDgQiBCBNSnciFRaLZZTIfoJlDkOPHzfDA+FLX55vPuBswruCOjAKBggqhkjO\n' + 'PQQDAgNHADBEAiBa6k7Cax+McCHy61Jma1vLuFZswBbnsC6DqbveiKdUoAIgeyAf\n' + 'HzWxMoVrLfPFwF75PqCjae7xnYq+RWlsHZlMGFU=\n' + +'-----END CERTIFICATE-----'; + const overrideCert = '-----BEGIN CERTIFICATE-----\n' + +'Override override override' + '-----END CERTIFICATE-----'; let mockConnectionProfileManager; @@ -71,6 +74,7 @@ describe('HLFConnectionManager', () => { sandbox.restore(); }); + describe('global.hfc.logger', () => { it('should insert a debug logger', () => { @@ -147,28 +151,23 @@ describe('HLFConnectionManager', () => { }); - describe('#createEventHub', () => { - - it('should create a new event hub', () => { - let eventHub = HLFConnectionManager.createEventHub(); - eventHub.should.be.an.instanceOf(EventHub); - }); - - }); - describe('#createCAClient', () => { it('should create a new CA client', () => { - let caClient = HLFConnectionManager.createCAClient('http://localhost:7054', null, '.my-key-store'); + let caClient = HLFConnectionManager.createCAClient('http://localhost:7054', null, null); caClient.should.be.an.instanceOf(FabricCAClientImpl); - caClient.getCrypto()._storeConfig.opts.path.should.equal('.my-key-store'); }); it('should create a new CA client with tls options', () => { const tlsOpts = {trustedRoots: ['cert1'], verify: false}; - let caClient = HLFConnectionManager.createCAClient('http://localhost:7054', tlsOpts, '.my-key-store'); + let caClient = HLFConnectionManager.createCAClient('http://localhost:7054', tlsOpts, null); + caClient.should.be.an.instanceOf(FabricCAClientImpl); + }); + + it('should create a new CA client with tls options and name', () => { + const tlsOpts = {trustedRoots: ['cert1'], verify: false}; + let caClient = HLFConnectionManager.createCAClient('http://localhost:7054', tlsOpts, 'aname'); caClient.should.be.an.instanceOf(FabricCAClientImpl); - caClient._fabricCAClient._tlsOptions.should.deep.equal(tlsOpts); }); }); @@ -181,38 +180,65 @@ describe('HLFConnectionManager', () => { }); it('should create a new CA client from string', () => { - HLFConnectionManager.parseCA('http://localhost:7054', '.my-key-store'); + HLFConnectionManager.parseCA('http://localhost:7054'); + sinon.assert.calledOnce(HLFConnectionManager.createCAClient); + sinon.assert.calledWith(HLFConnectionManager.createCAClient, 'http://localhost:7054', null, null); + }); + + it('should create a new CA client from simple object', () => { + const ca = { + url: 'http://localhost:7054' + }; + HLFConnectionManager.parseCA(ca); sinon.assert.calledOnce(HLFConnectionManager.createCAClient); - sinon.assert.calledWith(HLFConnectionManager.createCAClient, 'http://localhost:7054', null, '.my-key-store'); + sinon.assert.calledWith(HLFConnectionManager.createCAClient, 'http://localhost:7054', null, null); }); - it('should create a new CA client from object', () => { + it('should create a new CA client from simple object with name', () => { const ca = { url: 'http://localhost:7054', + name: 'aname' }; - HLFConnectionManager.parseCA(ca, '.my-key-store'); + HLFConnectionManager.parseCA(ca); sinon.assert.calledOnce(HLFConnectionManager.createCAClient); - sinon.assert.calledWith(HLFConnectionManager.createCAClient, 'http://localhost:7054', null, '.my-key-store'); + sinon.assert.calledWith(HLFConnectionManager.createCAClient, 'http://localhost:7054', null, 'aname'); }); - it('should create a new CA client with tls options', () => { + + it('should create a new CA client with tls options and no name', () => { + const ca = { + url: 'http://localhost:7054', + trustedRoots: ['cert1'], + verify: false + }; + HLFConnectionManager.parseCA(ca); + sinon.assert.calledOnce(HLFConnectionManager.createCAClient); + sinon.assert.calledWith(HLFConnectionManager.createCAClient, 'http://localhost:7054', + { + trustedRoots: ['cert1'], + verify:false + }, null); + }); + + it('should create a new CA client with tls options and name', () => { const ca = { url: 'http://localhost:7054', trustedRoots: ['cert1'], + name: 'aname2', verify: false }; - HLFConnectionManager.parseCA(ca, '.my-key-store'); + HLFConnectionManager.parseCA(ca); sinon.assert.calledOnce(HLFConnectionManager.createCAClient); sinon.assert.calledWith(HLFConnectionManager.createCAClient, 'http://localhost:7054', { trustedRoots: ['cert1'], verify:false - }, '.my-key-store'); + }, 'aname2'); }); }); describe('#parsePeer', () => { - let peerDef, mockPeer, mockEventHub; + let peerDef, mockPeer; beforeEach(() => { peerDef = { requestURL: 'grpc://localhost:7051', @@ -220,8 +246,6 @@ describe('HLFConnectionManager', () => { }; mockPeer = sinon.createStubInstance(Peer); sandbox.stub(HLFConnectionManager, 'createPeer').withArgs(peerDef.requestURL).returns(mockPeer); - mockEventHub = sinon.createStubInstance(EventHub); - sandbox.stub(HLFConnectionManager, 'createEventHub').returns(mockEventHub); }); it('should throw if peer definition is incorrect', () => { @@ -248,64 +272,137 @@ describe('HLFConnectionManager', () => { }); it('should create a new peer and eventHub with no tls', () => { - let eventHubs = []; - HLFConnectionManager.parsePeer(peerDef, eventHubs); + let eventHubDefs = []; + HLFConnectionManager.parsePeer(peerDef, 10, undefined, eventHubDefs); sinon.assert.calledOnce(HLFConnectionManager.createPeer); sinon.assert.calledWith(HLFConnectionManager.createPeer, peerDef.requestURL); - sinon.assert.calledOnce(HLFConnectionManager.createEventHub); - eventHubs.length.should.equal(1); - eventHubs[0].should.be.an.instanceof(EventHub); - sinon.assert.calledOnce(mockEventHub.setPeerAddr); - sinon.assert.calledWith(mockEventHub.setPeerAddr, peerDef.eventURL); - sinon.assert.calledOnce(mockEventHub.connect); + eventHubDefs.length.should.equal(1); + eventHubDefs.should.be.an.instanceof(Object); + eventHubDefs[0].should.deep.equal({ + 'eventURL': 'grpc://localhost:7053', + 'opts': { + 'request-timeout': 10000 + } + }); }); - it('should create a new peer and eventHub with tls and embedded certificate', () => { + it('should create a new peer with tls and embedded certificate', () => { peerDef.cert = embeddedCert; peerDef.hostnameOverride = 'localhost'; - let eventHubs = []; - HLFConnectionManager.parsePeer(peerDef, eventHubs); + let eventHubDefs = []; + HLFConnectionManager.parsePeer(peerDef, 9, undefined, eventHubDefs); sinon.assert.calledOnce(HLFConnectionManager.createPeer); sinon.assert.calledWith(HLFConnectionManager.createPeer, peerDef.requestURL, { - pem: peerDef.cert, + 'request-timeout': 9000, + pem: embeddedCert, 'ssl-target-name-override': peerDef.hostnameOverride }); - - sinon.assert.calledOnce(HLFConnectionManager.createEventHub); - eventHubs.length.should.equal(1); - eventHubs[0].should.be.an.instanceof(EventHub); - sinon.assert.calledOnce(mockEventHub.setPeerAddr); - sinon.assert.calledWith(mockEventHub.setPeerAddr, peerDef.eventURL, { - pem: peerDef.cert, - 'ssl-target-name-override': peerDef.hostnameOverride + eventHubDefs.length.should.equal(1); + eventHubDefs.should.be.an.instanceof(Object); + eventHubDefs[0].should.deep.equal({ + 'eventURL': 'grpc://localhost:7053', + 'opts': { + 'request-timeout': 9000, + pem: embeddedCert, + 'ssl-target-name-override': peerDef.hostnameOverride + } }); - sinon.assert.calledOnce(mockEventHub.connect); }); - it('should create a new peer and eventHub with tls and file system certificate', () => { + it('should create a new peer with tls and file system certificate', () => { sandbox.stub(fs,'readFileSync').returns(new Buffer('acert')); peerDef.cert = '/some/path/to/some/file'; - let eventHubs = []; - HLFConnectionManager.parsePeer(peerDef, eventHubs); + let eventHubDefs = []; + HLFConnectionManager.parsePeer(peerDef, 7, undefined, eventHubDefs); sinon.assert.calledOnce(HLFConnectionManager.createPeer); sinon.assert.calledWith(fs.readFileSync, peerDef.cert); sinon.assert.calledWith(HLFConnectionManager.createPeer, peerDef.requestURL, { + 'request-timeout': 7000, pem: 'acert' }); - sinon.assert.calledOnce(HLFConnectionManager.createEventHub); - eventHubs.length.should.equal(1); - eventHubs[0].should.be.an.instanceof(EventHub); - sinon.assert.calledOnce(mockEventHub.setPeerAddr); - sinon.assert.calledWith(mockEventHub.setPeerAddr, peerDef.eventURL, { + eventHubDefs.length.should.equal(1); + eventHubDefs.should.be.an.instanceof(Object); + eventHubDefs[0].should.deep.equal({ + 'eventURL': 'grpc://localhost:7053', + 'opts': { + 'request-timeout': 7000, + 'pem': 'acert' + } + }); + }); + + it('should create a new peer with tls and embedded certificate from global cert', () => { + peerDef.hostnameOverride = 'localhost'; + + let eventHubDefs = []; + HLFConnectionManager.parsePeer(peerDef, 9, embeddedCert, eventHubDefs); + sinon.assert.calledOnce(HLFConnectionManager.createPeer); + sinon.assert.calledWith(HLFConnectionManager.createPeer, peerDef.requestURL, { + 'request-timeout': 9000, + pem: embeddedCert, + 'ssl-target-name-override': peerDef.hostnameOverride + }); + eventHubDefs.length.should.equal(1); + eventHubDefs.should.be.an.instanceof(Object); + eventHubDefs[0].should.deep.equal({ + 'eventURL': 'grpc://localhost:7053', + 'opts': { + 'request-timeout': 9000, + pem: embeddedCert, + 'ssl-target-name-override': peerDef.hostnameOverride + } + }); + }); + + it('should create a new peer with tls and global file system certificate', () => { + sandbox.stub(fs,'readFileSync').returns(new Buffer('acert')); + let eventHubDefs = []; + HLFConnectionManager.parsePeer(peerDef, 7, '/some/path/to/some/file', eventHubDefs); + sinon.assert.calledOnce(HLFConnectionManager.createPeer); + sinon.assert.calledWith(fs.readFileSync, '/some/path/to/some/file'); + sinon.assert.calledWith(HLFConnectionManager.createPeer, peerDef.requestURL, { + 'request-timeout': 7000, pem: 'acert' }); - sinon.assert.calledOnce(mockEventHub.connect); + + eventHubDefs.length.should.equal(1); + eventHubDefs.should.be.an.instanceof(Object); + eventHubDefs[0].should.deep.equal({ + 'eventURL': 'grpc://localhost:7053', + 'opts': { + 'request-timeout': 7000, + 'pem': 'acert' + } + }); }); + it('should create a new peer with tls using cert field over global cert', () => { + peerDef.hostnameOverride = 'localhost'; + peerDef.cert = overrideCert; + + let eventHubDefs = []; + HLFConnectionManager.parsePeer(peerDef, 9, embeddedCert, eventHubDefs); + sinon.assert.calledOnce(HLFConnectionManager.createPeer); + sinon.assert.calledWith(HLFConnectionManager.createPeer, peerDef.requestURL, { + 'request-timeout': 9000, + pem: overrideCert, + 'ssl-target-name-override': peerDef.hostnameOverride + }); + eventHubDefs.length.should.equal(1); + eventHubDefs.should.be.an.instanceof(Object); + eventHubDefs[0].should.deep.equal({ + 'eventURL': 'grpc://localhost:7053', + 'opts': { + 'request-timeout': 9000, + pem: overrideCert, + 'ssl-target-name-override': peerDef.hostnameOverride + } + }); + }); }); @@ -317,9 +414,11 @@ describe('HLFConnectionManager', () => { }); it('should create a new orderer without tls', () => { - HLFConnectionManager.parseOrderer('http://localhost:7054'); + HLFConnectionManager.parseOrderer('http://localhost:7054', 4); sinon.assert.calledOnce(HLFConnectionManager.createOrderer); - sinon.assert.calledWith(HLFConnectionManager.createOrderer, 'http://localhost:7054'); + sinon.assert.calledWith(HLFConnectionManager.createOrderer, 'http://localhost:7054', { + 'request-timeout': 4000 + }); }); it('should create a new orderer with tls and embedded cert', () => { @@ -329,9 +428,10 @@ describe('HLFConnectionManager', () => { hostnameOverride: 'fred' }; - HLFConnectionManager.parseOrderer(ordererDef); + HLFConnectionManager.parseOrderer(ordererDef, 8); sinon.assert.calledOnce(HLFConnectionManager.createOrderer); sinon.assert.calledWith(HLFConnectionManager.createOrderer, 'https://localhost:7054', { + 'request-timeout': 8000, pem: embeddedCert, 'ssl-target-name-override': 'fred' }); @@ -345,21 +445,70 @@ describe('HLFConnectionManager', () => { cert: '/some/path/to/some/file', }; - HLFConnectionManager.parseOrderer(ordererDef); + HLFConnectionManager.parseOrderer(ordererDef, 15); sinon.assert.calledOnce(HLFConnectionManager.createOrderer); sinon.assert.calledWith(fs.readFileSync, ordererDef.cert); sinon.assert.calledWith(HLFConnectionManager.createOrderer, 'https://localhost:7054', { + 'request-timeout': 15000, pem: 'acert' }); }); + it('should create a new orderer with tls and global cert', () => { + let ordererDef = { + url: 'https://localhost:7054', + hostnameOverride: 'fred' + }; + + HLFConnectionManager.parseOrderer(ordererDef, 8, embeddedCert); + sinon.assert.calledOnce(HLFConnectionManager.createOrderer); + sinon.assert.calledWith(HLFConnectionManager.createOrderer, 'https://localhost:7054', { + 'request-timeout': 8000, + pem: embeddedCert, + 'ssl-target-name-override': 'fred' + }); + }); + + it('should create a new orderer with tls and file system cert', () => { + sandbox.stub(fs,'readFileSync').returns(new Buffer('acert')); + + let ordererDef = { + url: 'https://localhost:7054', + }; + + HLFConnectionManager.parseOrderer(ordererDef, 15, '/some/path/to/some/file'); + sinon.assert.calledOnce(HLFConnectionManager.createOrderer); + sinon.assert.calledWith(fs.readFileSync, '/some/path/to/some/file'); + sinon.assert.calledWith(HLFConnectionManager.createOrderer, 'https://localhost:7054', { + 'request-timeout': 15000, + pem: 'acert' + }); + }); + + it('should create a new orderer with tls and use the main cert over global cert', () => { + let ordererDef = { + url: 'https://localhost:7054', + hostnameOverride: 'fred', + cert: overrideCert + }; + + HLFConnectionManager.parseOrderer(ordererDef, 8, embeddedCert); + sinon.assert.calledOnce(HLFConnectionManager.createOrderer); + sinon.assert.calledWith(HLFConnectionManager.createOrderer, 'https://localhost:7054', { + 'request-timeout': 8000, + pem: overrideCert, + 'ssl-target-name-override': 'fred' + }); + }); + }); describe('#connect', () => { let connectOptions; - let mockClient, mockChain, mockOrderer, mockPeer, mockEventHub, mockCAClient, mockKeyValueStore, mockWallet; + let mockClient, mockChannel, mockOrderer, mockPeer, mockCAClient, mockKeyValueStore, mockWallet; + let configSettingStub; beforeEach(() => { connectOptions = { @@ -375,24 +524,23 @@ describe('HLFConnectionManager', () => { ca: 'http://localhost:7054', keyValStore: '/tmp/hlfabric1', channel: 'testchainid', + timeout: 123, mspID: 'MSP1Org' }; mockClient = sinon.createStubInstance(Client); sandbox.stub(HLFConnectionManager, 'createClient').returns(mockClient); - mockChain = sinon.createStubInstance(Chain); - mockClient.newChain.returns(mockChain); + mockChannel = sinon.createStubInstance(Channel); + mockClient.newChannel.returns(mockChannel); mockOrderer = sinon.createStubInstance(Orderer); sandbox.stub(HLFConnectionManager, 'createOrderer').withArgs(connectOptions.orderers[0]).returns(mockOrderer); mockPeer = sinon.createStubInstance(Peer); sandbox.stub(HLFConnectionManager, 'createPeer').withArgs(connectOptions.peers[0]).returns(mockPeer); - mockEventHub = sinon.createStubInstance(EventHub); - sandbox.stub(HLFConnectionManager, 'createEventHub').returns(mockEventHub); mockCAClient = sinon.createStubInstance(FabricCAClientImpl); sandbox.stub(HLFConnectionManager, 'createCAClient').withArgs(sinon.match.string).returns(mockCAClient); mockKeyValueStore = sinon.createStubInstance(KeyValueStore); sandbox.stub(Client, 'newDefaultKeyValueStore').resolves(mockKeyValueStore); + configSettingStub = sandbox.stub(Client, 'setConfigSetting'); mockWallet = sinon.createStubInstance(Wallet); - sandbox.stub(process, 'on').withArgs('exit').yields(); }); it('should throw if connectionProfile not specified', () => { @@ -472,13 +620,15 @@ describe('HLFConnectionManager', () => { }).should.throw(/The certificate authority URL has not been specified/); }); - it('should throw if keyValStore is not specified', () => { + it('should throw if keyValStore and wallet are not specified', () => { delete connectOptions.keyValStore; (() => { connectionManager.connect('hlfabric1', 'org.acme.biznet', connectOptions); - }).should.throw(/No key value store directory has been specified/); + }).should.throw(/No key value store directory or wallet has been specified/); }); + //TODO: should throw if wallet not of the right type. + it('should throw if channel is not specified', () => { delete connectOptions.channel; (() => { @@ -494,16 +644,21 @@ describe('HLFConnectionManager', () => { connection.should.be.an.instanceOf(HLFConnection); connection.getConnectionOptions().should.deep.equal(connectOptions); connection.client.should.deep.equal(mockClient); - connection.chain.should.deep.equal(mockChain); - connection.eventHubs.should.deep.equal([mockEventHub]); + connection.channel.should.deep.equal(mockChannel); connection.caClient.should.deep.equal(mockCAClient); - sinon.assert.calledWith(mockClient.newChain, connectOptions.channel); + sinon.assert.calledWith(mockClient.newChannel, connectOptions.channel); sinon.assert.calledOnce(HLFConnectionManager.createCAClient); - sinon.assert.calledWith(HLFConnectionManager.createCAClient, 'http://localhost:7054', null, connectOptions.keyValStore); + sinon.assert.calledWith(HLFConnectionManager.createCAClient, 'http://localhost:7054', null, null); sinon.assert.calledOnce(HLFConnectionManager.createPeer); sinon.assert.calledWith(HLFConnectionManager.createPeer, 'grpc://localhost:7051'); - sinon.assert.calledOnce(HLFConnectionManager.createEventHub); - sinon.assert.calledWith(mockEventHub.setPeerAddr, 'grpc://localhost:7053'); + sinon.assert.calledOnce(HLFConnectionManager.createOrderer); + sinon.assert.calledWith(HLFConnectionManager.createOrderer, 'grpc://localhost:7050'); + connection.eventHubDefs[0].should.deep.equal({ + 'eventURL': 'grpc://localhost:7053', + 'opts': { + 'request-timeout': 123000 + } + }); }); }); @@ -515,10 +670,61 @@ describe('HLFConnectionManager', () => { connection.should.be.an.instanceOf(HLFConnection); connection.getConnectionOptions().should.deep.equal(connectOptions); connection.client.should.deep.equal(mockClient); - connection.chain.should.deep.equal(mockChain); - connection.eventHubs.should.deep.equal([mockEventHub]); + connection.channel.should.deep.equal(mockChannel); connection.caClient.should.deep.equal(mockCAClient); - sinon.assert.calledWith(mockClient.newChain, connectOptions.channel); + sinon.assert.calledWith(mockClient.newChannel, connectOptions.channel); + sinon.assert.calledOnce(HLFConnectionManager.createCAClient); + sinon.assert.calledWith(HLFConnectionManager.createCAClient, 'http://localhost:7054', null, null); + sinon.assert.calledOnce(HLFConnectionManager.createPeer); + sinon.assert.calledWith(HLFConnectionManager.createPeer, 'grpc://localhost:7051'); + sinon.assert.calledOnce(HLFConnectionManager.createOrderer); + sinon.assert.calledWith(HLFConnectionManager.createOrderer, 'grpc://localhost:7050'); + connection.eventHubDefs[0].should.deep.equal({ + 'eventURL': 'grpc://localhost:7053', + 'opts': { + 'request-timeout': 123000 + } + }); + }); + }); + + it('should set message sizes to value specified', () => { + connectOptions.maxSendSize = 7; + connectOptions.maxRecvSize = 3; + return connectionManager.connect('hlfabric1', 'org.acme.biznet', connectOptions) + .then((connection) => { + sinon.assert.calledTwice(configSettingStub); + sinon.assert.calledWith(configSettingStub, 'grpc-max-send-message-length', 7 * 1024 * 1024); + sinon.assert.calledWith(configSettingStub, 'grpc-max-receive-message-length', 3 * 1024 * 1024); + }); + }); + + it('should set message sizes to -1 if -1 specified', () => { + connectOptions.maxSendSize = -1; + connectOptions.maxRecvSize = -1; + return connectionManager.connect('hlfabric1', 'org.acme.biznet', connectOptions) + .then((connection) => { + sinon.assert.calledTwice(configSettingStub); + sinon.assert.calledWith(configSettingStub, 'grpc-max-send-message-length', -1); + sinon.assert.calledWith(configSettingStub, 'grpc-max-receive-message-length', -1); + }); + }); + + it('should ignore a value of 0 for message size limits to leave as default', () => { + connectOptions.maxSendSize = 0; + connectOptions.maxRecvSize = 0; + return connectionManager.connect('hlfabric1', 'org.acme.biznet', connectOptions) + .then((connection) => { + configSettingStub.called.should.be.false; + }); + }); + + it('should ignore string values for message size limits', () => { + connectOptions.maxSendSize = '1'; + connectOptions.maxRecvSize = '2'; + return connectionManager.connect('hlfabric1', 'org.acme.biznet', connectOptions) + .then((connection) => { + configSettingStub.called.should.be.false; }); }); @@ -527,7 +733,7 @@ describe('HLFConnectionManager', () => { .then((connection) => { sinon.assert.calledOnce(HLFConnectionManager.createOrderer); sinon.assert.calledWith(HLFConnectionManager.createOrderer, 'grpc://localhost:7050'); - sinon.assert.calledOnce(mockChain.addOrderer); + sinon.assert.calledOnce(mockChannel.addOrderer); }); }); @@ -543,7 +749,7 @@ describe('HLFConnectionManager', () => { sinon.assert.calledWith(HLFConnectionManager.createOrderer, 'grpc://localhost:7050'); sinon.assert.calledWith(HLFConnectionManager.createOrderer, 'grpc://localhost:8050'); sinon.assert.calledWith(HLFConnectionManager.createOrderer, 'grpc://localhost:9050'); - sinon.assert.calledThrice(mockChain.addOrderer); + sinon.assert.calledThrice(mockChannel.addOrderer); }); }); @@ -552,7 +758,7 @@ describe('HLFConnectionManager', () => { .then((connection) => { sinon.assert.calledOnce(HLFConnectionManager.createPeer); sinon.assert.calledWith(HLFConnectionManager.createPeer, 'grpc://localhost:7051'); - sinon.assert.calledOnce(mockChain.addPeer); + sinon.assert.calledOnce(mockChannel.addPeer); }); }); @@ -568,36 +774,7 @@ describe('HLFConnectionManager', () => { sinon.assert.calledWith(HLFConnectionManager.createPeer, 'grpc://localhost:7051'); sinon.assert.calledWith(HLFConnectionManager.createPeer, 'grpc://localhost:8051'); sinon.assert.calledWith(HLFConnectionManager.createPeer, 'grpc://localhost:9051'); - sinon.assert.calledThrice(mockChain.addPeer); - }); - }); - - it('should connect a single event hub', () => { - return connectionManager.connect('hlfabric1', 'org.acme.biznet', connectOptions) - .then((connection) => { - sinon.assert.calledOnce(HLFConnectionManager.createEventHub); - sinon.assert.calledWith(mockEventHub.setPeerAddr, 'grpc://localhost:7053'); - sinon.assert.calledOnce(mockEventHub.connect); - }); - }); - - it('should ignore a disconnected event hub on process exit', () => { - mockEventHub.isconnected.returns(false); - return connectionManager.connect('hlfabric1', 'org.acme.biznet', connectOptions) - .then((connection) => { - sinon.assert.calledOnce(process.on); - sinon.assert.calledWith(process.on, 'exit'); - sinon.assert.notCalled(mockEventHub.disconnect); - }); - }); - - it('should disconnect a connected event hub on process exit', () => { - mockEventHub.isconnected.returns(true); - return connectionManager.connect('hlfabric1', 'org.acme.biznet', connectOptions) - .then((connection) => { - sinon.assert.calledOnce(process.on); - sinon.assert.calledWith(process.on, 'exit'); - sinon.assert.calledOnce(mockEventHub.disconnect); + sinon.assert.calledThrice(mockChannel.addPeer); }); }); @@ -605,7 +782,7 @@ describe('HLFConnectionManager', () => { return connectionManager.connect('hlfabric1', 'org.acme.biznet', connectOptions) .then((connection) => { sinon.assert.calledOnce(HLFConnectionManager.createCAClient); - sinon.assert.calledWith(HLFConnectionManager.createCAClient, 'http://localhost:7054', null, connectOptions.keyValStore); + sinon.assert.calledWith(HLFConnectionManager.createCAClient, 'http://localhost:7054', null, null); }); }); @@ -618,6 +795,7 @@ describe('HLFConnectionManager', () => { connectOptions.peers[0].hostnameOverride = 'peerOverride'; connectOptions.ca = { 'url': 'https://localhost:7054', + 'name': 'arealname', 'trustedRoots' : ['trusted'], 'verify': false }; @@ -628,28 +806,32 @@ describe('HLFConnectionManager', () => { connection.should.be.an.instanceOf(HLFConnection); connection.getConnectionOptions().should.deep.equal(connectOptions); connection.client.should.deep.equal(mockClient); - connection.chain.should.deep.equal(mockChain); - connection.eventHubs.should.deep.equal([mockEventHub]); + connection.channel.should.deep.equal(mockChannel); connection.caClient.should.deep.equal(mockCAClient); - sinon.assert.calledWith(mockClient.newChain, connectOptions.channel); + sinon.assert.calledWith(mockClient.newChannel, connectOptions.channel); sinon.assert.calledOnce(HLFConnectionManager.createCAClient); sinon.assert.calledWith(HLFConnectionManager.createCAClient, 'https://localhost:7054', { 'trustedRoots' : ['trusted'], 'verify': false - }, connectOptions.keyValStore); + }, 'arealname'); sinon.assert.calledOnce(HLFConnectionManager.createPeer); sinon.assert.calledWith(HLFConnectionManager.createPeer, 'grpc://localhost:7051', { + 'request-timeout': 123000, pem: embeddedCert, 'ssl-target-name-override': 'peerOverride' }); - sinon.assert.calledOnce(HLFConnectionManager.createEventHub); - sinon.assert.calledWith(mockEventHub.setPeerAddr, 'grpc://localhost:7053', { - pem: embeddedCert, - 'ssl-target-name-override': 'peerOverride' + connection.eventHubDefs[0].should.deep.equal({ + 'eventURL': 'grpc://localhost:7053', + 'opts': { + 'request-timeout': 123000, + pem: embeddedCert, + 'ssl-target-name-override': 'peerOverride' + } }); sinon.assert.calledOnce(HLFConnectionManager.createOrderer); sinon.assert.calledWith(HLFConnectionManager.createOrderer, 'grpcs://localhost:7051', { + 'request-timeout': 123000, pem: embeddedCert }); @@ -665,12 +847,21 @@ describe('HLFConnectionManager', () => { }); }); - it('should handle an error creating a default key value store', () => { + it('should handle an error creating a store using keyValStore', () => { Client.newDefaultKeyValueStore.rejects('wow such fail'); return connectionManager.connect('hlfabric1', 'org.acme.biznet', connectOptions) .should.be.rejectedWith(/wow such fail/); }); + it('should handle an error creating a store using a wallet', () => { + delete connectOptions.keyValStore; + connectOptions.wallet = {}; + sandbox.stub(Client, 'newCryptoKeyStore').throws('wow such fail'); + + return connectionManager.connect('hlfabric1', 'org.acme.biznet', connectOptions) + .should.be.rejectedWith(/wow such fail/); + }); + it('should configure a wallet proxy using the specified wallet if provided', () => { connectOptions = Object.assign(connectOptions, { wallet: mockWallet }); return connectionManager.connect('hlfabric1', 'org.acme.biznet', connectOptions) @@ -679,7 +870,6 @@ describe('HLFConnectionManager', () => { }); }); - it('should configure a wallet proxy if a singleton wallet is provided', () => { Wallet.setWallet(mockWallet); return connectionManager.connect('hlfabric1', 'org.acme.biznet', connectOptions) @@ -688,36 +878,154 @@ describe('HLFConnectionManager', () => { }); }); - it('should set a default deploy wait time', () => { + it('should set a default timeout', () => { + delete connectOptions.timeout; return connectionManager.connect('hlfabric1', 'org.acme.biznet', connectOptions) .then((connection) => { - connection.getConnectionOptions().deployWaitTime.should.equal(60); + connection.getConnectionOptions().timeout.should.equal(180); }); }); - it('should use a supplied deploy wait time', () => { - connectOptions.deployWaitTime = 30; + it('should use a supplied timeout', () => { + connectOptions.timeout = 30; return connectionManager.connect('hlfabric1', 'org.acme.biznet', connectOptions) .then((connection) => { - connection.getConnectionOptions().deployWaitTime.should.equal(30); + connection.getConnectionOptions().timeout.should.equal(30); }); }); + }); - it('should set a default invoke wait time', () => { - return connectionManager.connect('hlfabric1', 'org.acme.biznet', connectOptions) - .then((connection) => { - connection.getConnectionOptions().invokeWaitTime.should.equal(60); - }); + describe('#importIdentity', () => { + let mockClient, mockKeyValueStore, profile, mockCryptoSuite; + beforeEach(() => { + mockClient = sinon.createStubInstance(Client); + sandbox.stub(HLFConnectionManager, 'createClient').returns(mockClient); + mockKeyValueStore = sinon.createStubInstance(KeyValueStore); + mockCryptoSuite = sinon.createStubInstance(CryptoSuite); + mockCryptoSuite.setCryptoKeyStore = sinon.stub(); + sandbox.stub(Client, 'newDefaultKeyValueStore').resolves(mockKeyValueStore); + sandbox.stub(Client, 'newCryptoSuite').returns(mockCryptoSuite); + + profile = { + orderers: [ + 'grpc://localhost:7050' + ], + peers: [ + { + requestURL: 'grpc://localhost:7051', + eventURL: 'grpc://localhost:7053' + } + ], + ca: 'http://localhost:7054', + keyValStore: '/tmp/hlfabric1', + channel: 'testchainid', + timeout: 123, + mspID: 'MSP1Org' + }; }); - it('should use a supplied invoke wait time', () => { - connectOptions.invokeWaitTime = 30; - return connectionManager.connect('hlfabric1', 'org.acme.biznet', connectOptions) - .then((connection) => { - connection.getConnectionOptions().invokeWaitTime.should.equal(30); + it('should successfully import an identity', () => { + return connectionManager.importIdentity(profile, 'anid', 'acert', 'akey') + .then(() => { + sinon.assert.calledOnce(Client.newDefaultKeyValueStore); + sinon.assert.calledWith(Client.newDefaultKeyValueStore, { path: '/tmp/hlfabric1' }); + sinon.assert.calledOnce(mockClient.setStateStore); + sinon.assert.calledWith(mockClient.setStateStore, mockKeyValueStore); + sinon.assert.calledOnce(Client.newCryptoSuite); + sinon.assert.calledOnce(mockClient.setCryptoSuite); +// sinon.assert.calledWith(mockClient.newCryptoSuite, null, null, { path: '/tmp/hlfabric1' }); + sinon.assert.calledOnce(mockClient.createUser); + sinon.assert.calledWith(mockClient.createUser, { + username: 'anid', + mspid: 'MSP1Org', + cryptoContent: { + privateKeyPEM: 'akey', + signedCertPEM: 'acert' + } + }); }); }); + it('should throw if profileDefinition not specified', () => { + (() => { + connectionManager.importIdentity(); + }).should.throw(/profileDefinition not specified or not an object/); + }); + + it('should throw if profileDefinition not an object', () => { + (() => { + connectionManager.importIdentity('hlfabric1'); + }).should.throw(/profileDefinition not specified or not an object/); + }); + + it('should throw if id not specified', () => { + (() => { + connectionManager.importIdentity(profile); + }).should.throw(/id not specified or not a string/); + }); + + it('should throw if id not a string', () => { + (() => { + connectionManager.importIdentity(profile, []); + }).should.throw(/id not specified or not a string/); + }); + + it('should throw if publicKey not specified', () => { + (() => { + connectionManager.importIdentity(profile, 'anid'); + }).should.throw(/publicKey not specified or not a string/); + }); + + it('should throw if publicKey not a string', () => { + (() => { + connectionManager.importIdentity(profile, 'anid', []); + }).should.throw(/publicKey not specified or not a string/); + }); + it('should throw if key not specified', () => { + (() => { + connectionManager.importIdentity(profile, 'anid', 'acert'); + }).should.throw(/privateKey not specified or not a string/); + }); + + it('should throw if key not a string', () => { + (() => { + connectionManager.importIdentity(profile, 'anid', 'acert', 5); + }).should.throw(/privateKey not specified or not a string/); + }); + + + it('should throw if msp id is not specified', () => { + delete profile.mspID; + (() => { + connectionManager.importIdentity(profile, 'anid', 'acert', 'akey'); + }).should.throw(/No msp id defined/); + }); + + it('should throw if no keyValStore or wallet is not specified', () => { + delete profile.keyValStore; + (() => { + connectionManager.importIdentity(profile, 'anid', 'acert', 'akey'); + }).should.throw(/No key value store directory or wallet has been specified/); + }); + + it('should handle an error creating a default key value store', () => { + Client.newDefaultKeyValueStore.rejects('wow such fail'); + return connectionManager.importIdentity(profile, 'anid', 'acert', 'akey') + .should.be.rejectedWith(/wow such fail/); + }); + + it('should handle an error creating a new cryptosuite', () => { + mockClient.setCryptoSuite.throws(new Error('another fail')); + return connectionManager.importIdentity(profile, 'anid', 'acert', 'akey') + .should.be.rejectedWith(/another fail/); + }); + + it('should handle an error creating a user', () => { + mockClient.createUser.rejects('wow such fail'); + return connectionManager.importIdentity(profile, 'anid', 'acert', 'akey') + .should.be.rejectedWith(/wow such fail/); + }); + }); }); diff --git a/packages/composer-connector-hlfv1/test/hlfwalletproxy.js b/packages/composer-connector-hlfv1/test/hlfwalletproxy.js index b8bee41555..33bf427cea 100644 --- a/packages/composer-connector-hlfv1/test/hlfwalletproxy.js +++ b/packages/composer-connector-hlfv1/test/hlfwalletproxy.js @@ -30,7 +30,10 @@ describe('HLFWalletProxy', () => { beforeEach(() => { mockWallet = sinon.createStubInstance(Wallet); - walletProxy = new HLFWalletProxy(mockWallet); + return new HLFWalletProxy(mockWallet) + .then((proxy) => { + walletProxy = proxy; + }); }); describe('#extractEnrollmentID', () => { diff --git a/packages/composer-playground/src/app/add-connection-profile/add-connection-profile.component.spec.ts b/packages/composer-playground/src/app/add-connection-profile/add-connection-profile.component.spec.ts index c91e983cf5..07ac8f6a4f 100644 --- a/packages/composer-playground/src/app/add-connection-profile/add-connection-profile.component.spec.ts +++ b/packages/composer-playground/src/app/add-connection-profile/add-connection-profile.component.spec.ts @@ -109,6 +109,7 @@ describe('AddConnectionProfileComponent', () => { const KEY_VAL_STORE = 'kvs'; const DEPLOY_TIME = 100; const WAIT_TIME = 999; + const TIMEOUT = 134; const CERT = 'cert'; const CERT_PATH = 'certpath'; const PEERS = ['peers']; @@ -291,8 +292,7 @@ describe('AddConnectionProfileComponent', () => { ca: CA, peers: PEERS, keyValStore: KEY_VAL_STORE, - deployWaitTime: DEPLOY_TIME, - invokeWaitTime: WAIT_TIME, + timeout: TIMEOUT, channel: CHANNEL, mspID: MSPID, }; @@ -339,8 +339,7 @@ describe('AddConnectionProfileComponent', () => { component['addConnectionProfileKeyValStore'].should.equal(KEY_VAL_STORE); component['addConnectionProfileChannel'].should.equal(CHANNEL); component['addConnectionProfileMspId'].should.equal(MSPID); - component['addConnectionProfileDeployWaitTime'].should.equal(DEPLOY_TIME); - component['addConnectionProfileInvokeWaitTime'].should.equal(WAIT_TIME); + component['addConnectionProfileTimeout'].should.equal(TIMEOUT); addMock.should.have.been.called; })); @@ -447,7 +446,7 @@ describe('AddConnectionProfileComponent', () => { describe('#setV1Defaults', () => { - it('should create a new profile and set defaults for a v1 fabric', fakeAsync(() => { + it('should create a new profile and set defaults for a v1 fabric 1', fakeAsync(() => { let mockUpdate = sandbox.stub(component, 'updateConnectionProfiles').returns(Promise.resolve()); component['connectionProfiles'] = []; component.setV1Defaults(); @@ -457,7 +456,7 @@ describe('AddConnectionProfileComponent', () => { verifyUnchangeableDefaults(); })); - it('should create a new profile and set defaults for a v1 fabric', fakeAsync(() => { + it('should create a new profile and set defaults for a v1 fabric 2', fakeAsync(() => { let mockUpdate = sandbox.stub(component, 'updateConnectionProfiles').returns(Promise.resolve()); component['connectionProfiles'] = [{name: 'New Connection Profile'}]; component.setV1Defaults(); @@ -475,7 +474,8 @@ describe('AddConnectionProfileComponent', () => { hostnameOverride: '' }]); - component['addConnectionProfileCertificateAuthority'].should.equal('http://localhost:7054'); + component['addConnectionProfileCertificateAuthority'].url.should.equal('http://localhost:7054'); + component['addConnectionProfileCertificateAuthority'].name.should.equal(''); component['addConnectionProfilePeers'].should.deep.equal([{ requestURL: 'grpc://localhost:7051', eventURL: 'grpc://localhost:7053', @@ -485,8 +485,7 @@ describe('AddConnectionProfileComponent', () => { component['addConnectionProfileKeyValStore'].should.equal('/tmp/keyValStore'); component['addConnectionProfileChannel'].should.equal('mychannel'); component['addConnectionProfileMspId'].should.equal('Org1MSP'); - component['addConnectionProfileDeployWaitTime'].should.equal(300); - component['addConnectionProfileInvokeWaitTime'].should.equal(30); + component['addConnectionProfileTimeout'].should.equal(300); } }); @@ -527,6 +526,7 @@ describe('AddConnectionProfileComponent', () => { component['addConnectionProfilePeers'] = PEERS; component['addConnectionProfileChannel'] = CHANNEL; component['addConnectionProfileMspId'] = MSPID; + component['addConnectionProfileTimeout'] = TIMEOUT; })); it('should deal with the version being 0.6 and a certificate', fakeAsync(() => { @@ -561,9 +561,8 @@ describe('AddConnectionProfileComponent', () => { profile: { ca: CA, channel: CHANNEL, - deployWaitTime: DEPLOY_TIME, + timeout: TIMEOUT, description: DESC, - invokeWaitTime: WAIT_TIME, keyValStore: KEY_VAL_STORE, mspID: MSPID, orderers: [{url: 'orderers', cert: '', hostnameOverride: ''}], diff --git a/packages/composer-playground/src/app/add-connection-profile/add-connection-profile.component.ts b/packages/composer-playground/src/app/add-connection-profile/add-connection-profile.component.ts index f77c77ba4e..27ad551853 100644 --- a/packages/composer-playground/src/app/add-connection-profile/add-connection-profile.component.ts +++ b/packages/composer-playground/src/app/add-connection-profile/add-connection-profile.component.ts @@ -36,13 +36,14 @@ export class AddConnectionProfileComponent { private addConnectionProfileKeyValStore: string = null; private addConnectionProfileDeployWaitTime: number = null; private addConnectionProfileInvokeWaitTime: number = null; + private addConnectionProfileTimeout: number = null; private addConnectionProfileCertificate: string = null; private addConnectionProfileCertificatePath: string = null; // V1 attributes private addConnectionProfileOrderers: any[] = null; private addConnectionProfilePeers: any[] = null; - private addConnectionProfileCertificateAuthority: string = null; + private addConnectionProfileCertificateAuthority: any = null; private addConnectionProfileChannel: string = null; private addConnectionProfileMspId: string = null; @@ -132,8 +133,7 @@ export class AddConnectionProfileComponent { this.addConnectionProfileKeyValStore = profileData.keyValStore; this.addConnectionProfileChannel = profileData.channel; this.addConnectionProfileMspId = profileData.mspID; - this.addConnectionProfileDeployWaitTime = profileData.deployWaitTime; - this.addConnectionProfileInvokeWaitTime = profileData.invokeWaitTime; + this.addConnectionProfileTimeout = profileData.timeout; this.addConnectionProfile(); }); } else { @@ -176,8 +176,7 @@ export class AddConnectionProfileComponent { keyValStore: this.addConnectionProfileKeyValStore, channel: this.addConnectionProfileChannel, mspID: this.addConnectionProfileMspId, - deployWaitTime: this.addConnectionProfileDeployWaitTime, - invokeWaitTime: this.addConnectionProfileInvokeWaitTime + timeout: this.addConnectionProfileTimeout, }; }); } else { @@ -233,8 +232,7 @@ export class AddConnectionProfileComponent { keyValStore: this.addConnectionProfileKeyValStore, channel: this.addConnectionProfileChannel, mspID: this.addConnectionProfileMspId, - deployWaitTime: this.addConnectionProfileDeployWaitTime, - invokeWaitTime: this.addConnectionProfileInvokeWaitTime + timeout: this.addConnectionProfileTimeout }; } else { throw new Error('Unknown connection profile version selected'); @@ -298,7 +296,10 @@ export class AddConnectionProfileComponent { hostnameOverride: '' }]; - this.addConnectionProfileCertificateAuthority = 'http://localhost:7054'; + this.addConnectionProfileCertificateAuthority = { + url: 'http://localhost:7054', + name: '' + }; this.addConnectionProfilePeers = [{ requestURL: 'grpc://localhost:7051', eventURL: 'grpc://localhost:7053', @@ -308,8 +309,7 @@ export class AddConnectionProfileComponent { this.addConnectionProfileKeyValStore = '/tmp/keyValStore'; this.addConnectionProfileChannel = 'mychannel'; this.addConnectionProfileMspId = 'Org1MSP'; - this.addConnectionProfileDeployWaitTime = 5 * 60; - this.addConnectionProfileInvokeWaitTime = 30; + this.addConnectionProfileTimeout = 5 * 60; }); } diff --git a/packages/composer-playground/src/app/connection-profile-data/connection-profile-data.component.html b/packages/composer-playground/src/app/connection-profile-data/connection-profile-data.component.html index 74f7a7e66a..c5927e1206 100644 --- a/packages/composer-playground/src/app/connection-profile-data/connection-profile-data.component.html +++ b/packages/composer-playground/src/app/connection-profile-data/connection-profile-data.component.html @@ -222,7 +222,16 @@

Web Browser

Certificate Authority (CA)
- {{connectionProfileData.profile.ca}} +
+
+
URL
+
{{connectionProfileData.profile.ca.url}}
+
+
+
Name
+
{{connectionProfileData.profile.ca.name}}
+
+
@@ -273,18 +282,10 @@

Web Browser

- Deploy Wait Time (seconds) -
-
- {{connectionProfileData.profile.deployWaitTime}} -
-
-
-
- Invoke Wait Time (seconds) + Timeout (seconds)
- {{connectionProfileData.profile.invokeWaitTime}} + {{connectionProfileData.profile.timeout}}
@@ -516,10 +517,27 @@

Web Browser

Certificate Authority (CA)*
- -
- {{ v1FormErrors.ca }} -
+
+
+
+ CA URL* +
+
+ +
+ {{ v1FormErrors.ca.url}} +
+
+
+
+
+ CA Name +
+
+ +
+
+
@@ -534,7 +552,7 @@

Web Browser

Peer Request URL*
- +
{{ v1FormErrors.peers.requestURL }}
@@ -589,28 +607,15 @@

Web Browser

Advanced
- Deploy Wait Time (seconds) -
-
- -
- {{ v1FormErrors.deployWaitTime }} -
-
-
- -
-
- Invoke Wait Time (seconds) + Time out (seconds)
- -
- {{ v1FormErrors.invokeWaitTime }} + +
+ {{ v1FormErrors.timeout }}
-