From 3193e1f7183154a5d61fc4cbe91165994aff0fc0 Mon Sep 17 00:00:00 2001 From: Dave Kelsey Date: Tue, 18 Jul 2017 23:09:59 +0100 Subject: [PATCH] Runtime code for install and start (#1583) * Runtime code for install and start Signed-off-by: Dave Kelsey * comment improvements, fix system tests when not real fabric Signed-off-by: Dave Kelsey * up version to 0.10.0. Correct captilisation Signed-off-by: Dave Kelsey * add install/start support to all connectors in prep for playground Signed-off-by: Dave Kelsey * system tests can all try install/start Signed-off-by: Dave Kelsey * resolve merge conflict Signed-off-by: Dave Kelsey * missed tests for connection.js Signed-off-by: Dave Kelsey * added more tests as coverage has started failing on CLI Signed-off-by: Dave Kelsey --- packages/composer-admin/api.txt | 2 + packages/composer-admin/changelog.txt | 3 + .../composer-admin/lib/adminconnection.js | 52 ++ .../composer-admin/test/adminconnection.js | 67 +++ .../lib/cmds/network/lib/start.js | 124 +++++ .../lib/cmds/network/startCommand.js | 42 ++ packages/composer-cli/lib/cmds/runtime.js | 25 + .../lib/cmds/runtime/installCommand.js | 41 ++ .../lib/cmds/runtime/lib/install.js | 68 +++ packages/composer-cli/test/network/deploy.js | 12 +- packages/composer-cli/test/network/start.js | 282 +++++++++++ packages/composer-cli/test/runtime/install.js | 84 ++++ packages/composer-common/lib/connection.js | 26 + packages/composer-common/test/connection.js | 20 + .../lib/embeddedconnection.js | 28 +- .../test/embeddedconnection.js | 24 +- .../lib/hfcconnection.js | 39 +- .../test/hfcconnection.js | 31 +- .../lib/hlfconnection.js | 66 +-- .../test/hlfconnection.js | 476 +++++++++++++++++- .../lib/proxyconnection.js | 41 ++ .../test/proxyconnection.js | 47 ++ .../lib/connectorserver.js | 84 ++++ .../test/connectorserver.js | 126 +++++ .../lib/webconnection.js | 28 +- .../test/webconnection.js | 25 +- packages/composer-systests/systest/assets.js | 7 +- .../composer-systests/systest/identities.js | 7 +- .../systest/transactions.assets.js | 7 +- 29 files changed, 1824 insertions(+), 60 deletions(-) create mode 100644 packages/composer-cli/lib/cmds/network/lib/start.js create mode 100644 packages/composer-cli/lib/cmds/network/startCommand.js create mode 100644 packages/composer-cli/lib/cmds/runtime.js create mode 100644 packages/composer-cli/lib/cmds/runtime/installCommand.js create mode 100644 packages/composer-cli/lib/cmds/runtime/lib/install.js create mode 100644 packages/composer-cli/test/network/start.js create mode 100644 packages/composer-cli/test/runtime/install.js diff --git a/packages/composer-admin/api.txt b/packages/composer-admin/api.txt index fe8570fed7..4202201319 100644 --- a/packages/composer-admin/api.txt +++ b/packages/composer-admin/api.txt @@ -6,6 +6,8 @@ class AdminConnection { + Promise getProfile(string) + Promise getAllProfiles() + Promise disconnect() + + Promise install(BusinessNetworkIdentifier,Object) + + Promise start(BusinessNetworkDefinition,Object) + Promise deploy(BusinessNetworkDefinition,Object) + Promise undeploy(string) + Promise update(BusinessNetworkDefinition) diff --git a/packages/composer-admin/changelog.txt b/packages/composer-admin/changelog.txt index 1f3b416658..4b58abc9da 100644 --- a/packages/composer-admin/changelog.txt +++ b/packages/composer-admin/changelog.txt @@ -11,6 +11,9 @@ # # Note that the latest public API is documented using JSDocs and is available in api.txt. # +Version 0.10.0 {3af411e8da53bb013ab9718ed5980c20} 2017-07-17 +- added install and start method. + Version 0.9.1 {8b6c392e59b8ad38ea271315231ca0e5} 2017-06-30 - added getLogLevel & setLogLevel methods. added deployOptions to deploy method. diff --git a/packages/composer-admin/lib/adminconnection.js b/packages/composer-admin/lib/adminconnection.js index ae5e3fe3b5..3321674139 100644 --- a/packages/composer-admin/lib/adminconnection.js +++ b/packages/composer-admin/lib/adminconnection.js @@ -207,6 +207,58 @@ class AdminConnection { }); } + /** + * Installs the Hyperledger Composer runtime to the Hyperledger Fabric in preparation + * for the business network to be started. The connection mustbe connected for this method to succeed. + * You must pass the name of the business network that is defined in your archive that this + * runtime will be started with. + * @example + * // Install the Hyperledger Composer runtime + * var adminConnection = new AdminConnection(); + * var businessNetworkDefinition = BusinessNetworkDefinition.fromArchive(myArchive); + * return adminConnection.install(businessNetworkDefinition.getName()) + * .then(function(){ + * // Business network definition installed + * }) + * .catch(function(error){ + * // Add optional error handling here. + * }); + * @param {BusinessNetworkIdentifier} businessNetworkIdentifier - The name of business network which will be used to start this runtime. + * @param {Object} installOptions connector specific install options + * @return {Promise} A promise that will be fufilled when the business network has been + * deployed. + */ + install(businessNetworkIdentifier, installOptions) { + Util.securityCheck(this.securityContext); + return this.connection.install(this.securityContext, businessNetworkIdentifier, installOptions); + } + + /** + * Starts a business network within the runtime previously installed to the Hyperledger Fabric with + * the same name as the business network to be started. The connection must be connected for this + * method to succeed. + * @example + * // Start a Business Network Definition + * var adminConnection = new AdminConnection(); + * var businessNetworkDefinition = BusinessNetworkDefinition.fromArchive(myArchive); + * return adminConnection.start(businessNetworkDefinition) + * .then(function(){ + * // Business network definition is started + * }) + * .catch(function(error){ + * // Add optional error handling here. + * }); + * @param {BusinessNetworkDefinition} businessNetworkDefinition - The business network to start + * @param {Object} startOptions connector specific start options + * @return {Promise} A promise that will be fufilled when the business network has been + * deployed. + */ + start(businessNetworkDefinition, startOptions) { + Util.securityCheck(this.securityContext); + return this.connection.start(this.securityContext, businessNetworkDefinition, startOptions); + } + + /** * Deploys a new BusinessNetworkDefinition to the Hyperledger Fabric. The connection must * be connected for this method to succeed. diff --git a/packages/composer-admin/test/adminconnection.js b/packages/composer-admin/test/adminconnection.js index bf31c48668..4fae3d9433 100644 --- a/packages/composer-admin/test/adminconnection.js +++ b/packages/composer-admin/test/adminconnection.js @@ -65,6 +65,8 @@ describe('AdminConnection', () => { mockConnection.disconnect.resolves(); mockConnection.login.resolves(mockSecurityContext); mockConnection.deploy.resolves(); + mockConnection.install.resolves(); + mockConnection.start.resolves(); mockConnection.ping.resolves(); mockConnection.queryChainCode.resolves(); mockConnection.invokeChainCode.resolves(); @@ -200,6 +202,59 @@ describe('AdminConnection', () => { }); }); + describe('#install', () => { + + it('should be able to install a business network definition', () => { + adminConnection.connection = mockConnection; + adminConnection.securityContext = mockSecurityContext; + let businessNetworkDefinition = new BusinessNetworkDefinition('name@1.0.0'); + return adminConnection.install(businessNetworkDefinition) + .then(() => { + sinon.assert.calledOnce(mockConnection.install); + sinon.assert.calledWith(mockConnection.install, mockSecurityContext, businessNetworkDefinition); + }); + }); + + it('should be able to install a business network definition with install options', () => { + adminConnection.connection = mockConnection; + adminConnection.securityContext = mockSecurityContext; + let businessNetworkDefinition = new BusinessNetworkDefinition('name@1.0.0'); + return adminConnection.install(businessNetworkDefinition, {opt: 1}) + .then(() => { + sinon.assert.calledOnce(mockConnection.install); + sinon.assert.calledWith(mockConnection.install, mockSecurityContext, businessNetworkDefinition, {opt: 1}); + }); + }); + + }); + + describe('#start', () => { + + it('should be able to start a business network definition', () => { + adminConnection.connection = mockConnection; + adminConnection.securityContext = mockSecurityContext; + let businessNetworkDefinition = new BusinessNetworkDefinition('name@1.0.0'); + return adminConnection.start(businessNetworkDefinition) + .then(() => { + sinon.assert.calledOnce(mockConnection.start); + sinon.assert.calledWith(mockConnection.start, mockSecurityContext, businessNetworkDefinition); + }); + }); + + it('should be able to start a business network definition with start options', () => { + adminConnection.connection = mockConnection; + adminConnection.securityContext = mockSecurityContext; + let businessNetworkDefinition = new BusinessNetworkDefinition('name@1.0.0'); + return adminConnection.start(businessNetworkDefinition, {opt: 1}) + .then(() => { + sinon.assert.calledOnce(mockConnection.start); + sinon.assert.calledWith(mockConnection.start, mockSecurityContext, businessNetworkDefinition, {opt: 1}); + }); + }); + + }); + + describe('#deploy', () => { it('should be able to deploy a business network definition', () => { @@ -212,6 +267,18 @@ describe('AdminConnection', () => { sinon.assert.calledWith(mockConnection.deploy, mockSecurityContext, businessNetworkDefinition); }); }); + + it('should be able to deploy a business network definition with deployOptions', () => { + adminConnection.connection = mockConnection; + adminConnection.securityContext = mockSecurityContext; + let businessNetworkDefinition = new BusinessNetworkDefinition('name@1.0.0'); + return adminConnection.deploy(businessNetworkDefinition, {opt: 1}) + .then(() => { + sinon.assert.calledOnce(mockConnection.deploy); + sinon.assert.calledWith(mockConnection.deploy, mockSecurityContext, businessNetworkDefinition, {opt: 1}); + }); + }); + }); describe('#undeploy', () => { diff --git a/packages/composer-cli/lib/cmds/network/lib/start.js b/packages/composer-cli/lib/cmds/network/lib/start.js new file mode 100644 index 0000000000..ee89e3b858 --- /dev/null +++ b/packages/composer-cli/lib/cmds/network/lib/start.js @@ -0,0 +1,124 @@ +/* + * 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 Admin = require('composer-admin'); +const BusinessNetworkDefinition = Admin.BusinessNetworkDefinition; +const cmdUtil = require('../../utils/cmdutils'); +const fs = require('fs'); + +const ora = require('ora'); +const chalk = require('chalk'); +const LogLevel = require('../../network/lib/loglevel'); + + +/** + *

+ * Composer deploy command + *

+ *

+ * @private + */ +class Start { + + /** + * Command process for deploy command + * @param {string} argv argument list from composer command + * @param {boolean} updateOption true if the network is to be updated + * @return {Promise} promise when command complete + */ + static handler(argv, updateOption) { + + let updateBusinessNetwork = (updateOption === true) + ? true + : false; + let businessNetworkDefinition; + + let adminConnection; + let businessNetworkName; + let spinner; + let loglevel; + + if (argv.loglevel) { + // validate log level as yargs cannot at this time + // https://github.com/yargs/yargs/issues/849 + loglevel = argv.loglevel.toUpperCase(); + if (!LogLevel.validLogLevel(loglevel)) { + return Promise.reject(new Error('loglevel unspecified or not one of (INFO|WARNING|ERROR|DEBUG)')); + } + } + + return (() => { + console.log(chalk.blue.bold('Starting business network from archive: ')+argv.archiveFile); + let archiveFileContents = null; + // Read archive file contents + archiveFileContents = Start.getArchiveFileContents(argv.archiveFile); + return BusinessNetworkDefinition.fromArchive(archiveFileContents); + })() + .then ((result) => { + businessNetworkDefinition = result; + businessNetworkName = businessNetworkDefinition.getIdentifier(); + console.log(chalk.blue.bold('Business network definition:')); + console.log(chalk.blue('\tIdentifier: ')+businessNetworkName); + console.log(chalk.blue('\tDescription: ')+businessNetworkDefinition.getDescription()); + console.log(); + adminConnection = cmdUtil.createAdminConnection(); + return adminConnection.connect(argv.connectionProfileName, argv.startId, argv.startSecret, updateBusinessNetwork ? businessNetworkDefinition.getName() : null); + }) + .then((result) => { + if (updateBusinessNetwork === false) { + spinner = ora('Starting business network definition. This may take a minute...').start(); + let startOptions = cmdUtil.parseOptions(argv); + if (loglevel) { + startOptions.logLevel = loglevel; + } + return adminConnection.start(businessNetworkDefinition, startOptions); + } else { + spinner = ora('Updating business network definition. This may take a few seconds...').start(); + return adminConnection.update(businessNetworkDefinition); + } + }).then((result) => { + spinner.succeed(); + console.log(); + + return result; + }).catch((error) => { + + if (spinner) { + spinner.fail(); + } + + console.log(); + + throw error; + }); + } + + /** + * Get contents from archive file + * @param {string} archiveFile connection profile name + * @return {String} archiveFileContents archive file contents + */ + static getArchiveFileContents(archiveFile) { + let archiveFileContents; + if (fs.existsSync(archiveFile)) { + archiveFileContents = fs.readFileSync(archiveFile); + } else { + throw new Error('Archive file '+archiveFile+' does not exist.'); + } + return archiveFileContents; + } +} +module.exports = Start; diff --git a/packages/composer-cli/lib/cmds/network/startCommand.js b/packages/composer-cli/lib/cmds/network/startCommand.js new file mode 100644 index 0000000000..75803e62a1 --- /dev/null +++ b/packages/composer-cli/lib/cmds/network/startCommand.js @@ -0,0 +1,42 @@ +/* + * 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 Start = require ('./lib/start.js'); + +module.exports.command = 'start [options]'; +module.exports.describe = 'Starts a business network'; +module.exports.builder = { + archiveFile: {alias: 'a', required: true, describe: 'The business network archive file name', type: 'string' }, + connectionProfileName: {alias: 'p', optional: true, describe: 'The connection profile name', type: 'string' }, + loglevel: { alias: 'l', required: false, describe: 'The initial loglevel to set (INFO|WARNING|ERROR|DEBUG)', type: 'string' }, + option: { alias: 'o', required: false, describe: 'Options that are specific specific to connection. Multiple options are specified by repeating this option', type: 'string' }, + optionsFile: { alias: 'O', required: false, describe: 'A file containing options that are specific to connection', type: 'string' }, + startId: { alias: 'i', required: true, describe: 'The id of the user permitted to start a network', type: 'string' }, + startSecret: { alias: 's', required: false, describe: 'The secret of the user permitted to start a network, if required', type: 'string' } +}; + +module.exports.handler = (argv) => { + argv.thePromise = Start.handler(argv) + .then(() => { + return; + }) + .catch((error) => { + throw error; + + }); + + return argv.thePromise; +}; diff --git a/packages/composer-cli/lib/cmds/runtime.js b/packages/composer-cli/lib/cmds/runtime.js new file mode 100644 index 0000000000..0037e89aa1 --- /dev/null +++ b/packages/composer-cli/lib/cmds/runtime.js @@ -0,0 +1,25 @@ +/* + * 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'; + +exports.command = 'runtime '; +exports.desc = 'Composer runtime command'; +exports.builder = function (yargs) { + // apply commands in subdirectories + return yargs.commandDir('runtime'); +}; +exports.handler = function (argv) { + +}; diff --git a/packages/composer-cli/lib/cmds/runtime/installCommand.js b/packages/composer-cli/lib/cmds/runtime/installCommand.js new file mode 100644 index 0000000000..0975ae3c50 --- /dev/null +++ b/packages/composer-cli/lib/cmds/runtime/installCommand.js @@ -0,0 +1,41 @@ +/* + * 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 Install = require ('./lib/install.js'); + +module.exports.command = 'install [options]'; +module.exports.describe = 'Installs a business network to the Hyperledger Fabric'; +module.exports.builder = { + businessNetworkName: {alias: 'n', required: true, describe: 'The business network name', type: 'string' }, + connectionProfileName: {alias: 'p', required: true, describe: 'The connection profile name', type: 'string' }, + option: { alias: 'o', required: false, describe: 'Options that are specific specific to connection. Multiple options are specified by repeating this option', type: 'string' }, + optionsFile: { alias: 'O', required: false, describe: 'A file containing options that are specific to connection', type: 'string' }, + installId: { alias: 'i', required: true, describe: 'The id of the user permitted to install the runtime', type: 'string' }, + installSecret: { alias: 's', required: false, describe: 'The secret of the user permitted to install the runtime, if required', type: 'string' } +}; + +module.exports.handler = (argv) => { + argv.thePromise = Install.handler(argv) + .then(() => { + return; + }) + .catch((error) => { + throw error; + + }); + + return argv.thePromise; +}; diff --git a/packages/composer-cli/lib/cmds/runtime/lib/install.js b/packages/composer-cli/lib/cmds/runtime/lib/install.js new file mode 100644 index 0000000000..fdfbd850fd --- /dev/null +++ b/packages/composer-cli/lib/cmds/runtime/lib/install.js @@ -0,0 +1,68 @@ +/* + * 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 cmdUtil = require('../../utils/cmdutils'); + +const ora = require('ora'); + + +/** + *

+ * Composer deploy command + *

+ *

+ * @private + */ +class Install { + + /** + * Command process for deploy command + * @param {string} argv argument list from composer command + * @param {boolean} updateOption true if the network is to be updated + * @return {Promise} promise when command complete + */ + static handler(argv, updateOption) { + + let adminConnection; + let spinner; + + return (() => { + spinner = ora('Installing runtime for business network ' + argv.businessNetworkName + '. This may take a minute...').start(); + adminConnection = cmdUtil.createAdminConnection(); + return adminConnection.connect(argv.connectionProfileName, argv.installId, argv.installSecret, null); + })() + .then((result) => { + let installOptions = cmdUtil.parseOptions(argv); + return adminConnection.install(argv.businessNetworkName, installOptions); + }).then((result) => { + spinner.succeed(); + console.log(); + + return result; + }).catch((error) => { + + if (spinner) { + spinner.fail(); + } + + console.log(); + + throw error; + }); + } +} + +module.exports = Install; diff --git a/packages/composer-cli/test/network/deploy.js b/packages/composer-cli/test/network/deploy.js index 303220a5b1..9670b7d517 100644 --- a/packages/composer-cli/test/network/deploy.js +++ b/packages/composer-cli/test/network/deploy.js @@ -61,7 +61,7 @@ describe('composer deploy network CLI unit tests', function () { mockAdminConnection.connect.resolves(); mockAdminConnection.deploy.resolves(); - sandbox.stub(BusinessNetworkDefinition, 'fromArchive').returns(mockBusinessNetworkDefinition); + sandbox.stub(BusinessNetworkDefinition, 'fromArchive').resolves(mockBusinessNetworkDefinition); sandbox.stub(CmdUtil, 'createAdminConnection').returns(mockAdminConnection); sandbox.stub(process, 'exit'); }); @@ -370,6 +370,16 @@ describe('composer deploy network CLI unit tests', function () { sinon.assert.calledWith(mockAdminConnection.deploy, mockBusinessNetworkDefinition); }); }); + + it('show throw an error if loglevel not valid', function() { + let argv = {enrollId: 'WebAppAdmin' + ,enrollSecret: 'DJY27pEnl16d' + ,loglevel: 'BAD' + ,archiveFile: 'testArchiveFile.zip'}; + return Deploy.handler(argv) + .should.be.rejectedWith(/or not one of/); + + }); }); describe('Deploy getConnectOption() method tests', function () { diff --git a/packages/composer-cli/test/network/start.js b/packages/composer-cli/test/network/start.js new file mode 100644 index 0000000000..1c0ae43c8d --- /dev/null +++ b/packages/composer-cli/test/network/start.js @@ -0,0 +1,282 @@ +/* + * 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 Admin = require('composer-admin'); +const BusinessNetworkDefinition = Admin.BusinessNetworkDefinition; +const fs = require('fs'); +const Start = require('../../lib/cmds/network/lib/start.js'); +const StartCmd = require('../../lib/cmds/network/startCommand.js'); +const CmdUtil = require('../../lib/cmds/utils/cmdutils.js'); + +require('chai').should(); + +const chai = require('chai'); +const sinon = require('sinon'); +require('sinon-as-promised'); +chai.should(); +chai.use(require('chai-things')); +chai.use(require('chai-as-promised')); + +let testBusinessNetworkArchive = {bna: 'TBNA'}; +let testBusinessNetworkId = 'net-biz-TestNetwork-0.0.1'; +let testBusinessNetworkDescription = 'Test network description'; +let mockBusinessNetworkDefinition; +let mockAdminConnection; + +const VALID_ENDORSEMENT_POLICY_STRING = '{"identities":[{ "role": { "name": "member", "mspId": "Org1MSP" }}], "policy": {"1-of": [{"signed-by":0}]}}'; + +describe('composer start network CLI unit tests', function () { + + let sandbox; + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + + mockBusinessNetworkDefinition = sinon.createStubInstance(BusinessNetworkDefinition); + mockBusinessNetworkDefinition.getIdentifier.returns(testBusinessNetworkId); + mockBusinessNetworkDefinition.getDescription.returns(testBusinessNetworkDescription); + + mockAdminConnection = sinon.createStubInstance(Admin.AdminConnection); + mockAdminConnection.createProfile.resolves(); + mockAdminConnection.connect.resolves(); + mockAdminConnection.start.resolves(); + + sandbox.stub(BusinessNetworkDefinition, 'fromArchive').resolves(mockBusinessNetworkDefinition); + sandbox.stub(CmdUtil, 'createAdminConnection').returns(mockAdminConnection); + sandbox.stub(process, 'exit'); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('Deploy handler() method tests', function () { + + it('Good path, optional parameter -O /path/to/options.json specified.', function () { + + let argv = {startId: 'WebAppAdmin' + ,startSecret: 'DJY27pEnl16d' + ,archiveFile: 'testArchiveFile.zip' + ,connectionProfileName: 'testProfile' + ,optionsFile: '/path/to/options.json'}; + sandbox.stub(Start, 'getArchiveFileContents'); + const optionsObject = { + endorsementPolicy: { + identities: [{role: {name: 'member',mspId: 'Org1MSP'}}], + policy: {'1-of': [{'signed-by': 0}]} + } + }; + + // This would also work. + //const optionsObject = { + // endorsementPolicy: '{"identities": [{"role": {"name": "member","mspId": "Org1MSP"}}],"policy": {"1-of": [{"signed-by": 0}]}}'; + //}; + + const optionFileContents = JSON.stringify(optionsObject); + sandbox.stub(fs, 'readFileSync').withArgs('/path/to/options.json').returns(optionFileContents); + sandbox.stub(fs, 'existsSync').withArgs('/path/to/options.json').returns(true); + + Start.getArchiveFileContents.withArgs(argv.archiveFile).returns(testBusinessNetworkArchive); + + return StartCmd.handler(argv) + .then ((result) => { + sinon.assert.calledOnce(BusinessNetworkDefinition.fromArchive); + sinon.assert.calledWith(BusinessNetworkDefinition.fromArchive, testBusinessNetworkArchive); + sinon.assert.calledOnce(CmdUtil.createAdminConnection); + + sinon.assert.calledOnce(mockAdminConnection.connect); + sinon.assert.calledWith(mockAdminConnection.connect, argv.connectionProfileName, argv.startId, argv.startSecret); + sinon.assert.calledOnce(mockAdminConnection.start); + sinon.assert.calledWith(mockAdminConnection.start, mockBusinessNetworkDefinition, + { + endorsementPolicy: optionsObject.endorsementPolicy + }); + }); + }); + + it('Good path, optional parameter -o endorsementPolicyFile= specified.', function () { + + let argv = {startId: 'WebAppAdmin' + ,startSecret: 'DJY27pEnl16d' + ,archiveFile: 'testArchiveFile.zip' + ,connectionProfileName: 'testProfile' + ,option: 'endorsementPolicyFile=/path/to/some/file.json'}; + sandbox.stub(Start, 'getArchiveFileContents'); + + Start.getArchiveFileContents.withArgs(argv.archiveFile).returns(testBusinessNetworkArchive); + + return StartCmd.handler(argv) + .then ((result) => { + sinon.assert.calledOnce(BusinessNetworkDefinition.fromArchive); + sinon.assert.calledWith(BusinessNetworkDefinition.fromArchive, testBusinessNetworkArchive); + sinon.assert.calledOnce(CmdUtil.createAdminConnection); + + sinon.assert.calledOnce(mockAdminConnection.connect); + sinon.assert.calledWith(mockAdminConnection.connect, argv.connectionProfileName, argv.startId, argv.startSecret); + sinon.assert.calledOnce(mockAdminConnection.start); + sinon.assert.calledWith(mockAdminConnection.start, mockBusinessNetworkDefinition, + { + endorsementPolicyFile: '/path/to/some/file.json' + }); + }); + }); + + + it('Good path, optional parameter -o endorsementPolicy= specified.', function () { + + let argv = {startId: 'WebAppAdmin' + ,startSecret: 'DJY27pEnl16d' + ,archiveFile: 'testArchiveFile.zip' + ,connectionProfileName: 'testProfile' + ,option: 'endorsementPolicy=' + VALID_ENDORSEMENT_POLICY_STRING}; + + sandbox.stub(Start, 'getArchiveFileContents'); + + Start.getArchiveFileContents.withArgs(argv.archiveFile).returns(testBusinessNetworkArchive); + + return StartCmd.handler(argv) + .then ((result) => { + sinon.assert.calledOnce(BusinessNetworkDefinition.fromArchive); + sinon.assert.calledWith(BusinessNetworkDefinition.fromArchive, testBusinessNetworkArchive); + sinon.assert.calledOnce(CmdUtil.createAdminConnection); + + sinon.assert.calledOnce(mockAdminConnection.connect); + sinon.assert.calledWith(mockAdminConnection.connect, argv.connectionProfileName, argv.startId, argv.startSecret); + sinon.assert.calledOnce(mockAdminConnection.start); + sinon.assert.calledWith(mockAdminConnection.start, mockBusinessNetworkDefinition, + { + endorsementPolicy: VALID_ENDORSEMENT_POLICY_STRING + }); + }); + }); + + + it('Good path, all parms correctly specified.', function () { + + let argv = {startId: 'WebAppAdmin' + ,startSecret: 'DJY27pEnl16d' + ,archiveFile: 'testArchiveFile.zip' + ,connectionProfileName: 'testProfile'}; + + sandbox.stub(Start, 'getArchiveFileContents'); + + Start.getArchiveFileContents.withArgs(argv.archiveFile).returns(testBusinessNetworkArchive); + + return StartCmd.handler(argv) + .then ((result) => { + sinon.assert.calledOnce(BusinessNetworkDefinition.fromArchive); + sinon.assert.calledWith(BusinessNetworkDefinition.fromArchive, testBusinessNetworkArchive); + sinon.assert.calledOnce(CmdUtil.createAdminConnection); + + sinon.assert.calledOnce(mockAdminConnection.connect); + sinon.assert.calledWith(mockAdminConnection.connect, argv.connectionProfileName, argv.startId, argv.startSecret); + sinon.assert.calledOnce(mockAdminConnection.start); + sinon.assert.calledWith(mockAdminConnection.start, mockBusinessNetworkDefinition); + }); + }); + + it('Good path, all parms correctly specified, including optional loglevel.', function () { + + let argv = {startId: 'WebAppAdmin' + ,startSecret: 'DJY27pEnl16d' + ,archiveFile: 'testArchiveFile.zip' + ,connectionProfileName: 'testProfile' + ,loglevel: 'DEBUG'}; + + sandbox.stub(Start, 'getArchiveFileContents'); + + Start.getArchiveFileContents.withArgs(argv.archiveFile).returns(testBusinessNetworkArchive); + + return StartCmd.handler(argv) + .then ((result) => { + sinon.assert.calledOnce(BusinessNetworkDefinition.fromArchive); + sinon.assert.calledWith(BusinessNetworkDefinition.fromArchive, testBusinessNetworkArchive); + sinon.assert.calledOnce(CmdUtil.createAdminConnection); + + sinon.assert.calledOnce(mockAdminConnection.connect); + sinon.assert.calledWith(mockAdminConnection.connect, argv.connectionProfileName, argv.startId, argv.startSecret); + sinon.assert.calledOnce(mockAdminConnection.start); + sinon.assert.calledWith(mockAdminConnection.start, mockBusinessNetworkDefinition, {logLevel: 'DEBUG'}); + }); + }); + + it('Good path, no startment secret, all other parms correctly specified.', function () { + + let startmentSecret = 'DJY27pEnl16d'; + sandbox.stub(CmdUtil, 'prompt').resolves(startmentSecret); + + let argv = {startId: 'WebAppAdmin' + ,archiveFile: 'testArchiveFile.zip' + ,connectionProfileName: 'testProfile'}; + + sandbox.stub(Start, 'getArchiveFileContents'); + + Start.getArchiveFileContents.withArgs(argv.archiveFile).returns(testBusinessNetworkArchive); + + return Start.handler(argv) + .then ((result) => { + sinon.assert.calledOnce(BusinessNetworkDefinition.fromArchive); + sinon.assert.calledWith(BusinessNetworkDefinition.fromArchive, testBusinessNetworkArchive); + sinon.assert.calledOnce(CmdUtil.createAdminConnection); + + sinon.assert.calledOnce(mockAdminConnection.connect); + sinon.assert.calledWith(mockAdminConnection.connect, argv.connectionProfileName, argv.startId, argv.startSecret); + sinon.assert.calledOnce(mockAdminConnection.start); + sinon.assert.calledWith(mockAdminConnection.start, mockBusinessNetworkDefinition); + }); + }); + + it('show throw an error if loglevel not valid', function() { + let argv = {enrollId: 'WebAppAdmin' + ,enrollSecret: 'DJY27pEnl16d' + ,loglevel: 'BAD' + ,archiveFile: 'testArchiveFile.zip'}; + return Start.handler(argv) + .should.be.rejectedWith(/or not one of/); + + }); + + }); + + describe('Deploy getArchiveFileContents() method tests', function () { + + it('Archive file exists', function () { + + sandbox.stub(fs, 'existsSync').returns(true); + let testArchiveFileContents = JSON.stringify(testBusinessNetworkArchive); + sandbox.stub(fs, 'readFileSync').returns(testArchiveFileContents); + + let testArchiveFile = 'testfile.zip'; + let archiveFileContents = Start.getArchiveFileContents(testArchiveFile); + + archiveFileContents.should.deep.equal(testArchiveFileContents); + + }); + + it('Archive file does not exist', function () { + + sandbox.stub(fs, 'existsSync').returns(false); + let testArchiveFileContents = JSON.stringify(testBusinessNetworkArchive); + sandbox.stub(fs, 'readFileSync').returns(testArchiveFileContents); + + let testArchiveFile = 'testfile.zip'; + (() => {Start.getArchiveFileContents(testArchiveFile);}).should.throw('Archive file '+testArchiveFile+' does not exist.'); + + }); + + }); + +}); diff --git a/packages/composer-cli/test/runtime/install.js b/packages/composer-cli/test/runtime/install.js new file mode 100644 index 0000000000..f717a8c5ec --- /dev/null +++ b/packages/composer-cli/test/runtime/install.js @@ -0,0 +1,84 @@ +/* + * 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 Admin = require('composer-admin'); +const BusinessNetworkDefinition = Admin.BusinessNetworkDefinition; +const InstallCmd = require('../../lib/cmds/runtime/installCommand.js'); +const CmdUtil = require('../../lib/cmds/utils/cmdutils.js'); + +//require('../lib/deploy.js'); +require('chai').should(); + +const chai = require('chai'); +const sinon = require('sinon'); +require('sinon-as-promised'); +chai.should(); +chai.use(require('chai-things')); +chai.use(require('chai-as-promised')); + +let testBusinessNetworkId = 'net-biz-TestNetwork-0.0.1'; +let testBusinessNetworkDescription = 'Test network description'; +let mockBusinessNetworkDefinition; +let mockAdminConnection; + +describe('composer install runtime CLI unit tests', function () { + + let sandbox; + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + + mockBusinessNetworkDefinition = sinon.createStubInstance(BusinessNetworkDefinition); + mockBusinessNetworkDefinition.getIdentifier.returns(testBusinessNetworkId); + mockBusinessNetworkDefinition.getDescription.returns(testBusinessNetworkDescription); + + mockAdminConnection = sinon.createStubInstance(Admin.AdminConnection); + mockAdminConnection.createProfile.resolves(); + mockAdminConnection.connect.resolves(); + mockAdminConnection.deploy.resolves(); + + sandbox.stub(BusinessNetworkDefinition, 'fromArchive').returns(mockBusinessNetworkDefinition); + sandbox.stub(CmdUtil, 'createAdminConnection').returns(mockAdminConnection); + sandbox.stub(process, 'exit'); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('Install handler() method tests', function () { + + it('Good path, all parms correctly specified.', function () { + + let argv = {installId: 'PeerAdmin' + ,installSecret: 'Anything' + ,businessNetworkName: 'org-acme-biznet' + ,connectionProfileName: 'testProfile'}; + + + return InstallCmd.handler(argv) + .then ((result) => { + sinon.assert.calledOnce(CmdUtil.createAdminConnection); + sinon.assert.calledOnce(mockAdminConnection.connect); + sinon.assert.calledWith(mockAdminConnection.connect, argv.connectionProfileName, argv.installId, argv.installSecret, null); + sinon.assert.calledOnce(mockAdminConnection.install); + sinon.assert.calledWith(mockAdminConnection.install, argv.businessNetworkName, {}); + }); + }); + + }); + +}); diff --git a/packages/composer-common/lib/connection.js b/packages/composer-common/lib/connection.js index 1101350a45..ddad601824 100644 --- a/packages/composer-common/lib/connection.js +++ b/packages/composer-common/lib/connection.js @@ -87,6 +87,32 @@ class Connection extends EventEmitter { return Promise.reject(new Error('abstract function called')); } + /** + * Install the Hyperledger Composer runtime. + * @abstract + * @param {SecurityContext} securityContext The participant's security context. + * @param {string} businessNetworkIdentifier The identifier of the Business network that will be started in this installed runtime + * @param {Object} installOptions connector specific installation options + * @return {Promise} A promise that is resolved once the business network + * artifacts have been installed, or rejected with an error. + */ + install(securityContext, businessNetworkIdentifier, installOptions) { + return Promise.reject(new Error('abstract function called')); + } + + /** + * Start a business network definition. + * @abstract + * @param {SecurityContext} securityContext The participant's security context. + * @param {BusinessNetworkDefinition} businessNetworkDefinition The BusinessNetworkDefinition to install + * @param {Object} startOptions connector specific installation options + * @return {Promise} A promise that is resolved once the business network + * artifacts have been installed, or rejected with an error. + */ + start(securityContext, businessNetworkDefinition, startOptions) { + return Promise.reject(new Error('abstract function called')); + } + /** * Deploy a business network definition. * @abstract diff --git a/packages/composer-common/test/connection.js b/packages/composer-common/test/connection.js index 2091b87555..eab07ae7c0 100644 --- a/packages/composer-common/test/connection.js +++ b/packages/composer-common/test/connection.js @@ -81,6 +81,26 @@ describe('Connection', () => { }); + describe('#start', () => { + + it('should throw as abstract method', () => { + let c = new Connection(mockConnectionManager, 'debFabric1', 'org.acme.Business'); + return c.start() + .should.be.rejectedWith(/abstract function called/); + }); + + }); + + describe('#install', () => { + + it('should throw as abstract method', () => { + let c = new Connection(mockConnectionManager, 'debFabric1', 'org.acme.Business'); + return c.install() + .should.be.rejectedWith(/abstract function called/); + }); + + }); + describe('#deploy', () => { it('should throw as abstract method', () => { diff --git a/packages/composer-connector-embedded/lib/embeddedconnection.js b/packages/composer-connector-embedded/lib/embeddedconnection.js index e7a414ea33..66101c30ac 100644 --- a/packages/composer-connector-embedded/lib/embeddedconnection.js +++ b/packages/composer-connector-embedded/lib/embeddedconnection.js @@ -171,14 +171,38 @@ class EmbeddedConnection extends Connection { } /** - * Deploy all business network artifacts. + * For the embedded connector, this is just a no-op, there is nothing to install. + * @param {SecurityContext} securityContext The participant's security context. + * @param {string} businessNetworkIdentifier The identifier of the Business network that will be started in this installed runtime + * @param {Object} installOptions connector specific installation options + * @return {Promise} A resolved promise as this is a no-op + */ + install(securityContext, businessNetworkIdentifier, installOptions) { + return Promise.resolve(); + } + + /** + * Deploy a business network. For the embedded connector this just translates to + * a start request as no install is required. * @param {HFCSecurityContext} securityContext The participant's security context. * @param {BusinessNetwork} businessNetwork The BusinessNetwork to deploy - * @param {Object} deployOptions connector specific deployment options + * @param {Object} deployOptions connector specific deploy options * @return {Promise} A promise that is resolved once the business network * artifacts have been deployed, or rejected with an error. */ deploy(securityContext, businessNetwork, deployOptions) { + return this.start(securityContext, businessNetwork, deployOptions); + } + + /** + * Start a business network. + * @param {HFCSecurityContext} securityContext The participant's security context. + * @param {BusinessNetwork} businessNetwork The BusinessNetwork to deploy + * @param {Object} startOptions connector specific start options + * @return {Promise} A promise that is resolved once the business network + * artifacts have been deployed and started, or rejected with an error. + */ + start(securityContext, businessNetwork, startOptions) { let container = EmbeddedConnection.createContainer(); let identity = securityContext.getIdentity(); let chaincodeUUID = container.getUUID(); diff --git a/packages/composer-connector-embedded/test/embeddedconnection.js b/packages/composer-connector-embedded/test/embeddedconnection.js index c94387aaf9..9d3768f566 100644 --- a/packages/composer-connector-embedded/test/embeddedconnection.js +++ b/packages/composer-connector-embedded/test/embeddedconnection.js @@ -131,7 +131,27 @@ describe('EmbeddedConnection', () => { }); - describe('#deploy', () => { + describe('#install', () => { + it('should perform a no-op and return a resolved promise', () => { + return connection.install(mockSecurityContext, 'org-acme-biznet') + .then(() => { + }); + }); + }); + + describe('#deploy', () => { + it('should just call start', () => { + sinon.stub(connection, 'start').resolves(); + let mockBusinessNetwork = sinon.createStubInstance(BusinessNetworkDefinition); + return connection.deploy(mockSecurityContext, mockBusinessNetwork) + .then(() => { + sinon.assert.calledOnce(connection.start); + sinon.assert.calledWith(connection.start, mockSecurityContext, mockBusinessNetwork); + }); + }); + }); + + describe('#start', () => { it('should call the init engine method, ping, and store the chaincode ID', () => { let mockBusinessNetwork = sinon.createStubInstance(BusinessNetworkDefinition); @@ -146,7 +166,7 @@ describe('EmbeddedConnection', () => { sandbox.stub(EmbeddedConnection, 'createEngine').returns(mockEngine); mockEngine.init.resolves(); sinon.stub(connection, 'ping').resolves(); - return connection.deploy(mockSecurityContext, mockBusinessNetwork) + return connection.start(mockSecurityContext, mockBusinessNetwork) .then(() => { sinon.assert.calledOnce(mockEngine.init); sinon.assert.calledWith(mockEngine.init, sinon.match((context) => { diff --git a/packages/composer-connector-hlf/lib/hfcconnection.js b/packages/composer-connector-hlf/lib/hfcconnection.js index 258cd18a66..1a781da919 100644 --- a/packages/composer-connector-hlf/lib/hfcconnection.js +++ b/packages/composer-connector-hlf/lib/hfcconnection.js @@ -137,18 +137,18 @@ class HFCConnection extends Connection { } /** - * Deploy all business network artifacts. + * Start a business network definition. * @param {HFCSecurityContext} securityContext The participant's security context. - * @param {BusinessNetwork} businessNetwork The BusinessNetwork to deploy - * @param {Object} deployOptions connector specific deployment options + * @param {BusinessNetwork} businessNetwork The BusinessNetwork to start + * @param {Object} startOptions connector specific deployment options * @return {Promise} A promise that is resolved once the business network - * artifacts have been deployed, or rejected with an error. + * artifacts have been deployed and the business network started, or rejected with an error. */ - deploy(securityContext, businessNetwork, deployOptions) { + start(securityContext, businessNetwork, startOptions) { HFCUtil.securityCheck(securityContext); const self = this; let chaincodeId = null; - LOG.info('deploy', 'Deploying business network', businessNetwork.getIdentifier()); + LOG.info('start', 'Starting business network', businessNetwork.getIdentifier()); // check whether this client has already deployed this business network return self.getConnectionManager().getConnectionProfileManager().getConnectionProfileStore().load(self.connectionProfile) @@ -164,7 +164,7 @@ class HFCConnection extends Connection { .deployChainCode(securityContext, 'concerto', 'init', [buffer.toString('base64'), JSON.stringify(initArgs)], true); }) .then((result) => { - LOG.info('deploy', 'Deployed chaincode', result.chaincodeID); + LOG.info('start', 'Deployed chaincode', result.chaincodeID); chaincodeId = result.chaincodeID; return securityContext.setChaincodeID(result.chaincodeID); }) @@ -181,7 +181,7 @@ class HFCConnection extends Connection { return self.connectionManager.getConnectionProfileManager().getConnectionProfileStore().save(self.connectionProfile, profile); }) .then(() => { - LOG.info('deploy', 'Updated connection profile with chaincode id', self.getIdentifier()); + LOG.info('start', 'Updated connection profile with chaincode id', self.getIdentifier()); // note that we do NOT set self.businessNetworkIdentifier // here as that would change the identity of this admin connection // causing an exception when the connection is disconncted due to a @@ -356,6 +356,29 @@ class HFCConnection extends Connection { } } + /** + * Deploy a business network. For the hlf connector this just translates to + * a start request as no install is required. + * @param {HFCSecurityContext} securityContext The participant's security context. + * @param {BusinessNetwork} businessNetwork The BusinessNetwork to deploy + * @param {Object} deployOptions connector specific deploy options + * @return {Promise} A promise that is resolved once the business network + * artifacts have been deployed, or rejected with an error. + */ + deploy(securityContext, businessNetwork, deployOptions) { + return this.start(securityContext, businessNetwork, deployOptions); + } + + /** + * For the hlf connector, this is just a no-op, there is nothing to install + * @param {SecurityContext} securityContext The participant's security context. + * @param {string} businessNetworkIdentifier The identifier of the Business network that will be started in this installed runtime + * @param {Object} installOptions connector specific install options + * @return {Promise} An already resolved promise + */ + install(securityContext, businessNetworkIdentifier, installOptions) { + return Promise.resolve(); + } } module.exports = HFCConnection; diff --git a/packages/composer-connector-hlf/test/hfcconnection.js b/packages/composer-connector-hlf/test/hfcconnection.js index 318aa98f7e..118e88a55a 100644 --- a/packages/composer-connector-hlf/test/hfcconnection.js +++ b/packages/composer-connector-hlf/test/hfcconnection.js @@ -237,7 +237,7 @@ describe('HFCConnection', () => { }); - describe('#deploy', function() { + describe('#start', function() { it('should perform a security check', () => { sandbox.stub(HFCUtil, 'securityCheck'); @@ -253,14 +253,14 @@ describe('HFCConnection', () => { networks: { } }); - return connection.deploy(mockSecurityContext, businessNetworkStub) + return connection.start(mockSecurityContext, businessNetworkStub) .then(() => { sinon.assert.calledOnce(HFCUtil.securityCheck); sinon.assert.calledOnce(mockConnectionProfileStore.save); }); }); - it('should deploy the Concerto chain-code to the Hyperledger Fabric', function() { + it('should start the Concerto chain-code to the Hyperledger Fabric', function() { // Set up the responses from the chain-code. sandbox.stub(HFCUtil, 'deployChainCode').resolves({ @@ -275,7 +275,7 @@ describe('HFCConnection', () => { }); return connection - .deploy(mockSecurityContext, businessNetworkStub, '{}') + .start(mockSecurityContext, businessNetworkStub, '{}') .then(function() { // Check that the query was made successfully. @@ -305,7 +305,7 @@ describe('HFCConnection', () => { businessNetworkStub.toArchive.resolves(new Buffer([0x00, 0x01, 0x02])); return connection - .deploy(mockSecurityContext, businessNetworkStub) + .start(mockSecurityContext, businessNetworkStub) .then(function(assetRegistries) { throw new Error('should not get here'); }).catch(function(error) { @@ -324,7 +324,7 @@ describe('HFCConnection', () => { businessNetworkStub.toArchive.resolves(new Buffer([0x00, 0x01, 0x02])); return connection - .deploy(mockSecurityContext, businessNetworkStub) + .start(mockSecurityContext, businessNetworkStub) .then(function(assetRegistries) { throw new Error('should not get here'); }).catch(function(error) { @@ -728,4 +728,23 @@ describe('HFCConnection', () => { }); + describe('#install', () => { + it('should perform a no-op and return a resolved promise', () => { + return connection.install(mockSecurityContext, 'org-acme-biznet') + .then(() => { + }); + }); + }); + + describe('#deploy', () => { + it('should just call start', () => { + sinon.stub(connection, 'start').resolves(); + let mockBusinessNetwork = sinon.createStubInstance(BusinessNetworkDefinition); + return connection.deploy(mockSecurityContext, mockBusinessNetwork) + .then(() => { + sinon.assert.calledOnce(connection.start); + sinon.assert.calledWith(connection.start, mockSecurityContext, mockBusinessNetwork); + }); + }); + }); }); diff --git a/packages/composer-connector-hlfv1/lib/hlfconnection.js b/packages/composer-connector-hlfv1/lib/hlfconnection.js index 8624236b45..c4c51f579e 100644 --- a/packages/composer-connector-hlfv1/lib/hlfconnection.js +++ b/packages/composer-connector-hlfv1/lib/hlfconnection.js @@ -297,19 +297,22 @@ class HLFConnection extends Connection { } /** - * internal method to perform chaincode install + * Install a business network connection. * * @param {any} securityContext the security context - * @param {any} businessNetwork the business network - * @param {object} deployOptions any relevant deploy options for install - * @private + * @param {string} businessNetworkIdentifier the business network name + * @param {object} installOptions any relevant install options * @returns {Promise} a promise for install completion * * @memberOf HLFConnection */ - _install(securityContext, businessNetwork, deployOptions) { - const method = '_install'; - LOG.entry(method, securityContext, businessNetwork, deployOptions); + install(securityContext, businessNetworkIdentifier, installOptions) { + const method = 'install'; + LOG.entry(method, securityContext, businessNetworkIdentifier, installOptions); + + if (!businessNetworkIdentifier) { + throw new Error('businessNetworkIdentifier 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 @@ -346,7 +349,7 @@ class HLFConnection extends Connection { const request = { chaincodePath: chaincodePath, chaincodeVersion: runtimePackageJSON.version, - chaincodeId: businessNetwork.getName(), + chaincodeId: businessNetworkIdentifier, txId: txId, targets: this.channel.getPeers() }; @@ -355,11 +358,13 @@ class HLFConnection extends Connection { }) .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'); + if (installOptions && installOptions.ignoreCCInstalled) { + this._validateResponses(results[0], false, /chaincode .+ exists/); + LOG.debug(method, 'chaincode installed, or already installed'); + } else { + this._validateResponses(results[0], false); + LOG.debug(method, 'chaincode installed'); + } }) .then(() => { LOG.exit(method); @@ -389,19 +394,22 @@ class HLFConnection extends Connection { } /** - * instantiate the chaincode - * + * Instantiate the chaincode. * * @param {any} securityContext the security context * @param {any} businessNetwork the business network - * @param {Object} deployOptions an optional connection specific set of deployment options (see deploy for details) + * @param {Object} startOptions an optional connection specific set of deployment options (see deploy for details) * @returns {Promise} a promise for instantiation completion * * @memberOf HLFConnection */ - _instantiate(securityContext, businessNetwork, deployOptions) { + start(securityContext, businessNetwork, startOptions) { const method = '_instantiate'; - LOG.entry(method, securityContext, businessNetwork, deployOptions); + LOG.entry(method, securityContext, businessNetwork, startOptions); + + if (!businessNetwork) { + throw new Error('businessNetwork not specified'); + } let businessNetworkArchive; let finalTxId; @@ -418,8 +426,8 @@ class HLFConnection extends Connection { finalTxId = this.client.newTransactionID(); let initArgs = {}; - if (deployOptions && deployOptions.logLevel) { - initArgs.logLevel = deployOptions.logLevel; + if (startOptions && startOptions.logLevel) { + initArgs.logLevel = startOptions.logLevel; } const request = { @@ -431,15 +439,15 @@ class HLFConnection extends Connection { args: [businessNetworkArchive.toString('base64'), JSON.stringify(initArgs)] }; - if (deployOptions) { + if (startOptions) { // endorsementPolicy overrides endorsementPolicyFile try { - if (deployOptions.endorsementPolicy) { + if (startOptions.endorsementPolicy) { request['endorsement-policy'] = - (typeof deployOptions.endorsementPolicy === 'string') ? JSON.parse(deployOptions.endorsementPolicy) : deployOptions.endorsementPolicy; - } else if (deployOptions.endorsementPolicyFile) { + (typeof startOptions.endorsementPolicy === 'string') ? JSON.parse(startOptions.endorsementPolicy) : startOptions.endorsementPolicy; + } else if (startOptions.endorsementPolicyFile) { // we don't check for existence so that the error handler will report the file not found - request['endorsement-policy'] = JSON.parse(fs.readFileSync(deployOptions.endorsementPolicyFile)); + request['endorsement-policy'] = JSON.parse(fs.readFileSync(startOptions.endorsementPolicyFile)); } } catch (error) { const newError = new Error('Error trying parse endorsement policy. ' + error); @@ -452,7 +460,7 @@ class HLFConnection extends Connection { }) .then((results) => { // Validate the instantiate proposal results - LOG.debug(method, `Received ${results.length} results(s) from deploying the chaincode`, results); + LOG.debug(method, `Received ${results.length} results(s) from instantiating the chaincode`, results); let proposalResponses = results[0]; this._validateResponses(proposalResponses, true); @@ -480,7 +488,7 @@ class HLFConnection extends Connection { LOG.exit(method); }) .catch((error) => { - const newError = new Error('error trying instantiate chaincode. ' + error); + const newError = new Error('error trying to instantiate chaincode. ' + error); LOG.error(method, newError); throw newError; }); @@ -509,7 +517,7 @@ class HLFConnection extends Connection { throw new Error('businessNetwork not specified'); } - return this._install(securityContext, businessNetwork) + return this.install(securityContext, businessNetwork.getName(), {ignoreCCInstalled: true}) .then(() => { // check to see if the chaincode is already instantiated return this.channel.queryInstantiatedChaincodes(); @@ -523,7 +531,7 @@ class HLFConnection extends Connection { LOG.debug(method, 'chaincode already instantiated'); return Promise.resolve(); } - return this._instantiate(securityContext, businessNetwork, deployOptions); + return this.start(securityContext, businessNetwork, deployOptions); }) .then(() => { LOG.exit(method); diff --git a/packages/composer-connector-hlfv1/test/hlfconnection.js b/packages/composer-connector-hlfv1/test/hlfconnection.js index a9661a0af5..27c4536df4 100644 --- a/packages/composer-connector-hlfv1/test/hlfconnection.js +++ b/packages/composer-connector-hlfv1/test/hlfconnection.js @@ -356,24 +356,35 @@ describe('HLFConnection', () => { }); - describe('#_install', () => { + describe('#install', () => { + const tempDirectoryPath = path.resolve('tmp', 'composer1234567890'); + const targetDirectoryPath = path.resolve(tempDirectoryPath, 'src', 'composer'); + const versionFilePath = path.resolve(targetDirectoryPath, 'version.go'); + beforeEach(() => { sandbox.stub(process, 'on').withArgs('exit').yields(); sandbox.stub(HLFConnection, 'createEventHub').returns(mockEventHub); + sandbox.stub(connection, '_initializeChannel').resolves(); connection._connectToEventHubs(); }); - const tempDirectoryPath = path.resolve('tmp', 'composer1234567890'); + it('should throw if businessNetworkIdentifier not specified', () => { + (() => { + connection.install(mockSecurityContext, null); + }).should.throw(/businessNetworkIdentifier not specified/); + }); + + 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) + return connection.install(mockSecurityContext, mockBusinessNetwork) .should.be.rejectedWith(/some error 1/); }); it('should rethrow error if unable to copy chaincode source', () => { sandbox.stub(connection.temp, 'mkdir').withArgs('composer').resolves(tempDirectoryPath); sandbox.stub(connection.fs, 'copy').rejects(new Error('some error 2')); - connection._install(mockSecurityContext, mockBusinessNetwork) + return connection.install(mockSecurityContext, mockBusinessNetwork) .should.be.rejectedWith(/some error/); }); @@ -381,10 +392,465 @@ describe('HLFConnection', () => { sandbox.stub(connection.temp, 'mkdir').withArgs('composer').resolves(tempDirectoryPath); sandbox.stub(connection.fs, 'copy').resolves(); sandbox.stub(connection.fs, 'outputFile').rejects(new Error('some error 3')); - connection._install(mockSecurityContext, mockBusinessNetwork) + return connection.install(mockSecurityContext, mockBusinessNetwork) .should.be.rejectedWith(/some error 3/); }); + it('should install the runtime', () => { + sandbox.stub(connection.temp, 'mkdir').withArgs('composer').resolves(tempDirectoryPath); + sandbox.stub(connection.fs, 'copy').resolves(); + sandbox.stub(connection.fs, 'outputFile').resolves(); + + // This is the install proposal and response (from the peers). + const proposalResponses = [{ + response: { + status: 200 + } + }]; + const proposal = { proposal: 'i do' }; + const header = { header: 'gooooal' }; + mockClient.installChaincode.resolves([ proposalResponses, proposal, header ]); + sandbox.stub(connection, '_validateResponses').returns(); + return connection.install(mockSecurityContext, 'org-acme-biznet') + .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. + connection.fs.copy.firstCall.args[2].filter('some/path/here').should.be.true; + connection.fs.copy.firstCall.args[2].filter('some/node_modules/here').should.be.true; + 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(mockClient.installChaincode); + sinon.assert.calledWith(mockClient.installChaincode, { + chaincodePath: 'composer', + chaincodeVersion: connectorPackageJSON.version, + chaincodeId: 'org-acme-biznet', + txId: mockTransactionID, + targets: [mockPeer] + }); + }); + }); + + it('should throw error if peer rejects installation', () => { + sandbox.stub(connection.temp, 'mkdir').withArgs('composer').resolves(tempDirectoryPath); + sandbox.stub(connection.fs, 'copy').resolves(); + sandbox.stub(connection.fs, 'outputFile').resolves(); + + // This is the install proposal and response (from the peers). + const proposalResponses = [{ + response: { + status: 200 + } + }]; + const proposal = { proposal: 'i do' }; + const header = { header: 'gooooal' }; + mockClient.installChaincode.resolves([ proposalResponses, proposal, header ]); + sandbox.stub(connection, '_validateResponses').throws(new Error('Some error occurs')); + + return connection.install(mockSecurityContext, mockBusinessNetwork) + .should.be.rejectedWith(/Some error occurs/); + }); + + it('should throw error if peer says chaincode already installed and no ignore option', () => { + sandbox.stub(connection.temp, 'mkdir').withArgs('composer').resolves(tempDirectoryPath); + sandbox.stub(connection.fs, 'copy').resolves(); + sandbox.stub(connection.fs, 'outputFile').resolves(); + + // This is the install proposal and response (from the peers). + const proposalResponses = [{ + response: { + status: 200 + } + }]; + const proposal = { proposal: 'i do' }; + const header = { header: 'gooooal' }; + mockClient.installChaincode.resolves([ proposalResponses, proposal, header ]); + const errorResp = new Error('Error installing chaincode code systest-participants:0.5.11(chaincode /var/hyperledger/production/chaincodes/systest-participants.0.5.11 exists)'); + sandbox.stub(connection, '_validateResponses').throws(errorResp); + + return connection.install(mockSecurityContext, mockBusinessNetwork) + .should.be.rejectedWith(/Error installing chaincode/); + }); + + it('should check for chaincode exists message is ignoreCCinstalled flag set', () => { + sandbox.stub(connection.temp, 'mkdir').withArgs('composer').resolves(tempDirectoryPath); + sandbox.stub(connection.fs, 'copy').resolves(); + sandbox.stub(connection.fs, 'outputFile').resolves(); + + // This is the install proposal and response (from the peers). + const proposalResponses = [{ + response: { + status: 200 + } + }]; + const proposal = { proposal: 'i do' }; + const header = { header: 'gooooal' }; + mockClient.installChaincode.resolves([ proposalResponses, proposal, header ]); + sandbox.stub(connection, '_validateResponses').returns(); + + return connection.install(mockSecurityContext, mockBusinessNetwork, {ignoreCCInstalled: true}) + .then(() => { + sinon.assert.calledOnce(mockClient.installChaincode); + sinon.assert.calledOnce(connection._validateResponses); + sinon.assert.calledWith(connection._validateResponses, sinon.match.any, false, /chaincode .+ exists/); + }); + }); + + + }); + + describe('#start', () => { + + 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 businessNetwork not specified', () => { + (() => { + connection.start(mockSecurityContext, null); + }).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: [ { + requestURL: 'grpc://localhost:7051', + eventURL: 'grpc://localhost:7053' + }], + ca: 'http://localhost:7054', + keyValStore: '/tmp/hlfabric1', + channel: 'testchainid', + mspID: 'suchmsp', + timeout: 22 + }; + 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 instantiate proposal and response (from the peers). + const proposalResponses = [{ + response: { + status: 200 + } + }]; + const proposal = { proposal: 'i do' }; + const header = { header: 'gooooal' }; + const response = { + status: 'SUCCESS' + }; + 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.start(mockSecurityContext, mockBusinessNetwork) + .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, 22 * 1000); + }); + }); + + it('should start the business network with endorsement policy object', () => { + sandbox.stub(global, 'setTimeout'); + + // This is the instantiate proposal and response (from the peers). + const proposalResponses = [{ + response: { + status: 200 + } + }]; + const proposal = { proposal: 'i do' }; + const header = { header: 'gooooal' }; + mockChannel.sendInstantiateProposal.resolves([ proposalResponses, proposal, header ]); + // This is the orderer proposal and response. + const response = { + status: 'SUCCESS' + }; + mockChannel.sendTransaction.withArgs({ proposalResponses: proposalResponses, proposal: proposal, header: header }).resolves(response); + // This is the event hub response. + mockEventHub.registerTxEvent.yields(mockTransactionID.getTransactionID().toString(), 'VALID'); + + const policy = { + identities: [ + { role: { name: 'member', mspId: 'Org1MSP' }}, + { role: { name: 'member', mspId: 'Org2MSP' }}, + { role: { name: 'admin', mspId: 'Org1MSP' }} + ], + policy: { + '1-of': [ + { 'signed-by': 2}, + { '2-of': [{ 'signed-by': 0}, { 'signed-by': 1 }]} + ] + } + }; + const deployOptions = { + endorsementPolicy : policy + }; + return connection.start(mockSecurityContext, mockBusinessNetwork, deployOptions) + .then(() => { + sinon.assert.calledOnce(connection._initializeChannel); + sinon.assert.calledOnce(mockChannel.sendInstantiateProposal); + sinon.assert.calledWith(mockChannel.sendInstantiateProposal, { + chaincodePath: 'composer', + chaincodeVersion: connectorPackageJSON.version, + chaincodeId: 'org-acme-biznet', + txId: mockTransactionID, + fcn: 'init', + args: ['aGVsbG8gd29ybGQ=', '{}'], + 'endorsement-policy' : policy + }); + + sinon.assert.calledOnce(mockChannel.sendTransaction); + }); + }); + + it('should start the business network with endorsement policy string', () => { + sandbox.stub(global, 'setTimeout'); + // This is the instantiate proposal and response (from the peers). + const proposalResponses = [{ + response: { + status: 200 + } + }]; + const proposal = { proposal: 'i do' }; + const header = { header: 'gooooal' }; + mockChannel.sendInstantiateProposal.resolves([ proposalResponses, proposal, header ]); + // This is the orderer proposal and response (from the orderer). + const response = { + status: 'SUCCESS' + }; + mockChannel.sendTransaction.withArgs({ proposalResponses: proposalResponses, proposal: proposal, header: header }).resolves(response); + // This is the event hub response. + mockEventHub.registerTxEvent.yields(mockTransactionID.getTransactionID().toString(), 'VALID'); + + const policyString = '{"identities": [{"role": {"name": "member","mspId": "Org1MSP"}}],"policy": {"1-of": [{"signed-by": 0}]}}'; + const policy = JSON.parse(policyString); + const deployOptions = { + endorsementPolicy : policyString + }; + return connection.start(mockSecurityContext, mockBusinessNetwork, deployOptions) + .then(() => { + sinon.assert.calledOnce(connection._initializeChannel); + sinon.assert.calledOnce(mockChannel.sendInstantiateProposal); + sinon.assert.calledWith(mockChannel.sendInstantiateProposal, { + chaincodePath: 'composer', + chaincodeVersion: connectorPackageJSON.version, + chaincodeId: 'org-acme-biznet', + txId: mockTransactionID, + fcn: 'init', + args: ['aGVsbG8gd29ybGQ=', '{}'], + 'endorsement-policy' : policy + }); + + sinon.assert.calledOnce(mockChannel.sendTransaction); + }); + }); + + it('should start the business network with endorsement policy file', () => { + sandbox.stub(global, 'setTimeout'); + // This is the instantiate proposal and response (from the peers). + const proposalResponses = [{ + response: { + status: 200 + } + }]; + const proposal = { proposal: 'i do' }; + const header = { header: 'gooooal' }; + mockChannel.sendInstantiateProposal.resolves([ proposalResponses, proposal, header ]); + // This is the commit proposal and response (from the orderer). + const response = { + status: 'SUCCESS' + }; + mockChannel.sendTransaction.withArgs({ proposalResponses: proposalResponses, proposal: proposal, header: header }).resolves(response); + // This is the event hub response. + mockEventHub.registerTxEvent.yields(mockTransactionID.getTransactionID().toString(), 'VALID'); + + const policyString = '{"identities": [{"role": {"name": "member","mspId": "Org1MSP"}}],"policy": {"1-of": [{"signed-by": 0}]}}'; + sandbox.stub(fs, 'readFileSync').withArgs('/path/to/options.json').returns(policyString); + const deployOptions = { + endorsementPolicyFile : '/path/to/options.json' + }; + return connection.start(mockSecurityContext, mockBusinessNetwork, deployOptions) + .then(() => { + sinon.assert.calledOnce(connection._initializeChannel); + sinon.assert.calledOnce(mockChannel.sendInstantiateProposal); + sinon.assert.calledWith(mockChannel.sendInstantiateProposal, { + chaincodePath: 'composer', + chaincodeVersion: connectorPackageJSON.version, + chaincodeId: 'org-acme-biznet', + txId: mockTransactionID, + fcn: 'init', + args: ['aGVsbG8gd29ybGQ=', '{}'], + 'endorsement-policy' : JSON.parse(policyString) + }); + + sinon.assert.calledOnce(mockChannel.sendTransaction); + }); + }); + + it('should throw an error if the policy string isn\'t valid json', () => { + const policyString = '{identities: [{role: {name: "member",mspId: "Org1MSP"}}],policy: {1-of: [{signed-by: 0}]}}'; + const deployOptions = { + endorsementPolicy : policyString + }; + return connection.start(mockSecurityContext, mockBusinessNetwork, deployOptions) + .should.be.rejectedWith(/Error trying parse endorsement policy/); + }); + + it('should throw an error if the policy file doesn\'t exist', () => { + sandbox.stub(fs, 'readFileSync').withArgs('/path/to/options.json').throws(new Error('ENOENT')); + const deployOptions = { + endorsementPolicyFile : '/path/to/options.json' + }; + return connection.start(mockSecurityContext, mockBusinessNetwork, deployOptions) + .should.be.rejectedWith(/Error trying parse endorsement policy/); + }); + + it('should throw an error if the policy file isn\'t valid json', () => { + const policyString = '{identities: [{role: {name: "member",mspId: "Org1MSP"}}],policy: {1-of: [{signed-by: 0}]}}'; + sandbox.stub(fs, 'readFileSync').withArgs('/path/to/options.json').returns(policyString); + const deployOptions = { + endorsementPolicyFile : '/path/to/options.json' + }; + return connection.start(mockSecurityContext, mockBusinessNetwork, deployOptions) + .should.be.rejectedWith(/Error trying parse endorsement policy/); + }); + + it('should start the business network with no debug level specified', () => { + sandbox.stub(global, 'setTimeout'); + // This is the instantiate proposal and response (from the peers). + const proposalResponses = [{ + response: { + status: 200 + } + }]; + const proposal = { proposal: 'i do' }; + const header = { header: 'gooooal' }; + mockChannel.sendInstantiateProposal.resolves([ proposalResponses, proposal, header ]); + // This is the orderer proposal and response (from the orderer). + const response = { + status: 'SUCCESS' + }; + mockChannel.sendTransaction.withArgs({ proposalResponses: proposalResponses, proposal: proposal, header: header }).resolves(response); + // This is the event hub response. + mockEventHub.registerTxEvent.yields(mockTransactionID.getTransactionID().toString(), 'VALID'); + return connection.start(mockSecurityContext, mockBusinessNetwork) + .then(() => { + sinon.assert.calledOnce(connection._initializeChannel); + sinon.assert.calledOnce(mockChannel.sendInstantiateProposal); + sinon.assert.calledWith(mockChannel.sendInstantiateProposal, { + chaincodePath: 'composer', + chaincodeVersion: connectorPackageJSON.version, + chaincodeId: 'org-acme-biznet', + txId: mockTransactionID, + fcn: 'init', + args: ['aGVsbG8gd29ybGQ=', '{}'] + }); + + sinon.assert.calledOnce(mockChannel.sendTransaction); + }); + }); + + it('should start the business network with debug level set', () => { + sandbox.stub(global, 'setTimeout'); + // This is the instantiate proposal and response (from the peers). + const proposalResponses = [{ + response: { + status: 200 + } + }]; + const proposal = { proposal: 'i do' }; + const header = { header: 'gooooal' }; + mockChannel.sendInstantiateProposal.resolves([ proposalResponses, proposal, header ]); + // This is the orderer proposal and response (from the orderer). + const response = { + status: 'SUCCESS' + }; + mockChannel.sendTransaction.withArgs({ proposalResponses: proposalResponses, proposal: proposal, header: header }).resolves(response); + // This is the event hub response. + mockEventHub.registerTxEvent.yields(mockTransactionID.getTransactionID().toString(), 'VALID'); + return connection.start(mockSecurityContext, mockBusinessNetwork, {'logLevel': 'WARNING'}) + .then(() => { + sinon.assert.calledOnce(connection._initializeChannel); + sinon.assert.calledOnce(mockChannel.sendInstantiateProposal); + sinon.assert.calledWith(mockChannel.sendInstantiateProposal, { + chaincodePath: 'composer', + chaincodeVersion: connectorPackageJSON.version, + chaincodeId: 'org-acme-biznet', + txId: mockTransactionID, + fcn: 'init', + args: ['aGVsbG8gd29ybGQ=', '{"logLevel":"WARNING"}'] + }); + + sinon.assert.calledOnce(mockChannel.sendTransaction); + }); + }); + + it('should throw any instantiate fails to validate', () => { + const errorResp = new Error('such error'); + const instantiateResponses = [ errorResp ]; + const proposal = { proposal: 'i do' }; + const header = { header: 'gooooal' }; + mockChannel.sendInstantiateProposal.resolves([ instantiateResponses, proposal, header ]); + connection._validateResponses.withArgs(instantiateResponses).throws(errorResp); + // This is the event hub response. + //mockEventHub.registerTxEvent.yields(mockTransactionID.getTransactionID().toString(), 'VALID'); + return connection.start(mockSecurityContext, mockBusinessNetwork) + .should.be.rejectedWith(/such error/); + }); + + // TODO: should extract out _waitForEvents + it('should throw an error if the orderer throws an error', () => { + // This is the instantiate proposal and response (from the peers). + const proposalResponses = [{ + response: { + status: 200 + } + }]; + const proposal = { proposal: 'i do' }; + const header = { header: 'gooooal' }; + mockChannel.sendInstantiateProposal.resolves([ proposalResponses, proposal, header ]); + // This is the orderer proposal and response (from the orderer). + const response = { + status: 'FAILURE' + }; + mockChannel.sendTransaction.withArgs({ proposalResponses: proposalResponses, proposal: proposal, header: header }).resolves(response); + // This is the event hub response. + //mockEventHub.registerTxEvent.yields(mockTransactionID.getTransactionID().toString(), 'INVALID'); + return connection.start(mockSecurityContext, mockBusinessNetwork) + .should.be.rejectedWith(/Failed to commit transaction/); + }); + + it('should throw an error if peer says transaction not valid', () => { + // This is the instantiate proposal and response (from the peers). + const proposalResponses = [{ + response: { + status: 200 + } + }]; + const proposal = { proposal: 'i do' }; + const header = { header: 'gooooal' }; + mockChannel.sendInstantiateProposal.resolves([ proposalResponses, proposal, header ]); + // This is the orderer proposal and response (from the orderer). + const response = { + status: 'SUCCESS' + }; + mockChannel.sendTransaction.withArgs({ proposalResponses: proposalResponses, proposal: proposal, header: header }).resolves(response); + // This is the event hub response to indicate transaction not valid + mockEventHub.registerTxEvent.yields(mockTransactionID.getTransactionID().toString(), 'INVALID'); + return connection.start(mockSecurityContext, mockBusinessNetwork) + .should.be.rejectedWith(/Peer has rejected transaction '00000000-0000-0000-0000-000000000000'/); + }); + }); describe('#_validateResponses', () => { diff --git a/packages/composer-connector-proxy/lib/proxyconnection.js b/packages/composer-connector-proxy/lib/proxyconnection.js index 5cedca0383..6185157867 100644 --- a/packages/composer-connector-proxy/lib/proxyconnection.js +++ b/packages/composer-connector-proxy/lib/proxyconnection.js @@ -82,6 +82,47 @@ class ProxyConnection extends Connection { }); } + /** + * Install the Hyperledger Composer runtime. + * @param {SecurityContext} securityContext The participant's security context. + * @param {string} businessNetworkIdentifier The identifier of the Business network that will be started in this installed runtime + * @param {Object} installOptions connector specific install options + * @return {Promise} A promise that is resolved once the runtime has been installed, or rejected with an error. + */ + install(securityContext, businessNetworkIdentifier, installOptions) { + return new Promise((resolve, reject) => { + this.socket.emit('/api/connectionInstall', this.connectionID, securityContext.securityContextID, businessNetworkIdentifier, installOptions, (error) => { + if (error) { + return reject(ProxyUtil.inflaterr(error)); + } + resolve(); + }); + }); + } + + /** + * Start a business network definition. + * @param {SecurityContext} securityContext The participant's security context. + * @param {BusinessNetworkDefinition} businessNetworkDefinition The BusinessNetworkDefinition to start + * @param {Object} startOptions connector specific start options + * @return {Promise} A promise that is resolved once the business network has been started, + * or rejected with an error. + */ + start(securityContext, businessNetworkDefinition, startOptions) { + return businessNetworkDefinition.toArchive() + .then((businessNetworkArchive) => { + return new Promise((resolve, reject) => { + this.socket.emit('/api/connectionStart', this.connectionID, securityContext.securityContextID, businessNetworkArchive.toString('base64'), startOptions, (error) => { + if (error) { + return reject(ProxyUtil.inflaterr(error)); + } + resolve(); + }); + }); + }); + } + + /** * Deploy a business network definition. * @param {SecurityContext} securityContext The participant's security context. diff --git a/packages/composer-connector-proxy/test/proxyconnection.js b/packages/composer-connector-proxy/test/proxyconnection.js index 6fa64a084d..9d45beca30 100644 --- a/packages/composer-connector-proxy/test/proxyconnection.js +++ b/packages/composer-connector-proxy/test/proxyconnection.js @@ -94,6 +94,53 @@ describe('ProxyConnection', () => { }); + describe('#install', () => { + + it('should send a install call to the connector server', () => { + mockSocket.emit.withArgs('/api/connectionInstall', connectionID, securityContextID, 'org-acme-biznet', undefined, sinon.match.func).yields(null); + return connection.install(mockSecurityContext, businessNetworkIdentifier) + .then(() => { + sinon.assert.calledOnce(mockSocket.emit); + sinon.assert.calledWith(mockSocket.emit, '/api/connectionInstall', connectionID, securityContextID, 'org-acme-biznet', undefined, sinon.match.func); + }); + }); + + it('should handle an error from the connector server', () => { + mockSocket.emit.withArgs('/api/connectionInstall', connectionID, securityContextID, 'org-acme-biznet', undefined, sinon.match.func).yields(serializedError); + return connection.install(mockSecurityContext, businessNetworkIdentifier) + .should.be.rejectedWith(TypeError, /such type error/); + }); + + }); + + + describe('#start', () => { + + let mockBusinessNetworkDefinition; + + beforeEach(() => { + mockBusinessNetworkDefinition = sinon.createStubInstance(BusinessNetworkDefinition); + mockBusinessNetworkDefinition.toArchive.resolves(Buffer.from('hello world')); + }); + + it('should send a start call to the connector server', () => { + mockSocket.emit.withArgs('/api/connectionStart', connectionID, securityContextID, 'aGVsbG8gd29ybGQ=', undefined, sinon.match.func).yields(null); + return connection.start(mockSecurityContext, mockBusinessNetworkDefinition) + .then(() => { + sinon.assert.calledOnce(mockSocket.emit); + sinon.assert.calledWith(mockSocket.emit, '/api/connectionStart', connectionID, securityContextID, 'aGVsbG8gd29ybGQ=', undefined, sinon.match.func); + }); + }); + + it('should handle an error from the connector server', () => { + mockSocket.emit.withArgs('/api/connectionStart', connectionID, securityContextID, 'aGVsbG8gd29ybGQ=', undefined, sinon.match.func).yields(serializedError); + return connection.start(mockSecurityContext, mockBusinessNetworkDefinition) + .should.be.rejectedWith(TypeError, /such type error/); + }); + + }); + + describe('#deploy', () => { let mockBusinessNetworkDefinition; diff --git a/packages/composer-connector-server/lib/connectorserver.js b/packages/composer-connector-server/lib/connectorserver.js index 7abb68c8f2..87a1bf2485 100644 --- a/packages/composer-connector-server/lib/connectorserver.js +++ b/packages/composer-connector-server/lib/connectorserver.js @@ -205,6 +205,90 @@ class ConnectorServer { }); } + /** + * Handle a request from the client to install the runtime. + * @param {string} connectionID The connection ID. + * @param {string} securityContextID The security context ID. + * @param {string} businessNetworkIdentifier The business network identifier + * @param {Object} installOptions connector specific install options + * @param {function} callback The callback to call when complete. + * @return {Promise} A promise that is resolved when complete. + */ + connectionInstall(connectionID, securityContextID, businessNetworkIdentifier, installOptions, callback) { + const method = 'connectionDeploy'; + LOG.entry(method, connectionID, securityContextID, businessNetworkIdentifier, installOptions); + let connection = this.connections[connectionID]; + if (!connection) { + let error = new Error(`No connection found with ID ${connectionID}`); + LOG.error(error); + callback(ConnectorServer.serializerr(error)); + LOG.exit(method, null); + return Promise.resolve(); + } + let securityContext = this.securityContexts[securityContextID]; + if (!securityContext) { + let error = new Error(`No security context found with ID ${securityContextID}`); + LOG.error(error); + callback(ConnectorServer.serializerr(error)); + LOG.exit(method, null); + return Promise.resolve(); + } + return connection.install(securityContext, businessNetworkIdentifier, installOptions) + .then(() => { + callback(null); + LOG.exit(method); + }) + .catch((error) => { + LOG.error(error); + callback(ConnectorServer.serializerr(error)); + LOG.exit(method); + }); + } + + /** + * Handle a request from the client to start a business network. + * @param {string} connectionID The connection ID. + * @param {string} securityContextID The security context ID. + * @param {string} businessNetworkBase64 The business network archive, as a base64 encoded string. + * @param {Object} startOptions connector specific start options + * @param {function} callback The callback to call when complete. + * @return {Promise} A promise that is resolved when complete. + */ + connectionStart(connectionID, securityContextID, businessNetworkBase64, startOptions, callback) { + const method = 'connectionDeploy'; + LOG.entry(method, connectionID, securityContextID, businessNetworkBase64, startOptions); + let connection = this.connections[connectionID]; + if (!connection) { + let error = new Error(`No connection found with ID ${connectionID}`); + LOG.error(error); + callback(ConnectorServer.serializerr(error)); + LOG.exit(method, null); + return Promise.resolve(); + } + let securityContext = this.securityContexts[securityContextID]; + if (!securityContext) { + let error = new Error(`No security context found with ID ${securityContextID}`); + LOG.error(error); + callback(ConnectorServer.serializerr(error)); + LOG.exit(method, null); + return Promise.resolve(); + } + let businessNetworkArchive = Buffer.from(businessNetworkBase64, 'base64'); + return BusinessNetworkDefinition.fromArchive(businessNetworkArchive) + .then((businessNetworkDefinition) => { + return connection.start(securityContext, businessNetworkDefinition, startOptions); + }) + .then(() => { + callback(null); + LOG.exit(method); + }) + .catch((error) => { + LOG.error(error); + callback(ConnectorServer.serializerr(error)); + LOG.exit(method); + }); + } + /** * Handle a request from the client to deploy a business network. * @param {string} connectionID The connection ID. diff --git a/packages/composer-connector-server/test/connectorserver.js b/packages/composer-connector-server/test/connectorserver.js index 4e19926529..50cb02b438 100644 --- a/packages/composer-connector-server/test/connectorserver.js +++ b/packages/composer-connector-server/test/connectorserver.js @@ -109,6 +109,7 @@ describe('ConnectorServer', () => { '/api/connectionCreateIdentity', '/api/connectionDeploy', '/api/connectionDisconnect', + '/api/connectionInstall', '/api/connectionInvokeChainCode', '/api/connectionList', '/api/connectionLogin', @@ -116,6 +117,7 @@ describe('ConnectorServer', () => { '/api/connectionManagerImportIdentity', '/api/connectionPing', '/api/connectionQueryChainCode', + '/api/connectionStart', '/api/connectionUndeploy', '/api/connectionUpdate' ]); @@ -320,6 +322,129 @@ describe('ConnectorServer', () => { }); + describe('#connectionInstall', () => { + + beforeEach(() => { + connectorServer.connections[connectionID] = mockConnection; + connectorServer.securityContexts[securityContextID] = mockSecurityContext; + }); + + it('should install', () => { + mockConnection.install.withArgs(mockSecurityContext, businessNetworkIdentifier).resolves(); + sandbox.stub(uuid, 'v4').returns(securityContextID); + const cb = sinon.stub(); + return connectorServer.connectionInstall(connectionID, securityContextID, 'org-acme-biznet', {}, cb) + .then(() => { + sinon.assert.calledOnce(mockConnection.install); + sinon.assert.calledWith(mockConnection.install, mockSecurityContext, businessNetworkIdentifier); + sinon.assert.calledOnce(cb); + sinon.assert.calledWith(cb, null); + }); + }); + + it('should handle an invalid connection ID', () => { + const cb = sinon.stub(); + return connectorServer.connectionInstall(invalidID, securityContextID, 'org-acme-biznet', {}, cb) + .then(() => { + sinon.assert.calledOnce(cb); + const serializedError = cb.args[0][0]; + serializedError.name.should.equal('Error'); + serializedError.message.should.match(/No connection found with ID/); + serializedError.stack.should.be.a('string'); + }); + }); + + it('should handle an invalid security context ID ID', () => { + const cb = sinon.stub(); + return connectorServer.connectionInstall(connectionID, invalidID, 'org-acme-biznet', {}, cb) + .then(() => { + sinon.assert.calledOnce(cb); + const serializedError = cb.args[0][0]; + serializedError.name.should.equal('Error'); + serializedError.message.should.match(/No security context found with ID/); + serializedError.stack.should.be.a('string'); + }); + }); + + it('should handle install errors', () => { + mockConnection.install.rejects(new Error('such error')); + const cb = sinon.stub(); + return connectorServer.connectionInstall(connectionID, securityContextID, 'org-acme-biznet', {}, cb) + .then(() => { + sinon.assert.calledOnce(cb); + const serializedError = cb.args[0][0]; + serializedError.name.should.equal('Error'); + serializedError.message.should.equal('such error'); + serializedError.stack.should.be.a('string'); + }); + }); + + }); + + + describe('#connectionStart', () => { + + beforeEach(() => { + connectorServer.connections[connectionID] = mockConnection; + connectorServer.securityContexts[securityContextID] = mockSecurityContext; + }); + + it('should start', () => { + mockConnection.start.withArgs(mockSecurityContext, mockBusinessNetworkDefinition).resolves(); + sandbox.stub(uuid, 'v4').returns(securityContextID); + const cb = sinon.stub(); + return connectorServer.connectionStart(connectionID, securityContextID, 'aGVsbG8gd29ybGQ=', {}, cb) + .then(() => { + sinon.assert.calledOnce(BusinessNetworkDefinition.fromArchive); + const buffer = BusinessNetworkDefinition.fromArchive.args[0][0]; + Buffer.isBuffer(buffer).should.be.true; + Buffer.from('hello world').compare(buffer).should.equal(0); + sinon.assert.calledOnce(mockConnection.start); + sinon.assert.calledWith(mockConnection.start, mockSecurityContext, mockBusinessNetworkDefinition); + sinon.assert.calledOnce(cb); + sinon.assert.calledWith(cb, null); + }); + }); + + it('should handle an invalid connection ID', () => { + const cb = sinon.stub(); + return connectorServer.connectionStart(invalidID, securityContextID, 'aGVsbG8gd29ybGQ=', {}, cb) + .then(() => { + sinon.assert.calledOnce(cb); + const serializedError = cb.args[0][0]; + serializedError.name.should.equal('Error'); + serializedError.message.should.match(/No connection found with ID/); + serializedError.stack.should.be.a('string'); + }); + }); + + it('should handle an invalid security context ID ID', () => { + const cb = sinon.stub(); + return connectorServer.connectionStart(connectionID, invalidID, 'aGVsbG8gd29ybGQ=', {}, cb) + .then(() => { + sinon.assert.calledOnce(cb); + const serializedError = cb.args[0][0]; + serializedError.name.should.equal('Error'); + serializedError.message.should.match(/No security context found with ID/); + serializedError.stack.should.be.a('string'); + }); + }); + + it('should handle start errors', () => { + mockConnection.start.rejects(new Error('such error')); + const cb = sinon.stub(); + return connectorServer.connectionStart(connectionID, securityContextID, 'aGVsbG8gd29ybGQ=', {}, cb) + .then(() => { + sinon.assert.calledOnce(cb); + const serializedError = cb.args[0][0]; + serializedError.name.should.equal('Error'); + serializedError.message.should.equal('such error'); + serializedError.stack.should.be.a('string'); + }); + }); + + }); + describe('#connectionDeploy', () => { beforeEach(() => { @@ -383,6 +508,7 @@ describe('ConnectorServer', () => { }); + describe('#connectionUpdate', () => { beforeEach(() => { diff --git a/packages/composer-connector-web/lib/webconnection.js b/packages/composer-connector-web/lib/webconnection.js index 20544e6327..fac0491690 100644 --- a/packages/composer-connector-web/lib/webconnection.js +++ b/packages/composer-connector-web/lib/webconnection.js @@ -182,14 +182,38 @@ class WebConnection extends Connection { } /** - * Deploy all business network artifacts. + * For the web connector, this is just a no-op, there is nothing to install + * @param {SecurityContext} securityContext The participant's security context. + * @param {string} businessNetworkIdentifier The identifier of the Business network that will be started in this installed runtime + * @param {Object} installOptions connector specific install options + * @return {Promise} An already resolved promise + */ + install(securityContext, businessNetworkIdentifier, installOptions) { + return Promise.resolve(); + } + + /** + * Deploy a business network. For the web connector this just translates to + * a start request as no install is required. * @param {HFCSecurityContext} securityContext The participant's security context. * @param {BusinessNetwork} businessNetwork The BusinessNetwork to deploy - * @param {Object} deployOptions connector specific deployment options + * @param {Object} deployOptions connector specific deploy options * @return {Promise} A promise that is resolved once the business network * artifacts have been deployed, or rejected with an error. */ deploy(securityContext, businessNetwork, deployOptions) { + return this.start(securityContext, businessNetwork, deployOptions); + } + + /** + * Start a business network. + * @param {HFCSecurityContext} securityContext The participant's security context. + * @param {BusinessNetwork} businessNetwork The BusinessNetwork to deploy + * @param {Object} startOptions connector specific start options + * @return {Promise} A promise that is resolved once the business network + * artifacts have been deployed and the network started, or rejected with an error. + */ + start(securityContext, businessNetwork, startOptions) { let container = WebConnection.createContainer(); let identity = securityContext.getIdentity(); let chaincodeID = container.getUUID(); diff --git a/packages/composer-connector-web/test/webconnection.js b/packages/composer-connector-web/test/webconnection.js index e26dec9fa6..6e1e64ec40 100644 --- a/packages/composer-connector-web/test/webconnection.js +++ b/packages/composer-connector-web/test/webconnection.js @@ -163,7 +163,28 @@ describe('WebConnection', () => { }); - describe('#deploy', () => { + describe('#install', () => { + it('should perform a no-op and return a resolved promise', () => { + return connection.install(mockSecurityContext, 'org-acme-biznet') + .then(() => { + }); + }); + }); + + describe('#deploy', () => { + it('should just call start', () => { + sinon.stub(connection, 'start').resolves(); + let mockBusinessNetwork = sinon.createStubInstance(BusinessNetworkDefinition); + return connection.deploy(mockSecurityContext, mockBusinessNetwork) + .then(() => { + sinon.assert.calledOnce(connection.start); + sinon.assert.calledWith(connection.start, mockSecurityContext, mockBusinessNetwork); + }); + }); + }); + + + describe('#start', () => { it('should call the init engine method, ping, and store the chaincode ID', () => { let mockBusinessNetwork = sinon.createStubInstance(BusinessNetworkDefinition); @@ -178,7 +199,7 @@ describe('WebConnection', () => { sandbox.stub(WebConnection, 'createEngine').returns(mockEngine); mockEngine.init.resolves(); sinon.stub(connection, 'ping').resolves(); - return connection.deploy(mockSecurityContext, mockBusinessNetwork) + return connection.start(mockSecurityContext, mockBusinessNetwork) .then(() => { sinon.assert.calledOnce(mockEngine.init); sinon.assert.calledWith(mockEngine.init, sinon.match((context) => { diff --git a/packages/composer-systests/systest/assets.js b/packages/composer-systests/systest/assets.js index 8adf00ecc1..e5e301f148 100644 --- a/packages/composer-systests/systest/assets.js +++ b/packages/composer-systests/systest/assets.js @@ -42,7 +42,12 @@ describe('Asset system tests', function () { businessNetworkDefinition.getModelManager().addModelFile(modelFile.contents, modelFile.fileName); }); admin = TestUtil.getAdmin(); - return admin.deploy(businessNetworkDefinition) + console.log('testing install/start'); + // Have some system test perform install/start rather than deploy + return admin.install(businessNetworkDefinition.getName()) + .then(() => { + return admin.start(businessNetworkDefinition); + }) .then(() => { return TestUtil.getClient('systest-assets') .then((result) => { diff --git a/packages/composer-systests/systest/identities.js b/packages/composer-systests/systest/identities.js index bb12b5e878..047c04801f 100644 --- a/packages/composer-systests/systest/identities.js +++ b/packages/composer-systests/systest/identities.js @@ -53,7 +53,12 @@ describe('Identity system tests', () => { scriptManager.addScript(scriptManager.createScript(scriptFile.identifier, 'JS', scriptFile.contents)); }); admin = TestUtil.getAdmin(); - return admin.deploy(businessNetworkDefinition) + console.log('testing install/start'); + // Have some system test perform install/start rather than deploy + return admin.install(businessNetworkDefinition.getName()) + .then(() => { + return admin.start(businessNetworkDefinition); + }) .then(() => { return TestUtil.getClient('systest-identities') .then((result) => { diff --git a/packages/composer-systests/systest/transactions.assets.js b/packages/composer-systests/systest/transactions.assets.js index e3f5bf7a40..7f07dbf6b2 100644 --- a/packages/composer-systests/systest/transactions.assets.js +++ b/packages/composer-systests/systest/transactions.assets.js @@ -48,7 +48,12 @@ describe('Transaction (asset specific) system tests', () => { scriptManager.addScript(scriptManager.createScript(scriptFile.identifier, 'JS', scriptFile.contents)); }); admin = TestUtil.getAdmin(); - return admin.deploy(businessNetworkDefinition) + console.log('testing install/start'); + // Have some system test perform install/start rather than deploy + return admin.install(businessNetworkDefinition.getName()) + .then(() => { + return admin.start(businessNetworkDefinition); + }) .then(() => { return TestUtil.getClient('systest-transactions-assets') .then((result) => {